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