xtian
xtian

Reputation: 3169

Accessing application context beans from thymeleaf email template

I have followed this Thymeleaf tutorial "Rich HTML email in Spring with Thymeleaf" to generate an email using a Thymeleaf template. All is fine, but I can't access the ApplicationContext beans that I use elsewhere in the application.

To be more specific, in my email template I'd like to have something like:

<span th:text="${@myBean.doSomething()}">

where "myBean" is a @Component. Is that possible at all? I'm not looking for a workaround like

<span th:text="${myBean.doSomething()}">

where the bean is added as a variable in the template processing context.

The Configuration:

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
public class MyWebConfig extends WebMvcConfigurerAdapter {
[....]
public ClassLoaderTemplateResolver emailTemplateResolver() {
    ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver();
    resolver.setPrefix("emailtemplates/");
    resolver.setSuffix(".html");
    resolver.setCharacterEncoding("UTF-8");
    resolver.setTemplateMode("HTML5");
    return resolver;
}

@Bean
public SpringTemplateEngine emailTemplateEngine() {
    SpringTemplateEngine engine = new SpringTemplateEngine();
    engine.addTemplateResolver(emailTemplateResolver());
    engine.addDialect(new LayoutDialect()); // thymeleaf-layout-dialect
    addSpringSecurityDialect(engine); // thymeleaf-SpringSecurity-dialect
    return engine;
}
[....]
}

The email service:

@Service
public class MyEmailService {
@Resource SpringTemplateEngine emailTemplateEngine;
[....]
public boolean sendHtmlEmail(...) {
    final Context ctx = new Context(locale);
    ctx.setVariable("someVariable", "someValue"); // Don't want to add myBean here
    final String body = this.emailTemplateEngine.process("myTemplate", ctx);
    [....]

The Component:

@Component
public class MyBean {
    public String doSomething() {
        return "Something done!";
    }
}

The template:

<span th:text="${@myBean.doSomething()}">

The error:

EL1057E:(pos 1): No bean resolver registered in the context to resolve access to bean 'myBean'

I'm using thymeleaf-2.1.4 and spring-4.1.6

EDIT:

plese note that I can't use the HttpServletRequest because I send most of the emails in an @Async method where the request can't be reliably used. That's the reason why I use Context and not WebContext (although I didn't know about SpringWebContext - I guess that if somebody made that class for accessing beans via a "beans" variable, then using the @myBean notation must be something impossible).

Upvotes: 11

Views: 10103

Answers (3)

Dario Seidl
Dario Seidl

Reputation: 4630

It's confusing because we have three different context here:

  • The Spring ApplicationContext, which is not a WebApplicationContext, if we're sending a mail in a scheduled method.
  • The Thymeleaf org.thymeleaf.context.Context on which you set/add variables.
  • The SpEL evaluation context org.springframework.expression.EvaluationContext, used to evaluate SpEL expressions. For Thymeleaf that's the ThymeleafEvaluationContext.

If you don't have a Spring web context, all you need is to add the ThymeleafEvaluationContext with a reference to the application context as a variable to the Thymeleaf Context:

E.g. (in Kotlin)

@Service
class TemplateService(
        private val templateEngine: TemplateEngine,
        private val applicationContext: ApplicationContext
) {

    fun processTemplate(locale: Locale, template: String, vararg variables: Pair<String, Any?>): String {
        val context = Context(locale)

        // Set the Thymeleaf evaluation context to allow access to Spring beans with @beanName in SpEL expressions
        context.setVariable(ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME,
                ThymeleafEvaluationContext(applicationContext, null))

        // Set additional variables
        variables.forEach { (key, value) -> context.setVariable(key, value) }

        return templateEngine.process(template, context)
    }
}

Upvotes: 17

Alexandre Petrillo
Alexandre Petrillo

Reputation: 123

Found, you need to add a bean resolver in your SpringWebContext. Bean Resolver is created by ThymeleafEvaluationContext.

    Map<String, Object> mergedModel = new HashMap<>();
    ConversionService conversionService = (ConversionService) this.request
            .getAttribute(ConversionService.class.getName()); // might be null!
    ThymeleafEvaluationContext evaluationContext = new ThymeleafEvaluationContext(this.ac,
            conversionService);
    mergedModel.put(ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME,
            evaluationContext);
    SpringWebContext ctx = new SpringWebContext(this.request, this.response,
            this.request.getServletContext(), this.request.getLocale(), mergedModel, this.ac);

Upvotes: 3

Hank Lapidez
Hank Lapidez

Reputation: 2025

Simply put the complete Context in themeleaf context

@Service
   public class MyEmailService {
   @Resource SpringTemplateEngine emailTemplateEngine;
   [....]
   public boolean sendHtmlEmail(...) {
       final Context ctx = new Context(locale);
      ctx.setVariable("beans", new Beans(appContext)); 
    final String body = this.emailTemplateEngine.process("myTemplate", ctx);
      [....]



<span th:text="${beans.myBean.doSomething()}">

The @ notation could be a problem, because if I understood it right, the templates a evaluated with spring el expression. May be it can be done, but will be time-consuming.

Upvotes: 0

Related Questions