Spring 多租户环境中的 Boot + Thymeleaf + Database TemplateResolver

Spring Boot + Thymeleaf + Database TemplateResolver in a Multi-Tenant environment

我在多租户应用程序中使用 Spring Boot 和 Thymeleaf。我还使用 Thymeleaf 处理电子邮件模板并生成 html 电子邮件。为了做到这一点,我创建了一个 ITemplateResolver 来从数据库中检索前缀为 "db:" 的 thymeleaf 模板。

Spring 引导自动配置选择任何模板解析器并将它们添加到 SpringTemplateEngine。所以我有一个这样设置的模板解析器:

@Bean
@Scope(value = "tenant", proxyMode = ScopedProxyMode.INTERFACES)
public ITemplateResolver databaseTemplateResolver() {

    final DatabaseTemplateResolver resolver = 
        new DatabaseTemplateResolver(systemSettingService, emailTemplateService );

    resolver.setTemplateMode("HTML5");
    resolver.setCacheTTLMs((long) (1000*60*5)); // 5 Minutes
    resolver.setOrder(2);

    return resolver;
}

正如预期的那样,解析器被添加到 TemplateEngine 中,并且所有名称以 "db:" 开头的模板都从数据库中读取。这使我们能够存储由 Thymeleaf 引擎处理以生成结果 html.

的专用电子邮件模板

这看起来非常有效。上面指定的范围是由域确定的多租户环境中为一个租户定义的自定义范围。但我相信,出于这个问题的目的,这也可能是会话范围的。我的想法是 TemplateResolver 对于每个范围都是不同的。我们需要它,因为我们正在从租户的数据库中读取模板。

最后,我的症状:似乎 first 租户工作正常。对于任何后续租户,我在尝试处理数据库模板时遇到异常。

org.thymeleaf.exceptions.NotInitializedException: Template Resolver has not been initialized
    at org.thymeleaf.templateresolver.AbstractTemplateResolver.checkInitialized(AbstractTemplateResolver.java:156)
    at org.thymeleaf.templateresolver.AbstractTemplateResolver.resolveTemplate(AbstractTemplateResolver.java:316)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    ...

我试过为 Thymeleaf 禁用 Spring 启动自动配置并手动设置 TemplateEngine、ViewResolver、TemplateResolvers 等,但遇到同样的问题。我还尝试将所有 tenant 范围但 运行 变成完全不同的混乱并回溯。

我觉得我做错了什么,或者对依赖注入在这种情况下应该如何工作有错误的想法。或者 Thymeleaf 引擎的实现方式与代理对象不兼容。我倾向于后者。也许我需要以某种方式扩展模板引擎,以便它为每个租户初始化一次解析器?我相信,也许 Thymeleaf 认为解析器已经初始化,然后当 spring 注入一个新的解析器时,它永远不会被 Thymeleaf 初始化,因此是异常。

任何人都可以在正确的方向上推动我吗?谢谢。

编辑: 这是 Thymeaf 的 TemplateEngine 方法 initialize() 的代码,它在处理任何模板之前被调用。

/**
 * <p>
 *   Internal method that initializes the Template Engine instance. This method 
 *   is called before the first execution of {@link #process(String, IContext)} 
 *   in order to create all the structures required for a quick execution of 
 *   templates.
 * </p>
 * <p>
 *   THIS METHOD IS INTERNAL AND SHOULD <b>NEVER</b> BE CALLED DIRECTLY.
 * </p>
 * <p>
 *   If a subclass of <tt>TemplateEngine</tt> needs additional steps for
 *   initialization, the {@link #initializeSpecific()} method should
 *   be overridden.
 * </p>
 */
public final synchronized void initialize() {

    if (!isInitialized()) {

        logger.info("[THYMELEAF] INITIALIZING TEMPLATE ENGINE");

        this.configuration.initialize();

        this.templateRepository = new TemplateRepository(this.configuration);

        initializeSpecific();

        this.initialized = true;

        // Log configuration details
        this.configuration.printConfiguration();

        logger.info("[THYMELEAF] TEMPLATE ENGINE INITIALIZED");

    }

}

this.configuration.initialize();中初始化各种引擎配置。除其他事项外,该方法在所有 TemplateResolver 上初始化(调用 initialize() ),然后将引擎标记为 initialized.

一旦 TemplateEngine 被标记为 "initialized," 引擎将不会再次初始化,也不会初始化任何配置(按设计)。所以我在想也许我的想法是正确的,即永远不会初始化为新作用域注入的新 TemplateResolver。或者,更准确地说,它不会被标记为已初始化。

似乎使用所有这些 initialized 的主要原因之一是在完成配置之前将其标记为 运行 并防止配置更改一次 运行。

根据我的发现,并使用上述假设,我将 TemplateResolver 更改为始终在处理每个模板之前检查初始化。这种蛮力方法似乎有效,并且似乎不会干扰 Thymeleaf 作者的意图。 (当然是基于我的猜测。我真的不知道。我希望如此。)

版本:

通常,TemplateEngine 在引擎初始化期间会在 TemplateResolver 上调用 initialize()。但是在这种情况下,作用域注入的 TemplateResolvers 没有被初始化。

相反,我们将 create/inject 已经是 "initialized." 的 TemplateResolver 我们只需将该步骤添加到 bean 创建中:

@Bean(initMethod="initialize")
@Scope(value = "tenant", proxyMode = ScopedProxyMode.INTERFACES)
public ITemplateResolver databaseTemplateResolver() {

    final DatabaseTemplateResolver resolver = 
        new DatabaseTemplateResolver(systemSettingService, emailTemplateService );

    resolver.setTemplateMode("HTML5");
    resolver.setCacheTTLMs((long) (1000*60*5)); // 5 Minutes
    resolver.setOrder(2);

    return resolver;
}

我唯一担心的是,在此处手动调用初始化可能会出现一些无法预料的情况 side-effect。我对 Thymeleaf 的了解还不够肯定。然而,到目前为止,一切都很好。