1. 程式人生 > >JavaEE進階——FreeMarker模板引擎

JavaEE進階——FreeMarker模板引擎

I. 簡介

FreeMarker 是一款 模板引擎: 即一種基於模板和要改變的資料, 並用來生成輸出文字(HTML網頁,電子郵件,配置檔案,原始碼等)的通用工具。 它不是面向終端使用者的,而是一個Java類庫,是一款程式設計師可以嵌入他們所開發產品的元件。

模板編寫為FreeMarker Template Language (FTL)。 在模板中,你可以專注於如何展現資料, 而在模板之外可以專注於要展示什麼資料。 這種方式通常被稱為 MVC (模型 檢視 控制器) 模式,對於動態網頁來說,是一種特別流行的模式。

示意圖

上圖如果Template換成Jsp,對於大多數JavaEE開發者來說,會顯得非常熟悉。FreeMarker最初的設計,是被用來在MVC模式的Web開發框架中生成HTML頁面的,它沒有被繫結到 Servlet或HTML或任意Web相關的東西上。它也可以用於非Web應用環境中。 它完全可以替代Jsp,實現頁面的靜態化。

II. 入門程式

需求

利用FreeMarker 實現一個網站使用者登入顯示使用者資訊。

流程

  • 新增Jar包
  • 建立Configration物件
  • 建立模板
  • 獲取模板
  • 建立模板需要的資料
  • 合併模板與資料

建立工程

FreeMarker 不依賴於web容器,所以普通的java專案也可以使用FreeMarker 。FreeMarker 的依賴包下載可以在官網進行獲取,目前最新為FreeMarker 2.3.28版本。

利用IDEA建立工程,如果是普通專案則將 freemarker.jar 新增的 lib 目錄下。如果建立為Maven專案,則可以在pom檔案中新增:

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

如果是結合Spring使用FreeMarker,則需要額外新增依賴:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId
>
spring-context-support</artifactId> <version>${spring.version}</version> </dependency>

建立Configration物件

首先,需要建立一個 freemarker.template.Configuration 例項, 然後對其屬性進行設定。Configuration 例項是儲存 FreeMarker 應用級設定的核心部分。同時,它也處理建立和快取預解析模板(比如 Template 物件)的工作。

Configuration 的建立代價很高,不需要重複建立例項,尤其是會丟失快取。Configuration 例項就是應用級別的單例,不管一個系統有多少獨立的元件來使用 FreeMarker, 它們都會使用他們自己私有的 Configuration 例項。

只需要在應用(可能是servlet)生命週期的開始執行一次

// 建立Configuration例項,指定版本
Configuration configuration = new Configuration(Configuration.getVersion());
try {
    // 指定configuration物件模板檔案存放的路徑
    configuration.setDirectoryForTemplateLoading(new File("/where/you/store/templates"));
    // 設定config的預設字符集,一般是UTF-8
    configuration.setDefaultEncoding("UTF-8");
    // 設定錯誤控制器
    configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
} catch (IOException e) {
    e.printStackTrace();
}

關於錯誤控制器,FreeMarker 四個預先編寫的錯誤控制器:

  • DEBUG_HANDLER:列印堆疊資訊和重新丟擲異常,這是預設的異常控制器。
  • HTML_DEBUG_HANDLER:和DEBUG_HANDLER相同,但是可以格式化堆疊跟蹤資訊。HTML頁面,建議使用它而不是DEBUG_HANDLER。
  • IGNORE_HANDLER:簡單地忽略所有異常。它對處理異常沒有任何作用,也不會重新丟擲異常。
  • RETHROW_HANDLER: 簡單重新丟擲所有異常而不會做其他的事情。這個控制器對Web應用很好,因為它在生成的頁面發生錯誤的情況下,給了對Web應用的更多的控制權。

建立模板

/where/you/store/templates 資料夾下建立模板檔案 hello.ftl

