利用freemarker 靜態化網頁

一、介紹-FreeMarker是什麼
 
     模板引擎:一種基於模板的、用來生成輸出文本的通用工具
 
     基於Java的開發包和類庫
 
    
 
二、介紹-FreeMarker能作什麼
 
     MVC框架中的View層組件
 

     Html頁面靜態化
 

    代碼生成工具
 

    CMS模板引擎
 

  頁面欄目動態定製
 

三、介紹-爲何要用FreeMarker
 
      程序邏輯(Java 程序)和頁面設計(FreeMarker模板)分離
 
      分層清晰,利於分工合做
 
      主流Web框架良好的集成(struts2,springmvc)
 
      簡單易學、功能強大
 
      免費開源
 
四、FreeMarker優勢
 
     FreeMarker不依賴於Servlet,網絡或Web 環境
 
     FreeMarker一開始就是爲MVC設計的,它僅僅專一於展現
 
     你能夠從任意位置加載模板;從類路徑下,從數據庫中等
 
     易於定義特設的宏和函數
 

五、上面簡單介紹一下Freemarker,下面主要是利用Freemarker實習網頁靜態化的功能。
 
經過上面的介紹知道Freemarker是一種基於模板的、用來生成輸出文本的通用工具,因此咱們必需要定製符合本身業務的模板出來,而後生成的咱們得html頁面
 
Freemarker是經過freemarker.template.Configuration這個對象對模板進行加載的(它也處理建立和緩存預解析模板的工做),而後咱們經過getTemplate方法得到你想要的模板,有一點要記住freemarker.template.Configuration在你整個應用必須保證惟一實例。
 
5.一、在Configuration 中可使用下面的方法來方便創建三種模板加載
 




[java] view plaincopyprint?
01.void setDirectoryForTemplateLoading(File dir);  

 


[java] view plaincopyprint?
01.void setClassForTemplateLoading(Class cl, String prefix);  

 


[java] view plaincopyprint?
01.void setServletContextForTemplateLoading(Object servletContext, String path);  
上述的第一種方法在磁盤的文件系統上設置了一個明確的目錄,它肯定了從哪裏加載模板。不要說可能,File 參數確定是一個存在的目錄。不然,將會拋出異常。
 第二種調用方法使用了一個Class 類型的參數和一個前綴。這是讓你來指定何時經過相同的機制來加載模板,不過是用Java 的ClassLoader 來加載類。這就意味着傳
 入的Class 參數會被用來調用Class.getResource()方法來找到模板。參數prefix是給模板的名稱來加前綴的。在實際運行的環境中,類加載機制是首選用來加載模板的方法,由於一般狀況下,從類路徑下加載文件的這種機制,要比從文件系統的特定目錄位置加載安全並且簡單。在最終的應用程序中,全部代碼都使用.jar 文件打包也是不錯的,這樣用戶就能夠直接執行包含全部資源的.jar 文件了。
 第三種調用方式須要Web 應用的上下文和一個基路徑做爲參數,這個基路徑是Web 應用根路徑(WEB-INF 目錄的上級目錄)的相對路徑。那麼加載器將會從Web 應用目錄開
 始加載模板。儘管加載方法對沒有打包的.war 文件起做用, 由於它使用了ServletContext.getResource()方法來訪問模板,注意這裏咱們指的是「目錄」。若是忽略了第二個參數(或使用了」」),那麼就能夠混合存儲靜態文件(.html,.jpg 等)和.ftl 文件,只是.ftl 文件能夠被送到客戶端執行。固然必須在WEB-INF/web.xml中配置一個Servlet 來處理URI 格式爲*.ftl 的用戶請求,不然客戶端沒法獲取到模板,所以你將會看到Web 服務器給出的祕密提示內容。在站點中不能使用空路徑,這將成爲一個問題,你應該在WEB-INF 目錄下的某個位置存儲模板文件,這樣模板源文件就不會偶然
 void setDirectoryForTemplateLoading(File dir);
 void setClassForTemplateLoading(Class cl, String prefix);
 void setServletContextForTemplateLoading(Object
 servletContext, String path);
 地被執行到,這種機制對servlet 應用程序來加載模板來講,是很是好用的方式,並且模板能夠自動更新而不需重啓Web 應用程序,可是對於類加載機制,這樣就行不通了。
 
