This guide walks you through integrating TinyExpression v1.4.15 into a Java project.
- Java 21+
- Maven 3.8+
Add the dependency to your pom.xml:
<dependency>
<groupId>org.unlaxer</groupId>
<artifactId>tinyExpression</artifactId>
<version>1.4.15</version>
</dependency>If you run tests or use reflective access at runtime, add add-opens to the Surefire plugin:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
</argLine>
</configuration>
</plugin>Use this pattern when formulas are static or bundled with code.
CalculationContext context = CalculationContext.newConcurrentContext();
context.set("age", 25);
context.set("gender", "male");newConcurrentContext() returns a thread-safe context. You can safely share the same context across threads.
import org.unlaxer.tinyexpression.Source;
import org.unlaxer.tinyexpression.evaluator.javacode.JavaCodeCalculatorV3;
import org.unlaxer.tinyexpression.evaluator.javacode.SpecifiedExpressionTypes;
import org.unlaxer.tinyexpression.parser.ExpressionTypes;
PreConstructedCalculator calculator = new JavaCodeCalculatorV3(
new Source("if($age >= 20){100}else{0}"),
"AgeCheckCalc", // generated class name (must be valid Java identifier)
new SpecifiedExpressionTypes(ExpressionTypes._float, ExpressionTypes._float),
Thread.currentThread().getContextClassLoader());JavaCodeCalculatorV3 compiles the formula to Java bytecode at construction time. Reuse the calculator instance across multiple apply() calls — it is stateless after construction.
float result = ((Number) calculator.apply(context)).floatValue();
System.out.println(result); // 100.0Use this pattern for tenant-specific rules, business-managed formulas, or dependency-controlled pipelines.
src/main/resources/formula-root/
69/formulaInfo.txt
tags:NORMAL
description:base score
siteId:69
calculatorName:baseScore
var:baseScore
resultType:float
formula:
if($age >= 20){100}else{0}
---END_OF_PART---
tags:NORMAL
description:bonus score
siteId:69
calculatorName:bonusScore
dependsOn:baseScore
var:finalScore
resultType:float
formula:
$baseScore + 10
---END_OF_PART---
The dependsOn:baseScore field ensures baseScore is evaluated before bonusScore.
FormulaInfoAdditionalFields fields = new FormulaInfoAdditionalFields(
"siteId", // partition key field name
info -> info.calculatorName); // name extractor: how to identify each formula
// Set global default backend (optional, default is JAVA_CODE)
fields.setExecutionBackend(ExecutionBackend.JAVA_CODE);FileBaseTinyExpressionInstancesCache cache = new FileBaseTinyExpressionInstancesCache(
Path.of("src", "main", "resources", "formula-root"),
fields);The cache loads and compiles formulas lazily per tenant.
ResultConsumer receives each formula's result and decides what to do with it.
ResultConsumer resultConsumer = new ResultConsumer() {
@Override
public void accept(CalculationContext c, Calculator calculator, FormulaInfo info, Number result) {
// Write result to context variable named by "var" key
info.getValue("var").ifPresent(name -> c.set(name, result));
}
@Override
public void accept(CalculationContext c, Calculator calculator, FormulaInfo info, String result) {
info.getValue("var").ifPresent(name -> c.set(name, result));
}
@Override
public void accept(CalculationContext c, Calculator calculator, FormulaInfo info, Boolean result) {
info.getValue("var").ifPresent(name -> c.set(name, result));
}
@Override
public void accept(CalculationContext c, Calculator calculator, FormulaInfo info, Object result) {
info.getValue("var").ifPresent(name -> c.setObject(name, result));
}
};CalculationContext ctx = CalculationContext.newConcurrentContext();
ctx.set("age", 30);
TinyExpressionsExecutor executor = new TinyExpressionsExecutor();
List<CalculationResult> results = executor.execute(
TenantID.create(69),
ctx,
resultConsumer,
cache,
Comparator.comparingInt(Calculator::dependsOnByNestLevel).reversed(), // dependencies first
calculator -> true, // include all formulas
Thread.currentThread().getContextClassLoader());
System.out.println("executed: " + results.size()); // 2
System.out.println("finalScore: " + ctx.get("finalScore")); // 110.0The recommended production setup:
- Set the global default to
JAVA_CODE(already the default) - Override per-formula only when needed (e.g.,
backend:P4_AST_EVALUATORfor P4 grammar coverage) - Run parity tests before switching backends in production
See docs/backends.md for a full backend comparison.
To call a Java method from a formula:
- Implement the method in a Java class
package myapp;
import org.unlaxer.tinyexpression.CalculationContext;
public class RiskChecker {
public boolean isHighRisk(CalculationContext context, String region) {
return "HIGH_RISK".equals(region);
}
}- Register the object in the context before execution
context.set(new myapp.RiskChecker());- Reference it in the formula
import myapp.RiskChecker#isHighRisk as isHighRisk;
if(external returning as boolean isHighRisk($region)){100}else{0}
- Language Guide — complete language specification
- Backends — 6 backend comparison and generated-only boundary
- Architecture — parser, AST, evaluator internals