T(...) resolves only a short allowlist of safe types and new is
blocked (Calling Java). Your own code enters through one door, a
registered function: a public static Java method the console loads at startup and every
expression can call as #name(args). This page is the worked example, from the class to the
container, with what happens when a step is wrong.
Everything here was run against the published
alginte/alginte:0.11.0 image, and the two
behaviours a function can have, a value and an exception, are rows in the fidelity corpus that
asserts the editor preview, the validator and the deployed runtime agree.1. Write the class
No dependency on Alginte, no annotation, no interface. A class with a public static method:- The method is public static. Instance methods are not callable.
- The name is unambiguous in its class: no overloads. Two
maskEmailmethods with different parameter lists abort startup, even if only one would ever match. - Arguments arrive as what the expression evaluates to (a
Stringfromvalue.get('email')on a JSON Schema or Avro record, boxed numbers, maps, lists), and the return value is what the expression continues with. Kafka Streams calls the function on a stream thread, once per record, so it must be thread-safe and should not block.
2. Register it
One property per function, the name you will call it by on the left andclass#method on the
right:
KAFKA_STREAMS_TRANSFORMATION_FUNCTIONS_MASKEMAIL registers #maskemail. Either name works;
use the one you registered. A mounted properties file passed with
spring.config.additional-location keeps the case too (Configuration).
3. Put the jar on the classpath
The Docker image runs Spring Boot’s launcher over the exploded application in/app, and the
launcher lists /app/BOOT-INF/lib/ at start. So a jar file mounted there is on the classpath
with nothing else changed:
/app is owned by root and the launcher does not descend into
subdirectories of lib/. For a directory of jars, switch to the PropertiesLauncher and
point loader.path at the mount; the application’s own classes and libraries stay on the
classpath:
/actuator/health
after startup is the proof the jar was found.
4. Call it
In any expression, on any operator:john.doe@example.com on the in line reads j***@example.com on the out line before anything
is deployed. The validator knows the name: a call to a function that is not registered is
reported while you type, as an unknown function. Completion offers the generic #fn() snippet
rather than your function’s name.
A function that throws fails the record the same way everywhere: the preview reports the
exception on the step, the validator marks the expression, and the deployed stream fails the
record’s transformation with the same exception, as it does for any expression error. There is
no path on which the throw is swallowed.
When it cannot resolve
Resolution is fail-fast: the console does not start, and the message names the function and the reason. The four cases:What the sandbox does and does not cover
The expression sandbox restricts what an expression can reach: theT(...) allowlist and the
blocked new operator keep a one-line expression from calling into the process. A registered
function is outside that boundary by design. It is your Java, running in the console’s process
with the console’s rights, and nothing inspects what it does. Register only code you would run
in the console anyway, and keep the trusted network boundary the console assumes
(Production): whoever can author expressions can
call every registered function with any argument.