Thymeleaf, a server-side Java template engine, has been Broadleaf's go-to templating language for both our admin, and demo site since moving to it a couple of years ago, so we were excited when we heard that version 3 released in early May. Soon after the release we decided to start an effort to move to Thymeleaf 3. Being one of the first (and probably the first company) to do so we relied pretty heavily on documentation released by the developers of Thymeleaf along with well documented GitHub issues that were made for the major feats they wanted to accomplish in Thymeleaf 3. The ten minute guide, created for Thymeleaf 3, covers most of the basics on what changed from v2.1 and v3.0, but, for a more in depth explanation on what changed with the underlying processor APIs, we relied more on Thymeleaf's GitHub issues.

The Upgrade Process

Template Changes

  • th:inline is no longer needed to use the inline syntax (i.e. null)
  • HTML needs to be valid HTML5 because Thymeleaf 3 uses a new HTML parser instead of XML parser
Removing th:inline

All Thymeleaf 2 HTML works in Thymeleaf 3, but the th:inline="text" attribute is no longer needed. The functionality still exists but now you can simply just use the inline syntax, null, anywhere you like.

Fixing malformed HTML

Given this is never a good thing, malformed HTML was allowed in Thymeleaf 2 because malformed HTML doesn't mean it's malformed XML and Thymeleaf 2 used an XML parser. This means that <div/> was fine in v2.1 because the backend would convert it to <div></div> before sending it to the client. On the contrary, Thymeleaf 3 uses a DOM parser, AttoParser 2.0. Therefore, it doesn't mind parsing <div/> so it'll send it to the client. This is a problem, though, because the browser wants correct HTML5, but a div is a non void tag therefore the browser turns the <div/> into <div> and then guesses where the matching closing tag goes. This causes problems because it doesn't normally put the close tag where you originally intended.

Backend Changes

  • Updating some enums that changed (such as LEGACYHTML5 for template resolvers).
  • Updating our variable expression evaluator to use SpelVariableExpressionEvaluator instead of IExpressionObjectFactory.
  • Updating all of our processors to extend the new classes that were made in Thymeleaf 3.
  • Finding a new way to change the global model.
  • Moving to a Java based layout dialect since the dialect we previously used was rewritten in Groovy which caused a noticeable performance decrease.
Updating our processors

The large majority of upgrading to Thymeleaf 3 was updating our processors so that they use the new classes and API. Broadleaf has around 40 processors that extend various classes that Thymeleaf provides for creating processors and all of them have changed in the upgrade. There's obviously a learning curve to know which Thymeleaf 3 classes replaced the Thymeleaf 2 classes we were previously using, but there wasn't anything that we couldn't do in v3.0 that we could do in v2.1.

New approach to modifying the global model

The largest problem we ran into was modifying the global model that's used when evaluating the template. Needless to say this isn't the best thing to do even in Thymeleaf 2, but, nonetheless, we needed the functionality so that clients can seamlessly use their current HTML with Thymeleaf 3. Previously you could modify this via the Arguments parameter sent to the processor, but in version 3 Arguments was removed in favor of a new ITemplateContext object which doesn't have access to the global model. We worked around this by setting an attribute on the WebRequest. Since SpEL actually evaluates using the WebRequest this was a viable solution. In the future we plan to convert all of the processors that alter global state to just alter the node's local variables.

Fixing performance by switching layout dialects

Luckily around the time we were doing this upgrade ultraq's layout dialect had been updated to use the new Thymeleaf 3 APIs so were able to simply include it in our template engine like we had before. Unfortunately the dialect had been rewritten to use Groovy which, in turn, hurt the performance of Thymeleaf. After finding this out we searched for an alternative and found a fork of ultraq's project which was written completely in Java. We did performance testing with this new project and found that now Thymeleaf 3 was significantly more performant than it's predecessor. Compared to using Thymeleaf 2 with ultraq's layout dialect we had a 68% better average response time and 5% better throughput with 90 users, and 156% better average response time and 23% better throughput with 135 users using Thymeleaf 3 with the Java fork. During this search we also realized that in the future, when we deprecate Thymeleaf 2 support, that we could easily replace these third party dependencies by using Thymeleaf 3's fragment expression. Since this is an out of box solution, the performance should be even better.

Broadleaf's Backwards Compatibility Solution

Now that we have the initial upgrade done we were able to evaluate the differences and similarities so that we could create a common layer. The idea was to write the processors once so that they could live inside the core framework. This has huge advantages because we'd be able to completely remove the dependency of Thymeleaf in the event that a client wanted to run Broadleaf headless instead of it serving the HTML. We created a separate maven project that would hold the common layer, a sub module that would have no Thymeleaf dependency, so that it could be a dependency included in the core framework. This consists of mostly interfaces. We created two additional sub modules that would hold the implementations of the common layer interfaces for Thymeleaf 2 and Thymeleaf 3 separately. Even though this was a great design we did have to come up with a creative process of injecting the processor code that lived in the core framework into the version specific implementations of the processor APIs. We were able to accomplish this with Spring Java configuration since we were able to look up all of the beans that extended a specific class that lived in the common module.

// BroadleafProcessor is the common interface all of our processors implement
Collection<BroadleafProcessor> blcProcessors =
 applicationContext.getBeansOfType(BroadleafProcessor.class).values();
// We loop to see which exact type it is because many abstract methods

// exist in our common layer depending on the function the processor performs

for (BroadleafProcessor proc : blcProcessors) {
    if (BroadleafModelVariableModifierProcessor.class.isAssignableFrom(proc.getClass())) {
        iProcessors.add(createDelegatingModelVariableModifierProcessor((BroadleafModelVariableModifierProcessor) proc));
    } else if (BroadleafTagTextModifierProcessor.class.isAssignableFrom(proc.getClass())) {
        iProcessors.add(createDelegatingTagTextModifierProcessor((BroadleafTagTextModifierProcessor) proc));
    } ...
}

After capturing these classes we create a new instance of the version specific implementation that implemented the common class that we queried on and injected the bean found as a property on the new instance.

// Now we create the version specific implementation. This example is in our Thymeleaf 3

// Java config therefore we instantiate the Thymeleaf 3 implementation

protected static DelegatingThymeleaf3ModelVariableModifierProcessor createDelegatingModelVariableModifierProcessor(BroadleafModelVariableModifierProcessor processor) {

    return new DelegatingThymeleaf3ModelVariableModifierProcessor(processor.getName(), processor, processor.getPrecedence());
}
// This is the actual class that does all of the Thymeleaf 3 specific

// boiler plate functions before delegating the actual work to the

// broadleaf processor that we injected in our Java config

public class DelegatingThymeleaf3ModelVariableModifierProcessor extends AbstractElementTagProcessor {

    protected BroadleafModelVariableModifierProcessor processor;



    public DelegatingThymeleaf3ModelVariableModifierProcessor(String elementName, BroadleafModelVariableModifierProcessor processor, int precedence) {
        super(TemplateMode.HTML, processor.getPrefix(), elementName, true, null, false, precedence);
        this.processor = processor;
    }
    ...
    // This specific processor does a function called "populateModelVariables"

    // where it modifies "newModelVariables". After the method returns

    // DelegatingThymeleaf3ModelVariableModifierProcessor actually adds the

    // variables to the global model the way you do it in Thymeleaf 3

    processor.populateModelVariables(tag.getElementCompleteName(), attributes, newModelVariables, blcContext);
    ...
}

The redesign of how we incorporated Thymeleaf in Broadleaf gave us additional advantages such the ability to easily add support for a different template language or a new version of Thymeleaf. We would simply have to create a new submodule that implemented the classes in the common module. Additionally it gave us the ability to do all of the boiler plate code for setting up Thymeleaf in the submodules instead of in the core framework or in the client's code. This made it to where clients could simply add a maven module in their POM if they wanted Thymeleaf.

In conclusion, upgrading from Thymeleaf 2.1 to Thymeleaf 3.0 not only profited us new features and better performance, but it also gave us a chance to redesign how we supported Thymeleaf for our clients. Now clients can easily remove Thymeleaf as a dependency, use Thymeleaf 2, or use Thymeleaf 3 by simply adding a different dependency or not including one at all.