5.二、從多個位置加載模板
 



[java] view plaincopyprint?
01.import freemarker.cache.*; // 模板加載器在這個包下  
02....  
03.FileTemplateLoader ftl1 = new FileTemplateLoader(new File("/tmp/templates"));  
04.FileTemplateLoader ftl2 = new FileTemplateLoader(new File("/usr/data/templates"));  
05.ClassTemplateLoader ctl = new ClassTemplateLoader(getClass(),"");  
06.TemplateLoader[] loaders = new TemplateLoader[] { ftl1, ftl2,ctl };  
07.MultiTemplateLoader mtl = new MultiTemplateLoader(loaders);  
08.cfg.setTemplateLoader(mtl);  
如今,FreeMarker 將會嘗試從/tmp/templates 目錄加載模板,若是在這個目錄下沒有發現請求的模板,它就會繼續嘗試從/usr/data/templates 目錄下加載,若是仍是沒有發現請求的模板,那麼它就會使用類加載器來加載模板。
 
5.三、封裝freemarker用於建立模板和加載模板
 



[java] view plaincopyprint?
01.package com.ajun.template.utils;  
02.  
03.import java.io.IOException;  
04.import java.io.Writer;  
05.import java.util.Locale;  
06.import java.util.Map;  
07.  
08.import javax.servlet.ServletContext;  
09.  
10.import freemarker.template.Configuration;  
11.import freemarker.template.DefaultObjectWrapper;  
12.import freemarker.template.Template;  
13.import freemarker.template.TemplateException;  
14.  
15./** 
16. * @author ajun 
17. * @http://blog.csdn.net/ajun_studio   
18. **/  
19.public class FreeMarkertUtil {  
20.  
21.    private static  Configuration config = new Configuration();   
22.      
23.    /** 
24.     * @param templateName 模板名字 
25.     * @param root 模板根 用於在模板內輸出結果集 
26.     * @param out 輸出對象 具體輸出到哪裏 
27.     */  
28.    public static void processTemplate(String templateName, Map<?,?> root, Writer out){  
29.        try{  
30.            //得到模板  
31.            Template template=config.getTemplate(templateName,"utf-8");  
32.            //生成文件(這裏是咱們是生成html)  
33.            template.process(root, out);     
34.            out.flush();     
35.        } catch (IOException e) {  
36.            e.printStackTrace();  
37.        } catch (TemplateException e) {  
38.            e.printStackTrace();  
39.        }finally{  
40.             try {  
41.                out.close();  
42.                out=null;  
43.            } catch (IOException e) {  
44.                e.printStackTrace();  
45.            }  
46.        }  
47.    }  
48.    /** 
49.     * 初始化模板配置 
50.     * @param servletContext javax.servlet.ServletContext 
51.     * @param templateDir 模板位置 
52.     */  
53.    public static void initConfig(ServletContext servletContext,String templateDir){  
54.            config.setLocale(Locale.CHINA);  
55.            config.setDefaultEncoding("utf-8");  
56.            config.setEncoding(Locale.CHINA, "utf-8");  
57.            config.setServletContextForTemplateLoading(servletContext, templateDir);  
58.            config.setObjectWrapper(new DefaultObjectWrapper());  
59.    }  
60.}  

 
5.四、例子介紹
 
會用freemarker.jar本身google下載吧。
 

這個例子中咱們會Freemarker生成一個html文件 包括html的頭部和尾部,已經body,這三個部分會分別對應三個模板文件,以下:
 
在模板內要想輸出結果集 能夠用相似於EL表達式輸出${}
 
header.ftl
 



[plain] view plaincopyprint?
01.companyName==>${h.companyName}<br/>  
02.address==>${h.address}<br/>  
footer.ftl 




[plain] view plaincopyprint?
01.des==>${f.des}<br/>  
02.  
03.<a href="http://localhost/htmlpage/UpdateFooter.do"> 更新Footer </a>  

 body.ftl,這個模板include以上兩個模板文件 