<html>
<head>
    <title>歡迎  ${user.username}!</title>
</head>
<body>
    <h1>歡迎 ${user.username}!</h1>
    <h2>年齡: ${user.age}</h2>
    <h2>${user.record.id}:${user.record.name}</h2>
</body>
</html>

獲取模板

模板代表了 freemarker.template.Template 例項。 典型的做法是直接使用 Configuration 例項的 getTemplate() 方法獲取一個 Template 例項,例如:

// 獲取模版
Template template = configuration.getTemplate("hello.ftl");

當呼叫這個方法的時候,將會建立一個 hello.ftlTemplate 例項,通過讀取 */where/you/store/templates/hello.ftl 檔案,之後解析(編譯)它。Template 例項以解析後的形式儲存模板, 而不是以原始檔的文字形式。

Configuration 會快取 Template 例項,當需要再次獲得 hello.ftl 的時候,它可能不再讀取和解析模板檔案了, 而只是返回第一次獲取的 Template例項。

準備資料

需要給模板的資料往往來自真實的業務資料,可能從資料庫、檔案等獲得。在JavaEE中往往有單獨的Service和Dao層幫助我們準備好需要的資料。資料的包裝形式往往是JavaBean。

  • 使用 java.lang.String 來構建字串。
  • 使用 java.lang.Number 來派生數字型別。
  • 使用 java.lang.Boolean 來構建布林值。
  • 使用 java.util.List 或Java陣列來構建序列。
  • 使用 java.util.Map 來構建雜湊表。
  • 使用自定義的bean類來構建雜湊表,bean中的項和bean的屬性對應。

這裡我們定義兩個類,一個是User類,一個是使用者記錄Record類。

public class User {
    private String username;
    private int age;
    private Record record;

    /** setter and getter **/
}

public class Record {
    private long id;
    private String name;

    /** setter and getter **/
}

下面是構建這個資料模型的Java程式碼片段:

// 準備資料
Map<String, User> map = new HashMap<>();

Record record = new Record();
record.setId(1L);
record.setName("記錄一");

User user = new User();
user.setUsername("小明");
user.setAge(18);
user.setRecord(record);

map.put("user", user);

合併模板與資料

資料模型+模板=輸出,這一過程是由模板的 process 方法完成的。它需要資料和 Writer 物件兩部分作為引數,然後向 Writer 物件寫入產生的內容。這裡建立一個Writer物件,指定生成的檔案儲存的路徑及檔名:

// 建立一個Writer物件,指定生成的檔案儲存的路徑及檔名
Writer writer = new FileWriter(new File("/where/you/store/outputs"));
template.process(map, writer);

這裡用到了Java IO相關的操作,基於 out 物件,必須保證 out.close() 最後被呼叫。典型的Web應用程式中,不能關閉 out 物件,對此FreeMarker會在模板執行成功後呼叫 out.flush(),所以不必擔心 (這一功能也可以在 Configuration 中禁用) 。

還有一點,一旦獲得了 Template 例項, 就能將它和不同的資料模型進行不限次數的合併。此外, 當 Template例項建立之後的模板檔案 (hello.ftl) 才能訪問,而不是在呼叫 process 方法時。

結果

輸出結果

III. 書寫模板

布林型format

FreeMarker 無法直接對布林型的值直接輸出True、False,需要對其format才能輸出。

${布林變數名?string('yes', 'no')}

輸出

日期format

FreeMarker 對Java的Date型別物件取值分兩種情形,當用的sql包下的Date可以直接取值;當用的是util包下的Date則需要進行format才能輸出。因為Java API通常不區別 java.util.Date 只儲存日期部分, 時間部分,或兩者都存。 為了用本文正確顯示值,FreeMarker必須知道 java.util.Date 的日期究竟要顯示哪一部分。

${sqldate}
${utildate?string("yyyy/MM/dd HH:mm:ss")}

日期輸出

null或missing

