Advanced — New in 1.2.0
Scripting Support
rulii 1.2.0 introduces a scripting layer that lets you embed dynamic expressions — written in JavaScript, Groovy, or any JSR-223 language — directly inside rules, conditions, actions, and functions. Scripts are first-class citizens: they are compiled once, cached, and evaluated against the current RuleContext.
The Script API
A Script<T> is a typed, named script. Build one with Script.builder():
// A boolean script using JavaScript via JSR-223
Script<Boolean> script = Script.builder()
.with("js", "ctx.age >= 18")
.build();
// Reference bindings by name directly in the script body
Script<Boolean> script = Script.builder()
.with("js", "ctx.age >= ctx.minAge")
.build();
The ctx Binding Object
Rather than injecting each binding as a top-level variable in the script scope, rulii exposes all bindings as a single object named ctx. This was a deliberate design decision for two reasons:
- Namespace separation. Injecting every binding as its own top-level variable creates collisions with local script variables and language built-ins. Accessing bindings through
ctxkeeps the binding namespace isolated from anything the script declares locally. - Performance. Building a flat variable scope for a scripting engine requires copying every binding into the engine's bindings map on each evaluation. With
ctx, only a single object reference is injected — the engine receives one binding regardless of how many values theRuleContextholds, which is significantly cheaper at runtime.
// All rule context bindings are accessed through ctx
Script<Boolean> check = Script.builder()
.with("js", "ctx.age >= 18 && ctx.country === 'US'")
.build();
// Local script variables do not pollute the binding namespace
Script<Integer> calc = Script.builder()
.with("js",
"var tax = ctx.price * 0.2; ctx.price + tax")
.build();
// 'tax' is a local variable — it never leaks into the RuleContext bindings
Using Scripts in Conditions, Actions and Functions
The builder classes for Condition, Action, and Function all accept a Script directly:
Script<Boolean> ageCheck = Script.builder()
.with("js", "ctx.age >= 18")
.build();
// Wrap the script as a Condition
Condition condition = Condition.builder().build(ageCheck);
// Use it in a Rule
Rule rule = Rule.builder()
.with("AgeCheck")
.given(condition)
.then(Action.builder().build(Script.builder()
.with("js", "ctx.status = 'approved'")
.build()))
.build();
Script Processors & the Registry
A ScriptProcessor evaluates scripts for a specific language. The ScriptProcessorRegistry maps language names to their processors and is held by the RuleContext.
// Build a registry with JSR-223 auto-discovery enabled
ScriptProcessorRegistry registry = ScriptProcessorRegistry.builder()
.autoIncludeJSR223(true)
.build();
// Register a custom processor
registry.register(myCustomProcessor);
// Wire into RuleContext
RuleContext ctx = RuleContext.builder()
.with("MyContext")
.scriptProcessorRegistry(registry)
.build();
JSR-223 Bridge
When autoIncludeJSR223 is enabled, rulii automatically discovers a javax.script.ScriptEngine by language name on first use. If the engine implements Compilable, the script is compiled and cached in a WeakHashMap for reuse. The RuleContext bindings are injected into the script scope under the configured bindings name ("ctx" by default).
// Access bindings in the script via "ctx"
// The bindings map is exposed as a plain Map, so dot-access
// syntax depends on the scripting language.
// JavaScript — language name "js" (Nashorn / GraalJS)
"ctx.age >= 18 && ctx.country === 'US'"
// Groovy
"ctx.age >= 18 && ctx.country == 'US'"
Built-in Script Engines
rulii ships with two optional script engines — GraalJS and Janino. Both provide a ScriptProcessorFactory that is auto-discovered via Java's ServiceLoader. Add the dependency to your build and the engine registers itself automatically — no additional wiring required.
GraalJS — language "js"
GraalJS is Oracle's high-performance JavaScript engine built on the Truffle framework. rulii's GraalJsScriptProcessorFactory creates an engine pre-configured with ECMAScript 2022, full host access (HostAccess.ALL), and unrestricted host class lookup — so scripts can call Java methods directly via ctx. If the GraalJS classes are not on the classpath isAvailable() returns false and the engine is silently skipped.
Maven
<!-- GraalJS script engine -->
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js-scriptengine</artifactId>
<version>24.1.0</version>
</dependency>
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>js-community</artifactId>
<version>24.1.0</version>
<type>pom</type>
</dependency>
Gradle
implementation "org.graalvm.js:js-scriptengine:24.1.0"
implementation "org.graalvm.polyglot:js-community:24.1.0"
Once the dependencies are present, scripts using language "js" are compiled and cached automatically. Bindings are exposed through ctx and full Java interop is available.
// Boolean condition — age check
Script<Boolean> ageCheck = Script.builder()
.with("js", "ctx.age >= 18 && ctx.country === 'US'")
.build();
// Action — write a computed value back to a binding
Script<Void> applyDiscount = Script.builder()
.with("js", "ctx.total = ctx.price * (1 - ctx.discountRate)")
.build();
// Local variables are isolated from the RuleContext
Script<Void> withLocal = Script.builder()
.with("js", "var tax = ctx.price * 0.2; ctx.total = ctx.price + tax;")
.build();
// 'tax' stays local — it is never written back to the RuleContext
Janino — language "java"
Janino is a lightweight Java compiler that compiles script fragments to bytecode at runtime. Unlike the JSR-223 bridge, rulii's Janino integration uses a custom compiler that reads the live bindings at compile time to generate a fully typed method signature — each binding becomes a typed parameter, producing a JIT-compiled evaluator with no reflection overhead per call. The compiled evaluator is cached in the Script instance and reused on subsequent runs.
Maven
<!-- Janino JIT Java compiler -->
<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>janino</artifactId>
<version>3.1.12</version>
</dependency>
Gradle
implementation "org.codehaus.janino:janino:3.1.12"
Scripts are standard Java statement blocks. All bindings are accessed through ctx using the same dot-access syntax as the JavaScript engine. The Java standard library is available without any imports.
// Read a binding and write a result back
Script.<Void>builder()
.with("java", "ctx.total = ctx.price * ctx.quantity;")
.build();
// Local typed variable — stays local, never leaks into RuleContext
Script.<Void>builder()
.with("java", "int square = ctx.base * ctx.base;\nctx.result = square + ctx.base;")
.build();
// Ternary and standard library methods work as expected
Script.<Void>builder()
.with("java", "ctx.label = ctx.score >= 90 ? \"A\" : ctx.score >= 80 ? \"B\" : \"C\";")
.build();
Script.<Void>builder()
.with("java", "ctx.output = ctx.input.trim().toUpperCase();")
.build();
Constraints to be aware of
- Bindings must exist at compile time. Because the compiler generates a typed method signature from the live bindings, referencing a binding that has not been added to the
RuleContextbefore the script first runs will throw aBuildScriptException. ctxis reserved. Declaring a local variable namedctxis rejected at compile time.- No increment/decrement operators on bindings.
ctx.counter++andctx.counter--are not supported — usectx.counter = ctx.counter + 1instead.
Error Handling
Two exceptions cover the scripting error surface:
| Exception | When Thrown |
|---|---|
| BuildScriptException | Script compilation fails (syntax error, unknown language) |
| EvaluationException | Script evaluation fails at runtime (type error, missing binding) |
Spring users
If you're using rulii-spring, the SpEL Scripting page covers the Spring Expression Language integration, which is set up automatically and requires no extra configuration.