[plain] view plaincopyprint?
01.<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">  
02.<html>  
03.  <head>  
04.    <title>用戶列表</title>  
05.      
06.    <meta http-equiv="pragma" content="no-cache">  
07.    <meta http-equiv="cache-control" content="no-cache">  
08.    <meta http-equiv="expires" content="0">      
09.    <meta http-equiv="keywords" content="keyword1,keyword2,keyword3">  
10.    <meta http-equiv="description" content="This is my page">  
11.    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />  
12.    <!--  
13.    <link rel="stylesheet" type="text/css" href="styles.css">  
14.    -->  
15.   
16.  </head>  
17.    
18.  <body>  
19.  <#include "header.ftl" parse=true encoding="utf-8">  
20.  <hr/>  
21.  <a href="#">用戶列表</a><br/>  
22.  <table border="1">  
23.    <tr>  
24.        <td>用戶名</td>  
25.        <td>年齡</td>  
26.        <td>生日</td>  
27.        <td>id</td>  
28.        <td>操做</td>  
29.    </tr>  
30.    <#list users as user>  
31.        <tr>  
32.            <td>${user.name}</td>  
33.            <td>${user.age}</td>  
34.            <td>  
35.            ${user.birthday?string("yyyy-MM-dd HH:mm:ss")}  
36.            </td>  
37.            <td>${user.id}</td>  
38.            <td><a href="http://localhost/htmlpage/DelUser.do?id=${user.id}">刪除</a></td>  
39.        </tr>  
40.    </#list>  
41.      
42.  </table>  
43.<hr/>  
44.  <#include "footer.ftl" parse=true encoding="utf-8">  
45.  </body>  
46.</html>  

 這三個模板對應的三個實體類 

Footer.java
 



[java] view plaincopyprint?
01.package com.ajun.template.bean;  
02.  
03./** 
04. * @author ajun 
05. * @http://blog.csdn.net/ajun_studio   
06. **/  
07.public class Footer {  
08.  
09.    private String des;  
10.  
11.    public String getDes() {  
12.        return des;  
13.    }  
14.  
15.    public void setDes(String des) {  
16.        this.des = des;  
17.    }  
18.      
19.      
20.}  

 Header.java 




[java] view plaincopyprint?
01.package com.ajun.template.bean;  
02./** 
03. * @author ajun 
04. * @http://blog.csdn.net/ajun_studio   
05. **/  
06.public class Header {  
07.  
08.    private String companyName;  
09.      
10.    private String address;  
11.  
12.    public String getCompanyName() {  
13.        return companyName;  
14.    }  
15.  
16.    public void setCompanyName(String companyName) {  
17.        this.companyName = companyName;  
18.    }  
19.  
20.    public String getAddress() {  
21.        return address;  
22.    }  
23.  
24.    public void setAddress(String address) {  
25.        this.address = address;  
26.    }  
27.      
28.      
29.      
30.}  

 User.java 




[java] view plaincopyprint?
01.package com.ajun.template.bean;  
02.  
03.import java.util.Date;  
04.  
05.public class User {  
06.      
07.    private Integer id;  
08.  
09.    private String name ;  
10.      
11.    private int age;  
12.      
13.    private Date birthday;  
14.  
15.    public String getName() {  
16.        return name;  
17.    }  
18.  
19.    public void setName(String name) {  
20.        this.name = name;  
21.    }  
22.  
23.    public int getAge() {  
24.        return age;  
25.    }  
26.  
27.    public void setAge(int age) {  
28.        this.age = age;  
29.    }  
30.  
31.    public Date getBirthday() {  
32.        return birthday;  
33.    }  
34.  
35.    public void setBirthday(Date birthday) {  
36.        this.birthday = birthday;  
37.    }  
38.  
39.      
40.    public Integer getId() {  
41.        return id;  
42.    }  
43.  
44.    public void setId(Integer id) {  
45.        this.id = id;  
46.    }  
47.  
48.    public User(Integer id,String name, int age, Date birthday) {  
49.        super();  
50.        this.name = name;  
51.        this.age = age;  
52.        this.birthday = birthday;  
53.        this.id = id;  
54.    }  
55.  
56.    public User() {  
57.        super();  
58.    }  
59.      
60.      
61.}  

 下面模板一些業務邏輯操做,對這三個實體類 




