zoukankan      html  css  js  c++  java
  • 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即可。

  • 相关阅读:
    mysql安装部署
    SSH升级
    符号、特殊字符的英文读法
    用python开发视频压缩器
    VSCode配置项
    工厂模式(简单工厂模式,工厂方法模式,抽象工厂模式)
    单例模式
    Jquery 绑定事件
    中文分词 新建索引 更新索引
    微信自动回复机器人
  • 原文地址:https://www.cnblogs.com/codestory/p/7444476.html
Copyright © 2011-2022 走看看