Simon Hartl · System Integration  · 15 min read

Extending Apache Camel Simple with Your Own Functions

Apache Camel’s Simple language already covers much of the logic needed in integration routes. Functions can be composed directly in Simple, while custom Java functions provide a clean extension point for application-specific logic.

Apache Camel’s Simple language already covers much of the logic needed in integration routes. Functions can be composed directly in Simple, while custom Java functions provide a clean extension point for application-specific logic.

Extending Apache Camel Simple with Your Own Functions

Apache Camel’s Simple language is surprisingly powerful. It can access message bodies, headers, variables and exchange properties, work with strings and numbers, evaluate conditions, process dates, access JSON and XML data, and combine multiple functions into transformation pipelines. (Apache Camel – Simple Language)

For a large part of the logic required inside an integration route, this means that no Java code is necessary. Simple goes another step further: existing functions can even be composed into local custom functions directly inside an expression. (Apache Camel – Simple Advanced Features)

There will still be situations where application-specific logic does not fit naturally into an expression. For those cases, Camel allows us to extend Simple with functions implemented in Java without moving the complete route into Java.

In this article, we will use both approaches in one runnable example:

cleanName  → composed entirely from Simple functions
maskEmail  → implemented as a Java SimpleFunction

The complete example consists of only two files:

custom-simple-function/
├── route.camel.yaml
└── MaskEmailFunction.java

With Camel 4.22.0, we will run it using:

camel run --profile test route.camel.yaml MaskEmailFunction.java

The --profile test option is currently required because of a Camel 4.22.0 bug affecting $init{} functions in the default development profile. We will come back to that when we run the example.

The important part is that the integration flow remains a regular YAML route. We use Simple for the logic it already handles well and add a small piece of Java only where it provides additional value.

What we are going to build

Imagine an integration that receives some simple customer data. The customer name arrives with inconsistent whitespace:

  John   Doe

We want to normalize it to:

JOHN DOE

This transformation can already be expressed by combining existing Simple functions, so there is little reason to implement it in Java. We will create a local cleanName function that performs the following operations:

trim
  → normalizeWhitespace
  → uppercase

The same customer also has the following email address:

john.doe@example.com

Before writing that address to another message, we want to mask it:

j***@example.com

For the purpose of this example, we will treat the masking rule as application-specific logic and implement it as a reusable Java function:

${maskEmail(${header.customerEmail})}

The final route therefore demonstrates two different ways of extending what we can do from a Simple expression. cleanName is composed entirely from existing Simple functions, while maskEmail is implemented in Java and exposed back to the route as a Simple function.

Step 1: Create the example directory

Create a directory for the example and change into it:

mkdir custom-simple-function
cd custom-simple-function

Inside this directory, we will create the following two files:

route.camel.yaml
MaskEmailFunction.java

The complete integration flow stays in route.camel.yaml. The Java file contains only the small piece of functionality that we want to add to Simple.

Before using Java: composing functions with Simple

Before implementing our Java function, it is worth looking at what Simple itself can already do. In Camel 4.22, a Simple expression can contain an initialization block using $init{}. (Apache Camel – Simple Advanced Features)

$init{
    ...
}init$

Inside this block, we can define a local function by composing existing Simple functions with the ~:= operator. For example, our cleanName function can be defined like this:

$init{
  $cleanName ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()};
}init$

${cleanName(${header.customerName})}

The function definition combines three existing Simple functions using Camel’s chain operator ~>:

${trim()}
    ~> ${normalizeWhitespace()}
    ~> ${uppercase()}

The result of each function becomes the input of the next one. A value such as:

  John   Doe

therefore moves through the chain like this:

input


trim()


normalizeWhitespace()


uppercase()


JOHN DOE

Once defined, the complete transformation can be called through the much more meaningful expression:

${cleanName(${header.customerName})}

There is no Java implementation behind cleanName; the entire function is composed from functionality that Simple already provides. Functions defined in an $init{} block are local to the Simple expression in which they are declared, which makes them particularly useful for structuring larger transformations and giving them meaningful names. (Apache Camel – Simple Advanced Features)

If the required logic can be expressed clearly by combining existing Simple operations, this is often all we need. Our email masking requirement is deliberately different: instead of composing existing operations, we want to implement our own application-specific algorithm and expose it as a reusable function.

For that, we will create a Java-based SimpleFunction.

Step 2: Create the Java Simple function

Create a file called:

MaskEmailFunction.java

and add the following code:

import org.apache.camel.BindToRegistry;
import org.apache.camel.Exchange;
import org.apache.camel.spi.SimpleFunction;

@BindToRegistry("mask-email-function")
public class MaskEmailFunction implements SimpleFunction {

    @Override
    public String getName() {
        return "maskEmail";
    }

    @Override
    public Object apply(Exchange exchange, Object input) throws Exception {
        String email = input.toString().trim();

        int at = email.indexOf('@');

        if (at <= 0 || at == email.length() - 1) {
            return "***";
        }

        String localPart = email.substring(0, at);
        String domain = email.substring(at);

        return localPart.substring(0, 1) + "***" + domain;
    }
}

That is the complete Java implementation. To understand how Camel makes this available to Simple, there are three parts worth looking at.

Implement SimpleFunction

The class implements:

org.apache.camel.spi.SimpleFunction

The actual function logic is contained in the apply() method:

public Object apply(Exchange exchange, Object input)

input contains the value passed to the function. In our example, the input will be:

john.doe@example.com

and the function returns:

j***@example.com

The method also receives the current Camel Exchange. We do not need it for this simple example, but it gives more advanced functions access to information associated with the current message. (Apache Camel – Simple Advanced Features)

Give the function a name

The name exposed to the Simple language comes from:

@Override
public String getName() {
    return "maskEmail";
}

This means that the function can later be called from a Simple expression using:

${maskEmail(...)}

The Java class name therefore does not become part of the route. The route developer only needs to know the meaningful function name maskEmail.

Make the function available to Camel

The class is annotated with:

@BindToRegistry("mask-email-function")

When running this example with Camel CLI, the annotation makes an instance available through Camel’s Registry so that the SimpleFunction can be discovered. Camel CLI can run regular Java source files alongside route DSL files, which makes this particularly convenient for small standalone examples. (Apache Camel – Java Beans)

The registry bean name and the Simple function name are two different things:

Registry bean:   mask-email-function
Simple function: maskEmail

For somebody writing the route, maskEmail is the relevant name.

Step 3: Create the YAML route

Now create the second file:

route.camel.yaml

with the following content:

- route:
    id: custom-simple-function
    from:
      uri: "timer:demo?repeatCount=1"
      steps:
        - setHeader:
            name: customerName
            constant: "  John   Doe  "

        - setHeader:
            name: customerEmail
            constant: "john.doe@example.com"

        - log:
            message: |
              Original customer:
              Name: ${header.customerName}
              Email: ${header.customerEmail}

        - setBody:
            simple: |
              $init{
                $cleanName ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()};
              }init$

              Customer: ${cleanName(${header.customerName})}
              Contact: ${maskEmail(${header.customerEmail})}

        - log:
            message: |
              Processed customer:
              ${body}

This route now demonstrates both approaches in the same Simple expression.

The first function is defined directly inside the $init{} block:

$cleanName ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()};

and then called with:

${cleanName(${header.customerName})}

There is no Java implementation involved. Camel passes the header value through trim(), normalizeWhitespace() and uppercase(), resulting in JOHN DOE.

Immediately below it, we call:

${maskEmail(${header.customerEmail})}

From the perspective of somebody reading the YAML route, this looks very similar to cleanName. The difference is that the implementation comes from MaskEmailFunction.java.

That is an important property of this approach: the route does not have to change its programming model just because one operation requires Java. Both Simple-composed logic and application-specific Java functionality can be used naturally inside the same expression.

Step 4: Run the integration

At this point, the directory should look like this:

custom-simple-function/
├── route.camel.yaml
└── MaskEmailFunction.java

Camel CLI can run multiple files together, including YAML route definitions and regular Java source files. Normally, both files can therefore simply be started using: (Apache Camel – Java Beans)

camel run route.camel.yaml MaskEmailFunction.java

There is, however, a temporary issue to be aware of when running this particular example with Camel 4.22.0.

Camel 4.22.0: use the test profile

Camel 4.22.0 contains a bug tracked as CAMEL-24486 that affects custom functions declared in $init{} blocks when Camel is running with the dev profile. Because Camel CLI uses the dev profile by default, the cleanName function in our example can fail during route creation even though the expression itself is valid. (Apache Camel – Using Profiles)

A typical error looks like:

No custom simple function with name: cleanName

Until using a release containing the fix, start the example with the test profile instead:

camel run --profile test route.camel.yaml MaskEmailFunction.java

Camel CLI supports dev, test and prod profiles, with dev being the default when running integrations through the CLI. (Apache Camel – Using Profiles)

This is only a workaround for the affected Camel version; --profile test is not a general requirement for using $init{}. CAMEL-24486 is marked as fixed for Camel 4.22.1 and 4.23.0, so with a release containing that fix the regular command can be used again:

camel run route.camel.yaml MaskEmailFunction.java

Expected result

With Camel 4.22.0, run:

camel run --profile test route.camel.yaml MaskEmailFunction.java

The route executes once and should produce output similar to:

Original customer:
Name:   John   Doe
Email: john.doe@example.com

followed by:

Processed customer:
Customer: JOHN DOE
Contact: j***@example.com

Both extension mechanisms are now active in the same route. The customer name is transformed entirely through Simple, while the email address passes through our Java implementation:

customerName


cleanName

    ├── trim()
    ├── normalizeWhitespace()
    └── uppercase()


JOHN DOE


customerEmail


maskEmail

    └── MaskEmailFunction.java


j***@example.com

Using the current message body as input

A custom function does not necessarily need an explicit argument. Suppose the current message body already contains:

john.doe@example.com

Our Java function can then be called using:

${maskEmail}

or:

${maskEmail()}

Camel uses the current message body as the function input. This behavior is supported by custom Simple functions and allows them to fit naturally into transformation pipelines. (Apache Camel – Simple Advanced Features)

A simple route fragment could therefore look like this:

- setBody:
    constant: "john.doe@example.com"

- setBody:
    simple: "${maskEmail}"

- log:
    message: "Masked email: ${body}"

The same principle applies to a function composed inside $init{}:

$init{
  $cleanName ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()};
}init$

${cleanName()}

Without an explicit argument, the current message body passes through the complete function chain. This becomes particularly useful when building transformation pipelines.

Combining custom and built-in functions

Simple’s chain operator allows built-in and custom functions to be combined. The operator:

~>

passes the result of the expression on its left to the function on its right. (Apache Camel – Simple Advanced Features)

Instead of writing:

${maskEmail(${header.customerEmail})}

we can therefore write:

${header.customerEmail} ~> ${maskEmail}

We can also add built-in operations before our custom function:

${header.customerEmail}
    ~> ${trim()}
    ~> ${maskEmail}

The email address is first trimmed and the resulting value is then passed to the Java-based maskEmail function. Application-specific functionality therefore becomes another building block in the same transformation pipeline as Camel’s built-in functions.

Simple already avoids a lot of Java

A custom Java function should not be the first solution for every transformation. Simple itself already handles a considerable amount of route-level logic involving message bodies, headers, variables, exchange properties, strings, numbers, dates, collections, JSON, XML, predicates, conditions and type conversions. (Apache Camel – Simple Language)

For example:

${header.customerName}
    ~> ${trim()}
    ~> ${normalizeWhitespace()}
    ~> ${uppercase()}

is short enough to remain directly inside a route. Creating a Java processor or helper class for such a transformation would add complexity without adding much value.

If the expression becomes larger or is easier to understand under a meaningful name, $init{} gives us another option:

$init{
  $cleanName ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()};
}init$

${cleanName(${header.customerName})}

We still have not written any Java. Simple should therefore normally be the first place to look when implementing small pieces of route-level transformation or decision logic.

When Java is useful

There will still be cases where an expression language is no longer the right place for the implementation. Company-specific material-number normalization, proprietary naming rules, complex classifications, special validation algorithms, application-specific identifier generation or the reuse of existing Java libraries are all examples where a Java implementation may be clearer and easier to maintain.

In such cases, a SimpleFunction provides a small extension point without changing the architecture of the route. For example, we could implement:

public class MaterialNumberFunction implements SimpleFunction {
    // company-specific implementation
}

and expose that functionality to route developers as:

${normalizeMaterialNumber(...)}

The same principle could be applied to functions such as:

${calculateCustomerClass(...)}
${sanitizeForLogging(...)}

The implementation complexity stays behind the function, while the route only sees a concise and meaningful operation.

Local Simple functions or Java functions?

The two approaches serve slightly different purposes. A function declared inside $init{} is local to the Simple expression and is a good fit when existing Simple functionality already does what we need, but we want to compose several operations and give the result a meaningful name. (Apache Camel – Simple Advanced Features)

A Java SimpleFunction, on the other hand, is registered with Camel and is better suited to application-specific functionality that should be encapsulated independently of one particular expression.

A useful way to think about the decision is:

Can Simple already do it?

        ├── Yes
        │    │
        │    ▼
        │  Use built-in Simple


Is the expression becoming harder to read?

        ├── Yes
        │    │
        │    ▼
        │  Compose a local function with $init{}


Does it require genuinely custom logic?

        ├── Yes
        │    │
        │    ▼
        │  Implement a Java SimpleFunction


Use it from the YAML route like another function

This keeps the amount of Java in an integration proportional to the actual complexity of the problem rather than making Java the default choice for every transformation.

Accessing the Camel Exchange

A Java custom function receives not only its input value but also the current Camel Exchange:

public Object apply(Exchange exchange, Object input)

This means a function can do more than simply transform its argument. When required, it can access headers, variables, exchange properties, the Camel context or other information associated with the current message. (Apache Camel – Simple Advanced Features)

That makes SimpleFunction a lightweight bridge between declarative route expressions and more advanced application-specific functionality. For simple transformations such as our email masking function, however, the explicit input value is all we need.

Handling null values

There is one implementation detail worth knowing when implementing SimpleFunction. By default, Camel does not call apply() when the resolved function input is null. (Apache Camel – Simple Advanced Features)

For example:

${maskEmail(${header.customerEmail})}

will resolve to a null input if the customerEmail header does not exist. The default implementation of:

allowNull()

returns false, so Camel does not invoke the function in that case.

If handling a missing value should be part of the function itself, override allowNull():

@Override
public boolean allowNull() {
    return true;
}

The implementation can then decide how to deal with the missing input:

@Override
public Object apply(Exchange exchange, Object input) throws Exception {
    if (input == null) {
        return "not provided";
    }

    String email = input.toString();

    // ...
}

For many transformation functions, Camel’s default behavior is appropriate. Overriding allowNull() is only necessary when handling the missing value is explicitly part of the function’s responsibility.

Simple first, Java where useful

The main point of custom Simple functions is not that every transformation should eventually be implemented in Java. Quite the opposite: Simple already provides a powerful toolbox that can keep a significant amount of route logic directly inside the YAML definition. (Apache Camel – Simple Language)

For straightforward transformations, use Simple directly:

${header.customerName} ~> ${trim()} ~> ${uppercase()}

If several existing operations form a larger transformation, compose them into a local function:

$init{
  $cleanName ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()};
}init$

${cleanName(${header.customerName})}

Only when the requirement becomes genuinely application-specific do we add the missing functionality in Java:

${maskEmail(${header.customerEmail})}

The resulting architecture can remain simple:

YAML route


Simple expressions


Built-in functions


Composed local functions with $init{}


Custom Java functions where required

Java remains available whenever we need its full capabilities, but we do not have to use it for every small piece of route logic. Even when Java is necessary, SimpleFunction lets us encapsulate the implementation behind a concise expression while the integration flow itself remains declarative.

Complete runnable example

For reference, the complete example consists of these two files:

custom-simple-function/
├── route.camel.yaml
└── MaskEmailFunction.java

MaskEmailFunction.java

import org.apache.camel.BindToRegistry;
import org.apache.camel.Exchange;
import org.apache.camel.spi.SimpleFunction;

@BindToRegistry("mask-email-function")
public class MaskEmailFunction implements SimpleFunction {

    @Override
    public String getName() {
        return "maskEmail";
    }

    @Override
    public Object apply(Exchange exchange, Object input) throws Exception {
        String email = input.toString().trim();

        int at = email.indexOf('@');

        if (at <= 0 || at == email.length() - 1) {
            return "***";
        }

        String localPart = email.substring(0, at);
        String domain = email.substring(at);

        return localPart.substring(0, 1) + "***" + domain;
    }
}

route.camel.yaml

- route:
    id: custom-simple-function
    from:
      uri: "timer:demo?repeatCount=1"
      steps:
        - setHeader:
            name: customerName
            constant: "  John   Doe  "

        - setHeader:
            name: customerEmail
            constant: "john.doe@example.com"

        - log:
            message: |
              Original customer:
              Name: ${header.customerName}
              Email: ${header.customerEmail}

        - setBody:
            simple: |
              $init{
                $cleanName ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()};
              }init$

              Customer: ${cleanName(${header.customerName})}
              Contact: ${maskEmail(${header.customerEmail})}

        - log:
            message: |
              Processed customer:
              ${body}

Run with Camel 4.22.0

Because of CAMEL-24486, use a non-dev profile with Camel 4.22.0:

cd custom-simple-function

camel run --profile test route.camel.yaml MaskEmailFunction.java

Camel CLI profiles can be selected explicitly with --profile; for this example, test avoids the issue in the affected dev profile. (Apache Camel – Using Profiles)

The output should look similar to:

Original customer:
Name:   John   Doe
Email: john.doe@example.com

Processed customer:
Customer: JOHN DOE
Contact: j***@example.com

Once you are using a Camel release containing the fix for CAMEL-24486, the profile workaround is no longer required:

camel run route.camel.yaml MaskEmailFunction.java

With two files and one command, the example demonstrates both extension mechanisms: reusable logic composed directly from Simple functions and application-specific Java logic exposed back to the route as another Simple function.

Conclusion

Apache Camel’s Simple language can handle far more than just accessing a message body or header. For many integrations, its built-in functions and operators cover a significant part of the transformation and decision logic required directly inside a route. (Apache Camel – Simple Language)

When several existing operations belong together, $init{} allows us to compose them into meaningful local functions without writing Java. And when something genuinely application-specific is missing, a Java SimpleFunction provides a clean extension mechanism without forcing the integration route itself into Java. (Apache Camel – Simple Advanced Features)

The result is a useful balance: the route remains YAML, specialized implementations stay encapsulated, and both approaches can be consumed through concise Simple expressions such as:

${cleanName(...)}
${maskEmail(...)}

Use Simple wherever it keeps the route clear. Compose what you can. Add Java where it actually provides value.

Back to Blog

Related Posts

View All Posts »

Systemintegration muss nicht teuer sein

Viele Unternehmen scheuen vor Systemintegration zurück – aus Angst vor hohen Kosten und komplexer Umsetzung. Der Artikel zeigt, wie das Open-Source-Framework Apache Camel eine kostengünstige, flexible und zukunftssichere Lösung bietet. Mit standardisierten Integrationsmustern, hoher Wiederverwendbarkeit und einfacher Wartung ermöglicht Camel auch kleinen und mittleren Unternehmen den Einstieg in eine professionelle Integrationsarchitektur – ohne Vendor-Lock-in oder Lizenzkosten.