[java] view plaincopyprint?
01.package com.ajun.template.service;  
02.  
03.import com.ajun.template.bean.Footer;  
04./** 
05. * @author ajun 
06. * @http://blog.csdn.net/ajun_studio   
07. **/  
08.public class FooterService {  
09.  
10.    private static Footer f = new Footer();  
11.    static{  
12.        f.setDes("北京-廊坊-好公司呢!!!!哇哈哈!!!");  
13.    }  
14.      
15.    public static void update(String des){  
16.        f.setDes(des);  
17.    }  
18.      
19.    public static Footer gerFooter(){  
20.        return f;  
21.    }  
22.}  

 


[java] view plaincopyprint?
01.package com.ajun.template.service;  
02.  
03.import com.ajun.template.bean.Header;  
04./** 
05. * @author ajun 
06. * @http://blog.csdn.net/ajun_studio   
07. **/  
08.public class HeaderService {  
09.  
10.    private static Header h = new Header();  
11.      
12.    static{  
13.        h.setAddress("北京朝陽CBD");  
14.        h.setCompanyName("上海唐秀!!!");  
15.    }  
16.      
17.    public static void update(String address,String companyName){  
18.        h.setAddress(address);  
19.        h.setCompanyName(companyName);  
20.    }  
21.      
22.    public static Header getHeader(){  
23.        return h;  
24.    }  
25.}  



[java] view plaincopyprint?
01.package com.ajun.template.service;  
02.  
03.import java.util.ArrayList;  
04.import java.util.Date;  
05.import java.util.List;  
06.  
07.import com.ajun.template.bean.User;  
08./** 
09. * @author ajun 
10. * @http://blog.csdn.net/ajun_studio   
11. **/  
12.public class UserService {  
13.  
14.    private static List<User> users = new ArrayList<User>();  
15.      
16.    static{  
17.        for(int i=0;i<10;i++){  
18.            User u = new User(i,"ajun"+i,i+10,new Date());  
19.            users.add(u);  
20.        }  
21.    }  
22.      
23.    public static List<User> getUsers(){  
24.        return users;  
25.    }  
26.      
27.    public static void delete(int index){  
28.        for(int i=0 ;i<users.size();i++){  
29.            User u = users.get(i);  
30.            if(u.getId()==index){  
31.                users.remove(u);  
32.                //users.remove(index);  
33.            }  
34.        }  
35.    }  
36.}  

 上面主要是模板你的一些業務和dao層得操做,所以沒有涉及數據庫的操做,主要是爲實驗。 

生成html對外調用的方法,會用到FreeMarkertUtil這個類 這個類得代碼上面已經給出。
 



[java] view plaincopyprint?
01.package com.ajun.template.client;  
02.  
03.import java.io.Writer;  
04.import java.util.HashMap;  
05.import java.util.List;  
06.import java.util.Map;  
07.  
08.import com.ajun.template.bean.Footer;  
09.import com.ajun.template.bean.Header;  
10.import com.ajun.template.bean.User;  
11.import com.ajun.template.service.FooterService;  
12.import com.ajun.template.service.HeaderService;  
13.import com.ajun.template.service.UserService;  
14.import com.ajun.template.utils.FreeMarkertUtil;  
15.  
16./** 
17. * @author ajun 
18. * @http://blog.csdn.net/ajun_studio   
19. **/  
20.public class ProcessClient {  
21.  
22.    private static Map<String,Object> root = new HashMap<String,Object>();  
23.  
24.    /** 
25.     * 調用FreeMarkertUtil.java 
26.     * FreeMarkertUtil.processTemplate("body.ftl", root, out); 
27.     * 來生成html文件 
28.     * @param out 
29.     */  
30.    public static void processBody(Writer out){  
31.        Header h = HeaderService.getHeader();  
32.        root.put("h", h);  
33.        Footer f = FooterService.gerFooter();  
34.        root.put("f", f);  
35.        List<User> users = UserService.getUsers();  
36.        root.put("users", users);  
37.        FreeMarkertUtil.processTemplate("body.ftl", root, out);  
38.    }  
39.      
40.}  
此時我會提供一個servlet在客戶端進行第一次請求的時候 我會調用這個ProcessClient來生成html頁面,以後每次訪問就能夠直接訪問html,來作到真正的靜態化了
 



