Extremely Serious

Month: January 2026 (Page 1 of 3)

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.

MapStruct Inheritance: InheritConfiguration and InheritInverseConfiguration

MapStruct's @InheritConfiguration and @InheritInverseConfiguration annotations streamline complex mapping hierarchies and bidirectional conversions. These tools eliminate repetitive @Mapping definitions while maintaining type safety and compile-time validation.

Core Concepts

@InheritConfiguration copies forward mapping rules from a base method to a child method with identical source/target types. Local mappings override inherited ones for flexibility.

@InheritInverseConfiguration automatically reverses a forward mapping by swapping source and target types. It handles field renames and ignores, generating clean DTO-to-entity roundtrips.

Both require MappingInheritanceStrategy.EXPLICIT (MapStruct default).

Complete Working Example

Domain Models

public class Car {
    private String make;
    private int numberOfSeats;
    private int doors;

    public Car() {}
    public Car(String make, int numberOfSeats, int doors) {
        this.make = make;
        this.numberOfSeats = numberOfSeats;
        this.doors = doors;
    }

    // Getters and setters
    public String getMake() { return make; }
    public void setMake(String make) { this.make = make; }
    public int getNumberOfSeats() { return numberOfSeats; }
    public void setNumberOfSeats(int numberOfSeats) { this.numberOfSeats = numberOfSeats; }
    public int getDoors() { return doors; }
    public void setDoors(int doors) { this.doors = doors; }
}

public class CarDto {
    private String manufacturer;
    private Integer seatCount;
    private boolean hasNavigationSystem;

    public CarDto() {}
    public CarDto(String manufacturer, Integer seatCount, boolean hasNavigationSystem) {
        this.manufacturer = manufacturer;
        this.seatCount = seatCount;
        this.hasNavigationSystem = hasNavigationSystem;
    }

    // Getters and setters
    public String getManufacturer() { return manufacturer; }
    public void setManufacturer(String manufacturer) { this.manufacturer = manufacturer; }
    public Integer getSeatCount() { return seatCount; }
    public void setSeatCount(Integer seatCount) { this.seatCount = seatCount; }
    public boolean isHasNavigationSystem() { return hasNavigationSystem; }
    public void setHasNavigationSystem(boolean hasNavigationSystem) { this.hasNavigationSystem = hasNavigationSystem; }
}

Inheritance Hierarchy Example

import org.mapstruct.InheritConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper
public interface VehicleMapper {
    @Mapping(target = "hasNavigationSystem", ignore = true)
    @Mapping(target = "manufacturer", source = "make")
    @Mapping(target = "seatCount", source = "numberOfSeats")
    CarDto vehicleToDto(Car vehicle);
}

@Mapper
public interface CarMapper extends VehicleMapper {
    @InheritConfiguration(name = "vehicleToDto")
    CarDto carToCarDto(Car car);

    // Custom mapping overrides inheritance
    @InheritConfiguration(name = "vehicleToDto")
    @Mapping(target = "hasNavigationSystem", constant = "false")
    CarDto carToBasicDto(Car car);
}

Bidirectional Mapping Example

import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper
public interface BidirectionalCarMapper {
    // Forward mapping with field renaming
    @Mapping(target = "hasNavigationSystem", ignore = true)
    @Mapping(target = "seatCount", source = "numberOfSeats")
    @Mapping(target = "manufacturer", source = "make")
    CarDto toDto(Car car);

    // Automatically reverses: seatCount->numberOfSeats, manufacturer->make
    @InheritInverseConfiguration(name = "toDto")
    @Mapping(target = "doors", ignore = true)
    Car toEntity(CarDto dto);

    // Selective reverse with local overrides
    @InheritInverseConfiguration
    @Mapping(target = "numberOfSeats", ignore = true)
    @Mapping(target = "doors", ignore = true)
    Car toEntityNoSeats(CarDto dto);
}

Verification Demo

import org.mapstruct.factory.Mappers;

public class MapStructInheritanceDemo {
    public static void main(String[] args) {
        CarMapper carMapper = Mappers.getMapper(CarMapper.class);
        BidirectionalCarMapper biMapper = Mappers.getMapper(BidirectionalCarMapper.class);

        // Test inheritance
        Car ferrari = new Car("Ferrari", 2, 2);
        CarDto dto = carMapper.carToCarDto(ferrari);
        System.out.println(dto.getManufacturer()); // "Ferrari"
        System.out.println(dto.getSeatCount());    // 2

        // Test bidirectional
        CarDto sportsCarDto = new CarDto("Porsche", 4, true);
        Car entity = biMapper.toEntity(sportsCarDto);
        System.out.println(entity.getMake());        // "Porsche"
        System.out.println(entity.getNumberOfSeats()); // 4
    }
}