FreeMarker 不可以輸出null,類似於空指標異常。同樣,如果通過 ${變數名} 表示式取值,壓根不存在變數名,FreeMarker也會報出錯誤。 FreeMarker 對此加入了判空操作,利用 可以在null或missing的情況下不輸出或輸出預設文字。

${nullVar!'預設輸出'}
${missingVar!}

null或missing

變數定義賦值運算

通過assign標籤給變數進行賦值,賦值完的變數可以在 ${} 中進行相應的運算。

<#assign a=100 />
<#assign b=2 />
a + b = ${a + b}
a - b = ${a - b}
a x b = ${a * b}
a / b = ${a / b}
a % b = ${a % b}

變數運算

遍歷List

FreeMarker 可以採用list標籤遍歷Java中的List集合資料來輸出文字,極為方便。

<#list numList as item>
    list:${item}
</#list>

list

FreeMarker 也可以利用list標籤來遍歷map集合。需要取到map中的key,再通過key來取出相應的value。取map中的key可以通過 map?keys 來獲取:

<#list objectMap?keys as key>
    ${key}:${objectMap[key].username}
</#list>

map

if-else

FreeMarker 提供了邏輯判斷,採用if和else標籤實現。滿足條件則輸出,不滿足則忽略。

<#list numList as item>
    <#if item!=2 >
        list:${item}
    </#if>
</#list>

if

<#list numList as item>
    <#if item &gt; 3 >
        <font color="red">list:${item}</font>
    <#elseif item == 3>
        <font color="blue">list:${item}</font>
    <#else>
        <font color="green">list:${item}</font>
    </#if>
</#list>

if-else

其中&gt;是大於的轉義,同樣&lt;是小於的轉義。利用雙問號 ?? 或者 ?exists 可以對物件進行判空,例如:

<#if item??>
<#if item?exists>

同樣FreeMarker 也支援多條件判斷,用 &&|| 進行連線。

switch

FreeMarker 的switch和Java中的switch十分類似,同樣由switch、case、break和default標籤組成。switch標籤支援數值和字串兩種型別。

<#assign str="java" />
<#switch str>
    <#case "python"> 學習python <#break>
    <#case "java"> 學習java <#break>
    <#default> 學習別的。。。
</#switch>

<#assign str="11.1" />
<#switch str>
    <#case "11.1"> 學習python <#break>
    <#case "11.11"> 學習java <#break>
    <#default> 學習別的。。。
</#switch>

switch

IV. 使用函式

字串內建函式

<#assign a="hello"/>
<#assign b="world"/>
<li>連線</li>
${a + b}
<li>擷取</li>
${(a + b)?substring(5, 8)}
<li>長度</li>
${(a + b)?length}
<li>大寫</li>
${(a + b)?upper_case}
<li>小寫</li>
${(a + b)?lower_case}
<li>index</li>
${(a + b)?index_of('o')}
<li>last_index</li>
${(a + b)?last_index_of('o')}
<li>替換</li>
${(a + b)?replace('o', 'xx')}

字串函式

list內建函式

<#assign myList=[3, 4, 5, 6, 1, 3, 7, 9, 2] />
mySize大小:${myList?size}
mySize第三個元素:${myList[3]}
順序:
<#list myList?sort as item>
    ${item_index} : ${item}
</#list>
逆序:
<#list myList?sort?reverse as item>
    ${item_index} : ${item}
</#list>

list內建函式

常用內建函式

處理字串:

  • substring cap_first ends_with contains
  • date datetime time (字串轉日期時間)
  • starts_with index_of last_index_of split trim

