Java中使用FreeMaker实现模板渲染

一、引言

1.1 freemarker简介

FreeMarker是一个用Java语言编写的模板引擎,它基于模板来生成文本输出。FreeMarker与Web容器无关,即在Web运行时,它并不知道Servlet或HTTP。它不仅可以用作表现层的实现技术,而且还可以用于生成XML,JSP或Java 等。目前企业中,主要用Freemarker做静态页面或是页面展示。

模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。

这种方式通常被称为 MVC (模型 视图 控制器) 模式,对于动态网页来说,是一种特别流行的模式。 它帮助从开发人员(Java 程序员)中分离出网页设计师(HTML设计师)。设计师无需面对模板中的复杂逻辑, 在没有程序员来修改或重新编译代码时,也可以修改页面的样式。

而FreeMarker最初的设计,是被用来在MVC模式的Web开发框架中生成HTML页面的,它没有被绑定到 Servlet或HTML或任意Web相关的东西上。它也可以用于非Web应用环境中。

1.2 Freemarker的使用方法

把freemarker的jar包添加到工程中。

1.2.1 Maven工程

添加依赖:


  org.freemarker
  freemarker
  2.3.23

1.2.2 非 Maven工程

加入相应jar包到build path中

1.3 Freemarker的实现原理

freemaker overview.png

1.4 使用步骤

第一步:创建一个Configuration对象,直接new一个对象。构造方法的参数就是freemarker对于的版本号。
第二步:设置模板文件所在的路径。
第三步:设置模板文件使用的字符集。一般就是utf-8.
第四步:加载一个模板,创建一个模板对象。
第五步:创建一个模板使用的数据集,可以是pojo也可以是map。一般是Map。
第六步:创建一个Writer对象,一般创建一FileWriter对象,指定生成的文件名。
第七步:调用模板对象的process方法输出文件。
第八步:关闭流。

public void test() throws IOException, TemplateException { 
    // 第一步:创建一个Configuration对象,直接new一个对象。构造方法的参数就是freemarker对于的版本号。 
    Configuration configuration = new Configuration(Configuration.getVersion()); 

    // 第二步:设置模板文件所在的路径。 
    configuration.setDirectoryForTemplateLoading(new File("D:\\Java\\Eclipse\\workspace_Test\\FreeMarker\\src\\main\\webapp\\WEB-INF\\ftl")); 
    
    // 第三步:设置模板文件使用的字符集。一般就是utf-8. 
    configuration.setDefaultEncoding("utf-8"); 
    
    // 第四步:加载一个模板,创建一个模板对象。 Template template = configuration.getTemplate("hello.ftl"); 
    
    // 第五步:创建一个模板使用的数据集,可以是pojo也可以是map。一般是Map。 
    Map dataModel = new HashMap(); 
    // 向数据集中添加数据 
    dataModel.put("hello", "this is my first freemarker test."); 
    
    // 第六步:创建一个Writer对象,一般创建一FileWriter对象,指定生成的文件名。 
    Writer out = new FileWriter(new File("D:\\Java\\Eclipse\\workspace_Test\\FreeMarker\\out\\hello.html")); 
    
    // 第七步:调用模板对象的process方法输出文件。 
    template.process(dataModel, out); 

    // 第八步:关闭流。 
    out.close(); 
}

1.5 模板的基本语法

1.5.1 访问map中的key

${key}

1.5.2 访问pojo中的属性

Student对象。学号、姓名、年龄
${key.property}


  取学生的信息:
${stu.id}
${stu.name}
${stu.age}

1.5.3 取集合中的数据

循环使用格式:

<#list 要循环的数据 as 循环后的数据>

例如:



<#list studentlist as student>
  
${student.id} ${student.name} ${student.age}
List studentList=new ArrayList<>();
studentList.add(new Student(1,"张三",20));
studentList.add(new Student(2,"张三2",21));
studentList.add(new Student(3,"张三3",22));
studentList.add(new Student(4,"张三4",23));
studentList.add(new Student(5,"张三4",24));
modelMap.put("studentList",studentList);
Writer out=new FileWrite(new File("D://temp/first.html"));

1.5.4 取循环中的下标

<#list studentList as student>
    ${student_index}

例如:



<#list studentlist as student>
  
${student_index} ${student.id} ${student.name} ${student.age}

1.5.5 判断

<#if student_index % 2 == 0>
<#else>

例如:



