New posts for Spring and Hibernate

This commit is contained in:
2026-11-06 01:05:27 +05:30
parent 51913ad9d4
commit dc13293046
136 changed files with 5268 additions and 0 deletions
@@ -0,0 +1,14 @@
# A record target has no setters: MapStruct calls the canonical constructor
CustomerDto is a record : true
setters on CustomerDto : 0
setters on Customer (entity) : 14
--- the line the generated toDto uses to build the record ---
CustomerDto customerDto = new CustomerDto( id, fullName, email, birthDate, age, nickname, address, roles, attributes, orders, origin );
--- record as SOURCE: the generated toEntity reads accessors, not getters ---
customer.setFirstName( request.firstName() );
customer.setLastName( request.lastName() );
customer.setEmail( request.email() );
customer.setBirthDate( request.birthDate() );
@@ -0,0 +1,127 @@
# What MapStruct 1.6.3 generated, read from target/generated-sources (header and imports trimmed)
--- AddressMapperImpl ---
@Component
public class AddressMapperImpl implements AddressMapper {
@Override
public AddressDto toDto(Address address) {
if ( address == null ) {
return null;
}
String zip = null;
String country = null;
String street = null;
String city = null;
zip = address.getPostalCode();
if ( address.getCountry() != null ) {
country = address.getCountry();
}
else {
country = "IN";
}
street = address.getStreet();
city = address.getCity();
AddressDto addressDto = new AddressDto( street, city, zip, country );
return addressDto;
}
@Override
public Address toEntity(AddressDto dto) {
if ( dto == null ) {
return null;
}
Address address = new Address();
address.setPostalCode( dto.zip() );
address.setCountry( dto.country() );
address.setStreet( dto.street() );
address.setCity( dto.city() );
return address;
}
@Override
public void update(AddressDto dto, Address address) {
if ( dto == null ) {
return;
}
if ( dto.zip() != null ) {
address.setPostalCode( dto.zip() );
}
if ( dto.street() != null ) {
address.setStreet( dto.street() );
}
if ( dto.city() != null ) {
address.setCity( dto.city() );
}
if ( dto.country() != null ) {
address.setCountry( dto.country() );
}
}
}
--- CustomerMapperImpl: constructor injection, then toDto ---
@Component
public class CustomerMapperImpl implements CustomerMapper {
private final AddressMapper addressMapper;
private final OrderMapper orderMapper;
@Autowired
public CustomerMapperImpl(AddressMapper addressMapper, OrderMapper orderMapper) {
this.addressMapper = addressMapper;
this.orderMapper = orderMapper;
}
public CustomerDto toDto(Customer customer, Clock clock) {
if ( customer == null ) {
return null;
}
int age = 0;
String nickname = null;
Long id = null;
String email = null;
LocalDate birthDate = null;
AddressDto address = null;
Set<String> roles = null;
Map<String, String> attributes = null;
List<OrderDto> orders = null;
age = ageInYears( customer.getBirthDate(), clock );
if ( customer.getNickname() != null ) {
nickname = customer.getNickname();
}
else {
nickname = "anonymous";
}
id = customer.getId();
email = customer.getEmail();
birthDate = customer.getBirthDate();
address = addressMapper.toDto( customer.getAddress() );
Set<String> set = customer.getRoles();
if ( set != null ) {
roles = new LinkedHashSet<String>( set );
}
Map<String, String> map = customer.getAttributes();
if ( map != null ) {
attributes = new LinkedHashMap<String, String>( map );
}
orders = orderMapper.toDtos( customer.getOrders() );
String fullName = customer.getFirstName() + " " + customer.getLastName();
String origin = "api";
CustomerDto customerDto = new CustomerDto( id, fullName, email, birthDate, age, nickname, address, roles, attributes, orders, origin );
return customerDto;
}
+17
View File
@@ -0,0 +1,17 @@
# Customer (mutable entity) -> CustomerDto (record), with nested Address, a List of Orders, a Set and a Map
fullName : Asha Mehta <- expression = java(first + " " + last)
age : 36 <- qualifiedByName, with the clock passed as @Context
nickname : anonymous <- source was null, defaultValue = "anonymous"
origin : api <- constant = "api"
address : AddressDto[street=12 Park Road, city=Pune, zip=411001, country=IN] <- postalCode renamed to zip; null country got defaultValue IN
roles : [USER] same instance as the entity's? false
attributes : {newsletter=yes} same instance as the entity's? false
--- List<Order> -> List<OrderDto>, with a computed total ---
order id=100 status=PAID total=32.50
OrderLineDto[sku=BOOK-1, quantity=2, unitPrice=12.50, lineTotal=25.00]
OrderLineDto[sku=PEN-9, quantity=10, unitPrice=0.75, lineTotal=7.50]
--- what is NOT in the record ---
passwordHash is a component of CustomerDto? false
+13
View File
@@ -0,0 +1,13 @@
# CustomerRequest (record) -> Customer (entity); AddressMapper.toEntity inherits the rename in reverse
email after @AfterMapping : '[email protected]' (request carried ' [email protected] ')
createdAt (from @Context) : 2026-11-05T00:00:00Z
address.postalCode : 600001 <- @InheritInverseConfiguration turned zip back into postalCode
address.country : null <- defaultValue is NOT inverted; toDto gives IN, toEntity does not
tier : null <- the entity said STANDARD; the request said null
tier with nullValueCheckStrategy = ALWAYS : STANDARD
id / passwordHash / orders : null / null / null <- ignore = true
--- round trip: entity -> record -> entity ---
passwordHash survives the round trip? false
address country survives? null -> IN
+50
View File
@@ -0,0 +1,50 @@
# Null handling: IGNORE vs the default on a patch, and RETURN_DEFAULT on whole objects and collections
--- patch(): nullValuePropertyMappingStrategy = IGNORE, applied with a patch whose components are all null ---
before: [email protected], nickname=ash, roles=[USER], address=12 Park Road/Pune/411001
after : [email protected], nickname=ash, roles=[USER], address=12 Park Road/Pune/411001
--- replace(): the default strategy (SET_TO_NULL), same all-null patch ---
before: [email protected], nickname=ash, roles=[USER], address=12 Park Road/Pune/411001
after : email=null, nickname=null, roles=null, address=null
--- whole objects and collections: OrderMapper (default) vs LenientOrderMapper (RETURN_DEFAULT) ---
strict.toDto(null) : null
lenient.toDto(null) : OrderDto[id=null, status=null, placedAt=null, lines=null, total=null]
strict order, lines = null : lines=null
lenient order, lines = null : lines=[]
strict.toDtos(null) : null
--- the generated patch() (IGNORE): every assignment is guarded ---
public void patch(CustomerPatch patch, Customer customer) {
if ( patch == null ) {
return;
}
if ( patch.email() != null ) {
customer.setEmail( patch.email() );
}
if ( patch.nickname() != null ) {
customer.setNickname( patch.nickname() );
}
if ( patch.address() != null ) {
if ( customer.getAddress() == null ) {
customer.setAddress( new Address() );
}
addressMapper.update( patch.address(), customer.getAddress() );
}
if ( customer.getRoles() != null ) {
Set<String> set = patch.roles();
if ( set != null ) {
customer.getRoles().clear();
customer.getRoles().addAll( set );
}
}
else {
Set<String> set = patch.roles();
if ( set != null ) {
customer.setRoles( new LinkedHashSet<String>( set ) );
}
}
}
+11
View File
@@ -0,0 +1,11 @@
# @MappingTarget updates: nested objects and collections are changed in place
--- patch only the city and the roles ---
address: [email protected], nickname=null, roles=[USER, ADMIN], address=12 Park Road/Mumbai/411001
same Address instance as before? true <- AddressMapper.update was called on it
same roles Set instance as before? true <- cleared and refilled, not replaced
another holder of the old roles Set now sees: [AUDITOR]
--- an immutable collection on the target ---
patching roles on a Set.of(...) target threw: java.lang.UnsupportedOperationException
+40
View File
@@ -0,0 +1,40 @@
# @BeforeMapping / @AfterMapping on a record target versus a bean target (call order, recorded)
--- target is a record: toRecord(Pair) ---
@BeforeMapping(source)
@BeforeMapping(source, @MappingTarget Boxed) target=Boxed[name=pen, qty=3]
@AfterMapping(source, @MappingTarget Boxed) target=Boxed[name=pen, qty=3]
@AfterMapping(source)
--- target is a mutable bean: toBean(Pair) ---
@BeforeMapping(source)
@BeforeMapping(source, @MappingTarget BoxedBean) name=null
BoxedBean.setName(pen)
BoxedBean.setQty(3)
@AfterMapping(source, @MappingTarget BoxedBean) name=pen
@AfterMapping(source)
--- the generated toRecord, which is why ---
public Probes.Boxed toRecord(Probes.Pair pair) {
beforeSource( pair );
if ( pair == null ) {
return null;
}
String name = null;
int qty = 0;
name = pair.name();
qty = pair.qty();
Probes.Boxed boxed = new Probes.Boxed( name, qty );
beforeRecord( pair, boxed );
afterRecord( pair, boxed );
afterSource( pair );
return boxed;
}
@@ -0,0 +1,16 @@
# @ObjectFactory returning a record: the factory's object is the result, and nothing is copied into it
source : Pair[name=pen, qty=3]
result : Boxed[name=from-the-factory, qty=0]
compiled with unmappedTargetPolicy = ERROR, and no error was reported
--- the generated method ---
public Probes.Boxed toRecord(Probes.Pair pair) {
if ( pair == null ) {
return null;
}
Probes.Boxed boxed = create( pair );
return boxed;
}
@@ -0,0 +1,31 @@
# Post-processing a record: @AfterMapping on its builder, or a @DecoratedWith decorator
--- ReceiptMapper: the hook receives ReceiptDto.Builder ---
ReceiptDto[orderId=100, total=32.50, summary=order 100: 2 line(s)]
public ReceiptDto toReceipt(Order order) {
if ( order == null ) {
return null;
}
ReceiptDto.Builder receiptDto = ReceiptDto.builder();
receiptDto.orderId( order.getId() );
receiptDto.total( orderMapper.sumOfLines( order.getLines() ) );
summarise( order, receiptDto );
return receiptDto.build();
}
--- InvoiceMapper: @DecoratedWith, so two beans implement the interface ---
bean invoiceMapperImpl -> InvoiceMapperImpl
bean invoiceMapperImpl_ -> InvoiceMapperImpl_
injected InvoiceMapper (the @Primary one) : InvoiceDto[id=100, status=PAID, total=32.50, display=#100 [PAID] 32.50]
the generated delegate on its own : InvoiceDto[id=100, status=PAID, total=32.50, display=null]
--- the generated class that decorates ---
@Component
@Primary
public class InvoiceMapperImpl extends InvoiceMapperDecorator {
}
@@ -0,0 +1,28 @@
# @Context cycle tracking: it works for a bean target and cannot work for a record target
bean target + @Context CycleContext : ann.manager.manager is ann? true
record target + the same @Context : java.lang.StackOverflowError
--- why: the generated record mapping recurses BEFORE it can call the constructor ---
public Probes.EmployeeRecord toRecord(Probes.Employee employee, CycleContext context) {
Probes.EmployeeRecord target = context.lookup( employee, Probes.EmployeeRecord.class );
if ( target != null ) {
return target;
}
if ( employee == null ) {
return null;
}
String name = null;
Probes.EmployeeRecord manager = null;
name = employee.name;
manager = toRecord( employee.manager, context );
Probes.EmployeeRecord employeeRecord = new Probes.EmployeeRecord( name, manager );
context.remember( employee, employeeRecord );
return employeeRecord;
}
@@ -0,0 +1,10 @@
# Testing a generated mapper without Spring: Mappers.getMapper versus constructing the Impl
--- 1. Mappers.getMapper(...) ---
AddressMapper (no dependencies) : AddressMapperImpl
CustomerMapper (uses = {...}) : RuntimeException
root cause : java.lang.NoSuchMethodException: com.ankurm.mapstruct.mapper.CustomerMapperImpl.<init>()
--- 2. new CustomerMapperImpl(new AddressMapperImpl(), new OrderMapperImpl()) ---
no Spring, no reflection : Asha Mehta
@@ -0,0 +1,6 @@
# @SpringJUnitConfig(classes = {the generated impls}): only the beans the mapper needs
bean definitions in the context : 11
bean names : org.springframework.context.annotation.internalConfigurationAnnotationProcessor, org.springframework.context.annotation.internalAutowiredAnnotationProcessor, org.springframework.context.annotation.internalCommonAnnotationProcessor, org.springframework.context.event.internalEventListenerProcessor, org.springframework.context.event.internalEventListenerFactory, customerMapperImpl, addressMapperImpl, orderMapperImpl, contextConfigurationMapperTest.PinnedClock, org.springframework.test.context.support.internalDynamicPropertyRegistrarBeanInitializer, clock
mapper injected as : CustomerMapperImpl
mapped : Asha Mehta
@@ -0,0 +1,6 @@
# @SpringBootTest: the same mapper, inside the whole application
bean definitions in the context : 153
includes the web controller : true
clock is the application's own : Z
mapped : Asha Mehta
@@ -0,0 +1,15 @@
# Property-level mapper test: change one entity property, expect exactly the right DTO component to change
id changes when its source changes : true
fullName changes when its source changes : true
email changes when its source changes : true
birthDate changes when its source changes : true
age changes when its source changes : true
nickname changes when its source changes : true
address changes when its source changes : true
roles changes when its source changes : true
attributes changes when its source changes : true
orders changes when its source changes : true
record components : [address, age, attributes, birthDate, email, fullName, id, nickname, orders, roles]
covered by a test : [address, age, attributes, birthDate, email, fullName, id, nickname, orders, roles]
@@ -0,0 +1,15 @@
# MockMvc against CustomerController: records in, records out, Jackson 3 in the middle
JSON library bean : tools.jackson.databind.json.JsonMapper
--- GET /customers/1 ---
{"id":1,"fullName":"Asha Mehta","email":"[email protected]","birthDate":"1990-06-15","age":36,"nickname":"anonymous","address":{"street":"12 Park Road","city":"Pune","zip":"411001","country":"IN"},"roles":["USER"],"attributes":{"newsletter":"yes"},"orders":[{"id":100,"status":"PAID","placedAt":"2026-02-01T10:00:00Z","lines":[{"sku":"BOOK-1","quantity":2,"unitPrice":12.50,"lineTotal":25.00},{"sku":"PEN-9","quantity":10,"unitPrice":0.75,"lineTotal":7.50}],"total":32.50}],"origin":"api"}
--- POST /customers (no nickname, no tier, no country) ---
{"id":2,"fullName":"Ravi Iyer","email":"[email protected]","birthDate":"1985-01-02","age":41,"nickname":"anonymous","address":{"street":"1 Lake View","city":"Chennai","zip":"600001","country":"IN"},"roles":["ADMIN"],"attributes":{},"orders":null,"origin":"api"}
--- PATCH /customers/1 {"nickname":"ash"} (every other component of the patch record is null) ---
{"id":1,"fullName":"Asha Mehta","email":"[email protected]","birthDate":"1990-06-15","age":36,"nickname":"ash","address":{"street":"12 Park Road","city":"Pune","zip":"411001","country":"IN"},"roles":["USER"],"attributes":{"newsletter":"yes"},"orders":[{"id":100,"status":"PAID","placedAt":"2026-02-01T10:00:00Z","lines":[{"sku":"BOOK-1","quantity":2,"unitPrice":12.50,"lineTotal":25.00},{"sku":"PEN-9","quantity":10,"unitPrice":0.75,"lineTotal":7.50}],"total":32.50}],"origin":"api"}
--- GET /customers/99 ---
status 404
@@ -0,0 +1,13 @@
# INDICATIVE timings: hand-written vs MapStruct vs Dozer (setter-based beans)
Java 25.0.3+9-LTS-195, one JVM, 200000 iterations per round, best of 5 rounds after a 50000-iteration warm-up
mapping: Customer -> CustomerView (1 address, 1 order with 2 lines, 1 set, 1 map)
hand-written 95 ns/op
MapStruct 1.6.3 92 ns/op (1.0x hand-written)
Dozer 7.0.0 11498 ns/op (122x hand-written)
first call, cold: MapStruct 0.13 ms, Dozer 80.9 ms
results equal: true
Indicative only. Timings vary between runs and machines; do not quote them as a benchmark result.
@@ -0,0 +1,7 @@
# A record target component nobody mapped (unmappedTargetPolicy = ERROR)
$ javac -d target/broken/unmapped-target -cp $MAPSTRUCT_JAR -processorpath $MAPSTRUCT_PROCESSOR_JAR src/broken/unmapped-target/*.java
src/broken/unmapped-target/BrokenMapper.java:12: error: Unmapped target property: "note".
Dst map(Src src);
^
1 error
@@ -0,0 +1,7 @@
# Two methods can produce the same property
$ javac -d target/broken/ambiguous -cp $MAPSTRUCT_JAR -processorpath $MAPSTRUCT_PROCESSOR_JAR src/broken/ambiguous/*.java
src/broken/ambiguous/BrokenMapper.java:13: error: Ambiguous mapping methods found for mapping property "rokenMapper.Money price" to String: String plain(rokenMapper.Money m), String formatted(rokenMapper.Money m). See https://mapstruct.org/faq/#ambiguous for more info.
Dst map(Src src);
^
1 error
@@ -0,0 +1,7 @@
# A typo in @Mapping(source = ...)
$ javac -d target/broken/missing-source -cp $MAPSTRUCT_JAR -processorpath $MAPSTRUCT_PROCESSOR_JAR src/broken/missing-source/*.java
src/broken/missing-source/BrokenMapper.java:12: error: No property named "nmae" exists in source parameter(s). Did you mean "name"?
@Mapping(source = "nmae", target = "title")
^
1 error
@@ -0,0 +1,15 @@
# @MappingTarget on a record compiles, with unmappedTargetPolicy = ERROR, and generates this
$ javac -d target/broken/update-record -s target/broken/update-record -cp $MAPSTRUCT_JAR -processorpath $MAPSTRUCT_PROCESSOR_JAR src/broken/update-record/*.java
(exit status: 0)
$ cat target/broken/update-record/BrokenMapperImpl.java (header and imports trimmed)
public class BrokenMapperImpl implements BrokenMapper {
@Override
public void update(BrokenMapper.Patch patch, BrokenMapper.Dst target) {
if ( patch == null ) {
return;
}
}
}
@@ -0,0 +1,7 @@
# No way to turn one type into another
$ javac -d target/broken/cannot-map -cp $MAPSTRUCT_JAR -processorpath $MAPSTRUCT_PROCESSOR_JAR src/broken/cannot-map/*.java
src/broken/cannot-map/BrokenMapper.java:17: error: Can't map property "rokenMapper.Tag tag" to "LocalDate tag". Consider to declare/implement a mapping method: "LocalDate map(rokenMapper.Tag value)".
Dst map(Src src);
^
1 error
@@ -0,0 +1,10 @@
# @DecoratedWith with a constructor-injected delegate (componentModel = spring)
$ javac -d target/broken/decorator-constructor -cp $MAPSTRUCT_JAR:$SPRING_JARS -processorpath $MAPSTRUCT_PROCESSOR_JAR src/broken/decorator-constructor/*.java
target/broken/decorator-constructor/BrokenMapperImpl.java:13: error: constructor BrokenMapperDecorator in class BrokenMapperDecorator cannot be applied to given types;
public class BrokenMapperImpl extends BrokenMapperDecorator {
^
required: BrokenMapper
found: no arguments
reason: actual and formal argument lists differ in length
1 error
@@ -0,0 +1,24 @@
# The processor jar on the plain classpath: javac 25.0.3 compiles without generating anything
$ javac -d target/broken/processor-not-run -cp $MAPSTRUCT_JAR:$MAPSTRUCT_PROCESSOR_JAR src/broken/processor-not-run/UsesMapper.java
(exit status: 0)
$ ls target/broken/processor-not-run
UsesMapper$Dst.class
UsesMapper$Src.class
UsesMapper.class
$ java -cp $MAPSTRUCT_JAR:target/broken/processor-not-run UsesMapper
Exception in thread "main" java.lang.RuntimeException: java.lang.ClassNotFoundException: Cannot find implementation for UsesMapper
at org.mapstruct.factory.Mappers.getMapper(Mappers.java:61)
at UsesMapper.main(UsesMapper.java:15)
Caused by: java.lang.ClassNotFoundException: Cannot find implementation for UsesMapper
$ javac -proc:full -d target/broken/processor-not-run -cp $MAPSTRUCT_JAR:$MAPSTRUCT_PROCESSOR_JAR src/broken/processor-not-run/UsesMapper.java
(exit status: 0)
$ ls target/broken/processor-not-run
UsesMapper$Dst.class
UsesMapper$Src.class
UsesMapper.class
UsesMapperImpl.class
UsesMapperImpl.java
@@ -0,0 +1,25 @@
# Lombok + MapStruct + a record target: what the processor path order and lombok-mapstruct-binding change
--- MapStruct first, Lombok second, no binding ---
$ javac -d target/broken/lombok -cp $MAPSTRUCT_JAR:$LOMBOK_JAR -processorpath $MAPSTRUCT_PROCESSOR_JAR:$LOMBOK_JAR src/broken/lombok/LombokMapper.java
src\broken\lombok\LombokMapper.java:19: error: Unmapped target properties: "name, age".
PersonDto map(Person person);
^
1 error
(exit status: 1)
LombokMapperImpl generated: no
--- Lombok first, MapStruct second, no binding ---
$ javac -d target/broken/lombok -cp $MAPSTRUCT_JAR:$LOMBOK_JAR -processorpath $LOMBOK_JAR:$MAPSTRUCT_PROCESSOR_JAR src/broken/lombok/LombokMapper.java
(exit status: 0)
LombokMapperImpl generated: yes
--- MapStruct first, then Lombok and lombok-mapstruct-binding ---
$ javac -d target/broken/lombok -cp $MAPSTRUCT_JAR:$LOMBOK_JAR -processorpath $MAPSTRUCT_PROCESSOR_JAR:$LOMBOK_JAR:$BINDING_JAR src/broken/lombok/LombokMapper.java
(exit status: 0)
LombokMapperImpl generated: yes
--- Lombok, binding, then MapStruct ---
$ javac -d target/broken/lombok -cp $MAPSTRUCT_JAR:$LOMBOK_JAR -processorpath $LOMBOK_JAR:$BINDING_JAR:$MAPSTRUCT_PROCESSOR_JAR src/broken/lombok/LombokMapper.java
(exit status: 0)
LombokMapperImpl generated: yes
@@ -0,0 +1,20 @@
# mapstruct-processor 1.6.3: what the jar itself declares
--- META-INF/services/javax.annotation.processing.Processor (comments dropped) ---
org.mapstruct.ap.MappingProcessor
--- Implementation-Version in the manifest ---
Implementation-Version: 1.6.3
--- options the processor accepts (-Akey=value), from @SupportedOptions via javap ---
mapstruct.alwaysGenerateServicesFile
mapstruct.defaultComponentModel
mapstruct.defaultInjectionStrategy
mapstruct.disableBuilders
mapstruct.nullValueIterableMappingStrategy
mapstruct.nullValueMapMappingStrategy
mapstruct.suppressGeneratorTimestamp
mapstruct.suppressGeneratorVersionInfoComment
mapstruct.unmappedSourcePolicy
mapstruct.unmappedTargetPolicy
mapstruct.verbose