美文网首页java 基础
Java中使用FreeMaker实现模板渲染

Java中使用FreeMaker实现模板渲染

作者: GeekerLou | 来源:发表于2018-12-15 19:39 被阅读0次

    一、引言

    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工程

    添加依赖:

    <dependency>
      <groupId>org.freemarker</groupId>
      <artifactId>freemarker</artifactId>
      <version>2.3.23</version>
    </dependency>
    

    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}

    <body>
      取学生的信息:</br>
    <label>学号:</label>${stu.id}<br>
    <label>姓名:</label>${stu.name}<br>
    <label>年龄 :</label>${stu.age}<br>
    </body>
    

    1.5.3 取集合中的数据

    循环使用格式:

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

    例如:

    <body>
    <table boder=1>
    <#list studentlist as student>
      <tr>
            <td>${student.id}</td>
            <td>${student.name}</td>
            <td>${student.age}</td>
      </tr>
    </list>
    </table>
    </body>
    
    List<Student> 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>
    

    例如:

    <body>
    <table boder=1>
    <#list studentlist as student>
      <tr>
            <td>${student_index}</td>
            <td>${student.id}</td>
            <td>${student.name}</td>
            <td>${student.age}</td>
      </tr>
    </list>
    </table>
    </body>
    

    1.5.5 判断

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

    例如:

    <body>
    <table boder=1>
    <#list studentlist as student>
      <#if student_index %2 ==0>
           <tr bgcolor="blue">
     <#else>
           <tr bgcolor="red">
    </list>
    </table>
    </body>
    

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

    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 引入包依赖

    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
        <version>2.3.19</version>
     </dependency>
    

    3.2 编写邮件模板

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

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title></title>
        <style type="text/css">
            /* What it does: Remove spaces around the email design added by some email clients. */
            /* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
            html,
            body {
                margin: 0;
                padding: 0;
                height: 100% !important;
                width: 100% !important;
            }
    
            /* What it does: Stops email clients resizing small text. */
            * {
                -ms-text-size-adjust: 100%;
                -webkit-text-size-adjust: 100%;
            }
    
            /* What it does: Forces Outlook.com to display emails full width. */
            .ExternalClass {
                width: 100%;
            }
    
            /* What it does: Stops Outlook from adding extra spacing to tables. */
            table,
            td {
                mso-table-lspace: 0pt;
                mso-table-rspace: 0pt;
            }
    
            /* What it does: Fixes webkit padding issue. */
            table {
                border-spacing: 0 !important;
            }
    
            /* What it does: Fixes Outlook.com line height. */
            .ExternalClass,
            .ExternalClass * {
                line-height: 100%;
            }
    
            /* What it does: Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */
            table {
                border-collapse: collapse;
                margin: 0 auto;
            }
    
            /* What it does: Uses a better rendering method when resizing images in IE. */
            img {
                -ms-interpolation-mode: bicubic;
            }
    
            /* What it does: Overrides styles added when Yahoo's auto-senses a link. */
            .yshortcuts a {
                border-bottom: none !important;
            }
    
            /* What it does: Overrides blue, underlined links auto-detected by iOS Mail. */
            /* More Info: https://litmus.com/blog/update-banning-blue-links-on-ios-devices */
            .mobile-link--footer a {
                color: #666666 !important;
            }
    
            /* What it does: Overrides styles added images. */
            img {
                border: 0 !important;
                outline: none !important;
                text-decoration: none !important;
            }
    
            /* What it does: Apple Mail doesn't support max-width, so a media query constrains the email container width. */
            @media only screen and (min-width: 801px) {
                .email-container {
                    width: 960px !important;
                }
            }
    
            /* What it does: Apple Mail doesn't support max-width, so a media query constrains the email container width. */
            @media only screen and (max-width: 800px) {
                .email-container {
                    width: 100% !important;
                    max-width: none !important;
                }
            }
    
            table tr td .head {
                padding: 10px 0;
                font-family: "Microsoft YaHei", sans-serif;
                text-align: center;
                font-size: 12px;
                line-height: 1.3;
                color: #333333;
                font-weight: bold;
                word-wrap: break-word;
                border-bottom: 1px solid #cccccc;
            }
    
            table tr td .text {
                padding: 10px 0;
                font-family: "Microsoft YaHei", sans-serif;
                text-align: center;
                font-size: 12px;
                line-height: 1.3;
                color: #333333;
                border-bottom: 1px solid #eeeeee;
            }
    
            table tr td .htext {
                padding: 10px 0;
                font-family: "Microsoft YaHei", sans-serif;
                text-align: center;
                font-size: 12px;
                line-height: 1.3;
                color: #333333;
                font-weight: bold;
                border-bottom: 1px solid #eeeeee;
            }
    
        </style>
    </head>
    
    <body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0" bgcolor="#222222" style="margin:0; padding:0; -webkit-text-size-adjust:none; -ms-text-size-adjust:none;">
    <table cellpadding="0" cellspacing="0" border="0" height="100%" width="100%" bgcolor="#222222"
           style="border-collapse:collapse;">
        <tr>
            <td>
                <!-- Beginning of Outlook-specific wrapper : BEGIN -->
                <!--[if (gte mso 9)|(IE)]>
                <table width="1000" align="center" cellpadding="0" cellspacing="0" border="0">
                    <tr>
                        <td>
                <![endif]-->
                <!-- Beginning of Outlook-specific wrapper : END -->
    
                <!-- Email wrapper : BEGIN -->
                <table border="0" width="100%" cellpadding="0" cellspacing="0" align="center"
                       style="max-width:960px; margin:auto;" class="email-container">
                    <tbody>
                    <tr>
                        <td>
                            <table border="0" width="100%" cellpadding="0" cellspacing="0">
                                <tbody>
                                <tr>
                                    <td valign="middle"
                                        style="padding:10px 0; padding-left: 2%; text-align:left; font-family: 'Microsoft YaHei', sans-serif; font-size: 16px; color: #d6d6d6;"
                                        width="135">
                                    ${reportNamespaceDesc}
                                    </td>
                                    <td valign="middle"
                                        style="padding:10px 0; padding-right: 2%; text-align:right; line-height:1.1; font-family: 'Microsoft YaHei', sans-serif; font-size: 16px; color: #d6d6d6;">
                                    ${reportDate}
                                    </td>
                                </tr>
                                </tbody>
                            </table>
    
                            <table border="0" width="100%" cellpadding="0" cellspacing="0" bgcolor="#222222"
                                   style="padding:2%">
                                <tbody>
                                <tr>
                                    <td align="center"
                                        style="padding-top:5%; padding-bottom:0; font-family: 'Microsoft YaHei', sans-serif; font-size: 24px; line-height: 1.3; color: #222;"></td>
                                </tr>
                                </tbody>
                            </table>
                            <table border="0" width="100%" cellpadding="0" cellspacing="0" bgcolor="#ffffff"
                                   style="padding:2%">
                                <tbody>
                                <tr>
                                    <td align="center"
                                        style="padding-top:4%; padding-bottom:0; font-family: 'Microsoft YaHei', sans-serif; font-size: 26px; font-weight:600; line-height: 1.3; color: #444444;">
                                    ${reportTitle}
                                    </td>
                                </tr>
                                </tbody>
                            </table>
                            <!-- Service 统计数据 BEGIN-->
                        <#list reportTableList as reportTable>
                            <table border="0" width="100%" cellpadding="0" cellspacing="0" bgcolor="#ffffff">
                                <tbody>
                                <tr>
                                    <td style="padding-left: 4%; padding-top: 2%; padding-bottom: 0; font-family:'Microsoft YaHei', sans-serif; font-size: 16px; line-height: 1.3; color: #666666;">
                                    ${reportTable.dimensionName}:
                                        <span style="color: #ff1d33;font-weight: bold;">${reportTable.dimension}</span>
                                    </td>
                                </tr>
                                <tr>
                                    <td style="padding-left: 4%; padding-top: 2%; padding-bottom: 0; font-family:'Microsoft YaHei', sans-serif; font-size: 16px; line-height: 1.3; color: #666666;">
                                        说明:统计周期:最近一天,统计方式:${reportTable.statistics}
                                    </td>
                                </tr>
                                <tr>
                                    <td style="padding: 4%; padding-top: 2%; padding-bottom: 2%">
                                        <table cellspacing="0" cellpadding="0" border="0" width="100%" style="">
                                            <tbody>
                                            <!-- 服务报表 title BEGIN -->
                                            <tr>
                                                <#list reportTable.serviceDataList as serviceData>
                                                    <td valign="top" align="left"
                                                        style="padding: 10px 0;font-family: 'Microsoft YaHei', sans-serif; text-align: center; font-size: 12px; line-height: 1.3; color: #333333; font-weight: bold; border-bottom: 1px solid #cccccc;">
                                                    ${serviceData.metricName}(${serviceData.metricUnit})
                                                    </td>
                                                </#list>
                                            </tr>
                                            <!-- 服务报表 title END -->
    
                                            <!-- 服务报表 数据 BEGIN-->
                                            <tr>
                                                <#list reportTable.serviceDataList as serviceData>
                                                    <td valign="top" align="left"
                                                        style="padding: 10px 0;font-family: 'Microsoft YaHei', sans-serif; text-align: center; font-size: 12px; line-height: 1.3; color: #333333; font-weight: bold; border-bottom: 1px solid #eeeeee;">
                                                    ${serviceData.metricValue}
                                                    </td>
                                                </#list>
                                            </tr>
                                            <!-- 服务报表 数据 END-->
                                            </tbody>
                                        </table>
                                    </td>
                                </tr>
                                </tbody>
                            </table>
                        </#list>
                            <!-- Service 统计数据 END-->
                        </td>
                    </tr>
    
                    <!-- Footer : BEGIN -->
                    <tr>
                        <td style="text-align:center; padding:4% 0; font-family:'Microsoft YaHei', sans-serif; font-size:13px; line-height:1.2; color:#666666;">
                            收到该邮件是因为您在 XXX上订阅了 XXX 的相关数据,可登录 XXX 更新订阅配置。
                        </td>
                    </tr>
                    <!-- Footer : END -->
    
                    </tbody>
                </table>
                <!-- Email wrapper : END -->
            </td>
        </tr>
    </table>
    </body>
    </html>
    

    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<String, Object> 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<String> generateShards() {
            return getSingleShard();
        }
    
        // 报表发送任务的线程池
        private ExecutorService executorService = new ThreadPoolExecutor(32, 32, 0L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(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<ServiceData> 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<ServiceData> getServiceDataList() {
                return serviceDataList;
            }
    
            public void setServiceDataList(List<ServiceData> 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<ReportSetting> 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<String> 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<String, Object> params = new HashMap<>();
                        params.put("reportNamespaceDesc", descService.getNamespaceDesc(namespace));
                        params.put("reportTitle", title);
                        params.put("reportDate", dateFormat.format(new Date()));
                        List<ReportTable> reportTableList = new ArrayList<>();
    
                        String dimensionName;
                        if (metricNameKeyArray.length > 0) {
                            dimensionName = metricNameKeyArray[0].split("##")[2];
                        } else {
                            continue;
                        }
                        // 获取用户的实例资源列表
                        List<String> 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<ServiceData> 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实现模板渲染

        本文链接:https://www.haomeiwen.com/subject/qsexkqtx.html