<#list studentlist as student>
  <#if student_index %2 ==0>
       
 <#else>
       

可以利用索引加上判断,实现表格中奇数和偶数行以不同的颜色来进行标识。

1.5.6 日期类型格式化

直接取值:${date}(date是属性名)如果传来的是一个Date型数据会报错

${date?date} //2016-9-13
${date?time} //17:53:55
${date?datetime} //2016-9-13 17:53:55

如果感觉freemaker提供的功能太弱,可以在java中格式化好之后再通过普通字符串的形式传入进来。

1.5.7 Null值的处理

如果直接取一个不存在的值(值为null)时会报异常

${aaa}

如果允许变量可以为空,并且不希望报出异常,可以在变量后面加上一个!

${aaa!}

1.5.8 Include标签

如果希望在某个FTL文件中引入另外一个FTL文件,可以使用FTL关键字。格式如下:

<#include “模板名称”> //(相当于jstl中的包含)

例如:

<#include "hello.ftl">

注意:上述引用方式表明hello.ftl与当前文件处于同一个目录下,如果两个文件在不同的目录下,需要使用相对路径。

二、需求描述

需要实现一个定时发送监控数据报表的功能,技术实现上考虑使用freemaker来实现邮件内容的渲染。

三、实战

3.1 引入包依赖


    org.freemarker
    freemarker
    2.3.19
 

3.2 编写邮件模板

编写的邮件模板report.ftl文件放在resources/report/report.ftl路径下。模板的完整内容如下:




    
    
    
    
    



3.3 编写模板渲染工具类

import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Map;

/**
 * 邮件模板生成工具类
 */
public class TemplateUtil {
    /**
     *
     * @param ftl FTL文件地址
     * @param params 填充参数
     * @return
     * @throws IOException
     * @throws TemplateException
     */
    public static String generateTemplate(String ftl, Map params)
            throws IOException, TemplateException {
        Configuration configuration = new Configuration();
        configuration.setClassForTemplateLoading(TemplateUtil.class, "/");
        configuration.setDefaultEncoding("UTF-8");
        configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);

        Template template = configuration.getTemplate(ftl);
        Writer stringWriter = new StringWriter();

        template.process(params, stringWriter);

        return stringWriter.toString();
    }
}

3.4 编写定时任务

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

@Service
public class DailyReportTask extends Task {
    private static final Logger logger = Logger.getLogger(DailyReportTask.class);

    //TFL模板文件的相对位置
    private static final String FTL_FILE = "report/report.ftl";

    private static final Integer RECENT_HOURS = 24;

    private static final String TIME_PATTERN = "HH:mm";

    private static final String DATE_PATTERN = "yyyy年MM月dd日";

    private static final FastDateFormat TIME_FORMAT = FastDateFormat.getInstance(TIME_PATTERN, TimeZone.getTimeZone("Asia/Shanghai"));

    @Override
    public Set generateShards() {
        return getSingleShard();
    }