處理數字:

  • string x?string(“0.##”) (保留兩位小數)
  • round floor ceiling

處理list:

  • first last seq_contains seq_index_of
  • size reverse sort sort_by
  • chunk(分塊處理)

其他:

  • is函式:is_string is_number is_method
  • has_content
  • eval求值

V. 自定義功能

在FreeMarker內部中可用的變數都是實現了freemarker.template.TemplateModel 介面的Java物件,而我們可以使用基本的Java集合類作為變數,是因為FreeMarker提供了一種物件包裝的功能特性,我們用的基本的Java集合類變數會在內部被替換為適當的 TemplateModel 型別。

在自定義函式和指令中,我們需要使用FreeMarker中的資料型別,而非直接使用Java中的資料型別,所以有必要先熟悉FreeMarker中定義了哪些資料型別。

標量

FreeMarker中定義了四種類型標量:布林值,數字,字串以及日期。每一種標量都是 TemplateXxxModel 介面的實現,Xxx 是Java中相關型別的名稱。比如 TemplateBooleanModel 。這些介面中都只定義了一個方法用於轉換型別為Java型別:getAsXxx() 。在名稱上,只有字串標量有些例外,字串標量的介面是 TemplateScalarModel,而不是TemplateStringModel

除了 SimpleBoolean 型別,這些介面的一個簡單的實現是 freemarker.template 包下的 SimpleXxx 類。為了代表布林值, 可以使用 TemplateBooleanModel.TRUETemplateBooleanModel.FALSE 來單獨使用。 同樣,字串標量的實現類是 SimpleScalar,而不是 SimpleString

容器

除了標量,對於Java的集合陣列型別FreeMarker也定義了相關的資料型別稱為容器。容器包括雜湊表序列集合三種類型。

  • 雜湊表

    雜湊表是實現了 TemplateHashModel 介面的Java物件。TemplateHashModel 有兩個方法: TemplateModel get(String key) 方法根據給定的名稱返回子變數, boolean isEmpty()方法表明雜湊表是否含有子變數。get 方法當在給定的名稱沒有找到子變數時返回null。

    TemplateHashModelEx 介面擴充套件了 TemplateHashModel。它增加了更多的方法,使得可以使用內建函式 values 和 keys 來列舉雜湊表中的子變數。

    經常使用的實現類是 SimpleHash,該類實現了 TemplateHashModelEx 介面。從內部來說,它使用一個 java.util.Hash 型別的物件儲存子變數。 SimpleHash 類的方法可以新增和移除子變數。 這些方法應該用來在變數被建立之後直接初始化。

  • 序列(陣列)

    序列是實現了 TemplateSequenceModel 介面的Java物件。它包含兩個方法:TemplateModel get(int index)int size()

    經常使用的實現類是 SimpleSequence。該類內部使用一個 java.util.List 型別的物件儲存它的子變數。 SimpleSequence 有新增子元素的方法。 在序列建立之後應該使用這些方法來填充序列。

  • 集合

    集合是實現了 TemplateCollectionModel 介面的Java物件。這個介面定義了一個方法: TemplateModelIterator iterator()TemplateModelIterator 介面和 java.util.Iterator 相似,但是它返回 TemplateModels 而不是 Object, 而且它能丟擲 TemplateModelException 異常。

    通常使用的實現類是 SimpleCollection

自定義函式

有時候FreeMarker內建函式不一定能夠滿足我們的處理需要,我們可以自定義函式來處理資料並展示。FreeMarker自定義函式需要自定義處理資料的類,該類需要實現 TemplateMethodModel 介面 ,介面中的 TemplateModel exec(java.util.List arguments) 方法也需要我們重新實現。當呼叫自定義函式時,自定義類的exec 方法將會被呼叫。 形參將會包含FTL方法呼叫形參的值。exec 方法的返回值給出了FTL方法呼叫表示式的返回值。

我們以自定義一個數組排序的函式為例,首先新建一個 MySortMethod 類實現 TemplateMethodModel 介面,並重寫 exec 方法。

public class MySortMethod implements TemplateMethodModelEx {

    /**
     * 自定義函式需要實現的方法
     * @param list 在.ftl模板中呼叫自定義方法傳的引數
     * @return 返回結果
     * @throws TemplateModelException
     */
    @Override
    public Object exec(List list) throws TemplateModelException {

        // 接收傳入的List
        DefaultListAdapter defaultListAdapter = (DefaultListAdapter) list.get(0);
        List<Integer> arrayList = (List<Integer>) defaultListAdapter.getAdaptedObject(Integer.class);

        // 接收傳入的升序還是降序布林值
        boolean asc = "yes".equals(((SimpleScalar) list.get(1)).getAsString()) ? true : false;

        Collections.sort(arrayList, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                if(asc) {
                    return o1 - o2;
                }else {
                    return o2 - o1;
                }
            }
        });
        return arrayList;
    }
}

