OneBlog开源博客-详细介绍如何实现freemarker自定义标签

前言

OneBlog中使用到了springboot + freemarker的技术,同时项目里多个controller中都需要查询一个公有的数据集合,一般做法是直接在每个controller的方法中通过 model.addAttribute("xx",xx);的方式手动设置,但这样就有个明显的问题:重复代码。同一个实现需要在不同的controller方法中设置,除了重复代码外,还会给后期维护造成不必要的麻烦。在以往的jsp项目中,可以通过taglib实现自定义标签,那么,在freemarker中是否也可以实现这种功能呢?今天就尝试一下在freemarker中如何使用自定义标签。

TemplateDirectiveModel

在freemarker中实现自定义的标签,主要就是靠 TemplateDirectiveModel类。如字面意思:模板指令模型,主要就是用来扩展自定义的指令(和freemarker的宏类似,自定义标签也属于这个范畴)

1 public interface TemplateDirectiveModel extends TemplateModel {
2     void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException;
3 }

TemplateDirectiveModel是一个接口,类中只有一个execute方法供使用者实现,而我们要做的就是通过实现execute方法,实现自定义标签的功能。当页面模板中使用自定义标签时,会自动调用该方法。

先来看一下execute方法的参数含义

env : 表示模板处理期间的运行时环境。该对象会存储模板创建的临时变量集、模板设置的值、对数据模型根的引用等等,通常用它来输出相关内容,如Writer out = env.getOut()。
params : 传递给自定义标签的参数(如果有的话)。其中map的key是自定义标签的参数名,value值是TemplateModel实例【1】。
loopVars : 循环替代变量 (未发现有什么用,希望知道的朋友能指教一二)
body : 表示自定义标签中嵌套的内容。说简单点就是自定义标签内的内容体。如果指令调用没有嵌套内容(例如,就像<@myDirective />或者<@myDirective>),那么这个参数就会为空。

【1】:TemplateModel是一个接口类型,代表FreeMarker模板语言(FTL)数据类型的接口的公共超接口,即所有的数据类型都会被freemarker转成对应的TemplateModel。通常我们都使用TemplateScalarModel接口来替代它获取一个String 值,如TemplateScalarModel.getAsString();当然还有其它常用的替代接口,如TemplateNumberModel获取number等

类型FreeMarker接口FreeMarker实现
字符串 TemplateScalarModel SimpleScalar
数值 TemplateNumberModel SimpleNumber
日期 TemplateDateModel SimpleDate
布尔 TemplateBooleanModel TemplateBooleanModel.TRUE
哈希 TemplateHashModel SimpleHash
序列 TemplateSequenceModel SimpleSequence
集合 TemplateCollectionModel SimpleCollection
节点 TemplateNodeModel NodeModel

实现自定义标签

前面了解了 TemplateDirectiveModel的基本含义和用法,那么,接下来我们就以OneBlog中的例子来简单解释下如何实现自定义标签。

ps:为了方便阅读,本例只摘出了一部分关键代码,详细内容,请参考我的开源博客:https://gitee.com/yadong.zhang/DBlog

一、创建类实现TemplateDirectiveModel接口

 1 @Component
 2 public class CustomTagDirective implements TemplateDirectiveModel {
 3     private static final String METHOD_KEY = "method";
 4     @Autowired
 5     private BizTagsService bizTagsService;
 6 
 7     @Override
 8     public void execute(Environment environment, Map map, TemplateModel[] templateModels, TemplateDirectiveBody templateDirectiveBody) throws TemplateException, IOException {
 9         if (map.containsKey(METHOD_KEY)) {
10             DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_25);
11             String method = map.get(METHOD_KEY).toString();
12             switch (method) {
13                 case "tagsList":
14                     // 将数据对象转换成对应的TemplateModel
15                     TemplateModel tm = builder.build().wrap(bizTagsService.listAll())
16                     environment.setVariable("tagsList", tm);
17                     break;
18                 case other...
19                 default:
20                     break;
21             }
22         }
23         templateDirectiveBody.render(environment.getOut());
24     }
25 }

二、创建freemarker的配置类

 1 @Configuration
 2 public class FreeMarkerConfig {
 3 
 4     @Autowired
 5     protected freemarker.template.Configuration configuration;
 6     @Autowired
 7     protected CustomTags customTags;
 8 
 9     /**
10      * 添加自定义标签
11      */
12     @PostConstruct
13     public void setSharedVariable() {
14         /*
15          * 向freemarker配置中添加共享变量;
16          * 它使用Configurable.getObjectWrapper()来包装值,因此在此之前设置对象包装器是很重要的。(即上一步的builder.build().wrap操作)
17          * 这种方法不是线程安全的;使用它的限制与那些修改设置值的限制相同。    
18          * 如果使用这种配置从多个线程运行模板,那么附加的值应该是线程安全的。
19          */
20         configuration.setSharedVariable("zhydTag", customTags);
21     }
22 }

