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 ctx keeps 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 the RuleContext holds, 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 RuleContext before the script first runs will throw a BuildScriptException.
  • ctx is reserved. Declaring a local variable named ctx is rejected at compile time.
  • No increment/decrement operators on bindings. ctx.counter++ and ctx.counter-- are not supported — use ctx.counter = ctx.counter + 1 instead.

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.