Thymeleaf引擎支持Multi Prefix

  • 2019 年 10 月 8 日
  • 筆記

最近團隊的一個項目在重構,希望引入Thymeleaf減少頁面端的代碼複雜性。在重構過程中,發現html文件需要保存在多個不同的目錄中,但Thymeleaf缺省的實現不支持這種方式。

1 背景

Maven項目,前端使用SpringMVC,沒有使用任何模板引擎。所有的頁面內容,都是通過靜態HTML+AJAX+JSON形式實現。

1.1 項目結構

Html文件,通過mvc:resource 定義路徑。

1.1.1 Html保存路徑

/hardess_finance/src/main/webapp/WEB-INF/htmls

在該目錄放一個Demo文件/demo/hello.html

1.1.2 Spring配置文件

<mvc:resources mapping="/**/**.html" location="/WEB-INF/htmls/"/>

項目啟動後,瀏覽器訪問 http://localhost:8080/demo.hello.html,就可以訪問到demo文件。

1.2 添加Thymeleaf支持

Spring Boot 項目缺省使用Thymeleaf模板,但普通SpringMVC項目,需要手工添加支持。大致步驟包括:

1.2.1 Pom.xml增加thymeleaf dependency

<dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf</artifactId> <version>3.0.6.RELEASE</version></dependency><dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring3</artifactId> <version>3.0.6.RELEASE</version></dependency>

1.2.2 修改Spring Config文件

<bean id="templateResolver" class="org.thymeleaf.spring3.templateresolver.SpringResourceTemplateResolver"> <property name="prefix"><value>/WEB-INF/html/</value></property> <property name="suffix"><value>.html</value></property> <property name="templateMode"><value>HTML</value></property> <property name="characterEncoding"><value>UTF-8</value></property> <property name="cacheable" value="false"/></bean><bean id="templateEngine" class="org.thymeleaf.spring3.SpringTemplateEngine"> <property name="templateResolver" ref="templateResolver" /></bean><bean class="org.thymeleaf.spring3.view.ThymeleafViewResolver"> <property name="templateEngine" ref="templateEngine" /> <property name="characterEncoding" value="UTF-8"/></bean>

<property name="prefix"><value>/WEB-INF/html/</value></property>這個參數,指定的就是html文件的保存路徑。

1.2.3 添加Thymeleaf Controller

在代碼中增加一個Controller

@Controllerpublic class ThymeleafCommonController { @RequestMapping(value = { "/**/**.html" }) public ModelAndView index(HttpServletRequest request) { return new ModelAndView(); }}

至此,項目重新啟動後,所有html的訪問,都已經通過Thymeleaf引擎,html中能夠使用 th:text 等各種thymeleaf語法。

1.3 項目重構希望添加另一個html保存路徑

在重構過程中,希望將html文件保存到新的目錄 src/main/resources/templates目錄,原因有二:

1、 這是Spring Boot項目的缺省模板路徑,適應將來可能升級到SpringBoot的需求。

2、 重構後的代碼,希望同原目錄有所區分,簡化開發複雜度。

1.3.1 簡單嘗試

在 mvc:resources標籤中,location可以是用逗號隔開的多個路徑,如

<mvc:resources mapping="/scripts/**" location="/WEB-INF/scripts/, classpath:/static/scripts/"/>

因此,嘗試在spring config配置文件中,嘗試修改配置

<property name="prefix"><value>/WEB-INF/html/,classpath:/templates/</value></property>

重啟後測試,發現項目無法工作,原有的界面都無法加載了。

1.3.2 原因

Debug了一下thymeleaf的相關源碼,發現它使用下面的語句生成最終的完整路徑名,並沒有判斷 prefix 是否是逗號分隔的數組。

AbstractConfigurableTemplateResolver.computeResourceName(…)

return prefix + unaliasedName + suffix;

2 Thymeleaf源碼解讀

解讀Thymeleaf的源代碼,發現幾個相關類

2.1 相關類結構

2.2 final computeTemplateResource()