[java] view plaincopyprint?
01.package com.ajun.template.servlet;  
02.  
03.import java.io.File;  
04.import java.io.FileOutputStream;  
05.import java.io.IOException;  
06.import java.io.OutputStreamWriter;  
07.import java.io.Writer;  
08.  
09.import javax.servlet.ServletConfig;  
10.import javax.servlet.ServletException;  
11.import javax.servlet.http.HttpServlet;  
12.import javax.servlet.http.HttpServletRequest;  
13.import javax.servlet.http.HttpServletResponse;  
14.  
15.import com.ajun.template.client.ProcessClient;  
16.import com.ajun.template.utils.DirectoryFilter;  
17.import com.ajun.template.utils.FreeMarkertUtil;  
18.  
19./** 
20. * @author ajun 
21. * @http://blog.csdn.net/ajun_studio   
22. **/  
23.public class Index extends HttpServlet {  
24.  
25.    private static final long serialVersionUID = 7474850489594438527L;  
26.  
27.    public Index() {  
28.        super();  
29.    }  
30.  
31.      
32.    public void doGet(HttpServletRequest request, HttpServletResponse response)  
33.            throws ServletException, IOException {  
34.  
35.        this.doPost(request, response);  
36.    }  
37.  
38.      
39.    public void doPost(HttpServletRequest request, HttpServletResponse response)  
40.            throws ServletException, IOException {  
41.        //html生成以後存放的路徑  
42.        String dirPath = request.getSession().getServletContext().getRealPath("/templates/html");  
43.        File path = new File(dirPath);  
44.        //生成的文件的名字  
45.        String indexFileName = "index.html";  
46.        /** 
47.         * 判斷是否已經存在該html文件,存在了就直接訪問html ,不存在生成html文件 
48.         */  
49.        String[] indexfileList = path.list(new DirectoryFilter(indexFileName));  
50.        if(indexfileList.length<=0){  
51.            Writer out = new OutputStreamWriter(new FileOutputStream(dirPath+"/"+indexFileName),"UTF-8");  
52.            //生成html文件  
53.            ProcessClient.processBody(out);  
54.            request.getRequestDispatcher("/templates/html/index.html").forward(request, response);   
55.        }else{  
56.            request.getRequestDispatcher("/templates/html/"+indexfileList[0]).forward(request, response);   
57.        }  
58.          
59.      
60.    }  
61.  
62.      
63.  
64.    /** 
65.     * 初始化模板配置,供之後得到模板,在init里加載也主要是爲保證Configuration實例惟一 
66.     */  
67.    public void init(ServletConfig config) throws ServletException {  
68.        String templateDir = config.getInitParameter("templateDir");  
69.        FreeMarkertUtil.initConfig(config.getServletContext(), templateDir);  
70.    }  
71.  
72.      
73.}  
web.xml配置 




[html] view plaincopyprint?
01.<servlet>  
02.   <description>This is the description of my J2EE component</description>  
03.   <display-name>This is the display name of my J2EE component</display-name>  
04.   <servlet-name>Index</servlet-name>  
05.   <servlet-class>com.ajun.template.servlet.Index</servlet-class>  
06.   <init-param>  
07.    <param-name>templateDir</param-name>模板存放位置,是基於app的根目錄的  
08.    <param-value>/templates</param-value>  
09.   </init-param>  
10.   <load-on-startup>3</load-on-startup>爲了啓動的時候初始化模板配置  
11. </servlet>  
12.  
13. <servlet-mapping>  
14.   <servlet-name>Index</servlet-name>  
15.   <url-pattern>/Index.do</url-pattern>  
16. </servlet-mapping>  

 部署到tomcat上,輸入:http://localhost/htmlpage/Index.do 

頁面效果:
 


頁面是作好了,可是內容變化了 ,更新怎麼辦呢,我這裏是當列表內容變化以後 ,我是刪除原來的html ,利用模板而後從新生成的符合新結果的html頁面
 
當我刪除一條的時候,我會從新生成html頁面,可是因爲瀏覽器緩存的緣由,便是你刪除了,從新生成了新html頁面,但是瀏覽器仍是保存原來的頁面,不刷新兩次是不行的,
 
這裏我採用的沒更新的時候,都會給這個html改個名字,讓瀏覽器去加載最新的,就能夠了
 
具體的刪除操做以下:
 