在呼叫函式前,我們需要在模板資料裡新增:

List<Integer> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
    list.add(i+1);
}
map.put("numList", list);
map.put("my_sort", new MySortMethod());

在模板中呼叫自定義函式 my_sort ,傳入兩個引數:

順序:
<#list my_sort(numList, true?string('yes', 'no')) as item>
${item_index} : ${item}
</#list>
逆序:
<#list my_sort(numList, false?string('yes', 'no')) as item>
${item_index} : ${item}
</#list>

輸出結果為:

排序輸出

注:新版本的Api好像不太一樣,在 MySortMethod.java 的13、14行中獲取傳入的list引數部分,我這裡的寫法是臨時摸索出來的,有知道正確的獲取list資料姿勢的請不吝賜教。

自定義指令

類似自定義函式,我們也可以自定義指令,類似於if-else、assign這樣的指令。自定義指令的需要使用 @ 符號,而不是 # 符號。可以使用 TemplateDirectiveModel 介面在Java程式碼中實現自定義指令。TemplateDirectiveModel 在 FreeMarker 2.3.11 版本時才加入, 來代替快被廢棄的 TemplateTransformModel

下面舉個栗子,自定義用來校驗使用者名稱密碼的指令:

public class MyValidationDirective implements TemplateDirectiveModel {

    /**
     *
     * @param environment 環境變數(實現複雜功能時可能會用)
     * @param map 在.ftl模板中使用自定義指令傳的引數(key-value形式)
     * @param templateModels 返回值,陣列形式
     * @param templateDirectiveBody 指令內容
     * @throws TemplateException
     * @throws IOException
     */
    @Override
    public void execute(Environment environment,
                        Map map,
                        TemplateModel[] templateModels,
                        TemplateDirectiveBody templateDirectiveBody)
            throws TemplateException, IOException {

        SimpleScalar username = (SimpleScalar) map.get("username");
        SimpleScalar password = (SimpleScalar) map.get("password");

        if("admin".equals(username.getAsString()) && "123456".equals(password.getAsString())) {
            templateModels[0] = TemplateBooleanModel.TRUE;
        } else {
            templateModels[0] = TemplateBooleanModel.FALSE;
        }

        List<String> rights = new ArrayList<>();
        rights.add("insert");
        rights.add("delete");
        rights.add("update");
        rights.add("select");
        templateModels[1] = new SimpleSequence(rights);

        templateDirectiveBody.render(environment.getOut());
    }
}

模板編寫:

模板

利用 <@role /> 標籤需要傳入模版資料前進行新增:

map.put("role", new MyValidationDirective());

或者在模板檔案中使用內建函式 new() 將指令放到一個FTL庫中:

<#assign role="directive.MyValidationDirective"?new() />

輸出結果:

輸出

macro巨集指令

macro語法:

<#macro 指令名稱 param1 param2 param3 paramN>
    template_code 可以獲取引數${param1}
    <#nested />
</#macro>

呼叫語法:

<@指令名稱 param1="xxx" param2="xxx" />

<@指令名稱 param1="xxx" param2="xxx">
    nested_template
</@指令名稱>

下面舉一些例子。