這個函數會讀取配置的prefix,並調用後續方法生成 resource name。注意,這個方式是 final ,無法重載。

@Overrideprotected final ITemplateResource computeTemplateResource( final IEngineConfiguration configuration, final String ownerTemplate, final String template, final Map<String, Object> templateResolutionAttributes) { final String resourceName = computeResourceName(configuration, ownerTemplate, template, this.prefix, this.suffix, this.forceSuffix, this.templateAliases, templateResolutionAttributes); return computeTemplateResource(configuration, ownerTemplate, template, resourceName, this.characterEncoding, templateResolutionAttributes);}

標紅部分讀取prefix參數值。

2.3 computeResourceName()

實際生成resource name(代碼有刪減,只保留核心部分)

protected String computeResourceName( final IEngineConfiguration configuration, final String ownerTemplate, final String template, final String prefix, final String suffix, final boolean forceSuffix, final Map<String, String> templateAliases, final Map<String, Object> attributes) { … String unaliasedName = templateAliases.get(template); if (unaliasedName == null) { unaliasedName = template; } … // hasPrefix && shouldApplySuffix return prefix + unaliasedName + suffix;}

2.4 computeResolvable()

判斷資源文件是否可用。

if (this.resolvablePatternSpec.isEmpty()) { return true;}return this.resolvablePatternSpec.matches(template);

這個代碼,實際沒有校驗html文件是否存在,只要語法不出錯即可。當系統定義了多個ITemplateResolver時,引擎回依次調用每個實例的computeResolvable()方法,如果返回null,則依次檢查下一個resolver,直到得到一個非空值。

3 解決方案

基於前面的代碼分析,要解決我們的需求,首先我們需要解決的是判斷資源文件是否真實存在。

3.1 判斷文件是否存在

通過Spring項目的ApplicationContext判斷文件是否存在的代碼片段。

resolvable = false;Resource resource = applicationContext.getResource(location);if (resource != null && resource.exists()) { resolvable = true;}

為了驗證解決方案的可行性,增加了一個新的html文件在 src/main/resources/templates/demo/world.html

3.2 方案一:定義多個 TemplateResolver

3.2.1 Custom TemplateResolver

Spring提供的實現類SpringResourceTemplateResolver,代碼比較簡單,我選擇直接替換該類,而不是從它繼承而來。

public class CodeStoryTemplateResolver extends AbstractConfigurableTemplateResolver implements ApplicationContextAware { private ApplicationContext applicationContext = null; public CodeStoryTemplateResolver() { super(); } public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } @Override protected boolean computeResolvable( IEngineConfiguration configuration, String ownerTemplate, String template, Map<String, Object> templateResolutionAttributes) { boolean resolvable = super.computeResolvable(configuration, ownerTemplate, template, templateResolutionAttributes); if (resolvable) { // 判斷文件是否存在 resolvable = false; String pathName = getPrefix() + template + getSuffix(); Resource resource = applicationContext.getResource(pathName); if (resource != null && resource.exists()) { resolvable = true; } } return resolvable; } @Override protected ITemplateResource computeTemplateResource( final IEngineConfiguration configuration, final String ownerTemplate, final String template, final String resourceName, final String characterEncoding, final Map<String, Object> templateResolutionAttributes) { return new SpringResourceTemplateResource( this.applicationContext, resourceName, characterEncoding); }}

3.2.2 修改Spring配置

<bean id="webinfoTemplateResolver" class="….CodeStoryTemplateResolver"> <property name="prefix"> <value>/WEB-INF/</value> </property> <property name="suffix"> <value>.html</value> </property> <property name="templateMode"> <value>HTML</value> </property> <property name="characterEncoding"> <value>UTF-8</value> </property> <property name="cacheable" value="false"/></bean> <bean id="webinfoTemplateResolver" class="….CodeStoryTemplateResolver"> <property name="prefix"> <value>classpath:/templates/</value> </property> <property name="suffix"> <value>.html</value> </property> <property name="templateMode"> <value>HTML</value> </property> <property name="characterEncoding"> <value>UTF-8</value> </property> <property name="cacheable" value="false"/></bean><bean id="templateEngine" class="org.thymeleaf.spring3.SpringTemplateEngine"> <property name="templateResolvers"> <set> <ref bean="webinfoTemplateResolver" /> <ref bean="classpathTemplateResolver" /> </set> </property></bean><bean class="org.thymeleaf.spring3.view.ThymeleafViewResolver"> <property name="templateEngine" ref="templateEngine" /> <property name="characterEncoding" value="UTF-8"/></bean>

重啟系統測試,/demo/hello.html和/demo/world.html都能正常訪問。

3.3 方案二:一個TemplateResolver支持prefixes

3.3.1 Custom TemplateResolver

理想的方案,是重載函數computeTemplateResource(),但這個函數被定義為final,無法重載,只好退而求其次選擇重載computeResourceName()。在這個函數中判斷是否定義了prefixes參數,如果是一次調用父類的computeResourceName()並判斷資源是否存在。

public class CodeStoryTemplateResolver extends AbstractConfigurableTemplateResolver implements ApplicationContextAware { private ApplicationContext applicationContext = null; public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } private String prefixes = null; public CodeStoryTemplateResolver() { super(); } public final String getPrefixes() { return this.prefixes; } public final void setPrefixes(final String prefixes) { this.prefixes = prefixes; } protected String computeResourceName(final IEngineConfiguration configuration, final String ownerTemplate, final String template, final String prefix, final String suffix, final boolean forceSuffix, final Map<String, String> templateAliases, final Map<String, Object> templateResolutionAttributes) { String resourceName = null; String[] prefixes = null; if (!StringUtils.isEmptyOrWhitespace(getPrefixes())) { prefixes = getPrefixes().split(","); } else if (!StringUtils.isEmptyOrWhitespace(getPrefix())) { prefixes = new String[] { getPrefix() }; } else { prefixes = new String[] { "" }; } for (String onePrefix : prefixes) { onePrefix = StringUtil.trimLeft(StringUtil.trimRight(onePrefix)); resourceName = super.computeResourceName(configuration, ownerTemplate, template, onePrefix, suffix, forceSuffix, templateAliases, templateResolutionAttributes); Resource resource = applicationContext.getResource(resourceName); if (resource != null && resource.exists()) { break; } else { resourceName = null; } } return resourceName; } @Override protected ITemplateResource computeTemplateResource( final IEngineConfiguration configuration, final String ownerTemplate, final String template, final String resourceName,final String characterEncoding, final Map<String, Object> templateResolutionAttributes) { return new SpringResourceTemplateResource(this.applicationContext, resourceName, characterEncoding); }}

3.3.2 修改Spring配置

<bean id="multiTemplateResolver" class="….CodeStoryTemplateResolver"> <property name="prefixes"> <value>/WEB-INF/,classpath:/templates/</value> </property> <property name="suffix"> <value>.html</value> </property> <property name="templateMode"> <value>HTML</value> </property> <property name="characterEncoding"> <value>UTF-8</value> </property> <property name="cacheable" value="false"/></bean><bean id="templateEngine" class="org.thymeleaf.spring3.SpringTemplateEngine"> <property name="templateResolvers"> <set> <ref bean="multiTemplateResolver" /> </set> </property></bean><bean class="org.thymeleaf.spring3.view.ThymeleafViewResolver"> <property name="templateEngine" ref="templateEngine" /> <property name="characterEncoding" value="UTF-8"/></bean>

重啟系統測試,/demo/hello.html和/demo/world.html都能正常訪問。

3.4 啰嗦幾句

兩種方案區別不大,都是嘗試增加文件判斷,便於Thymeleaf找到真正的html所在路徑。配置上,第二種方案相對簡單一點。

性能方面,沒有做仔細測試,估計比Spring缺省的TemplateResolver會慢一些。

當然,這個方案有點多次一舉,最簡單的處理方式,把目錄src/main/webapps/WEB-INF/htmls 移動到 src/main/resources/templates即可。