- Blueprint is a single class, lightweight, easily extensible, zero external dependencies, fully thread-safe and java-8 compatible templating engine for Java with same easily readable syntax as Nunjucks.
- Blueprint is pretty fast too. Following results are on a M1 Mac 8GB:
- Single core:
164_688 ops/s, 4-core:473_629 ops/sfor a large template using all available capabilities (full.blu). - Single core:
3_330_601 ops/s(3+Million) , 4-core:9_586_955 ops/s(9+Million) for a small template using variables and if-else conditions only (small.blu). - See
Performancesection for details.
- Single core:
-
Variable Interpolation
Embed variables directly in your templates using the{{ ... }}syntax. -
Conditional Statements
Use{% if %},{% else %}, and{% endif %}to control what content is rendered based on dynamic conditions. -
Loops
Iterate over collections with{% for item in list %} ... {% endfor %}. A specialloopvariable is automatically injected, offering iteration details such as the current index (loop.index.. starts from 0). -
Set Assignment
Assign values to new or existing variables within your template using{% set variable = expression %}. -
Macros
Define reusable template snippets with parameters using{% macro name(params) %} ... {% endmacro %}. Macros allow you to encapsulate logic and rendering blocks that you can reuse elsewhere in your template. -
Raw Blocks
Prevent processing of content (e.g., when you need to output template syntax literally) with{% raw %} ... {% endraw %}. -
Custom Functions and Filters
Extend Blueprint by registering your own functions and filters. Filters are applied via the pipe operator (|) on expressions. -
Rich Expression Support
Blueprint’s expression parser supports:- Arithmetic Operators:
+,-,*,/,%,**(power) - Comparison Operators:
==,!=,>,>=,<,<= - Logical Operators:
and,or,not - Unary Operators:
-(negation) - Parentheses for grouping
- Object & Array Literals: e.g.,
{ firstName: "Alice", age: 30 }and[1, 2, 3] - Function Calls: e.g.,
upper(name) - Filters: Chain transformations such as
{{ name | capitalize }}
- Arithmetic Operators:
-
Dynamic Property Resolution
Supports dot–notation (user.name) and bracket–notation (matrix[1][2]) to access nested object properties and array elements. Reflection-based property lookup (when using class instances instead of Map for context data) is cached for improved performance. -
Separate compile and render phases
For repeated usage of same template (with different data/context), compile the template once and re-use the template for different data sets. SeeSampleUsage.javaorBenchmarkRunner.java. -
Thread Safety
Blueprint is thread-safe for concurrent rendering. For details, see Thread Safety.
- Just include one java class:
Blueprint.javasource file in your Java project (Not published on maven yet). No other dependencies are there. - In case you need a few utility functions and filters too, like,
upper,length,join,truncate,reverse,replace, etc., you can add StdUtils.java too. This is not needed otherwise. Check out StdUtils section for details.
Below is a quick example showing how to register a custom function, compile a template, and render it with a context:
import com.freakynit.blueprint.Blueprint;
import com.freakynit.blueprint.Blueprint.TemplateFunction;
import java.util.HashMap;
import java.util.Map;
public class BlueprintExample {
public static void main(String[] args) {
// Create a new Blueprint engine instance.
Blueprint engine = new Blueprint();
// Register a custom function "upper" to convert text to uppercase.
engine.registerFunction("upper", (context, argsList) -> {
String input = argsList.get(0).toString();
return input.toUpperCase();
});
// Define a simple template with variable interpolation and filter usage.
String templateSource = "Hello, {{ upper(name) }}!";
// Create a context with variables.
Map<String, Object> context = new HashMap<>();
context.put("name", "world");
// Render the template.
String output = engine.render(templateSource, context);
System.out.println(output); // Output: "Hello, WORLD!"
}
}Note: You can also load your template from resources directory or local file system. See BenchmarkRunner.java for reference.
The official extension for Blueprint templating engine is blu.
Embed variables using double curly braces:
Hello, {{ user.name }}!If user.name is "Alice", the rendered output will be:
Hello, Alice!
Render content conditionally using {% if %} blocks:
{% if user.age >= 18 %}
Welcome, adult user!
{% else %}
Sorry, you are too young.
{% endif %}Depending on the value of user.age, the appropriate branch is rendered.
Iterate over collections with a for loop:
{% for color in user.colors %}
Color: {{ color }}
{% endfor %}For user.colors = ["red", "green", "blue"], the rendered output will be:
Color: red
Color: green
Color: blue
A special loop variable is available to provide iteration details (e.g., loop.index).
Assign values to variables within the template:
{% set greeting = "Hello" %}
{{ greeting }}, World!This produces:
Hello, World!
Define reusable template blocks with macros:
{% macro shout(text) %}
{{ upper(text) }}!!!
{% endmacro %}
{{ shout("hello") }}In this example:
- A macro named
shoutis defined to take a parametertext. - It calls the custom
upperfunction (registered in your Java code) to convert text to uppercase. - Rendering produces:
HELLO!!!
Output content verbatim without processing:
{% raw %}
This will not be processed: {{ this is raw }}
{% endraw %}Everything inside the raw block is output exactly as written.
Blueprint supports complex expressions, including arithmetic and logical operations:
{{ 5 + 3 * 2 }} {# Evaluates to 11 #}
{{ (a + b) * c }} {# Uses parenthesized grouping #}
{{ user.age >= 18 and user.active }}You can also define object and array literals:
{% set person = { firstName: "Alice", lastName: "Smith", age: 30 } %}
{% set colors = ["red", "green", "blue"] %}Blueprint allows you to register your own functions and filters to extend template capabilities. The only difference is that filters automatically receive the current value as the first argument.
For example, to register a filter that capitalizes text:
engine.registerFilter("capitalize", (context, argsList) -> {
String input = argsList.get(0).toString();
if (input.isEmpty()) return input;
return input.substring(0, 1).toUpperCase() + input.substring(1);
});You can then use it in your template like so:
{{ name | capitalize }}If name is "alice", the rendered output will be "Alice".
Blueprint parses your template into an Abstract Syntax Tree (AST) composed of various node types:
- TextNode: Represents plain text.
- VariableNode: Handles variable interpolation.
- IfNode: Manages conditional blocks.
- ForNode: Iterates over collections.
- SetNode: Represents variable assignments.
- MacroNode: Holds macro definitions.
Each node implements a render method that outputs content based on the provided context. The engine’s expression parser further supports literals, variable references, function calls, filters, and both binary and unary operators.
The engine also uses reflection to resolve properties from objects (supporting dot–notation and bracket–notation) and caches getter method lookups for optimal performance.
Contains definitions for many commonly used functions and filters. You can refer these for understanding if needed. To register all these:
new StdUtils().registerAll(engine);Or register only selective ones, for example:
// Function
new StdUtils.Functions().registerUpper("myUpper", engine);
// Usage example: Hello, {{ myUpper(name) }}
// Filter
new StdUtils.Filters().registerReverse("myReverse", engine);
// Usage example: Reversed: {{ name | myReverse }}Usage is same as defined above for functions and filters
Blueprint is thread-safe for concurrent rendering.
- The
render()method creates a newRenderContextandStringBuilderfor each render call, avoiding shared mutable state. Each rendering operation works with its own isolated context, making concurrent template rendering safe. - Uses ConcurrentHashMap for the
getterCache, which provides thread-safe concurrent access for method lookup optimization. Multiple threads can safely cache and retrieve reflection-based getters simultaneously. - Function and filter lookups use a copy-on-write map with a
volatilereference. registerFunctionandregisterFiltermethods are synchronized, ensuring safe updates without data races.- Reads (
getFunction,getFilter) are lock-free and fast.
This design favors read-heavy workloads (like template rendering) while keeping runtime registration safe.
- JMH benchmarks are integrated. Check out
BenchmarkRunner.java. - Numbers:
- Detailed results available in jmh_report_template_full.txt and jmh_report_template_small.txt.
- Tested on M1 Mac, 8GB
- Running benchmark with demo template (
full.blu)
You can adjust the template in
BenchmarkRunner.javaby adjusting just this single line:String templateFileName = "full.blu"; // or small.blu
mvn clean package
java -jar target/blueprint-1.0.1.jarContributions, feedback, and feature requests are welcome! Feel free to fork the repository, submit pull requests, or open issues to help improve Blueprint.
Blueprint is released under an open-source license. See the LICENSE file for details.