Foxnic-SQL (13) —— 外部SQL与SQL模版
概述
在前几节中,已经介绍过 Foxnic-SQL 将 SQL 语句对象化并执行。那么,为什么还要引入外部 SQL 和 SQL 模板的特性呢?
首先,大多数时候,我们的第一反应是用字符串去拼接 SQL 语句,这说明字符串拼接方式其实是最直观的。其次,使用对象化的方式拼接 SQL ,还是有其局限性,大量的 SQL 文本也不宜直接写在 Java 类中。所以,Foxnic-SQL 将原本要写在 Java 类中的 SQL 语句放到一个外部文件中,每个语句用一个 ID 去标识,在 SQL 执行时,只要指定 ID 就可以了。在此基础上,Foxnic-SQL 加入了模板引擎、SQL 语句置换、热加载等特性,使其变得更加好用。
SQL 模板的设计目的是在 SQL 语句外置的同时,可以在语句中加入逻辑、替换变量、置换语句。SQL 模板对象同时支持内置和外置的SQL语句,当然大多数情况下,模板SQL都是外部语句。
本文中的示例代码均可在 https://gitee.com/LeeFJ/foxnic-samples 项目中找到。
使用外部SQL
在调用外部语句前,首先要定义好外部语句,外部语句可以定义在 Java 文件同一个包里,如图所示:
下面的示例,我要在 TemplateTop 类内执行 db0.tql 内定义的 SQL 语句。以下是 db0.tql 文件的部分内容:
[query_0]
select 'moduleTop@query_0'
[top_query_1]
select 'moduleTop@query_1'
如上代码所示,语句的 ID 被写在 SQL 语句上方的中括号内,这个 ID 将在 Java 代码内被使用。以下是 TemplateTop 类的部分代码:
public class TemplateTop {
public void initSQLoader(DAO dao) {
// 设置SQL扫描范围
SQLoader.addTQLScanPackage(dao, TemplateTop.class.getPackage().getName());
SQLoader.setPrintDetail(true);
}
/**
* 简单示例
* */
@Test
public void demo_0() {
// 创建 DAO
DAO dao = DBInstance.DEFAULT.dao();
// 初始化 SQLoader
initSQLoader(dao);
String result = null;
// 执行当前包下的语句
result = dao.queryString("#query_0");
System.out.println(result);
}
}
TemplateTop 类中包含了两个方法,其中 initSQLoader 方法用于初始化SQL扫描器。使用 SQL 扫描器时需要添加扫描的 package ,被添加包以及子包均会被扫描。
demo_0 方法用于执行语句,以#开头的 SQL ID 既表示要执行的不是 SQL 语句,而是指定一个 SQL ID 并加载到已经定义好的 SQL 语句去执行。
外部SQL的作用域
外部定义的 SQL 也是有其作用域的,通俗点讲就是命名空间,每个定义的 SQL ID 并非在应用内全局有效,而只是在其包内或父级包内才有效,有点类似继承的效果。
如上图所示,在 template 包内有两个子包,TemplateTop 仅可以访问 db_top.tql 内定义的 SQL 语句。而 TemplateM1 仅可以访问 db_m1_tql 和 db_top.tql 内定义的 SQL,而不可以访问 db_m2_tql 内定义的 SQL 语句。下面我们来看一下在 TemplateM1 内可见和不可见的 SQL 语句示例:
public class TemplateM1 extends TemplateTop {
@Test
public void scope_demo_in_M1() {
// 创建 DAO
DAO dao= DBInstance.DEFAULT.dao();
// 初始化 SQLoader
initSQLoader(dao);
//
String result = null;
result = dao.queryString("#query_0");
System.out.println(result);
assertTrue("module1@query_0".equals(result));
result = dao.queryString("#m1_query_1");
System.out.println(result);
assertTrue("module1@query_1".equals(result));
// 允许执行上级级包内定义的语句
result = dao.queryString("#top_query_1");
System.out.println(result);
assertTrue("moduleTop@query_1".equals(result));
// 不允许执行其它同级包内定义的语句
try {
dao.queryString("#m2_query_1");
assertTrue(false);
} catch (Exception e) {
assertTrue(true);
}
}
}
同样,我们可以看一下 TemplateTop 可见和不可见的 SQL 语句示例:
public class TemplateTop {
public void initSQLoader(DAO dao) {
// 设置SQL扫描范围
SQLoader.addTQLScanPackage(dao, TemplateTop.class.getPackage().getName());
SQLoader.setPrintDetail(true);
}
@Test
public void scope_demo_in_Top() {
// 创建 DAO
DAO dao= DBInstance.DEFAULT.dao();
// 初始化 SQLoader
initSQLoader(dao);
String result = null;
// 执行当前包下的语句
result = dao.queryString("#query_0");
System.out.println(result);
assertTrue("moduleTop@query_0".equals(result));
// 不允许执行下级包内定义的语句
try {
dao.queryString("#m1_query_1");
assertTrue(false);
} catch (Exception e) {
assertTrue(true);
}
}
}
由于篇幅所限,关于外部语句命名空间的两个示例未给出完整示例,完整代码请大家到 https://gitee.com/LeeFJ/foxnic-samples 项目中查看。
使用SQL模板
SQL 模板(SQLTpl)用于更为复杂的外部 SQL 拼装。SQLTpl 首先使用模板引擎对既定的语句进行处理,再对 SQL 语句进行置换,最后代入绑定变量执行语句。以下代码首先定义了一个外部 SQL 模板:
[top_query_3]
select #for(f:fields)#(f)#(for.last?' ':', ')#end
from sys_dict_item itm
LEFT JOIN sys_dict dict ON dict.code=itm.dict_code
WHERE dict.code like ?
#{QUESTION_LIST_1}
and itm.code like ?
#{QUESTION_LIST_2}
and valid = ?
order by dict.id asc
SQLTpl 使用 了 JFinal Enjoy 的模板引擎,关于其模板引擎相关指令请大家移步 https://jfinal.com/doc/6-1 自行查看。Foxnic-SQL 额外增加了 #{} 指令,用于 SQL 表达式的置换。示例如下:
/**
* 获取并处理模板对象
* */
@Test
public void demo_2_template() {
// 创建 DAO
DAO dao= DBInstance.DEFAULT.dao();
// 初始化 SQLoader
initSQLoader(dao);
// 获得模板对象
SQLTpl template= dao.getTemplate("#top_query_3");
// 设置模板变量
template.putVar("fields",new String[]{"itm.code","label"});
// 设置 SQL 置换对象
template.putVar("QUESTION_LIST_1",new Expr("and itm.label like ?","%修%"));
template.putVar("QUESTION_LIST_2",new Expr("and itm.code like ?","%r%"));
// 设置模板中的 SQL 绑定变量
template.setParameters("%e%","%r%",1);
// 输出
System.out.println(template.getSQL());
// 执行查询,利用 QueryableSQL 接口特性
RcdSet rs=template.query();
// 遍历与输出
for (Rcd r : rs) {
System.out.println(r.toJSONObject());
}
}
数据库适配
Foxnic-SQL 利用外部 SQL 解决数据库适配的问题,Foxnic-SQL 在 DAO 以及 Service 中自动生成的 SQL 语句是多库兼容的。但是,某些手写的语句要做到多库兼容就需要一些额外的处理,在外部 SQL 定义时,可以指定执行的数据库类型:
[top_query_2:mysql]
select 'moduleTop@query_2#mysql'
[top_query_2]
select 'moduleTop@query_2#default'
如上代码所示,第一个语句的 SQL ID 冒号后面 mysql 指定的数据库类型。这样,默认情况下执行第五行的 SQL 语句,但若当前库是 mysql 那么就执行第二行的 SQL 语句。如下代码所示:
/**
* 执行特定数据库的语句
* */
@Test
public void demo_1_dbType() {
// 创建 DAO
DAO dao= DBInstance.DEFAULT.dao();
// 初始化 SQLoader
initSQLoader(dao);
String result = null;
// 执行当前包下的语句
result = dao.queryString("#top_query_2");
System.out.println(result);
if(dao.getDBType()== DBType.MYSQL) {
assertTrue("moduleTop@query_2#mysql".equals(result));
} else {
assertTrue("moduleTop@query_2#default".equals(result));
}
}
小结
本节主要介绍了在 Foxnic-SQL 中外置的 SQL 语句与 SQL 模板,同时也简单介绍了 Foxni-SQL 对于多库适配的支持方式。Foxnic-SQL 中外置 SQL 是对语句对象化补充与扩展,在一些复杂 SQL 且不适合对象化 SQL 的场合较为适用。
相关项目
https://gitee.com/LeeFJ/foxnic
https://gitee.com/LeeFJ/foxnic-web
https://gitee.com/lank/eam
https://gitee.com/LeeFJ/foxnic-samples
官方文档
http://foxnicweb.com/docs/doc.html
附录(Enjoy 模版引擎使用摘要)
Enjoy 模版引擎相关指令,如要学习 Enjoy 模版引擎,请移步 https://jfinal.com/doc/6-1
6.4 指令
Enjoy Template Engine一如既往地坚持极简设计,核心只有 #if、#for、#switch、#set、#include、#define、#(…) 这七个指令,便实现了传统模板引擎几乎所有的功能,用户如果有任意一门程序语言基础,学习成本几乎为零。
如果官方提供的指令无法满足需求,还可以极其简单地在模板语言的层面对指令进行扩展,在com.jfinal.template.ext.directive 包下面就有五个扩展指令,Active Record 的 sql 模块也针对sql管理功能扩展了三个指令,参考这些扩展指令的代码,便可无师自通,极为简单。
注意,Enjoy 模板引擎指令的扩展是在词法分析、语法分析的层面进行扩展,与传统模板引擎的自定义标签类的扩展完全不是一个级别,前者可以极为全面和自由的利用模板引擎的基础设施,在更加基础的层面以极为简单直接的代码实现千变万化的功能。参考 Active Record的 sql 管理模块,则可知其强大与便利。
1、输出指令#( )
与几乎所有 java 模板引擎不同,Enjoy Template Engine消灭了插值指令这个原本独立的概念,而是将其当成是所有指令中的一员,仅仅是指令名称省略了而已。因此,该指令的定界符与普通指令一样为小括号,从而不必像其它模板引擎一样引入额外的如大括号般的定界符。
#(…) 输出指令的使用极为简单,只需要为该指令传入前面6.4节中介绍的任何表达式即可,指令会将这些表达式的求值结果进行输出,特别注意,当表达式的值为 null 时没有任何输出,更不会报异常。所以,对于 #(value) 这类输出不需要对 value 进行 null 值判断,如下是代码示例:
#(value)
#(object.field)
#(object.field ??)
#(a > b ? x : y)
#(seoTitle ?? "JFinal 俱乐部")
#(object.method(), null)
如上图所示,只需要对输出指令传入表达式即可。注意上例中第一行代码 value 参数可以为 null,而第二行代码中的 object 为 null 时将会报异常,此时需要使用第三行代码中的空合安全取值调用运算符:object.field ??
此外,注意上图最后一行代码中的输出指令参数为一个逗号表达式,逗号表达式的整体求值结果为最后一个表达式的值,而输出指令对于null值不做输出,所以这行代码相当于是仅仅调用了 object.method() 方法去实现某些操作。
输出指令可以自由定制,只需要继承 OutputDirectiveFactory 类并覆盖其中的 getOutputDirective 方法,然后在 configEngine(Engine me)方法中,通过 me. setOutputDirectiveFactory(…) 切换即可。
2、#if 指令
直接举例:
如上图所示,if指令需要一个 cond 表达式作为参数,并且以 #end 为结尾符,cond 可以为 6.3 章节中介绍的所有表达式,包括逗号表达式,当 cond 求值为 true 时,执行 if 分支之中的代码。
if 指令必然支持 #else if 与 #else 分支块结构,以下是示例:
由于#else if、#else用法与java语法完全一样,在此不在赘述。(注意:jfinal 3.3 之前的版本 #else if 之间不能有空格字符需要写成:#elseif,否则会报异常:Can not match the #end of directive #if )
3、#for 指令
Enjoy Template Engine 对 for 指令进行了极为人性化的扩展,可以对任意类型数据进行迭代输出,包括支持 null 值迭代。以下是代码示例:
上例代码中的第一个 for 指令是对 list 进行迭代,用法与 java 语法完全一样。
第二个 for 指令是对 map 进行迭代,取值方式为 item.key 与 item.value。该取值方式是 enjoy 对 map 迭代的增强功能,可以节省代码量。仍然也可以使用传统的 java map 迭代方式:#for( x : map.entrySet() ) #(x.key) #(x.value) #end
注意:当被迭代的目标为 null 时,不需要做 null 值判断,for 指令会自动跳过,不进行迭代。从而可以避免 if 判断,节省代码提高效率。
for指令还支持对其状态进行获取,代码示例:
以上代码中的 #(for.index)、#(for.outer.index) 是对 for 指令当前状态值进行获取,前者是获取当前 for 指令迭代的下标值(从0开始的整数),后者是内层for指令获取上一层for指令的状态。这里注意 for.outer 这个固定的用法,专门用于在内层 for 指令中引用上层for指令状态。
注意:for指令嵌套时,各自拥有自己的变量名作用域,规则与java语言一致,例如上例中的两个#(x.field)处在不同的for指令作用域内,会正确获取到所属作用域的变量值。
for指令支持的所有状态值如下示例:
具体用法在上面代码中用中文进行了说明,在此不再赘述。
除了 Map、List 以外,for指令还支持 Collection、Iterator、array 普通数组、Iterable、Enumeration、null 值的迭代,用法在形式上与前面的List迭代完全相同,都是 #for(id : target) 的形式,对于 null 值,for指令会直接跳过不迭代。
此外,for指令还支持对任意类型进行迭代,此时仅仅是对该对象进行一次性迭代,如下所示:
上例中的article为一个普通的java对象,而非集合类型对象,for循环会对该对象进行一次性迭代操作,for表达式中的x即为article对象本身,所以可以使用 #(x.title) 进行输出。
for 指令还支持 #else 分支语句,在for指令迭代次数为0时,将执行 #else 分支内部的语句,如下是示例:
以上代码中,当blogList.size() 为0或者blogList为null值时,也即迭代次数为0时,会执行#else分支,这种场景在web项目中极为常见。
最后,除了上面介绍的for指令迭代用法以外,还支持更常规的for语句形式,以下是代码示例:
与java语法基本一样,唯一的不同是变量声明不需要类型,直接用赋值语句即可,Enjoy Template Engine中的变量是动态弱类型。
注意:以上这种形式的for语句,比前面的for迭代少了for.size与for.last两个状态,只支持如下几个状态:for.index、for.count、for.first、for.odd、for.even、for.outer
#for 指令还支持 #continue、#break 指令,用法与java完全一致,在此不再赘述。
4、#switch 指令(3.6 版本新增指令)
#switch 指令对标 java 语言的 switch 语句。基本用法一致,但做了少许提升用户体验的改进,用法如下:
如上代码所示,#case 分支指令支持以逗号分隔的多个参数,这个功能就消解掉了 #break 指令的必要性,所以 enjoy 模板引擎是不需要 #break 指令的。
#case 指令参数还可以是任意表达式,例如:
上述代码中用逗号分隔的表达式先会被求值,然后再逐一与 #switch(value) 指令中的 value 进行比较,只要有一个值与其相等则该 case 分支会被执行。
#case 支持逗号分隔的多参数,从而无需引入 #break 指令,不仅减少了代码量,而且避免了忘写 #break 指令时带来的错误隐患。还有一个与 java 语法有区别的地方是 #case、#default 指令都未使用冒号字符。
5、#set 指令
set指令用于声明变量同时对其赋值,也可以是为已存在的变量进行赋值操作。set指令只接受赋值表达式,以及用逗号分隔的赋值表达式列表,如下是代码示例:
以上代码中,第一行代码最为简单为x赋值为123,第二行代码是一个赋值表达式列表,会从左到右依次执行赋值操作,如果等号右边出现表达式,将会对表达式求值以后再赋值。最后一行代码是输出上述赋值以后各变量的值,其她所有指令也可以像输出指令一样进行变量的访问。
请注意,#for、#include、#define这三个指令会开启新的变量名作用域,#set指令会首先在本作用域中查找变量是否存在,如果存在则对本作用域中的变量进行操作,否则继续向上层作用域查找,找到则操作,如果找不到,则将变量定义在顶层作用域中,这样设计非常有利于在模板中传递变量的值。
当需要明确指定在本层作用域赋值时,可以使用#setLocal指令,该指令所需参数与用法与#set指令完全一样,只不过作用域被指定为当前作用域。#setLocal 指令通常用于#define、#include指令之内,用于实现模块化,从而希望其中的变量名不会与上层作用域发生命名上的冲突。
重要:由于赋值表达式本质也是表达式,而其它指令本质上支持任意表达式,所以 #set 指令对于赋值来说并不是必须的,例如可以在 #() 输出指令中使用赋值表达式:
以上代码在输出指令中使用了多个赋值表达式,可以实现 #set 的功能,在最后通过一个 null 值来避免输出表达式输出任何东西。类似的,别的指令内部也可以这么来使用赋值表达式。
6、#include 指令
include指令用于将外部模板内容包含进来,被包含的内容会被解析成为当前模板中的一部分进行使用,如下是代码示例:
#include 指令第一个参数必须为 String 常量,当以 ”/” 打头时将以 baseTemplatePath 为相对路径去找文件,否则将以使用 #include 指令的当前模板的路径为相对路径去找文件。
baseTemplatePath 可以在 configEngine(Engine me) 中通过 me.setBaseTemplatePath(…) 进行配置。
此外,include指令支持传入无限数量的赋值表达式,十分有利于模块化,例如:如下名为 ”_hot_list.html” 的模板文件用于展示热门项目、热门新闻等等列表:
上图中的 title、list、url 是该html片段需要的变量,使用include指令分别渲染“热门项目”与“热门新闻”的用法如下:
上面两行代码中,为“_hot_list.html”中用到的三个变量title、list、url分别传入了不同的值,实现了对“_hot_list.html”的模块化重用。
7、#render 指令
render指令在使用上与include指令几乎一样,同样也支持无限量传入赋值表达式参数,主要有两点不同:
- render指令支持动态化模板参数,例如:#render(temp),这里的temp可以是任意表达式,而#include指令只能使用字符串常量:#include(“abc.html”)
- render指令中#define定义的模板函数只在其子模板中有效,在父模板中无效,这样设计非常有利于模块化
8、#define 指令
#define指令是模板引擎主要的扩展方式之一,define指令可以定义模板函数(Template Function)。通过define指令,可以将需要被重用的模板片段定义成一个一个的 template function,在调用的时候可以通过传入参数实现千变万化的功能。
在此给出使用define指令实现的layout功能,首先创建一个layout.html文件,其中的代码如下:
#define layout()
<html>
<head>
<title>JFinal俱乐部</title>
</head>
<body>
#@content()
</body>
</html>
#end
以上代码中通过#define layout()定义了一个名称为layout的模板函数,定义以#end结尾,其中的 #@content() 表示调用另一个名为 content 的模板函数。
特别注意:模板函数的调用比指令调用多一个@字符,是为了与指令调用区分开来。
接下来再创建一个模板文件,如下所示:
上图中的第一行代码表示将前面创建的模板文件layout.html包含进来,第二行代码表示调用layout.html中定义的layout模板函数,而这个模板函数中又调用了content这个模板函数,该content函数已被定义在当前文件中,简单将这个过程理解为函数定义与函数调用就可以了。注意,上例实现layout功能的模板函数、模板文件名称可以任意取,不必像velocity、freemarker需要记住 nested、layoutContent这样无聊的概念。
通常作为layout的模板文件会在很多模板中被使用,那么每次使用时都需要#include指令进行包含,本质上是一种代码冗余,可以在configEngine(Engine me)方法中,通过me.addSharedFunction("layout.html")方法,将该模板中定义的所有模板函数设置为共享的,那么就可以省掉#include(…),通过此方法可以将所有常用的模板函数全部定义成类似于共享库这样的集合,极大提高重用度、减少代码量、提升开发效率。
Enjoy Template Engine彻底消灭掉了layout、nested、macro这些无聊的概念,极大降低了学习成本,并且极大提升了扩展能力。模板引擎本质是一门程序语言,任何可用于生产环境的语言可以像呼吸空气一样自由地去实现 layout 这类功能。
此外,模板函数必然支持形参,用法与java规则基本相同,唯一不同的是不需要指定参数类型,只需要参数名称即可,如下是代码示例:
以上代码中的模板函数test,有a、b、c三个形参,在函数体内仅简单对这三个变量进行了输出,注意形参必须是合法的java标识符,形参的作用域为该模板函数之内符合绝大多数程序语言习惯,以下是调用该模板函数的例子代码:
以上代码中,第一个参数传入的整型123,第二个是字符串,第三个是一个 field 取值表达式,从例子可以看出,实参可以是任意表达式,在调用时模板引擎会对表达式求值,并逐一赋值给模板函数的形参。
注意:形参与实参数量要相同,如果实参偶尔有更多不确定的参数要传递进去,可以在调用模板函数代码之前使用#set指令将值传递进去,在模板函数内部可用空合安全取值调用表达式进行适当控制,具体用法参考 jfinal-club 项目中的 _paginate.html 中的 append 变量的用法。
define 还支持 return 指令,可以在模板函数中返回,但不支持返回值。
9、模板函数调用与 #call 指令
调用define定义的模板函数的格式为:#@name(p1, p2…, pn),模板函数调用比指令调用多一个@字符,多出的@字符用来与指令调用区别开来。
此外,模板函数还支持安全调用,格式为:#@name?(p1, p2…, pn),安全调用只需在模板函数名后面添加一个问号即可。安全调用是指当模板函数未定义时不做任何操作。
安全调用适合用于一些模板中可有可无的内容部分,以下是一个典型应用示例:
以上代码示例定义了一个web应用的layout模板,注意看其中的两处:#@css?() 与 #@js?() 就是模板函数安全调用。
上述模板中引入的 jfinal.css 与 jfinal.js 是两个必须的资源文件,对大部分模块已经满足需要,但对于有些模块,除了需要这两个必须的资源文件以外,还需要额外的资源文件,那么就可以通过#define css() 与 #define js() 来提供,如下是代码示例:
以上代码中先是通过#@layout()调用了前面定义过的layout()这个模板函数,而这个模板函数中又分别调用了#@main()、#@css?()、#@js?()这三个模板函数,其中后两个是安全调用,所以对于不需要额外的css、js文件的模板,则不需要定义这两个方法,安全调用在调用不存在的模板函数时会直接跳过。
#call 指令是 jfinal 3.6 版本新增指令,使用 #call 指令,模板函数的名称与参数都可以动态指定,提升模板函数调用的灵活性,用法如下:
上述代码中的 funcName 为函数名,p1、p2、pn 为被调用函数所使用的参数。如果希望模板函数不存在时忽略其调用,添加常量值 true 在第一个参数位置即可:
10、#date 指令
date指令用于格式化输出日期型数据,包括Date、Timestamp等一切继承自Date类的对象的输出,使用方式极其简单:
上面的第一行代码只有一个参数,那么会按照默认日期格式进行输出,默认日期格式为:“yyyy-MM-dd HH:mm”。上面第二行代码则会按第二个参数指定的格式进行输出。
如果希望改变默认输出格式,只需要通过engine.setDatePattern()进行配置即可。
keepPara 问题:如果日期型表单域提交到后端,而后端调用了 Controller 的 keepPara() 方法,会将这个日期型数据转成 String 类型,那么 #date(...) 指令在输出这个 keepPara 过来的 String 时就会抛出异常,对于这种情况可以指令 keep 住其类型:
如上所示,第二行代码用 Date.class 参数额外指定了 createAt 域 keep 成 Date 类型,那么在页面 #date(createAt) 指令就不会抛出异常了。keepModel(...)、keepBean(...) 会保持原有类型,无需做上述处理。
11、#number 指令
number 指令用于格式化输出数字型数据,包括 Double、Float、Integer、Long、BigDecimal 等一切继承自Number类的对象的输出,使用方式依然极其简单:
上面的 #number指令第一个参数为数字类型,第二个参数为String类型的pattern。Pattern参数的用法与JDK中DecimalFormat中pattern的用法完全一样。当不知道如何使用pattern时可以在搜索引擎中搜索关键字DecimalFormat,可以找到非常多的资料。
#number指令的两个参数可以是变量或者复杂表达式,上例参数中使用常量仅为了方便演示。
12、#escape 指令
escape 指令用于 html 安全转义输出,可以消除 XSS 攻击。escape 将类似于 html 形式的数据中的大于号、小于号这样的字符进行转义,例如将小于号转义成:< 将空格转义成
使用方式与输出指令类似:
13、指令扩展
由于采用独创的 DKFF 和 DLRD 算法,Enjoy Template Engine 可以极其便利地在语言层面对指令进行扩展,而代码量少到不可想象的地步,学习成本无限逼近于 0。以下是一个代码示例:
以上代码中,通过继承Directive并实现exec方法,三行代码即实现一个#now指令,可以向模板中输出当前日期,在使用前只需通过me.addDirective(“now”, NowDirective.class) 添加到模板引擎中即可。以下是在模板中使用该指令的例子:
除了支持上述无#end块,也即无指令body的指令外,Enjoy Template Engine还直接支持包含#end与body的指令,以下是示例:
如上所示,Demo继承Directive覆盖掉父类中的hasEnd方法,并返回true,表示该扩展指令具有#end结尾符。上例中public void exec 方法中的三行代码,其中stat.exec(…)表示执行指令body中的代码,而该方法前后的write(…)方法分别输出一个字符串,最终的输出结果详见后面的使用示例。此外通过覆盖父类的setExprList(…)方法可以对指令的参数进行控制,该方法并不是必须的。
通过me.addDirective(“demo”, Demo.class)添加到引擎以后,就可以像如下代码示例中使用:
最后的输出结果如下:
上例中的#demo指令body中包含一串字符,将被Demo.exec(…)方法中的stat.exec(…)所执行,而stat.exec(…)前后的write(…)两个方法调用产生的结果与body产生的结果生成了最终的结果。
重要:指令中声明的属性是全局共享的,所以要保障指令中的属性是线程安全的。如下代码以 com.jfinal.template.ext.directive.DateDirective 的代码片段为例:
以上代码中有三个属性,类型是 Expr、Expr、int,其中 Expr 是线程安全的,而 int paraNum 虽然表面上看不是线程安全的,但在整个 DateDirective 类中只有构造方法对该值初始化的时候有写入操作,其它所有地方都是读操作,所以该 int 属性在这里是线程安全的。
6.5 注释
JFinal Template Engine支持单行与多行注释,以下是代码示例:
### 这里是单行注释
#--
这里是多行注释的第一行
这里是多行注释的第二行
--#
如上所示,单行注释使用三个#字符,多行注释以#--打头,以--#结尾。
与传统模板引擎不同,这里的单行注释采用三个字符,主要是为了减少与文本内容相冲突的可能性,模板是极其自由化的内容,使用三个字符,冲突的概率降低一个数量级。
jfinal 4.4 之前的版本注意:注释在与指令放在同一行时,输出结果会删掉注释后方的换行字符,例如:
以上模板的输出结果是:"AAABBB",如果希望输出结果严格遵守模板中的换行,只需将注释单独放在一行,例如:
以上模板的输出结果将会带有严格的换行,结果如下:
多行注释与单行注释也类似,只需将其单独放即可。
除了以上情况以外,其它任何情况都是严格按模板换行输出的,不必关注。jfinal 4.4 版本解决了此问题,建议升级到 4.4 或更高版本
6.6 原样输出
原样输出是指不被解析,而仅仅当成纯文本的内容区块,如下所示:
#[[
#(value)
#for(x : list)
#(x.name)
#end
]]#
如上所示,原样输出以 #[[ 三个字符打头,以 ]]# 三个字符结尾,中间被包裹的内容虽然是指令,但仍然被当成是纯文本,这非常有利于解决与前端javascript模板引擎的指令冲突问题。
无论是单行注释、多行注释,还是原样输出,都是以三个字符开头,目的都是为了降低与纯文本内容冲突的概率。
** 注意:用于注释、原样输出的三个控制字符之间不能有空格**