三、ftl模板中使用自定义标签

 1 <div class="sidebar-module">
 2     <h5 class="sidebar-title"><i class="fa fa-tags icon"></i><strong>文章标签</strong></h5>
 3     <ul class="list-unstyled list-inline">
 4         <@zhydTag method="tagsList" pageSize="10">
 5             <#if tagsList?exists && (tagsList?size > 0)>
 6                 <#list tagsList as item>
 7                     <li class="tag-li">
 8                         <a class="btn btn-default btn-xs" href="${config.siteUrl}/tag/${item.id?c}" title="${item.name?if_exists}">
 9                             ${item.name?if_exists}
10                         </a>
11                     </li>
12                 </#list>
13             </#if>
14         </@zhydTag>
15     </ul>
16 </div>

自定义标签的使用方法跟自定义宏(macro)用法一样,直接使用`<@标签名>${值}</@标签名>`即可。

注:ftl中通过@调用自定义标签时,后面可以跟任意参数,所有的参数都可以在execute方法的第二个参数(map)中获取,由此可以根据一个特定的属性开发一套特定的自定义标签,比如OneBlog中通过method参数判断调用不同的处理方式。

四、扩展FreeMarkerConfig

上面提到的自定义标签,都是通过 <@tagName>xxx</@tagName>方式调用的,那么针对我们系统中一些类环境变量的数据(全局的配置类属性等)如何像使用普通的el表达式一般直接通过${xx}获取呢? 看代码:

 1 @Configuration
 2 public class FreeMarkerConfig {
 3 
 4     @Autowired
 5     protected freemarker.template.Configuration configuration;
 6     @Autowired
 7     private SysConfigService configService;
 8 
 9     /**
10      * 添加自定义标签
11      */
12     @PostConstruct
13     public void setSharedVariable() {
14         try {
15             configuration.setSharedVariable("config", configService.get());
16         } catch (TemplateModelException e) {
17             e.printStackTrace();
18         }
19     }
20 }

如此而已,在使用的时候我们可以直接在页面上通过${config.siteName}调用config的参数即可。

五、可能遇到的问题

针对上面两种标签( 类宏模式el表达式模式),会有一个问题存在,如下图

在程序启动时会初始化FreemarkerConfig类(@PostConstruct),并且当且仅当程序启动时才会初始化一次。像 zhydTag这种自定义标签,因为是将整个自定义标签类(CustomTag)保存到了共享变量中,那么在使用自定义标签时,实际还是调用的相关接口获取数据库,当数据库发生变化时,也会同步更新到标签中;而像 config这种类el表达式的环境变量(如图,value的类型是一个StringModel),只会在程序初始化时加载一次,在后续调用标签时也只是调用的 SharedVariable中的config副本内容,并不会再次访问接口去数据库中获取数据。这样就造成了一个问题:当config表中的数据发生变化时,在前台通过${config.siteName}获取到的仍然是旧的数据

六、解决问题

在OneBlog中,我是通过实现一个简单的AOP,去监控、对比config表的内容,当config表发生变化时,将新的config副本保存到freeamrker的 SharedVariable中。如下实现

 1 /**
 2  * 用于监控freemarker自定义标签中共享变量是否发生变化,发生变化时实时更新到内存中
 3  *
 4  * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 5  * @version 2.0
 6  * @date 2018/5/17 17:06
 7  */
 8 @Slf4j
 9 @Component
10 @Aspect
11 @Order(1)
12 public class FreemarkerSharedVariableMonitorAspects {
13 
14     private static volatile long configLastUpdateTime = 0L;
15     @Autowired
16     protected freemarker.template.Configuration configuration;
17     @Autowired
18     private SysConfigService configService;
19 
20     @Pointcut(value = "@annotation(org.springframework.web.bind.annotation.GetMapping)" +
21             "|| @annotation(org.springframework.web.bind.annotation.RequestMapping)")
22     public void pointcut() {
23         // 切面切入点
24     }
25 
26     @After("pointcut()")
27     public void after(JoinPoint joinPoint) {
28         Config config = configService.get();
29         if (null == config) {
30             log.error("config为空");
31             return;
32         }
33         Long updateTime = config.getUpdateTime().getTime();
34         if (updateTime == configLastUpdateTime) {
35             log.debug("config表未更新");
36             return;
37         }
38         log.debug("config表已更新,重新加载config到freemarker tag");
39         configLastUpdateTime = updateTime;
40         try {
41             configuration.setSharedVariable("config", config);
42         } catch (TemplateModelException e) {
43             e.printStackTrace();
44         }
45     }
46 }

