01.15 SpringBoot&FreeMarker之零配置自定義指令(附Git源碼)

Freemarker是一個基於Java語言的多用途,輕量級模板引擎。它提供了很多內置的語法指令,例如條件選中,迭代,賦值,字符串和算術運算,格式化,宏定義等等。Freemarker最初是為了在Web MVC應用程序框架中生成HTML頁面而創建的,但它並不綁定到Servlet,HTML或者任何其他與Web相關的內容。此外,Freemarker還應用與非Web應用程序環境中。

SpringBoot&FreeMarker之零配置自定義指令(附Git源碼)

​ Freemarker自身已經提供了很多有用的語法指令,通過使用這些指令,能夠很輕鬆的讀取和操作Model中的數據。除此之外,Freemarker還允許開發者擴展Freemarker語法指令,自定義滿足具體需求的新指令。本文將著重講解利用Java的反射機制去優化SpringBoot與Freemarker整合後零配置自定義指令。

1. FreeMarker是什麼?

​ 在開始之前,先花一兩分鐘回顧一下Freemarker的基本概念。Apache FreeMarker是一個基於Java庫的視圖模板引擎,主要用於根據模板和模型數據渲染生成文本輸出(HTML網頁,電子郵件,配置文件,源代碼等等)。FreeMarker模板使用其特定的模板語言(FTL)進行編寫,這是一種簡單的專用標記語言。通常,使用通用編程語言(如Java)來準備模型數據;然後,Apache FreeMarker使用模板渲染準備好的數據。在模板中,開發者可以專注於如何顯示數據,在模型中,你則專注於數據的包裝。下面通過一張官圖瞭解一下FreeMarker的基本工作原理:

SpringBoot&FreeMarker之零配置自定義指令(附Git源碼)

2. SpringBoot整合Apache FreeMaker

​ 在SpringBoot中使用Apache FreeMarker(以下簡稱FreeMarker)是一件很容易的事情。首先,在pom.xml文件中添加Freemarker的starter依賴,IDE會根據當前SpringBoot的版本信息自動下載合適版本的FreeMarker依賴包到類路徑下,pom.xml的配置如下:

<code>...
<dependencies>
...
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-freemarker/<artifactid>
/<dependency>
...
/<dependencies>
...
/<code>

然後,你只需要在application.yml或application.properties配置文件中指定FreeMarker的相關參數(例如編碼方式,緩存,模板前綴/後綴,格式化樣式等等)即可。通常,SpringBoot已經為FreeMarker提供了一些默認的配置,我們可以在org.springframework.boot.autoconfigure.freemarker包下的FreeMarkerProperties類中找到相關的默認值,內容如下:

<code>@ConfigurationProperties(
prefix = "spring.freemarker"
)
public class FreeMarkerProperties extends AbstractTemplateViewResolverProperties {
public static final String DEFAULT_TEMPLATE_LOADER_PATH = "classpath:/templates/";
public static final String DEFAULT_PREFIX = "";
public static final String DEFAULT_SUFFIX = ".ftl";
private Map<string> settings = new HashMap();
private String[] templateLoaderPath = new String[]{"classpath:/templates/"};
private boolean preferFileSystemAccess = true;

public FreeMarkerProperties() {
super("", ".ftl");
}

public Map<string> getSettings() {
return this.settings;
}

public void setSettings(Map<string> settings) {
this.settings = settings;
}

public String[] getTemplateLoaderPath() {
return this.templateLoaderPath;
}

public boolean isPreferFileSystemAccess() {
return this.preferFileSystemAccess;
}

public void setPreferFileSystemAccess(boolean preferFileSystemAccess) {
this.preferFileSystemAccess = preferFileSystemAccess;
}

public void setTemplateLoaderPath(String... templateLoaderPaths) {
this.templateLoaderPath = templateLoaderPaths;
}
}
/<string>/<string>/<string>/<code>

我們可以在配置文件中對自動配置的默認值進行覆蓋,以滿足自身的需求,例如我們可以做如下的配置:

<code>spring:
freemarker:
cache: false
suffix: .html
settings:
datetime_format: yyyy-MM-dd HH:mm
number_format: 0.##
/<code>

在上述的配置中,將默認的模板後綴名由.ftl修改為.html,關閉了FreeMarker的緩存模板數據,同時設置了模板中日期的格式化模板和數值的格式化模板。

