FreeMarker自定义指令--代码实现

时间:2021-06-08 11:55:56

在进行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自定义指令--代码实现

做到这里,也许大家会有一个疑问,这三种方式是否可以同时使用,是否会存在冲突?

经过测试,发现这三种方式同时使用时,不会存在冲突。因此我做了一个猜想,可能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对象,出现以上异常。


希望以上的内容,会对你有所帮助。