    // 报表发送任务的线程池
    private ExecutorService executorService = new ThreadPoolExecutor(32, 32, 0L, TimeUnit.SECONDS, new ArrayBlockingQueue(512), new ThreadFactory() {
        private AtomicInteger count = new AtomicInteger(0);

        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(new ThreadGroup("cm-task"), r, "daily-report-send-"
                    + count.getAndIncrement());
            return thread;
        }
    }, new ThreadPoolExecutor.DiscardPolicy());

    /**
     * 报表
     */
    public class ReportTable {
        String dimensionName;

        String dimension;

        String statistics;

        List serviceDataList;

        public String getDimensionName() {
            return dimensionName;
        }

        public void setDimensionName(String dimensionName) {
            this.dimensionName = dimensionName;
        }

        public String getDimension() {
            return dimension;
        }

        public void setDimension(String dimension) {
            this.dimension = dimension;
        }

        public String getStatistics() {
            return statistics;
        }

        public void setStatistics(String statistics) {
            this.statistics = statistics;
        }

        public List getServiceDataList() {
            return serviceDataList;
        }

        public void setServiceDataList(List serviceDataList) {
            this.serviceDataList = serviceDataList;
        }
    }

    /**
     * 报表中的数据
     */
    public class ServiceData {
        String metricName;

        String metricUnit;

        Double metricValue;

        public String getMetricName() {
            return metricName;
        }

        public void setMetricName(String metricName) {
            this.metricName = metricName;
        }

        public String getMetricUnit() {
            return metricUnit;
        }

        public void setMetricUnit(String metricUnit) {
            this.metricUnit = metricUnit;
        }

        public Double getMetricValue() {
            return metricValue;
        }

        public void setMetricValue(Double metricValue) {
            this.metricValue = metricValue;
        }
    }

    @Override
    public boolean process(String taskShard) {
        try {
            List reportSettingList = reportSettingService.getReportSettingList();
            // 没有任务则直接返回
            if (reportSettingList.size() == 0) {
                return true;
            }
            logger.debug("reportSettingList size=" + reportSettingList.size());

            for (ReportSetting reportSetting : reportSettingList) {
                String projectId = reportSetting.getProjectId();
                String sendTime = reportSetting.getSendTime();
                String title = reportSetting.getTitle();
                String namespace = reportSetting.getNamespace();
                List reportEmailList = reportSetting.getReportEmailList();
                ReportTemplete reportTemplete = reportTempleteService.getReportTemplete(reportSetting.getTempletId());
                String statistics = reportTemplete.getStatistics();

                // 检查是否到报表发送时间
                if (checkSend(sendTime)) {
                    String[] metricNameKeyArray = reportTemplete.getMetricNameIdList().split(",");
                    FastDateFormat dateFormat = FastDateFormat.getInstance(DATE_PATTERN, TimeZone.getTimeZone("Asia/Shanghai"));
                    // 定义一封邮件的参数
                    Map params = new HashMap<>();
                    params.put("reportNamespaceDesc", descService.getNamespaceDesc(namespace));
                    params.put("reportTitle", title);
                    params.put("reportDate", dateFormat.format(new Date()));
                    List reportTableList = new ArrayList<>();

                    String dimensionName;
                    if (metricNameKeyArray.length > 0) {
                        dimensionName = metricNameKeyArray[0].split("##")[2];
                    } else {
                        continue;
                    }
                    // 获取用户的实例资源列表
                    List dimensionList = metricService.getActiveDimensionList(projectId, namespace, dimensionName, RECENT_HOURS);
                    if (dimensionList.size() == 0) {
                        continue;
                    }

                    // 每个实例标识一张表
                    for (String dimension : dimensionList) {
                        ReportTable reportTable = new ReportTable();
                        reportTable.setDimensionName("");
                        reportTable.setDimension(dimension);
                        reportTable.setStatistics(statistics);
                        List serviceDataList = new ArrayList<>();
                        for (String metricNameKey : metricNameKeyArray) {
                            // 获取监控项的描述、单位和数值
                            String metricNameDesc = "XXX";
                            String unitDesc = "XXX";
                            Double dimensionValue = 12.0;
                            ServiceData serviceData = new ServiceData();
                            serviceData.setMetricName(metricNameDesc);
                            serviceData.setMetricUnit(unitDesc);
                            serviceData.setMetricValue(dimensionValue);

                            serviceDataList.add(serviceData);
                        }

                        reportTable.setServiceDataList(serviceDataList);
                        reportTableList.add(reportTable);
                    }

                    params.put("reportTableList", reportTableList);
                    // 利用freemaker模板生成报表邮件内容
                    String htmlContent = TemplateUtil.generateTemplate(FTL_FILE, params);

                    logger.info("htmlContent:" + htmlContent);

                    // 发送邮件
                    for (String email : reportEmailList) {
                        // 改顺序发送为线程池调度发送
//                      boolean res = EmailUtils.SendEmail(email, title, htmlContent);
//                      if (!res){
//                          logger.error("邮件发送失败");
//                      }
                        executorService.submit(new EmailSendThread(email, title, htmlContent));
                    }
                }
            }
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 检查是否需要发送报表
     *
     * @param sendTime
     * @return
     */
    private boolean checkSend(String sendTime) {
        //略去部分细节
    }

}

4.5 测试结果

收到的测试邮件内容如下图所示:


邮件.jpg

五、其他

邮件发送的内容需要用到邮件服务器以及编写邮件发送工具类,这一细节不是本文的重点,后面考虑通过另外一篇文章单独进行介绍,这里忽略。

后续文章可能还会考虑介绍另外一种同样比较常用的模板渲染引擎Velocity,敬请期待。

六、参考资料

  1. FreeMaker中文在线用户手册
  2. Java中FreeMaker的使用

你可能感兴趣的:(Java中使用FreeMaker实现模板渲染)