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.
Leave a Reply