Skip to main content

Jactl Meets Apache Camel: Benchmarking camel-jactl

· 12 min read
James Crawford
Jactl Creator

camel-jactl is a new Apache Camel language module that lets you use Jactl as the scripting language inside Camel for things such as filter predicates, content-based routing rules, and message transformations. It registers itself through Camel's Language SPI under the name jactl.

import static io.jactl.camel.JactlLanguage.jactl;

from("direct:orders")
.filter(jactl("body.amount > 100 && body.status == 'NEW'"))
.to("direct:big-orders");

// Or via the Language SPI:
Language jactl = camelContext.resolveLanguage("jactl");
Predicate p = jactl.createPredicate("body.age > 20");
Expression e = jactl.createExpression("'Hello ' + body.name");

This post uses JMH benchmarks to compare the performance of Jactl against Camel's built-in Simple language and against Groovy (via camel-groovy) in the places where scripting languages typically appear in Camel applications, from single predicate evaluations up to a complete order-processing route, with a plain-Java route as the no-scripting baseline.

Jactl in Camel

Scripts in Camel routes usually make small decisions over message data: is this order valid, which queue does it go to, what should the summary body look like. Jactl is a good fit for this role:

  • It compiles to JVM bytecode: predicates and expressions run at close to Java speed once compiled.
  • It is sandboxed by default: Scripts can work freely with Map/List/String/numeric message data, but cannot call methods on host objects such as the exchange or a POJO body unless you opt in (JactlLanguage.createWithHostAccess(), with an optional predicate to whitelist specific classes).
  • It has no external dependencies: a single ~1.1MB jar with a shaded copy of ASM.

Jactl borrowed much of its syntax from Groovy so users familiar with Groovy but looking for a stronger secure sandbox model or better performance might want to consider Jactl as an alternative. Groovy is a more powerful and flexible language with its built-in metaobject protocol but this comes with some performance disadvantages compared to Jactl, and Groovy does not offer an out-of-the-box locked-down execution environment like Jactl does.

Scripts see the message data as ordinary variables (body, headers, variables), and the rest of Camel's usual script variables (exchangeProperties, exchange, camelContext, ...) are available by opting in with withAllowContextMapAll(true).

Scripts are cached so the compilation cost is only incurred once.

The Benchmarks

The benchmarks are modelled on the camel-jmh module in Apache's own camel-performance-tests project and can be found in the benchmarks subproject of camel-jactl.

ComponentVersion
Apache Camel4.20.0
Jactl2.9.2
Groovy (via camel-groovy)5.0.5
JMH1.37
JVMOpenJDK 25.0.2

Where the same script is run in more than one language, the setup asserts that all languages produce identical results before anything is timed, so every language is doing the same work.

note

The absolute numbers are not important since they depend on the machine the benchmarks are run on. It is the relative performance that matters.

The three languages compared:

  • Simple is Camel's built-in expression language. It is not a general-purpose language, but is commonly used.
  • Groovy (camel-groovy) compiles each script to a Groovy Script class which is cached and reused.
  • Jactl (camel-jactl) compiles each script to JVM bytecode via its embedded ASM and caches the compiled script.

1. Predicate and Expression Evaluation

The first set of benchmarks measures the pure evaluation cost of pre-compiled predicates and expressions, resolved through the Language SPI and evaluated against an exchange whose body is a Map (name: "tony", age: 44) with one header (gold: "123").

Map body predicate

body.name != null && body.age > 20

Jactl leads at 96.3M evaluations/s, 15.7x faster than Groovy (6.13M) and 56.1x faster than Simple (1.72M). The Simple predicate contains two ${...} expressions combined with &&, an indexed body['name'] access, and !=/> comparisons go through Simple's dynamic type-coercion rules on every evaluation. Simple parses the predicate once, but evaluation still walks that parsed structure, whereas Groovy and Jactl execute compiled bytecode.

Header predicate

headers.gold == '123'

This is a simpler predicate for Simple and allows it to shine compared to the previous benchmark with (56.8M evaluations/s) being 8.9x faster than Groovy. Jactl still comes out ahead at 112M evaluations/s, twice as fast as Simple and 17.6x faster than Groovy.

Value expression

'Hello ' + body.name

Jactl is fastest at 77.6M evaluations/s, 12.8x faster than Groovy (6.05M) and 25.6x faster than Simple (3.04M).


2. Filter Route

The next benchmark moves from raw evaluation to a real route: a Java DSL route whose filter predicate calls out to the scripting language, the way an application would actually use camel-groovy or camel-jactl. Each invocation sends two exchanges through the route — one that passes the filter and one that does not.

from("direct:jactl")
.filter(jactl("body.age > 20"))
.process(e -> { });

Once the full Camel routing engine is between the sender and the predicate, the gaps become smaller. Jactl completes 1.27M route invocations/s (each invocation is two exchanges), 1.8x ahead of Groovy (695K) and 2.0x ahead of Simple (650K).

This benchmark measure includes the cost of the overall routing machinery which is why the difference in performance is smaller than the raw execution speeds from the previous benchmarks.


3. Complex Scripts

This set of benchmarks evaluates the raw processing speed of more complex scripts over order bodies of 10 and 200 line items.