当然, 虽然OneBlog中是使用的AOP方式解决问题,我们使用过滤器、拦截器也是一样的道理,

代码调优

上面介绍的编码实现方式,我们必须通过 switch...case去挨个判断实际的处理逻辑,在同一个标签类中有太多具体标签实现时,就显得比较笨重。因此,我们简单的优化一下代码,使它看起来不是那么糟糕并且易于扩展。

一、首先,分析代码,将公共模块提取出来。

TemplateDirectiveModel类的 execute方法是每个自定义标签类都必须实现的,并且每个自定义标签都是根据 method参数去使用具体的实现,这一块我们可以提成公共模块:

 1 /**
 2  * 所有自定义标签的父类,负责调用具体的子类方法
 3  *
 4  * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 5  * @version 1.0
 6  * @website https://www.zhyd.me
 7  * @date 2018/9/18 16:19
 8  * @since 1.8
 9  */
10 public abstract class BaseTag implements TemplateDirectiveModel {
11 
12     private String clazzPath = null;
13 
14     public BaseTag(String targetClassPath) {
15         clazzPath = targetClassPath;
16     }
17 
18     private String getMethod(Map params) {
19         return this.getParam(params, "method");
20     }
21 
22     protected int getPageSize(Map params) {
23         int pageSize = 10;
24         String pageSizeStr = this.getParam(params, "pageSize");
25         if (!StringUtils.isEmpty(pageSizeStr)) {
26             pageSize = Integer.parseInt(pageSizeStr);
27         }
28         return pageSize;
29     }
30 
31     private void verifyParameters(Map params) throws TemplateModelException {
32         String permission = this.getMethod(params);
33         if (permission == null || permission.length() == 0) {
34             throw new TemplateModelException("The 'name' tag attribute must be set.");
35         }
36     }
37 
38     String getParam(Map params, String paramName) {
39         Object value = params.get(paramName);
40         return value instanceof SimpleScalar ? ((SimpleScalar) value).getAsString() : null;
41     }
42 
43     private DefaultObjectWrapper getBuilder() {
44         return new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_25).build();
45     }
46 
47     private TemplateModel getModel(Object o) throws TemplateModelException {
48         return this.getBuilder().wrap(o);
49     }
50 
51 
52     @Override
53     public void execute(Environment environment, Map map, TemplateModel[] templateModels, TemplateDirectiveBody templateDirectiveBody) throws TemplateException, IOException {
54         this.verifyParameters(map);
55         String funName = getMethod(map);
56         Method method = null;
57         try {
58             Class clazz = Class.forName(clazzPath);
59             method = clazz.getDeclaredMethod(funName, Map.class);
60             if (method != null) {
61                 Object res = method.invoke(this, map);
62                 environment.setVariable(funName, getModel(res));
63             }
64         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | ClassNotFoundException e) {
65             e.printStackTrace();
66         }
67         templateDirectiveBody.render(environment.getOut());
68     }
69 
70 }

BaseTag作为所有自定义标签的父类,只需要接受一个参数:clazzPath,即子类的类路径(全类名),在实际的 execute方法中,只需要根据制定的 method,使用反射调用子类的相关方法即可。

二、优化后的标签类

 1 /**
 2  * 自定义的freemarker标签
 3  *
 4  * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 5  * @version 1.0
 6  * @website https://www.zhyd.me
 7  * @date 2018/4/16 16:26
 8  * @since 1.0
 9  * @modify by zhyd 2018-09-20
10  *      调整实现,所有自定义标签只需继承BaseTag后通过构造函数将自定义标签类的className传递给父类即可。
11  *      增加标签时,只需要添加相关的方法即可,默认自定义标签的method就是自定义方法的函数名。
12  *      例如:<@zhydTag method="types" ...></@zhydTag>就对应 {{@link #types(Map)}}方法
13  */
14 @Component
15 public class CustomTags extends BaseTag {
16 
17     @Autowired
18     private BizTypeService bizTypeService;
19 
20     public CustomTags() {
21         super(CustomTags.class.getName());
22     }
23 
24     public Object types(Map params) {
25         return bizTypeService.listTypeForMenu();
26     }
27     
28     // 其他自定义标签的方法...
29 }

如上,所有自定义标签只需继承BaseTag后通过构造函数将自定义标签类的className传递给父类即可。增加标签时,只需要添加相关的方法即可,默认自定义标签的method就是自定义方法的函数名。

例如:<@zhydTag method="types" ...>就对应 CustomTags#types(Map)方法

如此一来,我们想扩展标签时,只需要添加相关的自定义方法即可,ftl中通过method指定调用哪个方法。

关注我的公众号

 

posted @ 2018-09-20 16:14  HandsomeBoy丶  阅读(8797)  评论(0编辑  收藏  举报