TL;DR: HubSpot is open-sourcing its fast, Jinja-compatible templating language runtime for Java.
HubSpot's Content Optimization System (COS) was originally developed in python, and the templating engine chosen to power it was Jinja. Jinja provides a robust and powerful foundation in which to craft page templates, in a syntax derived from django templates. While python was a fine choice at the start, and allowed us to rapidly iterate on the COS, we started to run into growing pains and scalability issues with python and django. Over the past year, we've been reimplementing parts of the system in Java.
While there are a multitude of mature and widely used Java templating engines out there, we realized rather quickly that it wouldn't be feasible to try and migrate our existing customers' templates to a different language; we've historically allowed our customers to use all of the language constructs, filters, tags, etc.
In searching for an off-the-shelf java implementation, the closest we found was an old implementation of django templating, jangod. This proved to be a good start to get the ball rolling with the COS renderer port, but there were many gaps which needed to be filled in order to bring us up to where we needed to be. After the first 100 commits or so, we ended up forking the project and pushing it to where we needed it to be, and Jinjava was born!
Perhaps differently than other templating libraries for Java, Jinjava sports an API which reflects our application's use of customer-created free form templates. This means that when you render a template, you can get as a result a listing of any errors encountered during the render complete with template line numbers.
Jinjava jinjava = new Jinjava();
RenderResult result = jinjava.renderForResult(template, context);
for(TemplateError error : result.getErrors()) {
renderError(error.getSeverity(), error.getMessage(),
error.getLineNumber());
}
There are four extension points in Jinjava where you can add custom functionality:
Tags are a standard construct in any templating language; in Jinjava they have a declaration syntax which is similar to mustache:
{% timestamp %}
{% if my_var > 0 %} ... {% endif %}
Adding new tags is a simple process; extend from com.hubspot.jinjava.lib.tag.Tag
, and register the tag with the Jinjava instance before you render anything with it. Tags can have inner content, and have the ability to modify the template context during execution, doing things like setting / updating variables, etc.
public class TimestampTag implements Tag
{
@Override
public String getName() { return "timestamp"; }
@Override
public String getEndTagName() { return null; }
@Override
public String interpret(TagNode tagNode,
JinjavaInterpreter interpreter)
{
return new String(System.currentTimeMillis());
}
}
register the tag with the global context, so it'll be available in all render operations on that jinjava instance:
Jinjava jinjava = new Jinjava();
jinjava.getGlobalContext().registerTag(new TimestampTag());
Filters are functions which can be used in expressions, invoked via a "pipe" operator syntax signifying their purpose of translating/transforming a prior value:
{{ my_val | uppercase | split(',') }}
You register filters in the same way as tags above. A filter's signature includes the "affected" object, as well as a parsed list of any arguments passed in:
public class ConcatFilter implements Filter {
@Override
public String getName() { return "concat"; }
@Override
public Object filter(Object var,
JinjavaInterpreter interpreter,
String... args)
{
String addend = args[0];
return var.toString() + addend;
}
}
An expression test is a special type of function invoked via the 'is' operator:
public class IsEvenExpTest implements ExpTest {
@Override
public String getName() {
return "even";
}
@Override
public boolean evaluate(Object var,
JinjavaInterpreter interpreter,
Object... args) {
if(var == null || !Number.class.isAssignableFrom(var.getClass())) {
return false;
}
return ((Number) var).intValue() % 2 == 0;
}
}
As you might expect, you can define raw functions which can be used in expressions. In their simplest form, Jinjava functions are public static java functions which you register with an instance of ELFunctionDefinition:
register(new ELFunctionDefinition("fn", "list", Lists.class,
"newArrayList", Object[].class));
which can then be used in a template expression:
{% set mylist = fn:list() %}
Additionally, we created a special proxy wrapper class so that we could define template functions which have access to Guice-injected instances like DAO's and API services: InjectedContextFunctionProxy. You can use the static function defineProxy(..) on this class to wrap an object instance with a static invocation wrapper, so it can be invoked in a template without a target object.
We did set up some performance benchmarks, to relatively compare ourselves to similar libraries on other platforms. Generally speaking, the overhead of the rendering engine is such that the likeliest bottleneck you're apt to have will come from custom tags which do time-intensive things like pull data from remote service APIs and such. With that said, here are our very unscientific results, with benchmarks from each compared engine ported to Jinjava (run on MacBook Pro 2.6 Ghz, OSX Yosemite 10.10.1):
Jinjava vs. Liquid | Liquid (@e2f8b28) on Ruby (2.0.0p481) | Jinjava |
---|---|---|
parse | 21.814 runs/s | 1383.319 runs/s |
parseAndRender | 14.840 runs/s | 627.996 runs/s |
Jinjava vs. Jinja2 | Jinja2 (@85820fc) on Python (2.7.6) | Jinjava |
---|---|---|
RealWorldish Benchmark | 1387.3108 runs/s | 1643.407 runs/s |
Jinjava has served us well so far, easing our transition from python to java. I think it can hold its own against similar rendering engines out there. It powers the HubSpot COS, what can it do for you?