The Groovy and Jactl sources are identical apart from idiomatic method names (Groovy collect/every vs Jactl map/allMatch) and both languages are verified to produce equal results before anything is timed. Prices are integer cents so the arithmetic is exact and type-identical in both languages.

Order summary transformation

A closure pipeline computing an order total, a tier-based discount, per-category subtotals, and a summary string:

def customer = body.customer
def items = body.items
def total = items.map{ it.qty * it.unitPrice }.sum()
def discount = customer.tier == 'gold' && total >= 10000 ? 1500 : customer.tier == 'silver' && total >= 10000 ? 500 : 0
def subtotals = [:]
items.each{ subtotals[it.category] = (subtotals[it.category] ?: 0) + it.qty * it.unitPrice }
def result = [customer:customer.name, total:total, discount:discount, payable:total - discount, categories:subtotals, summary:"${customer.name}: ${items.size()} items, payable ${total - discount}c".toString()]
result

At 10 line items Jactl evaluates the summary 2.66M times/s, 3.6x faster than Groovy (746K). At 200 line items Jactl's advantage grows to 4.7x (235K vs 50.1K evaluations/s).

Express-handling predicate

A whole-list scan (the data contains no 'fragile' items, so there is no early exit) plus short-circuit business rules:

def items = body.items
items.allMatch{ it.category != 'fragile' } && items.map{ it.qty * it.unitPrice }.sum() < 10000000 && body.customer.tier in ['gold','silver']

Here Jactl is 7.4x faster at 10 items (9.12M vs 1.23M evaluations/s) and 5.2x faster at 200 items (472K vs 90.8K).


4. Full Order-Processing Route

The final throughput benchmark puts it all together: a realistic order-processing route where the scripting language does the work in the three places scripts typically appear — validation, content-based routing, and transformation:

from("direct:orders-jactl")
.filter(jactl(VALIDATION)) // scripted validation
.choice()
.when(jactl(EXPRESS)) // scripted content-based routing
.setHeader("handling", constant("express"))
.otherwise()
.setHeader("handling", constant("standard"))
.end()
.setBody(jactl(SUMMARY)) // scripted transformation
.process(e -> { });

VALIDATION

body.customer != null && body.items && body.items.allMatch{ it.qty > 0 && it.unitPrice > 0 }

EXPRESS

body.customer != null && body.items && body.items.allMatch{ it.qty > 0 && it.unitPrice > 0 }

SUMMARY

body.customer != null && body.items && body.items.allMatch{ it.qty > 0 && it.unitPrice > 0 }

Each invocation sends one valid order and one invalid (filtered-out) order through the route, over bodies of 10 and 200 line items. A third route implements identical logic in plain Java as the no-scripting baseline, and the setup asserts all three routes produce equal summaries before anything is timed.

At 10 line items the plain-Java route manages 632K invocations/s, with Jactl at 572K (91% of the Java baseline) and 2.6x faster than Groovy (224K, or 35% of Java).

At 200 line items, Jactl holds 70% of the Java baseline (106K vs 151K) and is 3.9x faster than Groovy (27.4K, 18% of Java).


5. Compilation Speed

The benchmarks so far measure scripts that are compiled once and evaluated millions of times, the normal case, since both camel-groovy and camel-jactl cache compiled scripts by script text. The last benchmark measures the cost of the first compile: compiling a predicate and evaluating it once, with unique script text per invocation to bypass the caches.

body.name != null && body.age > 20   // plus a unique comment per invocation

Jactl can compile and evaluate 30.0K unique predicates per second which is 9.3x more than Groovy's 3.23K, even though both languages are generating and loading real JVM bytecode. For routes defined at startup this only affects startup time, but it matters if your application creates scripts dynamically.

Note that continually creating new scripts will eventually exhaust the heap as the class loader is bound to the JactlLanguage instance.


Summary

The table below shows throughput relative to the slowest language in each benchmark (labelled baseline). The higher the multiplier, the faster the language.

BenchmarkJactlGroovySimpleJava
Map body predicate56.1x3.6xbaseline-
Header predicate17.6xbaseline8.9x-
Value expression25.6x2.0xbaseline-
Filter route (end-to-end)2.0x1.1xbaseline-
Summary script (10 items)3.6xbaseline--
Summary script (200 items)4.7xbaseline--
Express predicate (10 items)7.4xbaseline--
Express predicate (200 items)5.2xbaseline--
Order route (10 items)2.6xbaseline-2.8x
Order route (200 items)3.9xbaseline-5.5x
Compile + first evaluation9.3xbaseline--

Conclusion

Jactl has both good runtime performance and compilation performance and provides the benefits of compiled code with the flexibility of a scripting language along with a configurable security sandbox model that allows the application to control what scripts can and cannot do.

Further Reading

The camel-jactl source, including these benchmarks, is at github.com/jaccomoc/camel-jactl.

There is a comparison of Jactl and Groovy here: Groovy vs Jactl.

There is also a benchmark of Jactl, Groovy, MEXL, JEXL, and SpEL.

The Jactl Documentation site has full documentation for the language and the source code can be found in the Jactl GitHub Repository.