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
expressionaccesses thecontext.findNameByIdmethod to set thelookUpValue. - Line 19, 20: this
expressionaccesses thenameContext.splitFullNamemethod to set thefirstNameandlastName. - Line 21: the
mapmethod 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
mapmethod injects context for complex mapping logic. - Line 39: the
default mapIdToNamemethod injectscontextfor 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
mapmethod injects two contexts for complex mapping logic. - Line 23, 30: the
@BeforeMappingannotation 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
mapmethod injects two contexts for complex mapping logic. - Line 23, 31: the
@AfterMappingannotation 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
mapmethod injects two contexts for complex mapping logic. - Line 33: the
@ObjectFactoryannotation 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 Interfaces and Abstract Mapper Classes 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
Thank you!
We will contact you soon.

This site uses Akismet to reduce spam. Learn how your comment data is processed.