Preface
This is the reference documentation of the MapStruct Spring Extensions, an annotation processor designed to complement the core MapStruct project with features specific to the Spring Framework. This guide covers all the functionality provided by the MapStruct Spring Extensions. In case this guide doesn’t answer all your questions just join the MapStruct Google group to get help.
This guide assumes some familiarity with the MapStruct core. If this is your first introduction to MapStruct, you might wish to start with the core documentation. |
You found a typo or other error in this guide? Please let us know by opening an issue in the MapStruct Spring Extensions GitHub repository, or, better yet, help the community and send a pull request for fixing it!
This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
1. Introduction
The MapStruct Spring Extensions are a Java annotation processor extending the well known MapStruct project with features specific to the Spring Framework.
All you have to do is to define your MapStruct mapper to extend Spring’s Converter interface. During compilation, the extensions will generate an adapter which allows the standard MapStruct mappers to use Spring’s ConversionService.
This enables the developer to define MapStruct mappers with only the ConversionService
in their uses
attribute rather than having to import every single Mapper individually, thus allowing for looser coupling between Mappers.
2. Set up
MapStruct Spring Extensions is a Java annotation processor based on JSR 269 and as such can be used within command line builds (javac, Ant, Maven etc.) as well as from within your IDE. The minimum JDK version is 11.
Also, you will need MapStruct itself (at least version 1.4.0.Final ) in your project.
|
It comprises the following artifacts:
-
org.mapstruct.extensions.spring:mapstruct-spring-annotations: contains the added annotations such as
@SpringMapperConfig
-
org.mapstruct.extensions.spring:mapstruct-spring-extensions: contains the annotation processor which generates Spring components
2.1. Apache Maven
For Maven based projects add the following to your POM file in order to use MapStruct Spring Extensions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
...
<properties>
<org.mapstruct.extensions.spring.version>1.1.2</org.mapstruct.extensions.spring.version>
</properties>
...
<dependencies>
<dependency>
<groupId>org.mapstruct.extensions.spring</groupId>
<artifactId>mapstruct-spring-annotations</artifactId>
<version>${org.mapstruct.extensions.spring.version}</version>
</dependency>
</dependencies>
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct.extensions.spring</groupId>
<artifactId>mapstruct-spring-extensions</artifactId>
<version>${org.mapstruct.extensions.spring.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
...
If you wish to use the test extensions, additionally add the following:
1
2
3
4
5
6
7
8
9
10
...
<dependencies>
<dependency>
<groupId>org.mapstruct.extensions.spring</groupId>
<artifactId>mapstruct-spring-test-extensions</artifactId>
<scope>test</scope>
<version>${org.mapstruct.extensions.spring.version}</version>
</dependency>
</dependencies>
...
If you are working with the Eclipse IDE, make sure to have a current version of the M2E plug-in. When importing a Maven project configured as shown above, it will set up the MapStruct Spring Extensions annotation processor so it runs right in the IDE, whenever you save a mapper type extending a Spring converter. Neat, isn’t it? To double check that everything is working as expected, go to your project’s properties and select "Java Compiler" → "Annotation Processing" → "Factory Path". The MapStruct Spring Extensions JAR should be listed and enabled there. Any processor options configured via the compiler plug-in (see below) should be listed under "Java Compiler" → "Annotation Processing". If the processor is not kicking in, check that the configuration of annotation processors through M2E is enabled.
To do so, go to "Preferences" → "Maven" → "Annotation Processing" and select "Automatically configure JDT APT".
Alternatively, specify the following in the Also make sure that your project is using Java 1.8 or later (project properties → "Java Compiler" → "Compile Compliance Level"). It will not work with older versions. |
2.2. Gradle
Add the following to your Gradle build file in order to enable MapStruct Spring Extensions:
1
2
3
4
5
6
7
8
9
10
11
...
dependencies {
...
implementation "org.mapstruct.extensions.spring:mapstruct-spring-annotations:${mapstructSpringExtensionsVersion}"
annotationProcessor "org.mapstruct.extensions.spring:mapstruct-spring-extensions:${mapstructSpringExtensionsVersion}"
// If you are using MapStruct Spring Extensions in test code
testAnnotationProcessor "org.mapstruct.extensions.spring:mapstruct-spring-extensions:${mapstructSpringExtensionsVersion}"
}
...
And for test extensions:
1
2
3
4
5
6
7
...
dependencies {
...
testImplementation "org.mapstruct.extensions.spring:mapstruct-spring-test-extensions:${mapstructSpringExtensionsVersion}"
}
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
plugins {
...
id 'net.ltgt.apt' version '0.20'
}
// You can integrate with your IDEs.
// See more details: https://github.com/tbroyer/gradle-apt-plugin#usage-with-ides
apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse'
dependencies {
...
implementation "org.mapstruct.extensions.spring:mapstruct-spring-annotations:${mapstructSpringExtensionsVersion}"
annotationProcessor "org.mapstruct.extensions.spring:mapstruct-spring-extensions:${mapstructSpringExtensionsVersion}"
// If you are using MapStruct Spring Extensions in test code
testAnnotationProcessor "org.mapstruct.extensions.spring:mapstruct-spring-extensions:${mapstructSpringExtensionsVersion}"
}
...
And for test extensions:
1
2
3
4
5
6
7
...
dependencies {
...
testImplementation "org.mapstruct.extensions.spring:mapstruct-spring-test-extensions:${mapstructSpringExtensionsVersion}"
}
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
plugins {
...
id 'net.ltgt.apt' version '0.20'
}
// You can integrate with your IDEs.
// See more details: https://github.com/tbroyer/gradle-apt-plugin#usage-with-ides
apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse'
dependencies {
...
compile "org.mapstruct.extensions.spring:mapstruct-spring-annotations:${mapstructSpringExtensionsVersion}"
annotationProcessor "org.mapstruct.extensions.spring:mapstruct-spring-extensions:${mapstructSpringExtensionsVersion}"
// If you are using MapStruct Spring Extensions in test code
testAnnotationProcessor "org.mapstruct.extensions.spring:mapstruct-spring-extensions:${mapstructSpringExtensionsVersion}"
}
...
And for test extensions:
1
2
3
4
5
6
7
...
dependencies {
...
testCompile "org.mapstruct.extensions.spring:mapstruct-spring-test-extensions:${mapstructSpringExtensionsVersion}"
}
...
2.3. Apache Ant
Add the javac
task configured as follows to your build.xml file in order to enable MapStruct Spring Extensions in your Ant-based project. Adjust the paths as required for your project layout.
1
2
3
4
5
6
7
8
9
...
<javac
srcdir="src/main/java"
destdir="target/classes"
classpath="path/to/mapstruct-spring-annotations1.1.2.jar">
<compilerarg line="-processorpath path/to/mapstruct-spring-extensions-1.1.2.jar"/>
<compilerarg line="-s target/generated-sources"/>
</javac>
...
3. Mappers as Converters
MapStruct Mappers nicely match Spring’s Converter idea:
1
2
3
4
5
@Mapper(componentModel = "spring")
public interface CarMapper extends Converter<Car, CarDto> {
@Mapping(target = "seats", source = "seatConfiguration")
CarDto convert(Car car);
}
This allows using the Mapper indirectly via the ConversionService
:
1
2
3
4
5
6
...
@Autowired
private ConversionService conversionService;
...
Car car = ...;
CarDto carDto = conversionService.convert(car, CarDto.class);
All this can be achieved already with MapStruct’s core functionality.
However, when a Mapper wants to invoke another one, it can’t take the route via the ConversionService
, because the latter’s convert
method does not match the signature that MapStruct expects for a mapping method.
Thus, the developer still has to add every invoked Mapper to the invoking Mapper’s uses
element.
This creates (aside from a potentially long list) a tight coupling between Mappers that the ConversionService
is designed to avoid.
This is where MapStruct Spring Extensions can help. Including the two artifacts in your build will generate an Adapter class that can be used by an invoking Mapper. Let’s say that the above CarMapper is accompanied by a SeatConfigurationMapper:
1
2
3
4
5
6
@Mapper
public interface SeatConfigurationMapper extends Converter<SeatConfiguration, SeatConfigurationDto> {
@Mapping(target = "seatCount", source = "numberOfSeats")
@Mapping(target = "material", source = "seatMaterial")
SeatConfigurationDto convert(SeatConfiguration seatConfiguration);
}
The generated Adapter class will look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class ConversionServiceAdapter {
private final ConversionService conversionService;
public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
this.conversionService = conversionService;
}
public CarDto mapCarToCarDto(final Car source) {
return (CarDto) conversionService.convert(source, TypeDescriptor.valueOf(Car.class), TypeDescriptor.valueOf(CarDto.class));
}
public SeatConfigurationDto mapSeatConfigurationToSeatConfigurationDto(
final SeatConfiguration source) {
return (SeatConfigurationDto) conversionService.convert(source, TypeDescriptor.valueOf(SeatConfiguration.class), TypeDescriptor.valueOf(SeatConfigurationDto.class));
}
}
Since this class' methods match the signature that MapStruct expects, we can now add it to the CarMapper:
1
2
3
4
5
@Mapper(uses = ConversionServiceAdapter.class)
public interface CarMapper extends Converter<Car, CarDto> {
@Mapping(target = "seats", source = "seatConfiguration")
CarDto convert(Car car);
}
3.1. Custom Names
By default, the generated class will be located in the package org.mapstruct.extensions.spring.converter
and receive the name ConversionServiceAdapter
.
Typically, you will want to change these names, most often at least the package.
This can be accomplished by adding the SpringMapperConfig
annotation on any class within your regular source code.
One natural candidate would be your shared configuration if you use this:
1
2
3
4
5
6
7
8
import org.mapstruct.MapperConfig;
import org.mapstruct.extensions.spring.SpringMapperConfig;
import org.mapstruct.extensions.spring.example.adapter.MyAdapter;
@MapperConfig(componentModel = "spring", uses = MyAdapter.class)
@SpringMapperConfig(conversionServiceAdapterPackage ="org.mapstruct.extensions.spring.example.adapter", conversionServiceAdapterClassName ="MyAdapter")
public interface MapperSpringConfig {
}
Note: If you do not specify the conversionServiceAdapterPackage
element, the generated Adapter class will reside in the same package as the annotated Config.
3.2. Specifying The Conversion Service Bean Name
If your application has multiple ConversionService
beans, you will need to specify the bean name.
The SpringMapperConfig
allows you to specify it using the conversionServiceBeanName
property.
1
2
3
4
5
6
7
import org.mapstruct.MapperConfig;
import org.mapstruct.extensions.spring.SpringMapperConfig;
@MapperConfig(componentModel = "spring", uses = ConversionServiceAdapter.class)
@SpringMapperConfig(conversionServiceBeanName = "myConversionService")
public interface MapperSpringConfig {
}
3.2.1. Modified ConverterScan
When the conversionServiceBeanName
property is set, the built-in ConverterScan cannot be used in tests as it does not pick up this property.
However, setting the property generateConverterScan
to true
will create an alternative inside the project.
Important to note: This version will not create a ConversionService
with the given bean name, but merely register all Mappers with the bean identified by the given name.
This leads to two practical differences:
-
Unlike its Test Extensions counterpart, this version is perfectly suited to be used in production code.
-
In a test, the developer will still have to provide a
ConfigurableConversionService
themselves, e.g.:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ExtendWith(SpringExtension.class)
public class ConversionServiceAdapterIntegrationTest {
@Configuration
@ConverterScan
static class AdditionalBeanConfiguration {
@Bean
ConfigurableConversionService myConversionService() {
return new DefaultConversionService();
}
}
@Autowired
@Qualifier("myConversionService")
private ConversionService conversionService;
}
3.3. Modifying the name for the generated adapter method
By default, the adapter class will contain method names of the form map<SourceTypeName>To<targetTypeName>
.
If you wish to change this, you can do so on a per-Mapper basis by applying the annotation @AdapterMethodName
:
1
2
3
4
5
6
@Mapper(config = MapperSpringConfig.class)
@AdapterMethodName("toDto")
public interface WheelMapper extends Converter<Wheel, WheelDto> {
@Override
WheelDto convert(Wheel source);
}
This changes the generated method name to be the annotation’s value
attribute:
1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class ConversionServiceAdapter {
private final ConversionService conversionService;
public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
this.conversionService = conversionService;
}
public WheelDto toDto(final Wheel source) {
return (WheelDto) conversionService.convert(source, TypeDescriptor.valueOf(Wheel.class), TypeDescriptor.valueOf(WheelDto.class));
}
}
4. External Conversions
Spring ships with a variety of builtin conversions, e.g. String
to Locale
or Object
to Optional
. In order to use these (or your own conversions from another module) in the same fashion, you can add them as externalConversions
to your SpringMapperConfig
:
1
2
3
4
5
6
7
8
9
10
import org.mapstruct.MapperConfig;
import org.mapstruct.extensions.spring.ExternalConversion;
import org.mapstruct.extensions.spring.SpringMapperConfig;
import java.util.Locale;
@MapperConfig(componentModel = "spring")
@SpringMapperConfig(
externalConversions = @ExternalConversion(sourceType = String.class, targetType = Locale.class))
public interface MapstructConfig {}
The processor will add the corresponding methods to the generated adapter so MapStruct can use them in the same fashion as the ones for the Converter Mappers in the same module:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.lang.String;
import java.util.Locale;
import javax.annotation.Generated;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.convert.ConversionService;
import org.springframework.stereotype.Component;
@Generated(
value = "org.mapstruct.extensions.spring.converter.ConversionServiceAdapterGenerator",
date = "2021-06-25T18:51:21.585Z"
)
@Component
public class ConversionServiceAdapter {
private final ConversionService conversionService;
public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
this.conversionService = conversionService;
}
public Locale mapStringToLocale(final String source) {
return (Locale) conversionService.convert(source, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Locale.class));
}
}
4.1. Modifying the name for the generated adapter method
By default, the adapter class will contain method names of the form map<SourceTypeName>To<targetTypeName>
.
If you wish to change this, you can do so on a per-conversion basis by setting the property adapterMethodName
:
1
2
3
4
@MapperConfig(componentModel = "spring")
@SpringMapperConfig(
externalConversions = @ExternalConversion(sourceType = Blob.class, targetType = byte[].class, adapterMethodName = "blob2Bytes"))
public interface MapstructConfig {}
This changes the generated method name to be the property’s value:
1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class ConversionServiceAdapter {
private final ConversionService conversionService;
public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
this.conversionService = conversionService;
}
public byte[] blob2Bytes(final Blob source) {
return (byte[]) conversionService.convert(source, TypeDescriptor.valueOf(Blob.class), TypeDescriptor.valueOf(byte[].class));
}
}
5. Test Extensions
5.1. ConverterScan
In a production environment, Spring will take care of initializing the ConversionService
and registering all generated Mappers inside it.
However, in integration tests, the developer will typically have to take care of all that themselves.
In order to simplify this task, the test-extensions
module provides a @ConverterScan
annotation which can be used much like Spring’s own @ComponentScan
.
It will perform the same scanning task and also provide a ConversionService
with all found Mappers already registered inside.
This is sufficient for most tests.
In its simplest form, the annotation can be used like this:
1
2
3
4
5
6
7
8
9
10
11
@ExtendWith(SpringExtension.class)
class ConversionServiceAdapterIntegrationTest {
@Configuration
@ConverterScan(basePackageClasses = MapperSpringConfig.class)
static class ScanConfiguration {}
@Autowired
private ConversionService conversionService;
[...]
}
6. Delegating Converters
Applying MapStruct’s inverse mappings requires a second mapping method inside a @Mapper
-annotated interface or abstract class.
Since this is not how Spring sees Converter
s, this inverted mapping method will be "invisible" to the ConversionService
.
Adding the annotation @DelegatingConverter
to the same method will lead to the generation of a separate class which implements Converter
and does nothing more than call the annotated method in its own convert
.
This class will have a method counterpart in the generated Adapter just like the ones annotated with @Mapper
.
Take this Converter
for example:
1
2
3
4
5
6
7
8
9
@Mapper(config = MapperSpringConfig.class)
public interface CarMapper extends Converter<Car, CarDto> {
@Mapping(target = "seats", source = "seatConfiguration")
CarDto convert(Car car);
@InheritInverseConfiguration
@DelegatingConverter
Car invertConvert(CarDto carDto);
}
Notice the combination of @InheritInverseConfiguration
and @DelegatingConverter
on the invertConvert
method.
The @DelegatingConverter
will lead to a class like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class CarDtoToCarConverter implements Converter<CarDto, Car> {
private CarMapper delegateMapper;
public CarDtoToCarConverter(@Autowired final CarMapper delegateMapper) {
this.delegateMapper = delegateMapper;
}
@Override
public Car convert(final CarDto source) {
return delegateMapper.invertConvert(source);
}
}
The generated Adapter will contain a method counterpart like this:
1
2
3
public Car mapCarDtoToCar(final CarDto source) {
return (Car) conversionService.convert(source, TypeDescriptor.valueOf(CarDto.class), TypeDescriptor.valueOf(Car.class));
}
Please note: The behaviour of @DelegatingConverter
is not tied to @InheritInverseConfiguration
; however, this is the only use case we are aware of where it provides meaningful value.