[java] view plaincopyprint?
01.package com.ajun.template.servlet;  
02.  
03.import java.io.File;  
04.import java.io.FileOutputStream;  
05.import java.io.IOException;  
06.import java.io.OutputStreamWriter;  
07.import java.io.Writer;  
08.import java.util.UUID;  
09.  
10.import javax.servlet.ServletException;  
11.import javax.servlet.http.HttpServlet;  
12.import javax.servlet.http.HttpServletRequest;  
13.import javax.servlet.http.HttpServletResponse;  
14.  
15.import com.ajun.template.client.ProcessClient;  
16.import com.ajun.template.service.UserService;  
17.import com.ajun.template.utils.DirectoryFilter;  
18./** 
19. * @author ajun 
20. * @http://blog.csdn.net/ajun_studio   
21. **/  
22.public class DelUser extends HttpServlet {  
23.  
24.      
25.    public void doGet(HttpServletRequest request, HttpServletResponse response)  
26.            throws ServletException, IOException {  
27.            this.doPost(request, response);  
28.    }  
29.  
30.    //刪除用戶  
31.    public void doPost(HttpServletRequest request, HttpServletResponse response)  
32.            throws ServletException, IOException {  
33.          
34.        String id = request.getParameter("id");  
35.        UserService.delete(Integer.valueOf(id));  
36.          
37.        //生成html的位置  
38.        String dirPath = request.getSession().getServletContext().getRealPath("/templates/html");  
39.        //文件名字  
40.        String indexFileName = "index.html";  
41.          
42.        //刪除原來的文件  
43.        delOldHtml(dirPath,indexFileName);  
44.          
45.        //防止瀏覽器緩存,用於從新生成新的html  
46.        UUID uuid = UUID.randomUUID();  
47.        Writer out = new OutputStreamWriter(new FileOutputStream(dirPath+"/"+uuid+indexFileName),"UTF-8");  
48.        ProcessClient.processBody(out);  
49.        response.sendRedirect("templates/html/"+uuid+"index.html");  
50.    }  
51.      
52.    /** 
53.     * 刪除原來的html文件 
54.     * @param htmlDir 
55.     * @param htmlName 
56.     */  
57.    private void delOldHtml(String htmlDir,String htmlName){  
58.        File path = new File(htmlDir);  
59.        String[] indexfileList = path.list(new DirectoryFilter(htmlName));  
60.        if(indexfileList.length>=0){  
61.            for(String f:indexfileList){  
62.                File delf = new File(htmlDir+"/"+f);  
63.                delf.delete();  
64.            }  
65.        }  
66.    }  
67.  
68.}  

 經過以上操做,每次更新html,就能夠不解決瀏覽器緩存的問題了。 

還有一個工具類須要介紹,就是判斷是否已經生成了特定的html文件的java類
 



[java] view plaincopyprint?
01.package com.ajun.template.utils;  
02.  
03.import java.io.File;  
04.import java.io.FilenameFilter;  
05./** 
06. * @author ajun 
07. * @http://blog.csdn.net/ajun_studio   
08. **/  
09.public class DirectoryFilter implements FilenameFilter {  
10.  
11.    String myString;  
12.    public DirectoryFilter(String myString)  
13.    {  
14.        this.myString = myString;  
15.    }  
16.      
17.    public boolean accept(File dir,String name)  
18.    {   //FilenameFilter.accept(File dir, String name)   
19.       // 測試指定文件是否應該包含在某一文件列表中。  
20.        String f= new File(name).getName();  
21.        if(f.contains(myString) || f.equals(myString)){  
22.            return true;  
23.        }  
24.        return false;  
25.    }  
26.  
27.}  

 
到這裏整個靜態化就完成了,靜態化更新機制,是根據你本身項目的業務進行定製的,能夠定時生成html文件,也能夠須要手動生成。
 
項目結構圖以下:
 




記住:網站不是全部的頁面都是須要靜態化的,主要是一些實時性不是很高的數據頁面進行靜態化(來提升訪問速度),其餘都是經過僞靜態來實現的,就是重寫utl。
 
頁面靜態化不是提升網站性能的惟一途徑,還能夠利用一些緩存產品來實現。
 



經常使用FreeMarker資源
 

官網主頁:http://www.freemarker.org/
 

Eclipse插件JbossTool:http://www.jboss.org/tools/download/
 

中文文檔:https://sourceforge.net/projects/freemarker/files/chinese-manual/FreeMarker_Manual_zh_CN.pdf/download
相關文章
相關標籤/搜索