1. Introduction
The org.mapstruct.Context
annotation from MapStruct, a Java annotation processor for generating type-safe bean mapping code, marks a parameter of a method to be treated as mapping context. The @context complex source mappings inject context objects into a mapping method. In this example, I will demonstrate @context complex source mappings via @Context, @AfterMapping, @BeforeMapping, @ObjectFactory, Expression, and QualifiedByName.
2. Setup
In this step, I will create a Gradle project along with Lombok and MapStruct libraries.
build.gradle
plugins { id 'java' id 'org.springframework.boot' version '3.4.5' id 'io.spring.dependency-management' version '1.1.7' } group = 'com.zheng.mapstruct.demo' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' implementation 'org.mapstruct:mapstruct:1.5.5.Final' annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { useJUnitPlatform() }
3. Java POJO & External Service
3.1 Source
In this step, I will create a Source.java
that has an Integer id
and String fullName
fields.
Source.java
package com.zheng.mapstruct.demo.data; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class Source { private String fullName; private Integer id; }
3.2 Target
In this step, I will create a Target.java
that has an Integer id
, String lookUpValue
, String firstName
,and String lastName
, fields. The String lookUpValue
value is set by the ExternalService.findNameById
method and the firstName
and lastName
are set by the NameContext.splitFullName
method.
Target.java
package com.zheng.mapstruct.demo.data; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class Target { private String firstName; private Integer id; private String lastName; private String lookUpValue; }
3.3 ExternalService
In this step, I will create an ExternalService.java
that has the findNameById
method to find the name
based on its id
. It will be used as a parameter in the Mapper.map()
method in step 4.
ExternalService.java
package com.zheng.mapstruct.demo.service; import org.springframework.stereotype.Service; @Service public class ExternalService { public String findNameById(int someId) { if (someId < 0) { throw new RuntimeException("Negative value is invalid!"); } return "lookUpValue_" + someId; } }
3.4 LookUpContext
In this step, I will create a LookUpContext.java
that has a resolveLookUpValue
method to set the lookUpValue
via the externalService.findNameById
method. It will be used as a parameter in the Mapper.map()
method in step 4.
LookUpContext.java
package com.zheng.mapstruct.demo.service; import org.springframework.stereotype.Component; import com.zheng.mapstruct.demo.data.Target; @Component public class LookUpContext { private final ExternalService service; public LookUpContext(ExternalService service) { super(); this.service = service; } public Target resolveLookUpValue(Integer someId) { Target ret = new Target(); if (someId == null) { throw new RuntimeException("Null is invalid."); } ret.setId(someId); ret.setLookUpValue(service.findNameById(someId.intValue())); return ret; } }
3.5 NameContext
In this step, I will create a NameContext.java
that has a splitFullName
method to split the fullName
into the firstName and lastName
. It will be used as a parameter in the Mapper.map()
method in step 4.
NameContext.java
package com.zheng.mapstruct.demo.service; import org.springframework.stereotype.Component; @Component public class NameContext { public String[] splitFullName(String fullName) { return fullName != null ? fullName.trim().split("\\s+", 2) : new String[] { "", "" }; } }
4. @Context Annotation Complex Source Mappings
MapStruct supports both interface and abstract classes. It’s better to define the Mapper as an Interface when All methods are either simple mappings or delegating to other mappers or default methods. However, when there is custom mapping logic or injecting dependencies via fields or constructors, then defining Mapper as the abstract class is the better choice. In this step, I will create two interfaces and three Abstract classes to demonstrate @context annotation complex source mappings.
4.1 Mapper Interface with @Context Expression
In this step, I will create a MapperWithExpression.java
interface that utilizes @Context
and calls its findNameById
method to set the target
‘s lookUpValue
data via the expression
attribute.
MapperWithExpression.java
package com.zheng.mapstruct.demo.mapper; import org.mapstruct.Context; import org.mapstruct.InjectionStrategy; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers; import com.zheng.mapstruct.demo.data.Source; import com.zheng.mapstruct.demo.data.Target; import com.zheng.mapstruct.demo.service.ExternalService; import com.zheng.mapstruct.demo.service.NameContext; @Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR) public interface MapperWithExpression { MapperWithExpression INSTANCE = Mappers.getMapper(MapperWithExpression.class); @Mapping(target = "lookUpValue", expression = "java(context.findNameById(source.getId()))") @Mapping(target = "firstName", expression = "java(nameContext.splitFullName(source.getFullName())[0])") @Mapping(target = "lastName", expression = "java(nameContext.splitFullName(source.getFullName())[1])") Target map(Source source, @Context ExternalService context, @Context NameContext nameContext); }
- Line 18: this
expression
accesses thecontext.findNameById
method to set thelookUpValue
. - Line 19, 20: this
expression
accesses thenameContext.splitFullName
method to set thefirstName
andlastName
. - Line 21: the
map
method injects two contexts for complex mapping logic.
4.2 Mapper Interface with @Context QualifiedByName
In this step, I will create a MapperWithQualifiedByName.java
interface that utilizes @Context
and calls its findNameById
method to set the target
‘s lookUpValue
data via the qualifiedByName
attribute.
MapperWithQualifiedByName.java
package com.zheng.mapstruct.demo.mapper; import org.mapstruct.Context; import org.mapstruct.InjectionStrategy; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Named; import org.mapstruct.factory.Mappers; import com.zheng.mapstruct.demo.data.Source; import com.zheng.mapstruct.demo.data.Target; import com.zheng.mapstruct.demo.service.ExternalService; @Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR) public interface MapperWithQualifiedByName { MapperWithQualifiedByName INSTANCE = Mappers.getMapper(MapperWithQualifiedByName.class); @Named("extractFirstName") static String extractFirstName(String fullName) { if (fullName == null) return ""; return fullName.trim().split("\\s+", 2)[0]; } @Named("extractLastName") static String extractLastName(String fullName) { if (fullName == null) return ""; String[] parts = fullName.trim().split("\\s+", 2); return parts.length > 1 ? parts[1] : ""; } @Mapping(source = "id", target = "lookUpValue", qualifiedByName = "lookUpValueMapper") @Mapping(target = "firstName", source = "fullName", qualifiedByName = "extractFirstName") @Mapping(target = "lastName", source = "fullName", qualifiedByName = "extractLastName") Target map(Source source, @Context ExternalService context); @Named("lookUpValueMapper") default String mapIdToName(int id, @Context ExternalService context) { return context.findNameById(id); } }
- Line 36: the
map
method injects context for complex mapping logic. - Line 39: the
default mapIdToName
method injectscontext
for complex mapping logic.
4.3 Abstract Mapper Class with @Context @BeforeMapping
In this step, I will create an abstract class MapperWithBeforeMapping.java
that utilizes @Context
and calls its findNameById
method to set the target
‘s lookUpValue
data via the @BeforeMapping
annotation. The method setLookupValue
marked with @BeforeMapping
is generated at the first line of mapping logic.
MapperWithBeforeMapping.java
package com.zheng.mapstruct.demo.mapper; import org.mapstruct.BeforeMapping; import org.mapstruct.Context; import org.mapstruct.InjectionStrategy; import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; import org.mapstruct.ReportingPolicy; import org.mapstruct.factory.Mappers; import com.zheng.mapstruct.demo.data.Source; import com.zheng.mapstruct.demo.data.Target; import com.zheng.mapstruct.demo.service.ExternalService; import com.zheng.mapstruct.demo.service.NameContext; @Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR, unmappedTargetPolicy = ReportingPolicy.IGNORE) public abstract class MapperWithBeforeMapping { public static final MapperWithBeforeMapping INSTANCE = Mappers.getMapper(MapperWithBeforeMapping.class); public abstract Target map(Source user, @Context ExternalService context, @Context NameContext nameContext); @BeforeMapping protected void setLookupValue(Source src, @MappingTarget Target target, @Context ExternalService context) { String lookupName = context.findNameById(src.getId()); target.setLookUpValue(lookupName); } @BeforeMapping protected void splitFullName(Source user, @MappingTarget Target dto, @Context NameContext nameContext) { String[] names = nameContext.splitFullName(user.getFullName()); dto.setFirstName(names[0]); dto.setLastName(names.length > 1 ? names[1] : ""); } }
- Line 21: the
map
method injects two contexts for complex mapping logic. - Line 23, 30: the
@BeforeMapping
annotation is marked.
4.4 Abstract Mapper Class with @Context @AfterMapping
In this step, I will create an abstract class MapperWithAfterMapping.java
that utilizes @Context
and calls its findNameById
method to set the target
‘s lookUpValue
data via the @AfterMapping
annotation. The method setLookupValue
marked with @AfterMapping
is generated at the last line of mapping logic.
MapperWithAfterMapping.java
package com.zheng.mapstruct.demo.mapper; import org.mapstruct.AfterMapping; import org.mapstruct.Context; import org.mapstruct.InjectionStrategy; import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; import org.mapstruct.ReportingPolicy; import org.mapstruct.factory.Mappers; import com.zheng.mapstruct.demo.data.Source; import com.zheng.mapstruct.demo.data.Target; import com.zheng.mapstruct.demo.service.ExternalService; import com.zheng.mapstruct.demo.service.NameContext; @Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR, unmappedTargetPolicy = ReportingPolicy.IGNORE) public abstract class MapperWithAfterMapping { public static final MapperWithAfterMapping INSTANCE = Mappers.getMapper(MapperWithAfterMapping.class); public abstract Target map(Source user, @Context ExternalService context, @Context NameContext nameContext); @AfterMapping protected void setLookupValue(Source src, @MappingTarget Target target, @Context ExternalService context) { String lookupName = context.findNameById(src.getId()); target.setLookUpValue(lookupName); } @AfterMapping protected void splitFullName(Source user, @MappingTarget Target dto, @Context NameContext nameContext) { String[] names = nameContext.splitFullName(user.getFullName()); dto.setFirstName(names[0]); dto.setLastName(names.length > 1 ? names[1] : ""); } }
- Line 21: the
map
method injects two contexts for complex mapping logic. - Line 23, 31: the
@AfterMapping
annotation is marked.
4.5 Abstract Mapper Class with @Context @ObjectFactory
In this step, I will create an abstract class MapperWithObjectFactory.java
that utilizes @Context
and calls its resolveLookUpValue
method to create the target
object and set its lookUpValue
data via the @ObjectFactory
annotation.
MapperWithObjectFactory.java
package com.zheng.mapstruct.demo.mapper; import org.mapstruct.Context; import org.mapstruct.InjectionStrategy; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.ObjectFactory; import org.mapstruct.ReportingPolicy; import org.mapstruct.factory.Mappers; import com.zheng.mapstruct.demo.data.Source; import com.zheng.mapstruct.demo.data.Target; import com.zheng.mapstruct.demo.service.LookUpContext; import com.zheng.mapstruct.demo.service.NameContext; @Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR, unmappedTargetPolicy = ReportingPolicy.IGNORE) public abstract class MapperWithObjectFactory { public static final MapperWithObjectFactory INSTANCE = Mappers.getMapper(MapperWithObjectFactory.class); @ObjectFactory protected Target createTarget(Source dto, @Context LookUpContext context, @Context NameContext nameContext) { Target target = context.resolveLookUpValue(dto.getId()); String[] names = nameContext.splitFullName(dto.getFullName()); target.setFirstName(names[0]); if (names.length > 1) { target.setLastName(names[1]); } return target; } @Mapping(target = "id", ignore = true) public abstract Target map(Source user, @Context LookUpContext context, @Context NameContext nameContext); }
- Line 21: the
map
method injects two contexts for complex mapping logic. - Line 33: the
@ObjectFactory
annotation is marked.
5. Generate Implementation Class
5.1 MapperWithExpressionImpl
In this step, I will print the generated MapperWithExpressionImpl.java
class.
MapperWithExpressionImpl.java
package com.zheng.mapstruct.demo.mapper; import com.zheng.mapstruct.demo.data.Source; import com.zheng.mapstruct.demo.data.Target; import com.zheng.mapstruct.demo.service.ExternalService; import com.zheng.mapstruct.demo.service.NameContext; import javax.annotation.processing.Generated; import org.springframework.stereotype.Component; @Generated( value = "org.mapstruct.ap.MappingProcessor", date = "2025-05-08T16:21:59-0500", comments = "version: 1.5.5.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-8.13.jar, environment: Java 17 (Oracle Corporation)" ) @Component public class MapperWithExpressionImpl implements MapperWithExpression { @Override public Target map(Source source, ExternalService context, NameContext nameContext) { if ( source == null ) { return null; } Target target = new Target(); target.setId( source.getId() ); target.setLookUpValue( context.findNameById(source.getId()) ); target.setFirstName( nameContext.splitFullName(source.getFullName())[0] ); target.setLastName( nameContext.splitFullName(source.getFullName())[1] ); return target; } }
5.2 MapperWithQualifiedByNameImpl
In this step, I will print the generated MapperWithQualifiedByNameImpl.java
class.
MapperWithQualifiedByNameImpl.java
package com.zheng.mapstruct.demo.mapper; import com.zheng.mapstruct.demo.data.Source; import com.zheng.mapstruct.demo.data.Target; import com.zheng.mapstruct.demo.service.ExternalService; import javax.annotation.processing.Generated; import org.springframework.stereotype.Component; @Generated( value = "org.mapstruct.ap.MappingProcessor", date = "2025-05-08T16:21:59-0500", comments = "version: 1.5.5.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-8.13.jar, environment: Java 17 (Oracle Corporation)" ) @Component public class MapperWithQualifiedByNameImpl implements MapperWithQualifiedByName { @Override public Target map(Source source, ExternalService context) { if ( source == null ) { return null; } Target target = new Target(); if ( source.getId() != null ) { target.setLookUpValue( mapIdToName( source.getId().intValue(), context ) ); } target.setFirstName( MapperWithQualifiedByName.extractFirstName( source.getFullName() ) ); target.setLastName( MapperWithQualifiedByName.extractLastName( source.getFullName() ) ); target.setId( source.getId() ); return target; } }
5.3 MapperWithBeforeMappingImpl
In this step, I will print the generated MapperWithBeforeMappingImpl.java
class.
MapperWithBeforeMappingImpl.java
package com.zheng.mapstruct.demo.mapper; import com.zheng.mapstruct.demo.data.Source; import com.zheng.mapstruct.demo.data.Target; import com.zheng.mapstruct.demo.service.ExternalService; import com.zheng.mapstruct.demo.service.NameContext; import javax.annotation.processing.Generated; import org.springframework.stereotype.Component; @Generated( value = "org.mapstruct.ap.MappingProcessor", date = "2025-05-08T16:21:59-0500", comments = "version: 1.5.5.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-8.13.jar, environment: Java 17 (Oracle Corporation)" ) @Component public class MapperWithBeforeMappingImpl extends MapperWithBeforeMapping { @Override public Target map(Source user, ExternalService context, NameContext nameContext) { if ( user == null ) { return null; } Target target = new Target(); setLookupValue( user, target, context ); splitFullName( user, target, nameContext ); target.setId( user.getId() ); return target; } }
5.4 MapperWithAfterMappingImpl
In this step, I will print the generated MapperWithAfterMappingImpl.java
class.
MapperWithAfterMappingImpl.java
package com.zheng.mapstruct.demo.mapper; import com.zheng.mapstruct.demo.data.Source; import com.zheng.mapstruct.demo.data.Target; import com.zheng.mapstruct.demo.service.ExternalService; import com.zheng.mapstruct.demo.service.NameContext; import javax.annotation.processing.Generated; import org.springframework.stereotype.Component; @Generated( value = "org.mapstruct.ap.MappingProcessor", date = "2025-05-08T16:21:59-0500", comments = "version: 1.5.5.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-8.13.jar, environment: Java 17 (Oracle Corporation)" ) @Component public class MapperWithAfterMappingImpl extends MapperWithAfterMapping { @Override public Target map(Source user, ExternalService context, NameContext nameContext) { if ( user == null ) { return null; } Target target = new Target(); target.setId( user.getId() ); setLookupValue( user, target, context ); splitFullName( user, target, nameContext ); return target; } }
5.5 MapperWithObjectFactoryImpl
In this step, I will print the generated MapperWithObjectFactoryImpl.java
class.
MapperWithObjectFactoryImpl.java
package com.zheng.mapstruct.demo.mapper; import com.zheng.mapstruct.demo.data.Source; import com.zheng.mapstruct.demo.data.Target; import com.zheng.mapstruct.demo.service.LookUpContext; import com.zheng.mapstruct.demo.service.NameContext; import javax.annotation.processing.Generated; import org.springframework.stereotype.Component; @Generated( value = "org.mapstruct.ap.MappingProcessor", date = "2025-05-08T16:21:59-0500", comments = "version: 1.5.5.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-8.13.jar, environment: Java 17 (Oracle Corporation)" ) @Component public class MapperWithObjectFactoryImpl extends MapperWithObjectFactory { @Override public Target map(Source user, LookUpContext context, NameContext nameContext) { if ( user == null ) { return null; } Target target = createTarget( user, context, nameContext ); return target; } }
6. Test Context Annotation Complex Source Mappings
6.1 TestBase
In this step, I will create TestBase.java
that defines the source
, target
, externalService
, lookUpContext
, nameContext
variables, and setupFields
method.
TestBase.java
package com.zheng.mapstruct.demo.mapper; import com.zheng.mapstruct.demo.data.Source; import com.zheng.mapstruct.demo.data.Target; import com.zheng.mapstruct.demo.service.ExternalService; import com.zheng.mapstruct.demo.service.LookUpContext; import com.zheng.mapstruct.demo.service.NameContext; public class TestBase { protected Source source; protected Target target; protected static final String LOOK_UP_VALUE_1 = "lookUpValue_1"; protected static final int ID1 = 1; protected static final String LAST_NAME = "LastName"; protected static final String FIRST_NAME = "FirstName"; protected static final String FULLNAME = "FirstName LastName"; protected ExternalService externalService = new ExternalService(); protected LookUpContext lookUpContext = new LookUpContext(externalService); protected NameContext nameContext = new NameContext(); public TestBase() { super(); } protected void setupFields(Integer someId, String fullName) { source = new Source(); source.setId(someId); source.setFullName(fullName); } }
6.2 MapperWithExpressionTest
In this step, I will create a MapperWithExpressionTest.java
that verifies the target
‘s lookUpValue
field is set by the @context
via expression
and the firstName
and lastName
are set via @nameContext
.
MapperWithExpressionTest.java
package com.zheng.mapstruct.demo.mapper; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; class MapperWithExpressionTest extends TestBase { private MapperWithExpression testClass = MapperWithExpression.INSTANCE; @Test void test_lookup_negative() { setupFields(-1, FULLNAME); assertThrows(RuntimeException.class, () -> testClass.map(source, externalService,nameContext)); } @Test void test_lookup_null() { setupFields(null, FULLNAME); assertThrows(RuntimeException.class, () -> testClass.map(source, externalService,nameContext)); } @Test void test_lookup_positive() { setupFields(ID1, FULLNAME); target = testClass.map(source, externalService,nameContext); assertEquals(ID1, target.getId().intValue()); assertEquals(LOOK_UP_VALUE_1, target.getLookUpValue()); assertEquals(FIRST_NAME, target.getFirstName()); assertEquals(LAST_NAME, target.getLastName()); } @Test void test_source_null() { assertNull(testClass.map(source, externalService,nameContext)); } }
6.3 MapperWithQualifiedByNameTest
In this step, I will create a MapperWithQualifiedByNameTest.java
that verifies the target
‘s lookUpValue
field is set by the @context
via qualifiedByName
and the firstName
and lastName
are set via @nameContext
.
MapperWithQualifiedByNameTest.java
package com.zheng.mapstruct.demo.mapper; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; class MapperWithQualifiedByNameTest extends TestBase { private MapperWithQualifiedByName testClass = MapperWithQualifiedByName.INSTANCE; @Test void test_lookup_negative() { setupFields(-1, FULLNAME); assertThrows(RuntimeException.class, () -> testClass.map(source, externalService)); } @Test void test_lookup_null() { setupFields(null, FULLNAME); target = testClass.map(source, externalService); assertNull(target.getId()); } @Test void test_lookup_positive() { setupFields(ID1, FULLNAME); target = testClass.map(source, externalService); assertEquals(ID1, target.getId().intValue()); assertEquals(LOOK_UP_VALUE_1, target.getLookUpValue()); assertEquals(FIRST_NAME, target.getFirstName()); assertEquals(LAST_NAME, target.getLastName()); } @Test void test_source_null() { assertNull(testClass.map(source, externalService)); } }
6.4 MapperWithBeforeMappingTest
In this step, I will create a MapperWithQualifiedByNameTest.java
that verifies the target
‘s lookUpValue
field is set by the @context
and the firstName
and lastName
are set via @nameContext
.
MapperWithBeforeMappingTest.java
package com.zheng.mapstruct.demo.mapper; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; class MapperWithBeforeMappingTest extends TestBase { private MapperWithBeforeMapping testClass = MapperWithBeforeMapping.INSTANCE; @Test void test_lookup_negative() { setupFields(-1, FULLNAME); assertThrows(RuntimeException.class, () -> testClass.map(source, externalService, nameContext)); } @Test void test_lookup_null() { setupFields(null, FULLNAME); assertThrows(RuntimeException.class, () -> testClass.map(source, externalService, nameContext)); } @Test void test_lookup_positive() { setupFields(ID1, FULLNAME ); target = testClass.map(source, externalService, nameContext); assertEquals(ID1, target.getId().intValue()); assertEquals(LOOK_UP_VALUE_1, target.getLookUpValue()); assertEquals(FIRST_NAME, target.getFirstName()); assertEquals(LAST_NAME, target.getLastName()); } @Test void test_source_null() { assertNull(testClass.map(source, externalService, nameContext)); } }
6.5 MapperWithAfterMappingTest
In this step, I will create a MapperWithQualifiedByNameTest.java
that verifies the target
‘s lookUpValue
field is set by the @context
and the firstName
and lastName
are set via @nameContext
.
MapperWithAfterMappingTest.java
package com.zheng.mapstruct.demo.mapper; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; class MapperWithAfterMappingTest extends TestBase { private MapperWithAfterMapping testClass = MapperWithAfterMapping.INSTANCE; @Test void test_lookup_negative() { setupFields(-1, FULLNAME); assertThrows(RuntimeException.class, () -> testClass.map(source, externalService,nameContext)); } @Test void test_lookup_null() { setupFields(null, FULLNAME); assertThrows(RuntimeException.class, () -> testClass.map(source, externalService,nameContext)); } @Test void test_lookup_positive() { setupFields(ID1, FULLNAME); target = testClass.map(source, externalService,nameContext); assertEquals(ID1, target.getId().intValue()); assertEquals(LOOK_UP_VALUE_1, target.getLookUpValue()); assertEquals(FIRST_NAME, target.getFirstName()); assertEquals(LAST_NAME, target.getLastName()); } @Test void test_source_null() { assertNull(testClass.map(source, externalService,nameContext)); } }
6.6 MapperWithObjectFactoryTest
In this step, I will create a MapperWithQualifiedByNameTest.java
that verifies the target
‘s lookUpValue
field is set by the @context
and the firstName
and lastName
are set via @nameContext
.
MapperWithObjectFactoryTest.java
package com.zheng.mapstruct.demo.mapper; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; class MapperWithObjectFactoryTest extends TestBase { private MapperWithObjectFactory testClass = MapperWithObjectFactory.INSTANCE; @Test void test_lookup_negative() { setupFields(-1, FULLNAME); assertThrows(RuntimeException.class, () -> testClass.map(source, lookUpContext,nameContext)); } @Test void test_lookup_null() { setupFields(null, FULLNAME); assertThrows(RuntimeException.class, () -> testClass.map(source, lookUpContext,nameContext)); } @Test void test_lookup_positive() { setupFields(ID1, FULLNAME); target = testClass.map(source, lookUpContext,nameContext); assertEquals(ID1, target.getId().intValue()); assertEquals(LOOK_UP_VALUE_1, target.getLookUpValue()); assertEquals(FIRST_NAME, target.getFirstName()); assertEquals(LAST_NAME, target.getLastName()); } @Test void test_source_null() { assertNull(testClass.map(source, lookUpContext,nameContext)); } }
Run the tests and capture the results in Figure 1.
7. Conclusion
In this example, I demonstrated context annotation complex source mappings by creating Mapper Interface
s and Abstract Mapper Class
es with @Context
, @AfterMapping
, @BeforeMapping
, @ObjectFactory
, Expression
, and QualifiedByName
. I outlined the difference in the following table.
@AfterMapping | @BeforeMapping | @ObjectFactory | Expression | QualifiedByName | |
Description | Marks a method to be invoked at the end of a generated mapping method, right before the last return statement of the mapping method. | Marks a method to be invoked at the beginning of a generated mapping method. | Marks a method as a factory method to create beans. | An expression String based on which the specified target property is to be set. | String-based form of qualifiers |
Why Interface? | Simple Mapping | Default method | |||
Why Abstract Class? | Custom Mapping and Injecting the dependencies | Custom Mapping and Injecting the dependencies | Custom Mapping and Injecting the dependencies | ||
Why ExternalService? | Only change the mapped field | Only change the mapped field | LookUpContext | Only change the mapped field | Only change the mapped field |
Why LookUpContext? | Use the target created via the ObjectFactory |
8. Download
This was an example of a Gradle project which included mapstruct context annotation complex source mappings example.
You can download the full source code of this example here: @Context Complex Source Mappings Example