<h2>無引數的macro</h2>
<#macro test1>
    我是無引數的macro
</#macro>
<@test1 />

<h2>有引數的macro</h2>
<#macro test2 param1 param2>
    我是有引數的macro,引數是${param1}和${param2}
</#macro>
<@test2 param1="hello" param2="world"/>

<h2>有預設引數的macro</h2>
<#macro test3 param1 param2="world">
    我是有預設引數的macro,引數是${param1}和${param2}
</#macro>
<@test3 param1="hello" />
<@test3 param1="hello" param2="earth"/>

<h2>有多個引數的macro</h2>
<#macro test4 param1 param2 paramExt...>
    我是有多個引數的macro,引數是${param1}、${param2}、${paramExt['param3']}和${paramExt['param4']}
</#macro>
<@test4 param1="hello" param2="world" param3="hi" param4="man"/>

<h2>有nested的macro</h2>
<#macro test5 param1 param2="hi" paramExt...>
    我是有多個引數的macro,固定引數是${param1}和${param2}
    <#nested paramExt['param3'] paramExt['param4']/>
</#macro>
<@test5 param1="hello" param2="world" param3="hi" param4="man"; loopVar1, loopVar2>
    可變引數為${loopVar1}和${loopVar2}
</@test5>

輸出結果

function方法

function語法:

<#function 方法名 param1 param2>
    <#return param1param2的操作>
</#function>

呼叫語法:

${方法名(param1, param2)}

下面舉一些例子。

<#function doAdd param1 param2>
    <#return param1+param2 />
</#function>
${doAdd(2, 3)}

輸出為5。

VI. 結合Spring

以商城專案生成商品詳情頁面為例,一般訪問url中每個商品id有個靜態頁面,例如京東商品頁面。每個商品靜態頁面可以利用FreeMarker來生成。

京東

配置Configration

Spring要結合FreeMarker需要在配置檔案中進行配置Configration。

<!--配置FreeMarker-->
<bean id="freemarkerConfig"
      class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
    <property name="templateLoaderPath" value="/WEB-INF/ftl/" />
    <property name="defaultEncoding" value="UTF-8" />
</bean>

靜態檔案生成時機

  1. 當用戶第一次訪問時生成靜態檔案。此方案當高併發時,會出現生成一半的頁面顯示。不推薦該方案。
  2. 提前生成好靜態頁面。當後臺管理員新增、編輯商品時生成靜態網頁。

生成頁面

/**
 * 生成靜態頁面Service
 */
public class StaticPageServiceImpl implements StaticPageService {

    @Autowired
    private ItemService itemService;
    @Autowired
    private FreeMarkerConfig freeMarkerConfig;
    @Value("${STATIC_PAGE_PATH}")
    private String STATIC_PAGE_PATH;

    @Override
    public TaotaoResult getHtml(Long itemId) throws IOException, TemplateException {
        // 獲取商品基本資訊、詳細介紹和商品引數
        TbItem tbItem = itemService.getItemById(itemId);
        String desc = itemService.getItemDescById(itemId);
        String param = itemService.getItemParamById(itemId);

        // 生成靜態頁面
        Configuration configuration = freeMarkerConfig.getConfiguration();
        Template template = configuration.getTemplate("item.ftl");

        Map<String, Object> root = new HashMap<>();
        root.put("item", tbItem);
        root.put("itemDesc", desc);
        root.put("itemParam", param);

        Writer writer = new FileWriter(new File(STATIC_PAGE_PATH + itemId + ".html"));
        template.process(root, writer);
        writer.flush();
        writer.close();
        return TaotaoResult.ok();
    }
}

模版檔案

模版檔案的準備僅僅需要將jsp或html檔案修改成ftl模板即可。

利用Nginx訪問靜態資源

生成好的靜態html檔案,可以利用Nginx伺服器進行部署,這樣效能比Tomcat要好,其次也不需要各種延遲載入等操作。