### 高级功能 #### 配置GroupTemplate Beetl建议通过配置文件配置GroupTemplate,主要考虑到IDE插件未来可能会支持Beetl模板,模板的属性,和函数等如果能通过配置文件获取,将有助于IDE插件识别。 配置GroupTemplate有俩种方法 - 配置文件: 默认配置在/org/beetl/core/beetl-default.properties 里,Beetl首先加载此配置文件,然后再加载classpath里的beetl.properties,并用后者覆盖前者。配置文件通过Configuration类加载,因此加载完成后,也可以通过此类API来修改配置信息 - 通过调用GroupTemplate提供的方法来注册函数,格式化函数,标签函数等 配置文件分为三部分,第一部分是基本配置,在第一节讲到过。第二部分是资源类配置,可以在指定资源加载类,以及资源加载器的属性(这个配置在spring框架里,通过spring或者springboot的配置机制实现覆盖,并未起作用),如下 ```properties RESOURCE_LOADER=org.beetl.core.resource.ClasspathResourceLoader #资源配置,resource后的属性只限于特定ResourceLoader #classpath 根路径 RESOURCE.root= / #是否检测文件变化 RESOURCE.autoCheck= true ``` 第1行指定了模板加载器类,在beetl与其他框架集成的时候,**模板加载器不一定根据这个配置**,比如spring,它的RESOURCE_LOADER以spring的配置为准 第4行指定了模板根目录的路径,此处/ 表示位于classpath 根路径下,同loader一样,依赖使用的框架 第6行是否自动检测模板变化,默认为true,开发环境下自动检测模板是否更改。关于如何自定义ResouceLoader,请参考下一章 配置文件第三部分是扩展部分,如方法,格式化函数等 ```properties ##### 扩展 ############## ## 内置的方法 FN.date = org.beetl.ext.fn.DateFunction FN.nvl = org.beetl.ext.fn.NVLFunction ................. ##内置的功能包 FNP.strutil = org.beetl.ext.fn.StringUtil ##内置的格式化函数 FT.dateFormat = org.beetl.ext.format.DateFormat FT.numberFormat = org.beetl.ext.format.NumberFormat ................. ##内置的默认格式化函数 FTC.java.util.Date = org.beetl.ext.format.DateFormat FTC.java.sql.Date = org.beetl.ext.format.DateFormat ## 标签类 TAG.include= org.beetl.ext.tag.IncludeTag TAG.includeFileTemplate= org.beetl.ext.tag.IncludeTag TAG.layout= org.beetl.ext.tag.LayoutTag TAG.htmltag= org.beetl.ext.tag.HTMLTagSupportWrapper ``` FN前缀表示Function,FNP前缀表示FunctionPackage,FT表示format函数,FTC表示类的默认Format函数,TAG表示标签类。Beetl强烈建议通过配置文件加载扩展。以便随后IDE插件能识别这些注册函数 #### 自定义方法 ##### 实现Function ```java public class Print implements Function{ public String call(Object[] paras, Context ctx){ Object o = paras[0]; if (o != null){ try{ ctx.byteWriter.write(o.toString()); }catch (IOException e){ throw new RuntimeException(e); } } return ""; } } ``` call方法有俩个参数,第一个是数组,这是由模板传入的,对应着模板的参数,第二个是Context,包含了模板的上下文,主要提供了如下属性 - byteWriter 输出流 - template 模板本身 - gt GroupTemplate - globalVar 该模板对应的全局变量 - byteOutputMode 模板的输出模式,是字节还是字符 - safeOutput 模板当前是否处于安全输出模式 - 其他属性建议不熟悉的开发人员不要乱动 > 1. call方法要求返回一个Object,如果无返回,返回null即可 > 2. 为了便于类型判断,call方法最好返回一个具体的类,如date函数返回的就是java.util.Date > 3. call方法里的任何异常应该抛出成Runtime异常 ##### 使用普通的java类 尽管实现Function对于模板引擎来说,是效率最高的方式,但考虑到很多系统只有util类,这些类里的方法仍然可以注册为模板函数。其规则很简单,就是该类的所有public方法。如果还需要Context 变量,则需要在方法最后一个参数加上Context即可,如 ```java public class util{ public String print(Object a, Context ctx){ //balabala... } } ``` 注意 > 1. 从beetl效率角度来讲,采用普通类效率不如实现Function调用 > 2. 采用的普通java类尽量少同名方法。这样效率更低。beetl调用到第一个适合的同名方法。而不像java那样找到最匹配的 > 3. 方法名支持可变数组作为参数 > 4. 方法名最后一个参数如果是Context,则beetl会传入这个参数。 ##### 使用模板文件作为方法 可以不用写java代码,模板文件也能作为一个方法。默认情况下,需要将模板文件放到Root的functions目录下,且扩展名为.html(可以配置文件属性来修改这俩个默认值) 方法参数分别是para0,para1….. 如下root/functions/page.fn ```javascript <% //para0,para1 由函数调用传入 var current = para0,total = para1,style=para2!'simple' %> 当前页面 ${current},总共${total} ``` 则在模板中 ```javascript <% page(current,total); %> ``` 允许使用return 表达式返回一个变量给调用者,如模板文件functions\now.html ```javascript <% return date(); %> ``` 在任何模板里都可以调用: ```javascript hello time is ${now(),'yyyy-MM-dd'} ``` 也可以在functions建立子目录,这样function则具有namespace,其值就是文件夹名 #### 自定义格式化函数 需要实现Format接口 ```java public class DateFormat implements Format{ public Object format(Object data, String pattern){ if (data == null) return null; if (Date.class.isAssignableFrom(data.getClass())){ SimpleDateFormat sdf = null; if (pattern == null){ sdf = new SimpleDateFormat(); }else{ sdf = new SimpleDateFormat(pattern); } return sdf.format((Date) data); }else{ throw new RuntimeException("Arg Error:Type should be Date"); } } } ``` data 参数表示需要格式化的对象,pattern表示格式化模式,开发时候需要考虑pattern为null的情况 也可以实现ContextFormat 类抽象方法,从而得到Context,获取外的格式化信息。 ```java public abstract Object format(Object data,String pattern,Context ctx); ``` #### 自定义标签 标签形式有俩种,一种是标签函数,第二种是html tag。第二种实际上在语法解析的时候会转化成第一种,其实现是HTMLTagSupportWrapper,此类将会寻找root/htmltag目录下同名的标签文件作为模板来执行。类似普通模板一样,在此就不详细说了 ##### 标签函数 标签函数类似jsp2.0的实现方式,需要实现Tag类的render方法即可 ```java public class DeleteTag extends Tag{ @Override public void render(){ // do nothing,just ignore body ctx.byteWriter.write("被删除了,付费可以看") } } ``` 如上一个最简单的Tag,将忽略tag体,并输出内容 ```java public class XianDeDantengTag extends Tag{ @Override public void render(){ doBodyRender(); } } ``` 此类将调用父类方法doBodyRender,渲染tag body体 ```java public class CompressTag extends Tag{ @Override public void render(){ BodyContent content = getBodyContent(); String content = content.getBody(); String zip = compress(cotnent); ctx.byteWriter.write(zip); } } ``` 此类将调用父类方法getBodyContent ,获得tag body后压缩输出 tag类提供了如下属性和方法供使用 - args 传入标签的参数 - gt GroupTemplate - ctx Context - bw 当前的输出流 - bs 标签体对应的语法树,不熟悉勿动 #### 自定义虚拟属性 可以为特定类注册一个虚拟属性,也可以为一些类注册虚拟属性 - public void registerVirtualAttributeClass(Class cls, VirtualClassAttribute virtual) 实现VirtualClassAttribute方法可以为特定类注册一个需要属性,如下代码: ```java gt.registerVirtualAttributeClass(User.class, new VirtualClassAttribute() { @Override public String eval(Object o, String attributeName, Context ctx){ User user = (User) o; if(attributeName.equals("ageDescritpion")){ if (user.getAge() < 10){ return "young"; }else{ return "old"; } } } }); ``` User类的所有虚拟属性将执行eval方法,此方法根据年纪属性来输出对应的描述。 - public void registerVirtualAttributeEval(VirtualAttributeEval e) 为一些类注册需要属性,VirtualAttributeEval.isSupport方法将判断是否应用虚拟属性到此类 如下是虚拟属性类的定义 ```java public interface VirtualClassAttribute{ public Object eval(Object o, String attributeName, Context ctx); } public interface VirtualAttributeEval extends VirtualClassAttribute{ public boolean isSupport(Class c, String attributeName); } ``` #### 使用额外的资源加载器 某些情况下,模板来源不止一处,GroupTemplate配置了一个默认的资源加载器,如果通过gt.getTemplate(key),将调用默认的ResourceLoader,获取模板内容,然后转化为beetl脚本放入到缓存里。你也可以传入额外的资源管理器加载模板,通过调用gt.getTemplate(key,otherLoader)来完成; ```java GroupTemplate gt = new GroupTemplate(conf,fileLoader) //自定义,参考下一节 MapResourceLoader dbLoader = new MapResourceLoader(getData()); Template t = gt.getTemplate("db:1", dbLoader); private Map getData(){ Map data = new HashMap(); data.put("db:1", "${a}"); return data; } ``` 对于更复杂的模板资源来源,也可以自定义一个资源加载来完成,参考下一节 #### 自定义资源加载器 如果模板资源来自其他地方,如数据库,或者混合了数据库和物理文件,或者模板是加密的,则需要自定义一个资源加载器。资源加载器需要实现ResourceLoader类。如下: ```java public interface ResourceLoader{ /** * 根据key获取Resource * * @param key * @return */ public Resource getResource(String key); /** 检测模板是否更改,每次渲染模板前,都需要调用此方法,所以此方法不能占用太多时间,否则会影响渲染功能 * @param key * @return */ public boolean isModified(Resource key); /** * 关闭ResouceLoader,通常是GroupTemplate关闭的时候也关闭对应的ResourceLoader */ public void close(); /** 一些初始化方法 * @param gt */ public void init(GroupTemplate gt); /** 用于include,layout等根据相对路径计算资源实际的位置. * @param resource 当前资源 * @param key * @return */ public String getResourceId(Resource resource, String key); } ``` 如下是一个简单的内存ResourceLoader ```java public class MapResourceLoader implements ResourceLoader{ Map data; public MapResourceLoader(Map data){ this.data = data; } @Override public Resource getResource(String key){ String content = (String) data.get(key); if (content == null) return null; return new StringTemplateResource(content, this); } @Override public boolean isModified(Resource key){ return false; } @Override public boolean exist(String key){ return data.contain(key); } @Override public void close(){ } @Override public void init(GroupTemplate gt){ } @Override public String getResourceId(Resource resource, String id){ //不需要计算相对路径 return id; } } ``` init方法可以初始化GroupTemplate,比如读取配置文件的root属性,autoCheck属性,字符集属性,以及加载functions目录下的所有模板方法 如FileResourceLoader 的 init方法 ```java @Override public void init(GroupTemplate gt){ Map<String, String> resourceMap = gt.getConf().getResourceMap(); if (this.root == null){ this.root = resourceMap.get("root"); } if (this.charset == null){ this.charset = resourceMap.get("charset"); } if (this.functionSuffix == null){ this.functionSuffix = resourceMap.get("functionSuffix"); } this.autoCheck = Boolean.parseBoolean(resourceMap.get("autoCheck")); File root = new File(this.root, this.functionRoot); this.gt = gt; if (root.exists()){ readFuntionFile(root, "", "/".concat(functionRoot).concat("/")); } } ``` readFuntionFile 方法将读取functions下的所有模板,并注册为方法 ```java protected void readFuntionFile(File funtionRoot, String ns, String path){ String expected = ".".concat(this.functionSuffix); File[] files = funtionRoot.listFiles(); for (File f : files){ if (f.isDirectory()){ //读取子目录 readFuntionFile(f, f.getName().concat("."), path.concat(f.getName()).concat("/")); } else if (f.getName().endsWith(functionSuffix)){ String resourceId = path + f.getName(); String fileName = f.getName(); fileName = fileName.substring(0, (fileName.length() - functionSuffix.length() - 1)); String functionName = ns.concat(fileName); FileFunctionWrapper fun = new FileFunctionWrapper(resourceId); gt.registerFunction(functionName, fun); } } } ``` Resource类需要实现OpenReader方法,以及isModified方法。对于模板内容存储在数据库中,openReader返回一个Clob,isModified 则需要根据改模板内容对应的lastUpdate(通常数据库应该这么设计)来判断模板是否更改 ```java public abstract class Resource{ /** * 打开一个新的Reader * * @return */ public abstract Reader openReader(); /** * 检测资源是否改变 * * @return */ public abstract boolean isModified(); ``` 参考例子可以参考beetl自带的ResourceLoader #### 使用CompositeResourceLoader 组合加载器,可以包含多个已有的ResourceLoader,如下代码将创建一个包含俩个文件和内存的ResourceLoader ```java FileResourceLoader fileLoader1 = new FileResourceLoader(path1); FileResourceLoader fileLoader2 = new FileResourceLoader(path2); Map data = getData(); // 根据id加载 MapResourceLoader mapLoader = new MapResourceLoader(data); CompositeResourceLoader loader = new CompositeResourceLoader(); loader.addResourceLoader(new StartsWithMatcher("http:").withoutPrefix(), fileLoader2); loader.addResourceLoader(new StartsWithMatcher("db:"), mapLoader); loader.addResourceLoader(new AllowAllMatcher(), fileLoader1); GroupTemplate gt = new GroupTemplate(loader, conf); Template t = gt.getTemplate("/xxx.html"); ``` 如上例子,groupTemplate从CompositeResourceLoader里加载/xxx.html,由于http:和db:前缀都不匹配,因此,将实际采用fileLoader1加载path1+/xxx.html,如下是xxx.html文件内容 ```javascript <% include("/xxx2.html"){} include("http:/xxx.html"){} %> ``` 第2行仍然是由fileLoader1加载,但第3行以http:前缀开头,因此将fileLoader2加载path2+/xxx.html.xxx.html内容如下 ```javascript <% include("db:1"){} %> ``` 因为以db:开头,因此会采用MapResourceLoader加载,内容是key为db:1对模板 #### 自定义错误处理器 错误处理器需要实现ErrorHandler接口的processExcption(BeetlException beeExceptionos, Writer writer); - beeExceptionos,模板各种异常 - writer 模板使用的输出流。系统自带的并未采用此Writer,而是直接输出到控制台 自定义错误处理可能是有多个原因,比如 1. 想将错误输出到页面而不是控制台 2. 错误输出美化一下,而不是自带的格式 3. 错误输出的内容做调整,如不输出错误行的模板内容,而仅仅是错误提示 4. 错误输出到日志系统里 5. 不仅仅输出日志,还抛出异常。默认自带的不会抛出异常,ReThrowConsoleErrorHandler 继承了ConsoleErrorHandler方法,打印异常后抛出 ```java public class ReThrowConsoleErrorHandler extends ConsoleErrorHandler{ @Override public void processExcption(BeetlException ex, Writer writer){ super.processExcption(ex, writer); throw ex; } } ``` beetl 提供 ErrorInfo类来wrap BeetlException,转化为较为详细的提示信息,他具有如下信息 - type 一个简单的中文描述 - errorCode 内部使用的错误类型标识 - errorTokenText 错误发生的节点文本 - errorTokenLine 错误行 - msg 错误消息,有可能没有,因为有时候errorCode描述的已经很清楚了 - cause 错误的root 异常,也可能没有。 BeetlException 也包含了一个关键信息就是 resourceId,即出错所在的模板文件 #### 自定义安全管理器 所有模板的本地调用都需要通过安全管理器校验,默认需要实现NativeSecurityManager 的public boolean permit(String resourceId, Class c, Object target, String method) 方法 如下是默认管理器的实现方法 ```java public class DefaultNativeSecurityManager implements NativeSecurityManager{ @Override public boolean permit(String resourceId, Class c, Object target, String method){ if (c.isArray()){ //允许调用,但实际上会在在其后调用中报错。不归此处管理 return true; } String name = c.getSimpleName(); String pkg = c.getPackage().getName(); if (pkg.startsWith("java.lang")){ if (name.equals("Runtime") || name.equals("Process") || name.equals("ProcessBuilder") || name.equals("System")){ return false; } } return true; } } ``` #### 注册全局共享变量 `groupTemplate.setSharedVars(Map<String, Object> sharedVars)` #### 布局 布局可以通过Beetl提供的include,layout 以及模板变量来完成。模板变量能完成复杂的布局 - 采用layout include ```javascript <% //content.html内容如下: layout("/inc/layout.html"){ %> this is 正文 .......... <% } %> ``` 如上一个子页面将使用layout布局页面,layout 页面内容如下 ```javascript <% include("/inc/header.html"){} %> this is content:${layoutContent} this is footer: ``` layoutContent 是默认变量,也可以改成其他名字,具体请参考layout标签函数 全局变量总是能被布局用的页面所使用,如果布局页面需要临时变量,则需要显示的传入,如: ```javascript <% var user= model.user; include("/inc/header.html",{title:'这是一个测试页面',user:user}){} %> ``` 这样,title和user成为全局变量,能被header.html 及其子页面引用到 - 继承布局:采用模板变量和include ```javascript <% var jsPart = { %> web页面js部分 <% }; %> <% var htmlPart = { %> web页面html部分 <% }; include("/inc/layout.html",{jsSection:jsPart,htmlSection:htmlPart}){} %> ``` layout.html页面如下: ```html <body> <head> ${jsSection} </head> <body> ....... ${htmlSection} </body> ``` > include, includeUrl,includeJSP,还有includeFragement都是Beetl提供的include系列标签函数,includeUrl,includeJSP考虑到需要WEB环境,并没有内置,需要手工注册,参考IncludeJSPTag.java,IncludeUrlTag.java #### 性能优化 Beetl性能已经很快了,有些策略能更好提高性能 - 使用二进制输出,此策略可以使模板在语法分析的时候将静态文本转化为二进制,省去了运行时刻编码时间,这是主要性能提高方式。但需要注意,此时需要提供一个二进制输出流,而不是字符流,否则性能反而下降 - 使用FastRuntimeEngine,默认配置。 此引擎能对语法树做很多优化,从而提高运行性能,如生成字节码来访问属性而不是传统的反射访问。关于引擎,可能在新的版本推出更好的引擎,请随时关注。 - 自定义ResourceLoader的isModified必须尽快返回,因此每次渲染模板的时候都会调用此方法 #### 定制输出 占位符输出允许定制。如所有日期类型都按照某个格式化输出,而不需显式的使用格式化输出,或者为了防止跨脚本站点攻击,需要对类型为String的值做检查等,不必使用格式化函数,可以直接对占位符输出进行定制,代码如下 ```java PlaceholderST.output = new PlaceholderST.Output(){ @Override public void write(Context ctx, Object value) throws IOException { //定制输出 ctx.byteWriter.writeString("ok"+value!=null?value.toString:""); } }; ``` 如果PlaceholderST静态变量output 不为null,将使用output 来输出 #### 定制模板引擎 Beetl在线体验([http://ibeetl.com/beetlonline/](http://ibeetl.com/beetlonline/))面临一个挑战,允许用户输入任何脚本做练习或者分享代码。但又需要防止用户输入恶意的代码,如 ```javascript <% while(true){ //其他代码 } %> ``` 此时,需要定制模板引擎,遇到while循环的时候,应该限制循环次数,譬如,在线体验限制最多循环5次,这是通过定义扩展模板引擎来实现的,以jar包自带的实例代码OnlineTemplateEngine来说明 ```java public class OnlineTemplateEngine extends DefaultTemplateEngine { public static int MAX = 5; public static String ERROR = "错误:在线引擎不允许循环次数超过 " + MAX; @Override protected GrammarCreator getGrammerCreator(GroupTemplate gt) { GrammarCreator grammar = new OnlineGrammarCreator(); return grammar; } } ``` 扩展DefaultTemplateEngine,重载getGrammerCreator方法,返回一个我们自己的语法定义类OnlineGrammarCreator OnlineGrammarCreator继承了默认的 ```java static class OnlineGrammarCreator extends GrammarCreator { @Override public WhileStatement createWhile(Expression exp, Statement whileBody, GrammarToken token) { WhileStatement whileStat = new RestrictWhileStatement(exp, whileBody, token); return whileStat; } } ``` 需要对for循环重新定义,使用新的 RestrictWhileStatement,定义如下 ~~~java static class RestrictWhileStatement extends WhileStatement { public RestrictWhileStatement(Expression exp, Statement whileBody, GrammarToken token) { super(exp, whileBody, token); } @Override public void execute(Context ctx) { int i = 0; while (i < OnlineTemplateEngine.MAX) { Object result = exp.evaluate(ctx); if (result instanceof Boolean) { if ((Boolean) result) { whileBody.execute(ctx); } else { break; } } else { BeetlException be = new BeetlException(BeetlException.BOOLEAN_EXPECTED_ERROR); be.token = exp.token; throw be; } i++; } if (i >= OnlineTemplateEngine.MAX) { try { ctx.byteWriter.writeString(ERROR); } catch (IOException e) { // ignore } } } } ~~~ 完成这些代码后,在配置文件中申明使用新的引擎 ```properties ENGINE=org.beetl.core.engine.OnlineTemplateEngine ``` 这样就完成了模板引擎定制。 > BeetlSQL 正是支持定制,所以才把## 占位符输出"?",而不是字面值,${} 负责输出字面值,可以查看BeetlSQL的org.beetl.sql.core.engine.SQLTemplateEngine #### 直接运行Beetl脚本 Beetl模板本质上会转化为Beetl脚本来执行,这点跟jsp转为servlet来执行类似。GroupTemplate提供方法可以直接执行Beetl脚本 - public Map runScript(String key, Map<String, Object> paras) throws ScriptEvalError - public Map runScript(String key, Map<String, Object> paras, Writer w) throws ScriptEvalError - public Map runScript(String key, Map<String, Object> paras, Writer w, ResourceLoader loader) throws ScriptEvalError key为资源名,paras为脚本的全局变量,w可选参数,如果执行脚本有输出,则输出到w里,loader参数可选,如果指定,则使用此laoder加载脚本 执行脚本完毕后,返回到Map里的值可能包含如下: - 模板的**顶级**的临时变量,key为临时变量名 - return 值将返回到map里 ,key为return 如下脚本(此时就不需要脚本定界符了) ```javascript var a = 1; var b = date(); var c = '2'; return a+1; ``` 调用runScript后,map里将返回key分别为a,b,c,return。 值分别为1,当前日期,字符串'2,以及3。 groupTemplate.validateScript 可以用于校验模板是否正确,如果不正确,返回BeetlException ~~~java BeetlException ex = groupTemplate.validateScript(script); if(ex!=null){ ErrorInfo info = new ErrorInfo(ex); System.out.println(info.toString()); } ~~~ #### 模板校验 GroupTemplate 提供了validateTemplate和 validateScript方法用来校验模板,如果模板或者脚本有语法错误,则返回BeetlException,BeetlException包含了错误的具体信息,可以参考ConsoleErrorHandler来了解如何处理异常,如下是一个简单的处理片段 ~~~java BeetlException ex = groupTemplate.validateTemplate("/index.html"); if(ex==null){ return } ErrorInfo error = new ErrorInfo(ex); int line = error.getErrorTokenLine(); String errorToken = error.getErrorTokenText(); String type = error.getType(); ~~~