首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java FreeMarker 模板引擎實戰:Web 靜態頁生成與自定義指令完整教程

     編輯:Java 相關知識

FreeMarker 核心定位

FreeMarker 是基於 Java 的模板引擎,專為生成文本輸出(HTML、XML、Email、代碼、配置)設計。遵循 MVC 模式:Java 準備數據模型,FreeMarker 負責視圖渲染。

核心依賴



    org.freemarker
    freemarker
    2.3.32

模板語法速查

變量輸出


${variable}           // 基本輸出
${variable!default}   // 空值預設
${variable?string}    // 內建轉字符串
${variable?c}         // 數字分組格式 (1,234)
${variable?html}      // HTML 轉義

條件判斷


<#if condition>
    ...
<#elseif condition2>
    ...
<#else>
    ...


<#if list?? && list?size > 0> ...  // 非空檢查

列表迭代


<#list items as item>
    ${item_index} - ${item}
    <#if item_has_next>,


<#list map?keys as key>
    ${key} = ${map[key]!"null"}

引入片段


<#include "header.ftl">
<#import "lib.ftl" as mylib>
<@mylib.copyright date="2024"/>

自定義指令


<@myDirective param1="value" param2=123>
    內容體

完整靜態頁生成範例

模板文件





    ${title}


    

${title}

描述: ${description}

用戶列表

<#list users as user>
${user_index + 1}. ${user.name} <#if user.role == "admin"> 管理員
<#include "footer.ftl">

Java 生成代碼


public class StaticPageGenerator {
    private final Configuration cfg;
    
    public StaticPageGenerator(String templateDir) throws IOException {
        cfg = new Configuration(Configuration.VERSION_2_3_32);
        cfg.setDirectoryForTemplateLoading(new File(templateDir));
        cfg.setDefaultEncoding("UTF-8");
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        cfg.setLogTemplateExceptions(false);
        cfg.setWrapUncheckedExceptions(true);
        cfg.setFallbackOnNullLoopVariable(false);
    }
    
    public void generate(String templateName, Map data, File outputFile) 
            throws IOException, TemplateException {
        Template template = cfg.getTemplate(templateName);
        try (Writer out = new OutputStreamWriter(new FileOutputStream(outputFile), "UTF-8")) {
            template.process(data, out);
        }
    }
    
    // 使用範例
    public static void main(String[] args) throws Exception {
        StaticPageGenerator gen = new StaticPageGenerator("templates");
        
        Map data = new HashMap<>();
        data.put("title", "用戶報表");
        data.put("description", "2024 年度用戶統計");
        data.put("users", Arrays.asList(
            new User("張三", "admin"),
            new User("李四", "user"),
            new User("王五", "user")
        ));
        
        gen.generate("report.ftl", data, new File("output/report.html"));
        System.out.println("靜態頁生成成功!");
    }
}

自定義指令開發

實現 TemplateDirectiveModel


public class PaginateDirective implements TemplateDirectiveModel {
    private static final String PARAM_PAGE = "page";
    private static final String PARAM_SIZE = "size";
    private static final String PARAM_LIST = "list";
    
    @Override
    public void execute(Environment env, Map params, TemplateModel[] loopVars, 
                        TemplateDirectiveBody body) throws TemplateException, IOException {
        
        // 1. 解析參數
        int page = getIntParam(params, PARAM_PAGE, 1);
        int size = getIntParam(params, PARAM_SIZE, 10);
        TemplateSequenceModel listModel = (TemplateSequenceModel) params.get(PARAM_LIST);
        
        if (listModel == null) {
            throw new TemplateModelException("缺少必需參數: list");
        }
        
        // 2. 轉換為 Java List
        List items = Collections.list(listModel);
        int total = items.size();
        int start = (page - 1) * size;
        int end = Math.min(start + size, total);
        
        // 3. 設置循環變量
        if (loopVars.length >= 1) {
            List pageItems = items.subList(start, end);
            loopVars[0] = new SimpleSequence(pageItems, new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build());
        }
        
        // 4. 設置分頁信息變量
        env.setVariable("paginator", new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build().wrap(
            Map.of("page", page, "size", size, "total", total, "pages", (total + size - 1) / size)
        ));
        
        // 5. 渲染內容體
        if (body != null) {
            body.render(env.getOut());
        }
    }
    