​ 經過上述的兩個步驟,SpringBoot與Apache FreeMarker的整合工作就完成了。接下來,將瞭解如何擴展FreeMarker的語法指令。

3. 自定義FreeMarker語法指令

​ 自定義FreeMarker語法指令功能讓開發者在操作FreeMarker模板時更加靈活多樣。基於FreeMarker實現自定義指令只需讓自定義指令類實現TemplateDirectiveModel接口並重寫execute()方法即可。execute()方法是實現自定義指令的核心。

注意:

TemplateDirectiveModel是在FreeMarker 2.3.11版本中才引入的接口,如果你使用的是舊版本的FreeMarker,則需要實現TemplateTransformModel類。

下面是實現FreeMarker的一個示例代碼:

SpringBoot&FreeMarker之零配置自定義指令(附Git源碼)

接下來,我們只需要使用FreeMarker的Configuration類,將此自定義指令類的實例添加到FreeMarker共享變量中便可在模板中使用自己的指令。下面是配置示例:

<code>Configuration cfg = new Configuration(Configuration.VERSION_2_2_27);
cfg.setSharedVariable("myDirective",new MyTemplateDirective());
/<code>

注意:

在設置FreeMarker共享變量時,需要為自定義指令指定一個名稱。

4. SpringBoot中配置自定義指令

​ FreeMarker自定義指令的配置方式有很多種,在SpringBoot中配置FreeMarker自定義自定更為簡單,下面介紹其中一種方式。我們可以利用SpringBoot的組件掃描機制,統一管理FreeMarker自定義指令的配置工作,例如:

<code>@Component
public class FreemarkerCustomDirectveManager {

@Autowired
private Configuration cfg;
@Autowired
private ApplicationContext app;

@PostConstruct
public void setSharedVariable()throws TemplateModelException{
cfg.setSharedVariable ( "direct1",app.getBean ( Direct1.class ) );
cfg.setSharedVariable ( "direct2",app.getBean ( Direct2.class ) );
cfg.setSharedVariable ( "direct3",app.getBean ( Direct3.class ) );
cfg.setSharedVariable ( "direct4",app.getBean ( Direct4.class ) );
cfg.setSharedVariable ( "direct5",app.getBean ( Direct5.class ) );
}
}
/<code>

在上述的配置中,@Component註解的作用是將普通的POJO實例化到Spring的容器中,而@PostConstruct註解的作用是在服務器加載Servlet的時候有且只執行一次被其標註的方法。另外,我們通過ApplicationContext(應用上下文)獲取自定義指令類的實例,並將其設置為FreeMarker的共享變量。

提示:

在Spring Framework中,被@PostConstruct註釋的方法會在類的init()方法執行之前,構造方法執行之後被執行,該註解註釋的方法在整個Bean初始化過程中被執行的順序如下:

Constructor(構造方法) -> @Autowired(依賴注入)-> @PostConstruct(註釋方法)

​ 然而,上述的這種方式有一個不好的地方,當新增一個FreeMarker自定義指令時,就需要手動修改一次配置代碼,當項目中自定義指令數量多的時候,指令名稱的命名規範以及配置將是一件很繁重的事情。接下來,將結合Java反射機制和Java抽象類來優化FreeMarker自定義指令配置問題。

5.優化FreeMarker自定義指令配置

​ 優化配置的重點在於零配置,首先,可以通過一定的規則生成指令的名稱,然後,通過Java的反射機制獲取自定義指令對象實例,最後將實例添加到FreeMarker的共享變量中,變量名為具有一定規則的名稱。

5.1 抽象自定義指令

​ 首先,我們需要定義一個抽象的自定義指令類,在此類中主要完成指令的自動配置工作,指令的細節將有具體的子類負責。由於是在SpringBoot中使用FreeMarker,我們可以用@Service此抽象指令類進行標記,這樣改類的子類就都會被Spring容器所管理,其次,使用@PostConstruct註解標註指令配置方法,最後,將excute()方法暴露給子類去執行。抽象自定義指令類的源代碼清單如下:

<code>@Service
public abstract class TemplateDirective extends ApplicationObjectSupport implements TemplateDirectiveModel {

@Autowired
FreeMarkerConfigurer cfg;

@PostConstruct
public void config() throws TemplateModelException{
//1. 獲取當前類名
String className = this.getClass().getName();
//2.截取類名(例如Directive.java截取為Directive)
className = className.substring(className.lastIndexOf(".")+1);
//3.獲得Bean的名稱
String beanName = StringUtils.uncapitalize(className);
//4.生成指令名稱
String directiveName = "ramostear_"+ NameUtils.humpToUnderline(beanName);
//5.設置指令
cfg.getConfiguration()
.setSharedVariable(tagName,this.getApplicationContext().getBean(beanName));
}

@Override
public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException {
try {
execute(new DirectiveHandler(env, params, loopVars, body));
}catch (Exception ex){
try {

throw ex;
} catch (Exception e) {
e.printStackTrace();
}
}
}

//暴露給子類
abstract public void execute(DirectiveHandler handler) throws Exception;

}
/<code>

在此類中,自定義的DirectiveHandlere類對FreeMarker的Enviroment,params,TemplateModel以及TemplateDirectiveBody做了封裝,其源代碼清單如下:

<code>public class DirectiveHandler {

private Environment env;

private Map<string> params;

private TemplateModel[] loopVars;

private TemplateDirectiveBody body;

private Environment.Namespace namespace;


/**
* 構造函數
* @param env
* @param params
* @param loopVars
* @param body
*/
public DirectiveHandler(Environment env,Map<string> params,TemplateModel[] loopVars,TemplateDirectiveBody body){
this.env = env;
this.loopVars = loopVars;
this.params = params;
this.body = body;
this.namespace = env.getCurrentNamespace();
}

public void render() throws IOException, TemplateException {
Assert.notNull(body, "must have template directive body");
body.render(env.getOut());
}


...

/**
* 包裝對象
* @param object
* @return
* @throws TemplateModelException
*/
public TemplateModel wrap(Object object) throws TemplateModelException {
return env.getObjectWrapper().wrap(object);
}

/**
* 獲取局部變量
* @param name
* @return
* @throws TemplateModelException
*/
public TemplateModel getEnvModel(String name) throws TemplateModelException {
return env.getVariable(name);
}

public void write(String text) throws IOException {
env.getOut().write(text);
}

private TemplateModel getModel(String name) {
return params.get(name);
}
/<string>/<string>/<code>

另外,NameUtils.humpToUnderline()方法主要是將類名的駝峰命名格式轉換為下劃線分割的新式,例如:

<code>BeanNameFactory ->NameUtils.humpToUnderline()-> bean_name_factory
/<code>

生成指令名稱的格式大家可以自定義,這裡僅僅提供其中一種轉換方式。

接下來,我們便可繼承TemplateDirective類,實現具體的自定義FreeMarker指令,而無需進行任何配置。

5.2 實現自定義指令

​ 前面我們已經對FreeMarker自定義指令做了抽象,並在其中完成了指令的自動配置工作。接下來,我們只需要繼承抽象的自定義指令類,專注完成具體的業務邏輯即可。例如,我們需要有一個生成文章歸檔的自定義指令,下面是具體的實現細節:

<code>@Service
public class Archives extends TemplateDirective {

@Autowired
private ArchiveService archiveService;

@Override
public void execute(DirectiveHandler handler) throws TemplateException, IOException {
List<archivevo> archiveVOList = archiveService.archives();
handler.put("results",archiveVOList).render();
}
}
/<archivevo>/<code>

只需這樣,無需其他的配置,我們便可在FreeMarker模板中使用@ramostear_archives>指令來獲取文章歸檔信息。例如:

<code>


博客歸檔








  • #list>
    @ramostear_archives>


/<code>

6. 總結

​ 通過上述的重構與改造,使得FreeMarker自定義標籤的配置工作得到簡化,同時指令的命名也以統一的格式和規範進行管理。另外,由於統一了命名方式,開發人員在看到自定義指令類時便可知道模板中對應的指令名稱是什麼,提高了代碼的可讀性。

最後附上項目測試短動畫和github項目地址。

SpringBoot&FreeMarker之零配置自定義指令(附Git源碼)

你可以訪問下面的地址獲取本文附屬的項目源碼:

https://github.com/ramostear/springboot-freemarker


SpringBoot&FreeMarker之零配置自定義指令(附Git源碼)


分享到:


相關文章: