Reputation: 41
I'm using Spring Boot and Thymeleaf in a multi-tenant application. I am also using Thymeleaf to process email templates and generate html email. In order to do this, I created a ITemplateResolver
that retrieves thymeleaf templates prefixed with "db:" from a database.
Spring Boot auto-configuration picks up any template-resolvers and adds them the the SpringTemplateEngine
. So I have a my template resolver set up like this:
@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;
}
As expected, the resolver gets added to the TemplateEngine
and any templates with names that start with "db:" are read from the database. This allows us to store specialized email templates that are processed by the Thymeleaf engine to produce the resulting html.
This worked very well, so it seemed. The scope specified above is a custom scope defined for one tenant in a multi-tenant environment determined by the domain. But this might as well be session scoped, I believe, for the purpose of this question. My thought here is that TemplateResolver is different for each scope. We need it to be because we are reading from the tenant's database for the template.
Finally, my symptom: It seems that the first tenant works fine. For any subsequent tenants, I get an exception when attempting to process a database template.
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)
...
I've tried disabling Spring Boot auto-configuration for Thymeleaf and manually setting up the TemplateEngine, ViewResolver, TemplateResolvers, etc. but have the same problem. I also tried making everything tenant
scope but ran into an entirely different mess and back-tracked.
I have a feeling that I'm doing something wrong, or have the wrong idea, of how dependency injection should work in this situation. Or the Thymeleaf engine is implemented in such a way that it is not compatible with the proxy object. I'm leaning towards the latter. Perhaps I need to somehow extend the template engine so that it initializes the resolvers once for each tenant? I believe, maybe, Thymeleaf thinks the resolver is already initialized, then when spring injects a new resolver, it is never initialized by Thymeleaf and hence the exception.
Can anyone give me a push in the right direction? Thank you.
EDIT:
Here is the code for Thymeaf's TemplateEngine
method initialize()
which is called before any templates are processed.
/**
* <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");
}
}
In this.configuration.initialize();
, various engine configuration is initialized. Among other things, the method initializes ( calls initialize() ) on all TemplateResolver
s and then marks the engine as initialized.
Once the TemplateEngine
is marked "initialized," the engine will not be initialized again, nor will any configuration be initialized (by design). So I am thinking that maybe my thought is correct that a new TemplateResolver
injected for a new scope will never be initialized. Or, more accurately, it will not be marked as initialized.
It seems that one of the major reasons for the use of all these initialized
flags it to keep from running before complete configuration and to prevent changes to configuration once running.
With what I found, and using the above assumptions, I changed my TemplateResolver
to always check for initialization before every template is processed. This brute force method appears to work and does not seem to interfere with the intent of the Thymeleaf authors. (Based on my guess, of course. I really do not know. I am hoping.)
VERSIONS:
Upvotes: 1
Views: 4405
Reputation: 41
Normally, the TemplateEngine
calls initialize()
on the TemplateResolver
s during Engine initialization. However in this case, the scoped injected TemplateResolver
s are not being initialized.
Instead, we'll create/inject a TemplateResolver
that is already "initialized." We just add that step to the bean creation:
@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;
}
My only concern is that there might be some unforeseen side-effect to manually calling initialize here. I don't know Thymeleaf well enough to say for certain. However, so far, so good.
Upvotes: 0