Comparison Table

Feature @InheritConfiguration @InheritInverseConfiguration
Direction Forward → Forward Forward → Reverse
Type Match Identical source/target Swapped source/target
Primary Use Class hierarchies DTO ↔ Entity roundtrips

Best Practices

For @InheritConfiguration:

  • Use in mapper hierarchies (extends)
  • Document base mappings clearly
  • Override selectively with @Mapping

For @InheritInverseConfiguration:

  • Specify name= when source mapper has multiple similar methods
  • Use @Mapping(ignore=true) for one-way fields

These annotations reduce mapping code by 70-80% in typical enterprise applications while maintaining full compile-time safety.

MapStruct Imports: Pure Java Code

MapStruct's imports attribute brings external types into scope for generated mappers. This guide focuses purely on the working code demonstrating imports in action.

Domain Model

Abstract base entity:

package com.example.domain;

public abstract class AbstractBaseEntity {
    private Long id;

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public abstract String getStatus();
}

Source entity:

package com.example.domain;

public class UserEntity extends AbstractBaseEntity {
    private String username;
    private String email;

    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    @Override
    public String getStatus() { return "ACTIVE"; }
}

Target DTO:

package com.example.dto;

public class UserDto {
    private Long id;
    private String username;
    private String email;
    private String statusInfo;

    // getters/setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    public String getStatusInfo() { return statusInfo; }
    public void setStatusInfo(String statusInfo) { this.statusInfo = statusInfo; }

    @Override
    public String toString() {
        return "UserDto{id=" + id + ", username='" + username + "', statusInfo='" + statusInfo + "'}";
    }
}

Mapper with Imports

package com.example.mapper;

import com.example.dto.UserDto;
import com.example.domain.UserEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import java.util.List;

@Mapper(imports = com.example.domain.AbstractBaseEntity.class)
public interface UserMapper {

    @Mapping(target = "id", source = "id")
    @Mapping(target = "username", source = "username")
    @Mapping(target = "email", source = "email")
    @Mapping(target = "statusInfo", expression = "java( ((AbstractBaseEntity) entity).getStatus() + \" (processed)\")")
    UserDto toDto(UserEntity entity);

    List<UserDto> toDtos(List<UserEntity> entities);
}

Usage Code

package com.example.demo;

import com.example.domain.UserEntity;
import com.example.dto.UserDto;
import com.example.mapper.UserMapper;
import org.mapstruct.factory.Mappers;

public class MapStructDemo {
    private static final UserMapper MAPPER = Mappers.getMapper(UserMapper.class);

    public static void main(String[] args) {
        UserEntity entity = new UserEntity();
        entity.setId(42L);
        entity.setUsername("john_doe");
        entity.setEmail("john@example.com");

        UserDto dto = MAPPER.toDto(entity);
        System.out.println("Mapped DTO: " + dto);
    }
}

Generated Code Insight

MapStruct generates UserMapperImpl with automatic import:

package com.example.mapper;

import com.example.domain.AbstractBaseEntity; // Added by imports attribute
import com.example.domain.UserEntity;
import com.example.dto.UserDto;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.processing.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    comments = "version: 1.6.3, compiler: IncrementalProcessingEnvironment from gradle-language-java-9.2.0.jar, environment: Java 25 (Oracle Corporation)"
)
public class UserMapperImpl implements UserMapper {

    @Override
    public UserDto toDto(UserEntity entity) {
        if ( entity == null ) {
            return null;
        }

        UserDto userDto = new UserDto();

        userDto.setId( entity.getId() );
        userDto.setUsername( entity.getUsername() );
        userDto.setEmail( entity.getEmail() );

        userDto.setStatusInfo( ((AbstractBaseEntity) entity).getStatus() + " (processed)" );

        return userDto;
    }

    @Override
    public List<UserDto> toDtos(List<UserEntity> entities) {
        if ( entities == null ) {
            return null;
        }

        List<UserDto> list = new ArrayList<UserDto>( entities.size() );
        for ( UserEntity userEntity : entities ) {
            list.add( toDto( userEntity ) );
        }

        return list;
    }
}

Why Imports Are Required

❌ Without imports attribute:

// GENERATED CODE FAILS TO COMPILE:
// error: cannot find symbol
// symbol: class AbstractBaseEntity
// ((AbstractBaseEntity) entity).getStatus()

✅ With imports = AbstractBaseEntity.class:

// GENERATED CODE WORKS PERFECTLY:
// import com.example.domain.AbstractBaseEntity;
// ((AbstractBaseEntity) entity).getStatus() ✓

Multiple Imports Pattern

@Mapper(imports = { 
    com.example.domain.AbstractBaseEntity.class,
    java.time.LocalDateTime.class 
})
public interface AdvancedUserMapper {

    @Mapping(target = "createdAt", expression = "java(LocalDateTime.now())")
    @Mapping(target = "fullInfo", expression = "java(entity.getUsername() + \" <\" + ((AbstractBaseEntity)entity).getStatus() + \">\")")
    UserDto toDto(UserEntity entity);
}

The imports attribute eliminates compilation errors when mappings reference types across packages or inheritance hierarchies.

MapStruct Builder Support with Java Records

Java records combined with explicit builder patterns provide immutable data classes that work seamlessly with MapStruct's builder detection. This approach leverages records' automatic getters and compact constructors while adding the required builder infrastructure for mapping.

Complete Record Target Class

The Person record with builder support:

public record Person(
    Long id,
    String firstName,
    String lastName
) {

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private Long id;
        private String firstName;
        private String lastName;

        public Builder id(Long id) {
            this.id = id;
            return this;
        }

        public Builder firstName(String firstName) {
            this.firstName = firstName;
            return this;
        }

        public Builder lastName(String lastName) {
            this.lastName = lastName;
            return this;
        }

        public Person build() {
            return new Person(id, firstName, lastName);
        }
    }
}

Complete Source Class

Simple mutable source class for demonstration:

public class ExternalPerson {
    private Long id;
    private String personFirstName;
    private String personLastName;

    public ExternalPerson(Long id, String personFirstName, String personLastName) {
        this.id = id;
        this.personFirstName = personFirstName;
        this.personLastName = personLastName;
    }

    // Getters and setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getPersonFirstName() {
        return personFirstName;
    }

    public void setPersonFirstName(String personFirstName) {
        this.personFirstName = personFirstName;
    }

    public String getPersonLastName() {
        return personLastName;
    }

    public void setPersonLastName(String personLastName) {
        this.personLastName = personLastName;
    }
}

Complete Mapper Interface

MapStruct automatically detects the record's static builder() method:

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

@Mapper
public interface PersonMapper {
    @Mapping(source = "personFirstName", target = "firstName")
    @Mapping(source = "personLastName", target = "lastName")
    Person toPerson(ExternalPerson source);
}

Generated Mapping Code

MapStruct generates efficient builder-based mapping:

import javax.annotation.processing.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    comments = "version: 1.6.3, compiler: IncrementalProcessingEnvironment from gradle-language-java-9.2.0.jar, environment: Java 25 (Oracle Corporation)"
)
public class PersonMapperImpl implements PersonMapper {

    @Override
    public Person toPerson(ExternalPerson source) {
        if ( source == null ) {
            return null;
        }

        Person.Builder person = Person.builder();

        person.firstName( source.getPersonFirstName() );
        person.lastName( source.getPersonLastName() );
        person.id( source.getId() );

        return person.build();
    }
}

Custom Builder Method Names

Override defaults using @Builder annotation on the mapper:

import org.mapstruct.Builder;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper(builder = @Builder(buildMethod = "construct"))
public abstract class PersonMapper {
    @Mapping(source = "personFirstName", target = "firstName")
    @Mapping(source = "personLastName", target = "lastName")
    abstract Person toPerson(ExternalPerson source);
}

For this configuration, change the record to:

// In Builder class:
public Person construct() {  // Changed from build()
    return new Person(id, firstName, lastName);
}

Key Benefits of Record + Builder Pattern

  • Records provide automatic equals(), hashCode(), toString(), and getters
  • Immutable by default with final components
  • Builder enables fluent construction for complex initialization
  • MapStruct auto-detects standard builder() + build() pattern

MapStruct Context: Guide with Lifecycle & ObjectFactory Examples

MapStruct's @Context annotation enables passing contextual data—like locales, services, or user state—through mapper method calls without treating it as source or target input. This propagates automatically to lifecycle methods (@BeforeMapping, @AfterMapping, @ObjectFactory) and custom qualifiers, making it ideal for complex mappings requiring external dependencies.

Core Concepts

@Context marks parameters for transparent propagation: upstream mapper methods forward them to downstream calls, including generated code, custom methods, and lifecycle hooks. Callers must supply values explicitly, as MapStruct performs no instantiation or defaulting. Propagation requires matching @Context declarations across methods, with type compatibility enforced.

Key benefits include locale-aware formatting, injecting services for lookups, or stateful transformations—all without cluttering method signatures with non-mapping data.

Complete Runnable Example

Save as ContextDemo.java.

import org.mapstruct.*;
import org.mapstruct.factory.Mappers;

import java.util.Locale;
import java.time.format.DateTimeFormatter;
import java.time.LocalDate;

// Source and Target DTOs
class Car {
    private String manufacturer;
    private String model;
    private LocalDate built;

    public Car(String manufacturer, String model, LocalDate built) {
        this.manufacturer = manufacturer;
        this.model = model;
        this.built = built;
    }

    // Getters/Setters
    public String getManufacturer() { return manufacturer; }
    public void setManufacturer(String manufacturer) { this.manufacturer = manufacturer; }
    public String getModel() { return model; }
    public void setModel(String model) { this.model = model; }
    public LocalDate getBuilt() { return built; }
    public void setBuilt(LocalDate built) { this.built = built; }
}

class CarDto {
    private String make;
    private String fullModel;
    private String formattedBuildDate;
    private String contextInfo;

    // Getters/Setters
    public String getMake() { return make; }
    public void setMake(String make) { this.make = make; }
    public String getFullModel() { return fullModel; }
    public void setFullModel(String fullModel) { this.fullModel = fullModel; }
    public String getFormattedBuildDate() { return formattedBuildDate; }
    public void setFormattedBuildDate(String formattedBuildDate) { this.formattedBuildDate = formattedBuildDate; }
    public String getContextInfo() { return contextInfo; }
    public void setContextInfo(String contextInfo) { this.contextInfo = contextInfo; }
}

// Mapper with full lifecycle + ObjectFactory
@Mapper(uses = LocalDateUtils.class)
abstract class CarMapper {
    // Top-level entry point - caller provides @Context
    @Mapping(source = "manufacturer", target = "make")
    @Mapping(source = "built", target = "formattedBuildDate", qualifiedByName = "formatDate")
    @Mapping(target = "fullModel", ignore = true)  // Set in @AfterMapping
    @Mapping(target = "contextInfo", ignore = true)  // Set in @ObjectFactory
    public abstract CarDto toDto(Car car, @Context Locale locale, @Context String userId);

    // ObjectFactory receives @Context for custom instantiation
    @ObjectFactory
    public CarDto createCarDto(@Context Locale locale, @Context String userId) {
        CarDto dto = new CarDto();
        dto.setContextInfo("Created for user: " + userId + " in locale: " + locale.getDisplayName());
        System.out.println("ObjectFactory: Initialized DTO with context - " + userId);
        return dto;
    }

    // Lifecycle methods receive @Context automatically
    @BeforeMapping
    void beforeMapping(Car car, @MappingTarget CarDto dto, @Context Locale locale, @Context String userId) {
        System.out.println("BeforeMapping: Pre-processing with " + locale.getDisplayName() + " for user " + userId);
    }

    @AfterMapping
    void afterMapping(Car car, @MappingTarget CarDto dto, @Context Locale locale, @Context String userId) {
        System.out.println("AfterMapping: Finalizing with " + locale.getDisplayName() + " for user " + userId);
        dto.setFullModel(dto.getMake() + " " + car.getModel() + " (" + locale.getCountry() + ")");
    }
}

// Custom qualifier helper (also receives @Context)
class LocalDateUtils {
    @Named("formatDate")
    public static String format(LocalDate date, @Context Locale locale) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM yyyy", locale);
        return date.format(formatter);
    }
}

// Demo runner
class ContextDemo {
    public static void main(String[] args) {
        CarMapper mapper = Mappers.getMapper(CarMapper.class);

        Car car = new Car("Toyota", "Camry", LocalDate.of(2020, 5, 15));

        // Provide @Context explicitly (Locale + custom userId)
        CarDto dto = mapper.toDto(car, Locale.forLanguageTag("de-DE"), "john.doe@company.com");

        System.out.println("\n=== Final Result ===");
        System.out.println("Make: " + dto.getMake());
        System.out.println("Full Model: " + dto.getFullModel());
        System.out.println("Build Date: " + dto.getFormattedBuildDate());
        System.out.println("Context Info: " + dto.getContextInfo());
    }
}

Expected output showing full lifecycle flow:

ObjectFactory: Initialized DTO with context - john.doe@company.com
BeforeMapping: Pre-processing with German (Germany) for user john.doe@company.com
AfterMapping: Finalizing with German (Germany) for user john.doe@company.com

=== Final Result ===
Make: Toyota
Full Model: Toyota Camry (DE)
Build Date: Mai 2020
Context Info: Created for user: john.doe@company.com in locale: German (Germany)

Lifecycle Integration Details

The complete flow works as: ObjectFactory@BeforeMappingproperty mappings@AfterMapping. Each receives @Context parameters automatically when declared with matching signatures. @ObjectFactory handles target instantiation (useful for dependency injection), while lifecycle methods enable pre/post-processing with full context awareness.

This pattern scales perfectly for enterprise applications requiring audit trails, localization, or service lookups during mapping.

« Older posts