Ron and Ella Wiki Page

Extremely Serious

Custom Log Masking in Apache Camel

Apache Camel's built-in masking can sometimes truncate after the first match. This article shows how to implement a custom MaskingFormatter that processes entire strings and masks all sensitive fields in JSON, query strings, and mixed formats without truncation.

Why Custom Masking?

The custom formatter:

  • Uses SensitiveUtils.getSensitiveKeys() (~100 built-in keywords like password, secret, token)
  • Adds your custom keywords (userId, ssn, creditCard)
  • Handles both JSON ("key": "value") and query strings (key=value)
  • Uses Matcher.appendReplacement() loop to mask every occurrence
  • Never truncates - processes complete input

Example

import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.spi.MaskingFormatter;
import org.apache.camel.support.SimpleRegistry;
import org.apache.camel.util.SensitiveUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Custom formatter that masks ALL sensitive fields without truncating.
 * Processes entire string and masks all occurrences of sensitive keywords.
 */
static class NonTruncatingMaskingFormatter implements MaskingFormatter {
    private final Pattern sensitivePattern;

    public NonTruncatingMaskingFormatter() {
        // Use SensitiveUtils for ~100 built-in keywords + custom ones
        var sensitiveKeys = new java.util.HashSet<>(SensitiveUtils.getSensitiveKeys());
        sensitiveKeys.add("userId");
        sensitiveKeys.add("ssn");
        sensitiveKeys.add("creditCard");

        String keywordGroup = String.join("|", sensitiveKeys);

        // Matches JSON: "keyword": "value" AND query: keyword=value
        this.sensitivePattern = Pattern.compile(
                "(?i)(?:([\"'])(" + keywordGroup + ")\\1\\s*:\\s*[\"']([^\"']+)[\"']" +  // JSON
                        "|(" + keywordGroup + ")\\s*=\\s*([^&\\s,}]+))",  // Query string
                Pattern.CASE_INSENSITIVE
        );
    }

    @Override
    public String format(String source) {
        if (source == null || source.isEmpty()) {
            return source;
        }

        Matcher matcher = sensitivePattern.matcher(source);
        StringBuilder result = new StringBuilder();

        while (matcher.find()) {
            String replacement;
            if (matcher.group(1) != null) {
                // JSON format: "key": "value" -> "key": "xxxxx"
                String quote = matcher.group(1);
                String keyword = matcher.group(2);
                replacement = quote + keyword + quote + ": \"xxxxx\"";
            } else {
                // Query string: key=value -> key=xxxxx
                String keyword = matcher.group(4);
                replacement = keyword + "=xxxxx";
            }
            matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
        }
        matcher.appendTail(result);

        return result.toString();
    }
}

static class MyRoutes extends RouteBuilder {
    @Override
    public void configure() {
        from("direct:test")
                .to("log:maskedLogger?showAll=true&multiline=true")
                .log("*** FULL BODY (custom masking): ${body}");
    }
}

void main() throws Exception {
    SimpleRegistry registry = new SimpleRegistry();

    // Register custom formatter
    NonTruncatingMaskingFormatter formatter = new NonTruncatingMaskingFormatter();
    registry.bind(MaskingFormatter.CUSTOM_LOG_MASK_REF, formatter);

    DefaultCamelContext context = new DefaultCamelContext(registry);
    context.setLogMask(true);  // Enable globally
    context.addRoutes(new MyRoutes());
    context.start();

    // Query string payload - ALL fields masked
    String queryPayload = "userId=12345&password=pass123&apiKey=abc-xyz-123&ssn=123-45-6789&creditCard=4111-1111-1111-1111&token=jwt.abc.def&anotherPassword=secret456";
    context.createProducerTemplate().sendBody("direct:test", queryPayload);

    // Complex nested JSON - ALL fields masked
    String jsonPayload = """
        {
            "userId": "user123",
            "username": "john.doe",
            "password": "secretPass456",
            "email": "john@example.com",
            "apiKey": "sk-live-abc123xyz",
            "ssn": "987-65-4321",
            "creditCard": "5555-4444-3333-2222",
            "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
            "profile": {
                "accessToken": "ghp_xxxxxxxxxxxx",
                "refreshToken": "refresh_abc123"
            }
        }
        """;
    context.createProducerTemplate().sendBody("direct:test", jsonPayload);

    Thread.sleep(2000);
    context.stop();
}

Expected Output

Query string (ALL 7 fields masked):

*** FULL BODY (custom masking): userId=xxxxx&password=xxxxx&apiKey=xxxxx&ssn=xxxxx&creditCard=xxxxx&token=xxxxx&anotherPassword=xxxxx

JSON (ALL 10+ fields masked):

*** FULL BODY (custom masking): {
    "userId": "xxxxx",
    "username": "xxxxx",
    "password": "xxxxx",
    "email": "john@example.com",
    "apiKey": "xxxxx",
    "ssn": "xxxxx",
    "creditCard": "xxxxx",
    "token": "xxxxx",
    "profile": {
        "accessToken": "xxxxx",
        "refreshToken": "xxxxx"
    }
}

Key Advantages

Feature Built-in Custom
Truncation ❌ Sometimes ✅ Never
Custom keywords Limited ✅ Full control
Nested objects ❌ Limited ✅ Complete scan

Usage Patterns

Route-level:

from("direct:secure")
    .logMask()  // Uses your custom formatter
    .log("${body}");

Endpoint-level:

.to("log:secure?logMask=true")

Global: context.setLogMask(true) (as shown)

Apache Camel Error Handling: DefaultErrorHandler and onException

Apache Camel's robust error handling ensures reliable integration routes even when failures occur. The DefaultErrorHandler provides automatic retries and logging, while onException clauses offer precise control over specific exceptions.

Core Concepts

DefaultErrorHandler is Camel’s default strategy for all routes, featuring retry logic, detailed logging, and flexible recovery paths. It logs exceptions at ERROR level by default and supports configurable redeliveries without requiring dead letter channels.

onException clauses intercept specific exception types globally or per-route, with priority based on exception specificity—most-derived classes match first. Use handled(true) to stop propagation and transform responses, or continued(true) to resume the route.

DefaultErrorHandler Configuration

Set global retry policies early in RouteBuilder’s configure() method:

errorHandler(defaultErrorHandler()
    .maximumRedeliveries(3)
    .redeliveryDelay(2000)
    .retryAttemptedLogLevel(LoggingLevel.WARN)
    .logStackTrace(true));

Key options control retry behavior: maximumRedeliveries caps attempts, redeliveryDelay sets wait time (with exponential backoff), and retryAttemptedLogLevel adjusts retry logging verbosity. Per-route overrides nest within from() endpoints for granular control.

onException Best Practices

Place onException handlers before route definitions for global scope. Multiple handlers for the same exception type chain by priority, with conditions via onWhen():

onException(IOException.class)
    .handled(true)
    .log("🚨 IO EXCEPTION: ${exception.message}")
    .to("direct:ioErrorHandler");

onException(IllegalArgumentException.class)
    .onWhen(exchange -> "high".equals(exchange.getIn().getHeader("error.priority")))
    .handled(true)
    .log("⚠️ HIGH PRIORITY: ${exception.message}");

The handled(true) flag clears the exception and prevents DefaultErrorHandler retries unless continued(true) is specified. Route to dedicated error flows like direct:ioErrorHandler for consistent processing.

Example

This self-contained demo showcases layered error handling across exception types, conditional logic, and retry policies:

import org.apache.camel.*;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;

public class CamelErrorHandlingDemo extends RouteBuilder {

    @Override
    public void configure() throws Exception {
        // Global DefaultErrorHandler with retries
        errorHandler(defaultErrorHandler()
            .maximumRedeliveries(3)
            .redeliveryDelay(2000)
            .retryAttemptedLogLevel(LoggingLevel.WARN)
            .logStackTrace(true));

        // Specific exception handlers (priority: most specific first)
        onException(java.io.IOException.class)
            .handled(true)
            .log("🚨 IO EXCEPTION HANDLED FIRST: ${exception.message}")
            .to("direct:ioErrorHandler");

        onException(IllegalArgumentException.class)
            .maximumRedeliveries(2)
            .redeliveryDelay(1000)
            .onWhen(exchange -> "high".equals(exchange.getIn().getHeader("error.priority")))
            .handled(true)
            .log("⚠️ HIGH PRIORITY ARG EXCEPTION: ${exception.message}")
            .to("direct:argErrorHandler");

        onException(IllegalArgumentException.class)
            .handled(true)
            .log("⚠️ DEFAULT ARG EXCEPTION: ${exception.message}")
            .to("direct:argErrorHandler");

        onException(Exception.class)
            .handled(true)
            .log("💀 GENERIC EXCEPTION: ${exception.message}")
            .to("direct:deadLetterQueue");

        // Main processing route
        from("direct:start")
            .routeId("mainRoute")
            .log("Processing: ${body}")
            .process(mainProcessor())
            .to("direct:success");

        from("direct:success").log("✅ SUCCESS: ${body}");
        from("direct:ioErrorHandler").log("📁 IO Error handled").setBody(constant("IO_PROCESSED"));
        from("direct:argErrorHandler").log("🔄 Arg error processed").setBody(constant("ARG_PROCESSED"));
        from("direct:deadLetterQueue").log("⚰️ DLQ: ${exception.stacktrace}").setBody(constant("DLQ_PROCESSED"));
    }

    private static Processor mainProcessor() {
        return exchange -> {
            String input = exchange.getIn().getBody(String.class);
            switch (input != null ? input : "") {
                case "io-fail":
                    exchange.getIn().setHeader("error.priority", "high");
                    throw new java.io.IOException("💾 Disk full");
                case "arg-high":
                    exchange.getIn().setHeader("error.priority", "high");
                    throw new IllegalArgumentException("❌ High priority invalid data");
                case "arg-normal":
                    throw new IllegalArgumentException("❌ Normal invalid data");
                case "generic-fail":
                    throw new RuntimeException("Unknown error");
                default:
                    exchange.getIn().setBody("✅ Processed: " + input);
            }
        };
    }

    public static void main(String[] args) throws Exception {
        try (CamelContext context = new DefaultCamelContext()) {
            context.getCamelContextExtension().setName("CamelErrorDemo");
            context.addRoutes(new CamelErrorHandlingDemo());
            context.start();

            ProducerTemplate template = context.createProducerTemplate();
            System.out.println("🚀 Camel Error Handling Demo...\n");

            template.sendBody("direct:start", "io-fail");      // → IO handler
            template.sendBody("direct:start", "arg-high");     // → High priority arg
            template.sendBody("direct:start", "arg-normal");   // → Default arg  
            template.sendBody("direct:start", "generic-fail"); // → Generic handler
            template.sendBody("direct:start", "good");         // → Success

            Thread.sleep(8000);
        }
    }
}

Expected Demo Output

textProcessing: io-fail
🚨 IO EXCEPTION HANDLED FIRST: 💾 Disk full
📁 IO Error handled

Processing: arg-high  
⚠️ HIGH PRIORITY ARG EXCEPTION: ❌ High priority invalid data
🔄 Arg error processed

Production Tips

Test all failure paths during development using predictable payloads like the demo. Monitor exchange.getProperty(Exchange.REDELIVERY_COUNTER) for retry stats. For complex flows, combine with doTry().doCatch() blocks for local exception handling within routes. This approach scales from simple REST APIs to enterprise integration patterns.

Dead Letter Channel in Apache Camel with Log Endpoint

Apache Camel's Dead Letter Channel (DLC) is an Enterprise Integration Pattern that gracefully handles failed messages by routing them to a designated endpoint, preventing route blockages. This article explores DLC configuration using a lightweight log endpoint—no JMS broker required—ideal for development, testing, or simple logging setups. You'll get a complete, runnable example to see it in action.

What is Dead Letter Channel?

The DLC activates when a message processing fails after exhausting retries. Unlike Camel's default error handler, which logs exceptions, DLC preserves the original input message and forwards it to a "dead letter" endpoint like a log, file, or queue. Key benefits include message isolation for later analysis and non-blocking route behavior.

It supports redelivery policies for automatic retries before final routing. This pattern shines in enterprise integration where failures (e.g., network issues, data validation errors) must not halt overall flow.

Why Use Log Endpoint?

The log component offers zero-setup error handling: it captures failures at ERROR level with full exchange details (body, headers, exception stack trace). Perfect for:

  • Quick debugging without external dependencies.
  • Console output in standalone apps or containers.
  • Custom formatting via options like showAll=true or multiline=true.

No database, file system, or broker needed—run it anywhere Camel supports logging (SLF4J/Logback by default).

Complete Working Example

This standalone Java app uses Camel Main to simulate failures via a timer-triggered route. It retries twice, then logs the failure.

Java DSL Code

import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.main.Main;

public class DeadLetterLogDemo extends RouteBuilder {
    @Override
    public void configure() {
        // Configure DLC with log endpoint
        errorHandler(deadLetterChannel("log:dead?level=ERROR&showAll=true&multiline=true")
            .useOriginalMessage()  // Preserves original input
            .maximumRedeliveries(2)
            .redeliveryDelay(1000)
            .retryAttemptedLogLevel(org.apache.camel.LoggingLevel.WARN));

        // Route that simulates failure
        from("timer:fail?period=5000&repeatCount=1")
            .log("🔥 Starting processing: ${body}")
            .process(exchange -> {
                // Simulate a runtime failure (e.g., bad data parsing)
                throw new RuntimeException("💥 Simulated processing error!");
            })
            .log("✅ Success log (won't reach here)");
    }

    public static void main(String[] args) throws Exception {
        Main main = new Main();
        main.configure().addRoutesBuilder(new DeadLetterLogDemo());
        main.run(args);  // Runs until Ctrl+C
    }
}

Expected Console Output

🔥 Starting processing:
Failed delivery for (MessageId: 25B7DFF9FB26DD8-0000000000000000 on ExchangeId: 25B7DFF9FB26DD8-0000000000000000). On delivery attempt: 0 caught: java.lang.RuntimeException: 💥 Simulated processing error!
Failed delivery for (MessageId: 25B7DFF9FB26DD8-0000000000000000 on ExchangeId: 25B7DFF9FB26DD8-0000000000000000). On delivery attempt: 1 caught: java.lang.RuntimeException: 💥 Simulated processing error!
Failed delivery for (MessageId: 25B7DFF9FB26DD8-0000000000000000 on ExchangeId: 25B7DFF9FB26DD8-0000000000000000). On delivery attempt: 2 caught: java.lang.RuntimeException: 💥 Simulated processing error!
Exchange[
  Id: 25B7DFF9FB26DD8-0000000000000000
  RouteGroup: null
  RouteId: route1
  ExchangePattern: InOnly
  Properties: {CamelExceptionCaught=java.lang.RuntimeException: 💥 Simulated processing error!, CamelFailureRouteId=route1, CamelFatalFallbackErrorHandler=[route1], CamelToEndpoint=log://dead?level=ERROR&multiline=true&showAll=true}

Advanced Customizations

Enhance with processors for richer logging:

errorHandler(deadLetterChannel("log:dead?level=ERROR")
    .onExceptionOccurred(exchange -> {
        exchange.getIn().setHeader("failureTime", System.currentTimeMillis());
        exchange.getIn().setHeader("errorDetails", exchange.getException().getMessage());
    }));

Best Practices

  • Always pair with useOriginalMessage() to avoid losing input data.
  • Set maximumRedeliveries=0 for immediate DLC without retries in tests.
  • Monitor log volume in production; consider rotating to file or alerting.
  • Test edge cases: network timeouts, serialization errors, poison messages.
  • Global DLC via camelContext.setErrorHandlerFactory() for app-wide coverage.

This setup provides robust, observable error handling. Experiment by changing the failure to a real processor (e.g., invalid JSON parsing) for your use cases.

Apache Camel Transformer

Apache Camel's Transformer EIP enables declarative, type-safe message conversion between POJOs in routes. Below is a fully self-contained, single-file example using nested classes and Camel's Main class.

Complete Single-File Example

import org.apache.camel.Message;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.main.Main;
import org.apache.camel.spi.DataType;
import org.apache.camel.spi.Transformer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Apache Camel Transformer EIP example demonstrating declarative type-safe message conversion.
 * All classes (Lead, Customer, LeadToCustomerTransformer) are in this single file.
 */
public class TransformerDemo {
    private static final Logger LOG = LoggerFactory.getLogger(TransformerDemo.class);

    /**
     * Lead POJO - Input model
     */
    public static class Lead {
        private String name;
        private String company;
        private String city;

        public Lead() {}

        public Lead(String name, String company, String city) {
            this.name = name;
            this.company = company;
            this.city = city;
        }

        // Getters and setters
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        public String getCompany() { return company; }
        public void setCompany(String company) { this.company = company; }
        public String getCity() { return city; }
        public void setCity(String city) { this.city = city; }

        @Override
        public String toString() {
            return "Lead{name='" + name + "', company='" + company + "', city='" + city + "'}";
        }
    }

    /**
     * Customer POJO - Output model
     */
    public static class Customer {
        private String fullName;
        private String organization;
        private String location;

        public Customer() {}

        public Customer(String fullName, String organization, String location) {
            this.fullName = fullName;
            this.organization = organization;
            this.location = location;
        }

        // Getters and setters
        public String getFullName() { return fullName; }
        public void setFullName(String fullName) { this.fullName = fullName; }
        public String getOrganization() { return organization; }
        public void setOrganization(String organization) { this.organization = organization; }
        public String getLocation() { return location; }
        public void setLocation(String location) { this.location = location; }

        @Override
        public String toString() {
            return "Customer{fullName='" + fullName + "', organization='" + organization + "', location='" + location + "'}";
        }
    }

    /**
     * Transformer implementation for converting Lead to Customer
     */
    public static class LeadToCustomerTransformer extends Transformer {
        @Override
        public void transform(Message message, DataType fromType, DataType toType) throws Exception {
            Lead lead = message.getMandatoryBody(Lead.class);
            Customer customer = new Customer(
                lead.getName().toUpperCase(),
                lead.getCompany(),
                lead.getCity()
            );
            message.setBody(customer);
        }
    }

    public static void main(String[] args) throws Exception {
        Main main = new Main();
        main.configure().addRoutesBuilder(new RouteBuilder() {
            @Override
            public void configure() {
                // Register the transformer
                transformer()
                    .fromType("java:TransformerDemo$Lead")
                    .toType("java:TransformerDemo$Customer")
                    .withJava(LeadToCustomerTransformer.class);

                // Main route with automatic transformation
                from("direct:start")
                    .inputType("java:TransformerDemo$Lead")
                    .log("INPUT Lead: ${body}")
                    .to("direct:process")  // Triggers transformer automatically
                    .log("OUTPUT Customer: ${body}");

                from("direct:process")
                    .inputType("java:TransformerDemo$Customer")
                    .log("PROCESSED Customer: ${body}")
                    .to("log:final?showAll=true");
            }
        });

        // Add a startup listener to send test message after Camel starts
        main.addMainListener(new org.apache.camel.main.MainListenerSupport() {
            @Override
            public void afterStart(org.apache.camel.main.BaseMainSupport mainSupport) {
                try {
                    LOG.info("Sending test Lead message...");
                    mainSupport.getCamelContext().createProducerTemplate()
                        .sendBody("direct:start", new Lead("John Doe", "Acme Corp", "Auckland"));
                    LOG.info("Test message sent successfully!");
                } catch (Exception e) {
                    LOG.error("Failed to send test message", e);
                }
            }
        });

        // Run Camel (will run until Ctrl+C is pressed)
        main.run(args);
    }
}

Expected Console Output

Sending test Lead message...
INPUT Lead: Lead{name='John Doe', company='Acme Corp', city='Auckland'}
PROCESSED Customer: Customer{fullName='JOHN DOE', organization='Acme Corp', location='Auckland'}
Exchange[Id: D0B1A9A95984A67-0000000000000000, RouteGroup: null, RouteId: route2, ExchangePattern: InOnly, Properties: {CamelToEndpoint=log://final?showAll=true}, Headers: {}, BodyType: TransformerDemo.Customer, Body: Customer{fullName='JOHN DOE', organization='Acme Corp', location='Auckland'}]
OUTPUT Customer: Customer{fullName='JOHN DOE', organization='Acme Corp', location='Auckland'}
Test message sent successfully!

Key Implementation Details

Nested Class References: Uses TransformerDemo$Lead syntax to reference nested classes in data type strings.

Transformer Method Signature: Uses transform(Message message, DataType fromType, DataType toType)—the modern Camel 4.x+ API.

Automatic Startup Test: MainListenerSupport.afterStart() sends test message automatically when Camel context starts.

Single-File Design: All POJOs, transformer, and routes contained in one compilable .java file.

How the Transformation Triggers

  1. Input validationinputType("java:TransformerDemo$Lead") expects Lead
  2. Route boundaryto("direct:process") requires Customer input type
  3. Type mismatch → Camel matches Lead→Customer transformer automatically
  4. Conversion executesLeadToCustomerTransformer.transform() runs
  5. Type contract satisfied → Route continues with Customer

Apache Camel Consumer Types Explained

Apache Camel consumers initiate message processing in routes via the from() endpoint. They fall into two primary categories: event-driven and polling.

Core Classifications

Camel officially recognizes two consumer types—event-driven and polling—each suited to different message sources.

Type Description Mechanism
Event-Driven Reacts immediately to incoming events or invocations. No periodic checks. External push (e.g., JMS) or internal calls (e.g., direct). Uses transport-provided threads.
Polling Camel actively queries the endpoint at set intervals. Scheduled checks via ScheduledExecutorService. Configurable delay/initialDelay.

Event-Driven Consumers

These handle real-time messages without polling overhead. Examples include JMS (queue/topic events), HTTP/Netty (request triggers), and in-VM options like direct/SEDA.

  • Direct: Synchronous, blocking calls within the same CamelContext—like method invocation.
  • SEDA: Asynchronous queuing with backpressure control (bounded queue).

Example:

from("direct:start")  // Event-driven: waits for producerTemplate.sendBody()
    .log("Processed: ${body}");

Polling Consumers

Ideal for batch sources like files or databases. Camel polls periodically, ensuring ordered processing.

  • Uses fixed delays; supports repeat/repeatAttempt options.
  • Examples: file (watches directories), FTP/IMAP (remote checks).

Example:

from("file:input?delay=5000&noop=true")  // Polls every 5s
    .to("log:output");

Special Cases: In-VM Consumers

Direct and SEDA are event-driven subtypes for route chaining:

  • Direct: Sync, single-context.
  • SEDA: Async queue.

They differ from transport-based event-driven (e.g., JMS) by lacking external brokers but share passive reception.

When to Use Each

  • Event-Driven: Low-latency, continuous streams (pub/sub, APIs).
  • Polling: Reliable pulls from passive sources (files, legacy systems).

This covers Camel's consumer landscape for effective route design.

Apache Camel SEDA

Apache Camel SEDA implements the Staged Event-Driven Architecture pattern, enabling in-VM asynchronous messaging that decouples producers from consumers via BlockingQueue. This excels in high-load scenarios where synchronous endpoints like Direct would block threads—SEDA queues messages instead, boosting scalability with configurable concurrent consumers.

Core Advantages

  • Non-blocking Producers: Senders complete instantly while slow consumers process from the queue, preventing cascade failures.
  • Thread Pool Efficiency: Multiple consumers (concurrentConsumers=3) parallelize work without manual thread management.
  • Configurable Resilience: Options like queueSize, discardWhenFull, and offerTimeout handle overload gracefully.

Example

This standalone app uses Camel Main (Camel 4.x) with a custom ExchangeFormatter to visualize thread names, exchange IDs, and route context—clearly demonstrating SEDA's parallel consumer threads. Producers fire 5 messages rapidly (100ms intervals) into SEDA, while consumers lag with 1s delays; logs reveal immediate sends followed by staggered, multi-threaded processing.

import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.spi.ExchangeFormatter;

/**
 * Apache Camel example demonstrating SEDA (Staged Event-Driven Architecture) pattern.
 * Shows asynchronous message processing with concurrent consumers and queuing.
 */
public class SedaExample {

    static void main(String[] args) throws Exception {
        // Create a new Camel Main instance (using fully qualified name to avoid conflict)
        org.apache.camel.main.Main main = new org.apache.camel.main.Main();

        // Add routes with SEDA processing
        main.configure().addRoutesBuilder(new RouteBuilder() {
            @Override
            public void configure() {
                // Create a custom ExchangeFormatter for detailed log output
                ExchangeFormatter customFormatter = exchange ->
                        String.format("[Thread: %s] Body: %s | ExchangeId: %s | RouteId: %s",
                                Thread.currentThread().getName(),
                                exchange.getIn().getBody(String.class),
                                exchange.getExchangeId(),
                                exchange.getFromRouteId());

                // Register the custom formatter in the Camel registry
                getContext().getRegistry().bind("customFormatter", customFormatter);

                // SEDA endpoint: queueId=myQueue, concurrent consumers for parallelism
                from("seda:myQueue?concurrentConsumers=3")
                    .log("Processing: ${body}")
                    .delay(1000)  // Simulate slow consumer (1s delay)
                    .to("log:output?showAll=true&exchangeFormatter=#customFormatter");

                // Producer route for demo
                from("timer:tick?repeatCount=5&delay=100")  // Fire 5 msgs quickly
                    .setBody().simple("Msg ${exchangeId}")
                    .log("Sending: ${body}")
                    .to("seda:myQueue");
            }
        });

        // Run Camel (will run until Ctrl+C is pressed)
        main.run(args);
    }
}

Running and Verification

Compile and run the Java class directly (requires Camel 4.14.0 on classpath). Sample output shows the advantage:

Sending: Msg 1CB44DE50955685-0000000000000000     // Producer thread - instant
Sending: Msg 1CB44DE50955685-0000000000000001     // Producer continues rapidly
output - [Thread: Camel (camel-1) thread #6 - Delay] Body: Msg 1CB44DE50955685-0000000000000000 | ExchangeId: 1CB44DE50955685-0000000000000002 | RouteId: route1
output - [Thread: Camel (camel-1) thread #7 - Delay] Body: Msg 1CB44DE50955685-0000000000000001 | ExchangeId: 1CB44DE50955685-0000000000000004 | RouteId: route1  // Parallel consumers

Contrast with direct:myQueue: sends block behind delays on single thread. SEDA's queue absorbs bursts across threads, perfect for enterprise workloads like order processing.

Apache Camel Bean EIP

Apache Camel's Bean Enterprise Integration Pattern (EIP) lets you invoke POJO methods directly within routes, seamlessly integrating custom business logic without heavy frameworks. This article presents a fully self-contained, runnable Java example using Camel Main, featuring header manipulation and enhanced logging for real-world demonstration. The code requires only camel-core on the classpath.

Key Features Demonstrated

  • Timer-triggered message flow every 2 seconds.
  • Bean method invocation with body and headers parameter binding.
  • Custom header enrichment (processor metadata, timestamps).
  • Split routes using direct for modularity.
  • Infinite runtime via Camel Main (Ctrl+C to stop).

The bean processes the message body, uppercases it, adds headers tracking processing details, and returns the transformed body.

Complete Runnable Code

Here's the entire application in a single file—compile and run directly:

import org.apache.camel.Body;
import org.apache.camel.Headers;
import org.apache.camel.builder.RouteBuilder;

import java.util.Map;

/**
 * Apache Camel example demonstrating bean method invocation in a route.
 * Uses a timer-based route that processes messages through a custom bean.
 */
public class BeanExampleApp {

    static void main(String[] args) throws Exception {
        // Create a new Camel Main instance (using fully qualified name to avoid conflict)
        org.apache.camel.main.Main main = new org.apache.camel.main.Main();

        // Add routes with bean processing
        main.configure().addRoutesBuilder(new RouteBuilder() {
            @Override
            public void configure() {
                // Route 1: Timer that fires every 2 seconds
                from("timer:beanTick?period=2000")
                        .setBody().constant("hello from timer")
                        .log("${body}")
                        .to("direct:process")
                        .log("${body}");

                // Route 2: Process the message using a bean
                from("direct:process")
                        .bean(new MyProcessor(), "process")  // Invoke bean method
                        .log("After bean: ${body}")
                        .log("Headers - ProcessedBy: ${header.ProcessedBy}, ProcessedAt: ${header.ProcessedAt}, OriginalBody: ${header.OriginalBody}");
            }
        });

        // Run Camel (will run until Ctrl+C is pressed)
        main.run(args);
    }

    /**
     * Custom processor bean that processes messages.
     * Accepts body as String and headers as Map for manipulation.
     */
    public static class MyProcessor {
        public String process(@Body String body, @Headers Map<String, Object> headers) {
            // Add custom headers
            headers.put("ProcessedBy", "MyProcessor");
            headers.put("ProcessedAt", System.currentTimeMillis());
            headers.put("OriginalBody", body);

            // Process and return the modified body
            return body.toUpperCase() + " - processed by MyProcessor";
        }
    }
}

Expected Output

Console logs repeat every 2 seconds:

hello from timer
After bean: HELLO FROM TIMER - processed by MyProcessor
Headers - ProcessedBy: MyProcessor, ProcessedAt: 1769775068470, OriginalBody: hello from timer
HELLO FROM TIMER - processed by MyProcessor

Best Practices Highlighted

  • Parameter Binding: Use annotations like @Body, @Headers, @Header("name") for type-safe access; raw types work for simple cases.

  • Method Selection: Explicitly name "process" method to avoid ambiguity with overloads.

  • Inline Beans: Perfect for simple processors; use registry lookup for complex or shared beans.

  • Route Modularity: direct: endpoints enable clean separation of concerns.

This pattern excels for transformations, validations, and custom logic in enterprise integration routes.

MapStruct Decorators with DecoratedWith

MapStruct's @DecoratedWith annotation enables extending generated mappers with custom business logic while preserving core mapping functionality.

Domain Models

Person.java

public class Person {
    private String firstName;
    private String lastName;
    private Address address;

    public Person() {}

    public Person(String firstName, String lastName, Address address) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.address = address;
    }

    // Getters and setters
    public String getFirstName() { return firstName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
    public String getLastName() { return lastName; }
    public void setLastName(String lastName) { this.lastName = lastName; }
    public Address getAddress() { return address; }
    public void setAddress(Address address) { this.address = address; }
}

Address.java

public class Address {
    private String street;
    private String city;

    public Address() {}
    public Address(String street, String city) {
        this.street = street;
        this.city = city;
    }

    public String getStreet() { return street; }
    public void setStreet(String street) { this.street = street; }
    public String getCity() { return city; }
    public void setCity(String city) { this.city = city; }
}

DTO Classes

PersonDto.java

public class PersonDto {
    private String name;  // Computed by decorator
    private AddressDto address;

    public PersonDto() {}

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public AddressDto getAddress() { return address; }
    public void setAddress(AddressDto address) { this.address = address; }
}

AddressDto.java

public class AddressDto {
    private String street;
    private String city;

    public AddressDto() {}

    public String getStreet() { return street; }
    public void setStreet(String street) { this.street = street; }
    public String getCity() { return city; }
    public void setCity(String city) { this.city = city; }
}

Mapper Interface

PersonMapper.java

import org.mapstruct.*;

@Mapper
@DecoratedWith(PersonMapperDecorator.class)
public interface PersonMapper {
    @Mapping(target = "name", ignore = true)  // Handled by decorator
    PersonDto personToPersonDto(Person person);

    AddressDto addressToAddressDto(Address address);
}

Decorator Implementation

PersonMapperDecorator.java

public abstract class PersonMapperDecorator implements PersonMapper {
    protected final PersonMapper delegate;

    public PersonMapperDecorator(PersonMapper delegate) {
        this.delegate = delegate;
    }

    @Override
    public PersonDto personToPersonDto(Person person) {
        // Delegate basic mapping first
        PersonDto dto = delegate.personToPersonDto(person);

        // Custom business logic: concatenate names
        dto.setName(person.getFirstName() + " " + person.getLastName());

        return dto;
    }

    // Other methods delegate automatically (remains abstract)
}

Usage Example

Main.java

import org.mapstruct.factory.Mappers;

public class Main {
    public static void main(String[] args) {
        PersonMapper mapper = Mappers.getMapper(PersonMapper.class);

        Address address = new Address("123 Main St", "Springfield");
        Person person = new Person("John", "Doe", address);

        // Uses decorator: name becomes "John Doe"
        PersonDto dto = mapper.personToPersonDto(person);
        System.out.println("Name: " + dto.getName());           // John Doe
        System.out.println("Street: " + dto.getAddress().getStreet());  // 123 Main St

        // Direct delegation (no custom logic)
        AddressDto addrDto = mapper.addressToAddressDto(address);
        System.out.println("City: " + addrDto.getCity());       // Springfield
    }
}

How It Works

MapStruct generates PersonMapperImpl handling all standard mappings. The decorator wraps this implementation via constructor injection. Mappers.getMapper() automatically returns the decorator instance containing the generated delegate. Only overridden methods (personToPersonDto) execute custom logic; others delegate transparently. This pattern ensures thread-safety, testability, and framework independence.

MapStruct SubclassMapping for Inheritance Hierarchies

MapStruct's @SubclassMapping elegantly solves polymorphic object mapping in inheritance scenarios, generating runtime type-safe conversions from parent to child classes. This annotation shines when mapping abstract base classes or interfaces with concrete subclasses, avoiding manual instanceof checks while ensuring correct target instantiation.

Core Problem It Solves

Without @SubclassMapping, MapStruct treats inheritance hierarchies naively—mapping a Vehicle source to VehicleDTO target ignores subclass details, losing Car-specific doors or Bus-specific seats. The annotation configures explicit source→target subclass pairs on parent-level mapper methods, generating dispatch logic like if (source instanceof Car) return mapCar((Car) source). Use it precisely when source/target hierarchies diverge or when abstract targets need concrete delegation.

Complete Compilable Example

// Source hierarchy
abstract class Vehicle {
    private String brand;
    public String getBrand() { return brand; }
    public void setBrand(String brand) { this.brand = brand; }
}

class Car extends Vehicle {
    private int doors;
    public int getDoors() { return doors; }
    public void setDoors(int doors) { this.doors = doors; }
}

class Bus extends Vehicle {
    private int seats;
    public int getSeats() { return seats; }
    public void setSeats(int seats) { this.seats = seats; }
}

// Target hierarchy (mirrors structure)
abstract class VehicleDTO {
    private String brand;
    public String getBrand() { return brand; }
    public void setBrand(String brand) { this.brand = brand; }
}

class CarDTO extends VehicleDTO {
    private int doors;
    public int getDoors() { return doors; }
    public void setDoors(int doors) { this.doors = doors; }
}

class BusDTO extends VehicleDTO {
    private int seats;
    public int getSeats() { return seats; }
    public void setSeats(int seats) { this.seats = seats; }
}

Mapper with @SubclassMapping

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.SubclassMapping;

@Mapper(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION)
interface VehicleMapper {
    @SubclassMapping(source = Car.class, target = CarDTO.class)
    @SubclassMapping(source = Bus.class, target = BusDTO.class)
    VehicleDTO toDTO(Vehicle vehicle);

    @Mapping(target = "doors", source = "doors")
    CarDTO toCarDTO(Car car);

    @Mapping(target = "seats", source = "seats")
    BusDTO toBusDTO(Bus bus);
}

Key Requirements: Define concrete subclass mappers (toCarDTO, toBusDTO) explicitly—MapStruct delegates to them. @SubclassMapping pairs source/target subclasses; repeat for each pair.

Generated Dispatch Logic

MapStruct produces clean instanceof dispatch:

public VehicleDTO toDTO(Vehicle vehicle) {
    if ( vehicle == null ) {
        return null;
    }

    if (vehicle instanceof Car) {
        return toCarDTO( (Car) vehicle );
    }
    else if (vehicle instanceof Bus) {
        return toBusDTO( (Bus) vehicle );
    }
    else {
        throw new IllegalArgumentException("Not all subclasses are supported for this mapping. Missing for " + vehicle.getClass());
    }
}

For abstract targets, it skips parent instantiation entirely. Configure subclassExhaustiveStrategy for custom fallbacks (e.g., default parent mapping).

Usage and Verification

import org.mapstruct.factory.Mappers;

public class Demo {
    public static void main(String[] args) {
        VehicleMapper mapper = Mappers.getMapper(VehicleMapper.class);

        Car car = new Car(); car.setBrand("Toyota"); car.setDoors(4);
        Bus bus = new Bus(); bus.setBrand("Volvo"); bus.setSeats(40);

        VehicleDTO carDTO = mapper.toDTO(car);  // instanceof CarDTO
        VehicleDTO busDTO = mapper.toDTO(bus);  // instanceof BusDTO

        System.out.println(carDTO.getClass().getSimpleName() + ": " + ((CarDTO)carDTO).getDoors());  // CarDTO: 4
        System.out.println(busDTO.getClass().getSimpleName() + ": " + ((BusDTO)busDTO).getSeats());  // BusDTO: 40
    }
}

Runtime polymorphism works seamlessly—input Car yields CarDTO with preserved subclass state.

MapStruct: MappingTarget and TargetType with Pure Java

MapStruct's @MappingTarget and @TargetType annotations solve distinct mapping challenges. @MappingTarget enables in-place updates of existing objects, while @TargetType provides runtime type information for dynamic object creation.

Core Concepts

@MappingTarget marks a parameter as an existing target for modification rather than creating a new instance. Use it for DTO-to-entity updates where the target object already exists in memory (e.g., from a database).

@TargetType injects the concrete target Class<T> as a parameter, essential for generic factories and lifecycle methods where compile-time type information is erased.

Complete Working Example

import org.mapstruct.*;
import java.lang.reflect.Constructor;

// 1. Plain Java DTOs and Entities
class CarDto {
    private String make;
    private String model;
    private int year;

    public CarDto() {}

    public CarDto(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    // Getters and setters
    public String getMake() { return make; }
    public void setMake(String make) { this.make = make; }

    public String getModel() { return model; }
    public void setModel(String model) { this.model = model; }

    public int getYear() { return year; }
    public void setYear(int year) { this.year = year; }
}

class CarEntity {
    private String make;
    private String model;
    private int year;
    private String vin;  // Entity-specific field

    public CarEntity() {}

    // Getters and setters
    public String getMake() { return make; }
    public void setMake(String make) { this.make = make; }

    public String getModel() { return model; }
    public void setModel(String model) { this.model = model; }

    public int getYear() { return year; }
    public void setYear(int year) { this.year = year; }

    public String getVin() { return vin; }
    public void setVin(String vin) { this.vin = vin; }
}

// 2. @TargetType Custom Factory
class CarEntityFactory {
    public <T extends CarEntity> T createEntity(@TargetType Class<T> entityClass) {
        try {
            Constructor<T> constructor = entityClass.getDeclaredConstructor();
            T entity = constructor.newInstance();
            entity.setVin("VIN-" + entityClass.getSimpleName());
            return entity;
        } catch (Exception e) {
            throw new RuntimeException("Cannot create entity: " + entityClass.getName(), e);
        }
    }
}

// 3. Mapper Demonstrating Both Annotations
@Mapper(uses = CarEntityFactory.class)
interface CarMapper {

    // Creates NEW object using @TargetType factory
    CarEntity toEntity(CarDto dto);

    // Updates EXISTING object - @MappingTarget required
    void updateEntity(@MappingTarget CarEntity entity, CarDto dto);

    // Updates and returns - also uses @MappingTarget
    CarEntity updateAndReturn(@MappingTarget CarEntity entity, CarDto dto);
}

// 4. Advanced Mapper with Lifecycle Methods
@Mapper(uses = CarEntityFactory.class)
abstract class AdvancedCarMapper {

    public abstract CarEntity toEntity(CarDto dto);

    @BeforeMapping
    void beforeUpdate(CarDto source,
                              @MappingTarget CarEntity target,
                              @TargetType Class<?> targetType) {
        System.out.println("🔄 Before: Updating " + targetType.getSimpleName());
        System.out.println("   Target VIN before: " + target.getVin());
    }

    @AfterMapping
    void afterUpdate(CarDto source,
                             @MappingTarget CarEntity target,
                             @TargetType Class<?> targetType) {
        System.out.println("✅ After: Updated " + targetType.getSimpleName());
        System.out.println("   Target VIN after: " + target.getVin());
    }
}

Usage Demonstration

import org.mapstruct.factory.Mappers;

public class MapStructDemo {
    public static void main(String[] args) {
        CarMapper mapper = Mappers.getMapper(CarMapper.class);
        AdvancedCarMapper advancedMapper = Mappers.getMapper(AdvancedCarMapper.class);

        System.out.println("=== 1. NEW Object Creation (uses @TargetType factory) ===");
        CarDto newDto = new CarDto("Toyota", "Camry", 2023);
        CarEntity newEntity = mapper.toEntity(newDto);
        System.out.println("New entity: " + newEntity.getMake() + " VIN: " + newEntity.getVin());
        // Output: VIN-CarEntity

        System.out.println("\n=== 2. Update Existing (uses @MappingTarget) ===");
        CarEntity existingEntity = new CarEntity();
        existingEntity.setVin("EXISTING-123");
        CarDto updateDto = new CarDto("Honda", "Civic", 2022);

        mapper.updateEntity(existingEntity, updateDto);
        System.out.println("Updated: " + existingEntity.getModel() + " VIN preserved: " + existingEntity.getVin());
        // VIN preserved! Only mapped fields updated

        System.out.println("\n=== 3. Lifecycle Hooks (both annotations) ===");
        advancedMapper.toEntity(new CarDto("Ford", "Mustang", 2024));
        // Prints before/after messages with type info
    }
}

Expected Output

=== 1. NEW Object Creation (uses @TargetType factory) ===
New entity: Toyota VIN: VIN-CarEntity

=== 2. Update Existing (uses @MappingTarget) ===
Updated: Civic VIN preserved: EXISTING-123

=== 3. Lifecycle Hooks (both annotations) ===
🔄 Before: Updating CarEntity
   Target VIN before: null
✅ After: Updated CarEntity
   Target VIN after: VIN-CarEntity

Decision Matrix: When to Use Each

Scenario @MappingTarget @TargetType
DTO → New Entity ❌ No ✅ Factory methods
Partial DTO Update ✅ Always ❌ No
Generic Collections ❌ No ✅ Essential
Lifecycle Processing ✅ Target param ✅ Type param
Polymorphic Mapping ❌ No ✅ Required

This pure Java example demonstrates both annotations in realistic enterprise scenarios, showing the clear distinction between new object creation and in-place updates.

« Older posts