項目背景
公司開發新的項目,選擇了分佈式系統架構,同時項目中有大量圖文展示的需求,考慮到開發效率和性能問題,需要集成cms的功能,程序實現後臺錄入數據靜態化。
技術架構
java開發、jdk1.8,項目整體的技術架構大致是這樣的:
前端採用的bootstrap、extJs,權限採用shiro框架,搜索採用solr或者lucene(這個暫時沒有實現)。
過往問題
在這個項目之前,公司有另一套cms系統,做爲公司主要經營業務的基礎,它有很多優點,最主要的優點是定製性強,能適應各種前端版式的頁面。另一個優點是開發速度特別快,基於模版標籤開發,屏蔽了代碼級的複雜度和開發人員水平的差距。
弱點有三個
一、在於標籤庫的實現,完全是基於正則替換,然後處理標籤表述的邏輯進行查庫、處理。標籤沒有實現語法樹,也沒有上下文,導致了多個標籤函數嵌套的時候,某些時候無法正確實現替換邏輯。
二、沒有應用更多的緩存技術(只實現了jvm內的HashMap緩存),數據量大的任務,生成速度在查庫上,有明顯的瓶頸。
三、生成失敗的模版,沒有給出足夠明確的錯誤提示,輔助使用者調試錯誤。
基本思路
整個大體流程
整個生成是基於freemarker模版引擎,項目初期,曾經對比過velocity與freemarker,因爲freemarker最後一次更新時間比較近,最終選擇了freemarker,其實兩者應該差不多。
關鍵點
先曬一下目前的類結構
一、自定義標籤與自定義函數
freemarker支持實現TemplateMethodModelEx接口的自定義函數,及實現TemplateDirectiveModel的自定義標籤。上面代碼,fun包下面的就是自定義函數,tags包下面的是自定義標籤。
兩者的區別,
1、從調用方法來看,
arts(10, 10, ”, ”)
這種是自定義函數 <@arts menu="15" pagesize="15" sort="" sql="";arts, page></@arts>
這種是自定義標籤
2、自定義函數有返回值,可以再套入別的函數進行計算或者迭代。自定義標籤沒有返回值,它會返回幾個變量,作用域只在標籤體內部。
3、自定義標籤傳入的參數有Environment、TemplateDirectiveBody等等,即可以獲取其他位置定義的模版變量,又可以追加變量,還可以輸出字符串文本到最終生成的頁面上。自定義函數,只能僅僅返回處理後的返回值。
二、freemarker生成靜態頁到指定路徑的方法
這個問題,基本上查api或者別的博客就能找到具體的方法,但是有些資料可能不準確,這裏我直接把我實現了的核心代碼貼出來。我使用的freemarker版本是2.3.23。
//成員變量
private static Configuration config = new Configuration(Configuration.VERSION_2_3_25);
//config設置
config.setLocale(Locale.CHINA);
config.setDefaultEncoding("utf-8");
config.setEncoding(Locale.CHINA, "utf-8");
config.setObjectWrapper(new DefaultObjectWrapper(Configuration.VERSION_2_3_25));
StringTemplateLoader stringLoader = new StringTemplateLoader();
//content是模版內容的字符串 stringLoader.putTemplate("siteTemplate",content);
config.setTemplateLoader(stringLoader);
try(FileWriter out = new FileWriter(filePath.toFile());) {
Template template = config.getTemplate("siteTemplate","utf-8");
config.setCustomAttribute("customDataModel", dataModel);
template.process(dataModel, out);
out.flush();
} catch (Exception e) {
//異常處理
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
三、列表頁分頁,如何根據一個模版,動態生成出一或多個分頁面
這個問題包括很多細節,比如生成後的地址規則。假設目標地址是news_index.html,那麼第二頁就不能用這個地址,我的處理方式是news_index_x.html,x代表第幾頁,可以是2~n。但是第一頁就是原地址。
最關鍵
如果你也需要基於freemarker實現這個功能,大概機會發現,最關鍵的問題,是如何在生成第一頁的時候,知道還有第二頁需要生成。因爲產生多個分頁面,一般是在自定義標籤裏面,比如有一個循環數據列表的標籤,發現一頁10條,無法顯示完,怎樣告知生成階段的代碼(就是上面那一段)。這是兩個完全不同的代碼區域和處理階段。
參考了另一款開源cms,我採取了一種有點技巧性的做法。核心思想就是,上面說到自定義標籤,可以在生成後的頁面上,任意輸出字符串。假設標籤中判斷還有下一頁,需要生成,就在頁面上輸出一段註釋,本頁面生成完以後,重新讀取生成的頁面,發現有包含此種註釋,則繼續生成下一頁。
下面貼一些代碼
//輸出還有下一頁的標識
env.getOut().write(LBLCommon.getHasNextPage(totalRecord, totalPage, pageIndex));
public static String getHasNextPage(int recordCount, int totalPage, int pageindex) {
return hasNextPage+"_"+recordCount+"_"+totalPage+"_"+pageindex+"-->";
}
- 1
- 2
- 3
- 4
- 5
- 6
//生成靜態化頁面 this.writeUseFreeMarker(name, content, url, dataModel);
//分頁機制,查詢是否需要分頁————————————————————
this.morePageProcess(name, dataModel, url, rootPath, content, entry);
//處理分頁
private void morePageProcess(String name, Map<String, Object> dataModel, String url, String rootPath, String content, Map.Entry<String, String> entry) throws Exception{
String html = this.readFile(url);
LBLPagerModel pager = checkHasNextPage(html);
if (pager != null) {
//重設置一個基礎的總記錄數
dataModel.put("totalRecord", pager.getRecordCount());
for (int i = 2; i<= pager.getTotalPage(); i ++) {
//設置頁碼
setPageIndex(dataModel, i);
url = rootPath + LBLCommon.getPageUrl(entry.getValue(), i);
this.writeUseFreeMarker(name, content, url, dataModel);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
整個靜態化的部分,還有很多別的細節和坑,比如標籤參數的轉型問題,生成靜態頁過程中模版exception的catch和反饋,查詢生成隊列情況的監控模塊(基於mongo),標籤設計,自動生成、定時生成、過濾生成…
總結
靜態化的功能,與業務結合的很緊密,好的標籤設計非常重要,這個可能得靠多踩坑才能提升一點躲開雷區的能力。另外,這次的開發過程,也讓我更深刻的認識到,深思熟慮,先慢後快,不寫一行垃圾代碼,纔是最節約時間的開發模式。