    private int getIntParam(Map params, String name, int defaultVal) throws TemplateModelException {
        TemplateModel model = (TemplateModel) params.get(name);
        if (model == null) return defaultVal;
        if (model instanceof TemplateNumberModel) {
            return ((TemplateNumberModel) model).getAsNumber().intValue();
        }
        if (model instanceof TemplateScalarModel) {
            return Integer.parseInt(((TemplateScalarModel) model).getAsString());
        }
        throw new TemplateModelException("參數 " + name + " 必須為數字");
    }
}

註冊與使用


// 註冊
cfg.setSharedVariable("paginate", new PaginateDirective());

// 模板中使用
<@paginate list=users page=1 size=5>
    <#list paginate as user>
        ${user.name}
    
    

Spring Boot 整合


@Configuration
public class FreeMarkerConfig {
    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates/");
        configurer.setDefaultEncoding("UTF-8");
        configurer.setConfiguration(freemarkerConfig -> {
            freemarkerConfig.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
            freemarkerConfig.setSharedVariable("paginate", new PaginateDirective());
        });
        return configurer;
    }
}

// Controller
@GetMapping("/report")
public String report(Model model) {
    model.addAttribute("users", userService.findAll());
    return "report"; // 解析為 report.ftl
}

進階技巧

1. 自定義函數


cfg.setSharedVariable("formatMoney", new TemplateMethodModelEx() {
    @Override
    public TemplateModel exec(List args) throws TemplateModelException {
        if (args.size() != 1) throw new TemplateModelException("需一個參數");
        Number num = ((TemplateNumberModel) args.get(0)).getAsNumber();
        return new SimpleScalar(NumberFormat.getCurrencyInstance(Locale.CHINA).format(num));
    }
});
// 模板: ${formatMoney(price)}

2. 佈局/繼承模式




<@block name="title"/> - 系統

    
通用頭部
<@block name="content"/>
通用底部
<#import "layout.ftl" as layout> <@layout.layout> <@block name="title">首頁 <@block name="content">歡迎內容

3. 異步/流式渲染大數據


template.process(data, new Writer() {
    private final OutputStream os = response.getOutputStream();
    private final byte[] buffer = new byte[8192];
    @Override public void write(char[] cbuf, int off, int len) throws IOException {
        os.write(new String(cbuf, off, len).getBytes(StandardCharsets.UTF_8));
    }
    @Override public void flush() throws IOException { os.flush(); }
    @Override public void close() throws IOException { os.close(); }
});

常見問題與優化

問題解決方案
中文亂碼模板文件 UTF-8、Java 讀寫指定 UTF-8、配置 `defaultEncoding`
Null 顯示 "null"使用 `${var!}` 或 `${var?default("")}`、`cfg.setClassicCompatible(true)`
大量數據渲染慢流式輸出、分頁指令、預編譯模板 `cfg.getTemplate()` 緩存
模板修改不生效關閉模板緩存 `cfg.setTemplateUpdateDelayMilliseconds(0)` (開發環境)
循環變量衝突使用 `#list ... as item` 明確命名、避免巢狀同名

版本遷移注意 (2.3.x → 2.3.30+)

  • 預設開啟 `auto_import`、`auto_include` 安全限制
  • `?html` 內建取代舊版 `?escape`
  • `Configuration.VERSION_2_3_32` 替代舊版本常量
  • `DefaultObjectWrapperBuilder` 替代直接 `new DefaultObjectWrapper()`
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved