Thymeleaf引擎支持Multi Prefix


最近團隊的一個項目在重構,希望引入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

@Controller

public 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 ,無法重載。

@Override

protected 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即可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM