Thymeleaf 整合 SpringBoot

Thymeleaf 整合 SpringBoot

操作

直接引入 starter。TODO

原理

主要就是看 ThymeleafAutoConfiguration 自动配置类

在与 Spring 进行整合的时候,大部分的组件都进行了拓展,然后使用拓展后的以 Spring 开头的组件,比如 SpringXXX。并注册为 bean

同时还注册了视图解析接口 ViewResolverThymeleaf 下的实现 ThymeleafViewResolver 的 ben,

我们要分析 ThymeleafAutoConfiguration,主要也是分析这几个 bean,

SpringTemplateEngine

@AutoConfiguration(after = { WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@Import({ TemplateEngineConfigurations.ReactiveTemplateEngineConfiguration.class,
        TemplateEngineConfigurations.DefaultTemplateEngineConfiguration.class })
public class ThymeleafAutoConfiguration {

其中导入了 TemplateEngineConfigurations.DefaultTemplateEngineConfigurationTemplateEngineConfigurations.ReactiveTemplateEngineConfiguration 为不同的 Web 环境导入模板引擎

class TemplateEngineConfigurations {

    @Configuration(proxyBeanMethods = false)
    static class DefaultTemplateEngineConfiguration {

        // 应用于Servlet类型的Web环境
        // 重点看这个,常用这个
        @Bean
        @ConditionalOnMissingBean(ISpringTemplateEngine.class)
        SpringTemplateEngine templateEngine(ThymeleafProperties properties, ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<IDialect> dialects) {
            SpringTemplateEngine engine = new SpringTemplateEngine();
            engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler());
            engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes());
            // 我们可以通过自定义 ITemplateResolver 类型的 bean,结合@Order注解,来自定义模板解析器同时指定顺序
            templateResolvers.orderedStreamaddTemplateResolver;
            // 我们可以通过自定义 IDialect 类型的 bean,结合@Order注解,来自定义方言同时指定顺序
            dialects.orderedStreamaddDialect;
            return engine;
        }

    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.REACTIVE)
    @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
    static class ReactiveTemplateEngineConfiguration {

        // 应用于 reactive类型的web环境
        @Bean
        @ConditionalOnMissingBean(ISpringWebFluxTemplateEngine.class)
        SpringWebFluxTemplateEngine templateEngine(ThymeleafProperties properties, ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<IDialect> dialects) {
            SpringWebFluxTemplateEngine engine = new SpringWebFluxTemplateEngine();
            engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler());
            engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes());
            templateResolvers.orderedStreamaddTemplateResolver;
            dialects.orderedStreamaddDialect;
            return engine;
        }

    }

}

现在我们来看看初始化方法,直接看 SpringTemplateEngine#initializeSpecific

关于,父方法 TemplateEngine#initializeSpecific 以及 TemplateEngine 的初始化流程,请看《Thymeleaf 模板引擎.md》的 TemplateEngine 小节

@Override
protected final void initializeSpecific() {

    // First of all, give the opportunity to subclasses to apply their own configurations
    // 空方法,钩子,待子类来实现
    initializeSpringSpecific();

    // Once the subclasses have had their opportunity, compute configurations belonging to SpringTemplateEngine
    // 调用父类也就是 TemplateEngine的initializeSpecific方法,本身就是空实现,所以这里调用了也相当于啥也没干
    super.initializeSpecific();

    // 如果 templateEngineMessageSource 为null,则使用 messageSource,否则使用 templateEngineMessageSource
    // 这里默认情况下就会使用 messageSource,而且此时messageSource不为空,且值为当前应用上下文 原因是,
    // SpringTemplateEngine 实现了 MessageSourceAware 接口,在 TemplateEngineConfigurations$DefaultTemplateEngineConfiguration 的 templateEngine 方法将其注册为bean之后,
    // Spring的Aware机制会调用 SpringTemplateEngine#setMessageSource 方法设置 messageSource 的值,
    // 在我们第一次渲染页面,也就是第一次调用 TemplateEngine#process的方法的时候,开始初始化 TemplateEngine,调用 TemplateEngine#initialize 方法,调用到了子类的 SpringTemplateEngine#initializeSpecific方法,
    // 此时,templateEngineMessageSource 为空 ,messageSource不为空
    final MessageSource messageSource = this.templateEngineMessageSource == null ? this.messageSource : this.templateEngineMessageSource;

    final IMessageResolver messageResolver;
    if (messageSource != null) {
        final SpringMessageResolver springMessageResolver = new SpringMessageResolver();
        // 其实 SpringMessageResolver 也实现了 MessageSourceAware 接口,但是 springMessageResolver 不是bean实例,所以这里手动设置 messageSource
        springMessageResolver.setMessageSource(messageSource);
        // 将 springMessageResolver 最为消息解析器
        messageResolver = springMessageResolver;
    } else {
        // 如果 messageSource 为空,则使用 thymeleaf 自带的消息解析器 
        messageResolver = new StandardMessageResolver();
    }

    // 设置消息解析器 
    super.setMessageResolver(messageResolver);

}

其实整个初始化方法就只做了一件事,就是设置消息解析器,而正常情况下,消息解析器的类型就是 SpringMessageResolver

SpringMessageResolver

为适配 Spring,自定义的消息解析器。

IMessageResolver 接口的实现,它将标准的 Spring 的消息解析方式整合到了 Thymeleaf 的消息解析中。

基于模板的解析是通过使用可用的 Spring 配置的 MessageSource 对象完成的。

Spring 中 MessageSource 相关原理,请看《Spring-MessageSource 相关解析.md

基于 origin 参数的解析与 StandardMessageResolver 中的方法完全相同。

origin 参数表示消息请求的来源,通常是一个处理器或表达式类。可以为空。

直接看核心方法 SpringMessageResolver#resolveMessage,可以看到先通过 Spring 的 messageSource 来解析,然后再用 Thymeleaf 默认的 StandardMessageResolver 来解析消息。

public final String resolveMessage( final ITemplateContext context, final Class<?> origin, final String key, final Object[] messageParameters) {

    Validate.notNull(context.getLocale(), "Locale in context cannot be null");
    Validate.notNull(key, "Message key cannot be null");

    /*
    * FIRST STEP: Look for the message using template-based resolution
    */
    // 先通过Spring的 messageSource 来解析
    if (context != null) {
        // 检查 messageSource 是否为空,为空就报错
        checkMessageSourceInitialized();
        if (logger.isTraceEnabled()) {
            logger.trace( "[THYMELEAF][{}] Resolving message with key \"{}\" for template \"{}\" and locale \"{}\". " + "Messages will be retrieved from Spring's MessageSource infrastructure.", new Object[]{TemplateEngine.threadIndex(), key, context.getTemplateData().getTemplate(), context.getLocale()});
        }
        try {
            // 直接通过 messageSource.getMessage 来获取消息值
            return this.messageSource.getMessage(key, messageParameters, context.getLocale());
        } catch (NoSuchMessageException e) {
            // Try other methods
        }
    }

    /*
    * SECOND STEP: Look for the message using origin-based resolution, delegated to the StandardMessageResolver
    */
    // 然后再用Thymeleaf默认的 StandardMessageResolver 来解析消息
    if (origin != null) {
        // We will be disabling template-based resolution when delegating in order to use only origin-based
        // 使用Thymeleaf默认的消息解析方式来解析
        final String message = this.standardMessageResolver.resolveMessage(context, origin, key, messageParameters, false, true, true);
        if (message != null) {
            return message;
        }
    }

    /*
    * NOT FOUND, return null
    */
    return null;

}

SpringResourceTemplateResolver

ThymeleafAutoConfiguration$DefaultTemplateResolverConfigurationdefaultTemplateResolver 方法。

@Bean
SpringResourceTemplateResolver defaultTemplateResolver() {
    SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
    resolver.setApplicationContext(this.applicationContext);
    // 对应配置 spring.thymeleaf.prefix
    resolver.setPrefix(this.properties.getPrefix());
    // 对应配置 spring.thymeleaf.suffix
    resolver.setSuffix(this.properties.getSuffix());
    // 对应配置 spring.thymeleaf.mode
    resolver.setTemplateMode(this.properties.getMode());
    // 对应配置 spring.thymeleaf.encoding
    if (this.properties.getEncoding() != null) {
        resolver.setCharacterEncoding(this.properties.getEncoding().name());
    }
    // 对应配置 spring.thymeleaf.cache
    resolver.setCacheable(this.properties.isCache());
    // 对应配置 spring.thymeleaf.templateResolverOrder
    Integer order = this.properties.getTemplateResolverOrder();
    if (order != null) {
        resolver.setOrder(order);
    }
    // // 对应配置 spring.thymeleaf.checkTemplate
    resolver.setCheckExistence(this.properties.isCheckTemplate());
    return resolver;
}

ThymeleafViewResolver

@Bean
@ConditionalOnMissingBean(name = "thymeleafViewResolver")
ThymeleafViewResolver thymeleafViewResolver(ThymeleafProperties properties, SpringTemplateEngine templateEngine) {
    ThymeleafViewResolver resolver = new ThymeleafViewResolver();
    // 这里将模板引擎实例设置到了 ViewResolver 的 templateEngine 属性中
    resolver.setTemplateEngine(templateEngine);
    // 对应配置 spring.thymeleaf.encoding
    resolver.setCharacterEncoding(properties.getEncoding().name());
    // 对应配置 spring.thymeleaf.servlet.content-type
    resolver.setContentType( appendCharset(properties.getServlet().getContentType(), resolver.getCharacterEncoding()));
    // 对应配置 spring.thymeleaf.servlet.produce-partial-output-while-processing
    resolver.setProducePartialOutputWhileProcessing( properties.getServlet().isProducePartialOutputWhileProcessing());
    // 对应配置 spring.thymeleaf.excludedViewNames
    resolver.setExcludedViewNames(properties.getExcludedViewNames());
    // 对应配置 spring.thymeleaf.viewNames
    resolver.setViewNames(properties.getViewNames());
    // This resolver acts as a fallback resolver (e.g. like a
    // InternalResourceViewResolver) so it needs to have low precedence
    resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5);
    // 对应配置 spring.thymeleaf.cache
    resolver.setCache(properties.isCache());
    return resolver;
}

注意,这里在创建 ThymeleafViewResolver 实例的时候将前面配置的模板引擎实例 SpringTemplateEngine 设置到了视图解析器的 ThymeleafViewResolver#templateEngine 字段中,然后在 ThymeleafViewResolver#loadView 方法根据视图名称(viewName 参数)来创建视图对象的时候(View),会将模板引擎设置到视图的 AbstractThymeleafView#templateEngine 字段中。在最终将视图的内容输出到响应的消息提中的时候,会调用 ThymeleafView#renderFragment 方法,其中会调用 ITemplateEngine#process 方法将视图内容渲染,然后写入到 Writer

// 是不是有熟悉的感觉了?
viewTemplateEngine.process(templateName, processMarkupSelectors, context, templateWriter);

// If a buffer was used, write it to the web server's output buffers all at once
if (!producePartialOutputWhileProcessing) {
    // 写入到响应中
    response.getWriter().write(templateWriter.toString());
    response.getWriter().flush();
}

具体的 Thymeleaf 的视图的相关分析,请看《SpringMVC- 第五篇:视图.md》的 ThymeleafViewResolver源码 小节

ThymeleafProperties 配置类

简单实践