在进行FreeMarker开发时,应该都会使用到FreeMarker的指令,但是FreeMarker为我们提供指令是很有限的,因此需要我们自定义指令,实现我们需要的功能。
在我的学习过程中,遇到了一下问题(坑),记录下来,以供大家参考:
要开发指令,需要我们实现TemplateDirectiveModel接口,该接口中,需要实现execute方法。
今天给出的实例,是根据官方文档的例子而来:代码如下:
</pre></p><p><span style="color:#CC0000;"><span style="color:#000000;"></span></span><pre name="code" class="java">package com.freemarker.learn.directive.define; import java.io.IOException; import java.io.Writer; import java.util.Map; import freemarker.core.Environment; import freemarker.template.TemplateDirectiveBody; import freemarker.template.TemplateDirectiveModel; import freemarker.template.TemplateException; import freemarker.template.TemplateModel; import freemarker.template.TemplateModelException; /** * <pre> * 自定义指令: * 1. 需要实现TemplateDirectiveModel接口 * 2. 实现execute的方法 * * 该指令作用: 将该标签内的内容,转换为大写 * </pre> * @author xianglj */ public class UpperDirective implements TemplateDirectiveModel{ @SuppressWarnings("rawtypes") public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException { //检查参数是否传入 if(!params.isEmpty()) { throw new TemplateModelException("该指令不支持参数传入"); } //判断是否有循环变量 if(null != loopVars && loopVars.length != 0) { throw new TemplateModelException("该指令不支持循环变量"); } //判断是否有非空的嵌入内容 if(null != body) { // 执行嵌入体部分,和 FTL 中的<#nested>一样,除了 // 我们使用我们自己的 writer 来代替当前的 output writer. body.render(new UpperCaseFilterWriter(env.getOut())); } } /** * 输出流 * @author xianglj */ private static class UpperCaseFilterWriter extends Writer{ private final Writer out; UpperCaseFilterWriter(Writer out) { this.out = out; } @Override public void write(char[] cbuf, int off, int len) throws IOException { char[] transferChars = new char[len]; for(int i = 0; i < len; i++) { transferChars[i] = Character.toUpperCase(cbuf[i + off]); } out.write(transferChars); } @Override public void flush() throws IOException { if(null != out) { out.flush(); } } @Override public void close() throws IOException { if(out != null) { out.close(); } } } }
该指令实现,在指令中的内容,都会被转成大写后,输出。
body的渲染部分,我们自定义一个writer,在写出字符时,先转换为大写,再写出。
最关键的部分,页面如何才能使用自定义的指令呢?有三种方式参考实现:
贴出我的模版转换工具类:
package com.freemarker.learn.util; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletContext; import com.alibaba.fastjson.JSONObject; import com.freemarker.learn.directive.define.UpperDirective; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.Version; /** * Created by mobao-xi on 16/4/12. */ public class TemplateTool { private static Map<String, Template> TEMPLATE_MAP = new HashMap<String, Template>(); private static Configuration cfg = new Configuration(new Version("2.3.23")); static { // Don't log exceptions inside FreeMarker that it will thrown at you anyway: cfg.setLogTemplateExceptions(false); cfg.setSharedVariable("upper", new UpperDirective()); } public static void settingConfig(ServletContext cxt) { cfg.setServletContextForTemplateLoading(cxt, "/"); } public static Template getTemplate(String path) throws IOException { Template template = TEMPLATE_MAP.get(path); if (null == template) { /*template = inputStream2String(new FileInputStream(path.toString())); TEMPLATE_MAP.put(path, template);*/ template = cfg.getTemplate(path); TEMPLATE_MAP.put(path, template); } return template; } public static String getTemplate(String path, Object data) throws Exception { Template template = getTemplate(path); //StringTemplateLoader loader = new StringTemplateLoader(); //loader.putTemplate("", source); //cfg.setTemplateLoader(loader); cfg.setDefaultEncoding("UTF-8"); //Template template = cfg.getTemplate(""); StringWriter writer = new StringWriter(); template.process(data, writer); String source = writer.toString(); return source; } /** * 将stream 转成字符串 * * @param is * @return * @throws IOException */ private static String inputStream2String(InputStream is) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); int i = -1; while ((i = is.read()) != -1) { baos.write(i); } return baos.toString(); } }
1. 将数据放在数据模型中。
以下为我的模型的封装:
package com.freemarker.learn.response; import java.util.HashMap; import java.util.Map; import com.freemarker.learn.annotation.Abbr; import com.freemarker.learn.directive.define.UpperDirective; /** * 自定义指令返回实例。 * @author xianglj */ @Abbr(name="directive/upper") public class UpperDirectiveRespEntity extends ResponseEntity{ private Map<String, Object> dataMap ; @Override public Map<String, Object> getData() { return dataMap; } @Override public String getFtlPath() { return "/pages/templates/upper_directive.html"; } @Override public void generateData() { dataMap = new HashMap<String, Object>(); dataMap.put("upper", new UpperDirective()); } }
通过以上代码,我们发现,我返回的数据模型是一个HashMap对象,在对象中我存储了一个 TemplateDirectiveModel 的子类,也就是我们定义的指令对象。而我对应的模版路径是在 " /pages/templates/upper_directive.html ",在该页面中,我就能够直接通过 <@upper></@upper>自定义指令方式进行使用。
2、 第二种方式,将自定义的指令,存放在Configuration公共变量中。也许第一次可能不会明白,其实就是在Configuration完成之后,调用
setSharedVariable(String name, Object obj)方法进行设置即可。
具体代码如下:具体参考上面(TemplateTool.java 的实现代码)
cfg.setSharedVariable("upper", new UpperDirective());
3. 在模版界面中进行创建,使用<#assign ""?new()>
<#assign upper="com.freemarker.learn.directive.define.UpperDirective"?new()>
upper_directive.html代码如下:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>自定义指令upper的使用</title> </head> <body> upper 外<br/> <@upper> upper 内<br> <#list ["red", "blue", "green"] as c> ${c}<br> </#list> </@upper> <#assign upper="com.freemarker.learn.directive.define.UpperDirective"?new()> </body> </html>
给出输出结果:
做到这里,也许大家会有一个疑问,这三种方式是否可以同时使用,是否会存在冲突?
经过测试,发现这三种方式同时使用时,不会存在冲突。因此我做了一个猜想,可能FreeMarker在查找自定义顺序为:
模版页面 -> 数据模型(data-model) -> Configuration 公共变量
具体我也没有查证过是否正确。如果有知道的高人,请指出一下。
碰到过的坑:
在学习过程中,难免会遇到各种各样的坑,最大的坑就是进行模版和数据结合时,始终不能找到自定义指令,异常信息为:
freemarker.core.NonUserDefinedDirectiveLikeException: [... Exception message was already printed; see it above ...] at freemarker.core.UnifiedCall.accept(UnifiedCall.java:113) at freemarker.core.Environment.visit(Environment.java:324) at freemarker.core.MixedContent.accept(MixedContent.java:54) at freemarker.core.Environment.visit(Environment.java:324) at freemarker.core.Environment.process(Environment.java:302) at freemarker.template.Template.process(Template.java:325) at com.freemarker.learn.util.TemplateTool.getTemplate(TemplateTool.java:59) at com.freemarker.learn.servlet.base.BaseServlet.doGet(BaseServlet.java:45) at javax.servlet.http.HttpServlet.service(HttpServlet.java:622) at javax.servlet.http.HttpServlet.service(HttpServlet.java:729) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79) at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:616) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:522) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1095) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:672) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1500) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1456) at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Unknown Source)始终无法找到自定义指令,最根本的原因在于:
public static String getTemplate(String path, Object data) throws Exception { Template template = getTemplate(path); //StringTemplateLoader loader = new StringTemplateLoader(); //loader.putTemplate("", source); //cfg.setTemplateLoader(loader); cfg.setDefaultEncoding("UTF-8"); //Template template = cfg.getTemplate(""); StringWriter writer = new StringWriter(); template.process(JSONObject.toJSON(data), writer); String source = writer.toString(); return source; }我在进行模版和数据模型进行合并时,将数据模型解析为JSON在进行合并,这样就会存在一个很严重的问题,FreeMarker不能将指令对象正确解析,因此在找到upper时,发现upper不是TemplateDirectiveModel对象,出现以上异常。
希望以上的内容,会对你有所帮助。