Skip to content

Commit 12b1fb5

Browse files
authored
87 provide a conversionservice bean if missing (#91)
* New module and first quick implementation. * Simple Spring Boot Test employing the new mechanism. * The annotation is actually not MapStruct specific, so let's just call it ConverterScan. * Split service creation from linking converters so the latter can be reused. * Extracted ConverterRegistration into another "runtime" module as it is useful for situations beyond testing, * Quick Unit Test * Quick Unit Test * Refactoring since we will generate several files. * Generate scan and configuration classes for custom bean name. * Compiler warning * Use generated ConverterScan annotation rather than ComponentScan. * Control registration class generation via new attribute. * Modified examples so they use the test extension. * Use ConverterScan where possible * Documentation. Also made generated registration class package-private as only @ConverterScan will be needed in real-life code.
1 parent 18a43e3 commit 12b1fb5

File tree

67 files changed

+2292
-515
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+2292
-515
lines changed

annotations/build.gradle

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,6 @@ dependencies {
55
implementation libs.spring.core
66
}
77

8-
java {
9-
withJavadocJar()
10-
withSourcesJar()
11-
}
12-
138
publishing {
149
publications {
1510
mavenJava(MavenPublication) {

annotations/src/main/java/org/mapstruct/extensions/spring/SpringMapperConfig.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@
4747
*/
4848
boolean lazyAnnotatedConversionServiceBean() default true;
4949

50+
/**
51+
* Indicates whether to generate a {@code ConverterScan} when using {@link
52+
* #conversionServiceBeanName()}. Has no effect when {@link #conversionServiceBeanName()} is not
53+
* set.
54+
*
55+
* @return {@code true} - Generate {@code ConverterScan}, {@code false} - otherwise
56+
*/
57+
boolean generateConverterScan() default false;
58+
5059
/**
5160
* Additional {@link ExternalConversion conversions} which should be made available through the
5261
* generated Adapter.

build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,15 @@ subprojects {
5555
apply plugin: "jacoco"
5656

5757
group = 'org.mapstruct.extensions.spring'
58-
version = '1.0.3-SNAPSHOT'
58+
version = '1.1.0-SNAPSHOT'
5959

6060
java {
6161
toolchain {
6262
languageVersion.set(JavaLanguageVersion.of(11))
6363
}
64+
65+
withJavadocJar()
66+
withSourcesJar()
6467
}
6568

6669
testing {

docs/src/docs/asciidoc/chapter-3-mapper-as-converter.asciidoc

Lines changed: 47 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
== Mappers as Converters
33

44
MapStruct Mappers nicely match Spring's https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#core-convert-Converter-API[Converter] idea:
5+
56
====
6-
[source, java, linenums]
7+
[source,java,linenums]
78
[subs="verbatim,attributes"]
89
----
910
@Mapper(componentModel = "spring")
@@ -17,7 +18,7 @@ public interface CarMapper extends Converter<Car, CarDto> {
1718
This allows using the Mapper indirectly via the `ConversionService`:
1819

1920
====
20-
[source, java, linenums]
21+
[source,java,linenums]
2122
[subs="verbatim,attributes"]
2223
----
2324
...
@@ -29,12 +30,17 @@ This allows using the Mapper indirectly via the `ConversionService`:
2930
----
3031
====
3132

32-
All this can be achieved already with MapStruct's core functionality. However, when a Mapper wants to https://mapstruct.org/documentation/stable/reference/html/#invoking-other-mappers[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.
33+
All this can be achieved already with MapStruct's core functionality.
34+
However, when a Mapper wants to https://mapstruct.org/documentation/stable/reference/html/#invoking-other-mappers[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.
35+
Thus, the developer still has to add every invoked Mapper to the invoking Mapper's `uses` element.
36+
This creates (aside from a potentially long list) a tight coupling between Mappers that the `ConversionService` is designed to avoid.
3337

34-
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:
38+
This is where MapStruct Spring Extensions can help.
39+
Including the two artifacts in your build will generate an Adapter class that _can_ be used by an invoking Mapper.
40+
Let's say that the above CarMapper is accompanied by a SeatConfigurationMapper:
3541

3642
====
37-
[source, java, linenums]
43+
[source,java,linenums]
3844
[subs="verbatim,attributes"]
3945
----
4046
@Mapper
@@ -49,7 +55,7 @@ public interface SeatConfigurationMapper extends Converter<SeatConfiguration, Se
4955
The generated Adapter class will look like this:
5056

5157
====
52-
[source, java, linenums]
58+
[source,java,linenums]
5359
[subs="verbatim,attributes"]
5460
----
5561
@Component
@@ -73,8 +79,9 @@ public class ConversionServiceAdapter {
7379
====
7480

7581
Since this class' methods match the signature that MapStruct expects, we can now add it to the CarMapper:
82+
7683
====
77-
[source, java, linenums]
84+
[source,java,linenums]
7885
[subs="verbatim,attributes"]
7986
----
8087
@Mapper(uses = ConversionServiceAdapter.class)
@@ -87,9 +94,14 @@ public interface CarMapper extends Converter<Car, CarDto> {
8794

8895
[[mappersAsConvertersCustomNames]]
8996
=== Custom Names
90-
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 https://mapstruct.org/documentation/stable/reference/html/#shared-configurations[shared configuration] if you use this:
97+
98+
By default, the generated class will be located in the package `org.mapstruct.extensions.spring.converter` and receive the name `ConversionServiceAdapter`.
99+
Typically, you will want to change these names, most often at least the package.
100+
This can be accomplished by adding the `SpringMapperConfig` annotation on any class within your regular source code.
101+
One natural candidate would be your https://mapstruct.org/documentation/stable/reference/html/#shared-configurations[shared configuration] if you use this:
102+
91103
====
92-
[source, java, linenums]
104+
[source,java,linenums]
93105
[subs="verbatim,attributes"]
94106
----
95107
import org.mapstruct.MapperConfig;
@@ -104,11 +116,15 @@ public interface MapperSpringConfig {
104116
105117
Note: If you do _not_ specify the `conversionServiceAdapterPackage` element, the generated Adapter class will reside in the same package as the annotated Config.
106118
====
119+
107120
[[customConversionService]]
108121
=== Specifying The Conversion Service Bean Name
109-
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.
122+
123+
If your application has multiple `ConversionService` beans, you will need to specify the bean name.
124+
The `SpringMapperConfig` allows you to specify it using the `conversionServiceBeanName` property.
125+
110126
====
111-
[source, java, linenums]
127+
[source,java,linenums]
112128
[subs="verbatim,attributes"]
113129
----
114130
import org.mapstruct.MapperConfig;
@@ -120,53 +136,34 @@ public interface MapperSpringConfig {
120136
}
121137
----
122138
====
123-
[[externalConversions]]
124-
=== External Conversions
125-
Spring ships with a variety of https://github.com/spring-projects/spring-framework/tree/main/spring-core/src/main/java/org/springframework/core/convert/support[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`:
126-
====
127-
[source, java, linenums]
128-
[subs="verbatim,attributes"]
129-
----
130-
import org.mapstruct.MapperConfig;
131-
import org.mapstruct.extensions.spring.ExternalConversion;
132-
import org.mapstruct.extensions.spring.SpringMapperConfig;
133139

134-
import java.util.Locale;
140+
[[generateConverterScan]]
141+
==== Modified ConverterScan
135142

136-
@MapperConfig(componentModel = "spring")
137-
@SpringMapperConfig(
138-
externalConversions = @ExternalConversion(sourceType = String.class, targetType = Locale.class))
139-
public interface MapstructConfig {}
140-
----
141-
====
143+
When the `conversionServiceBeanName` property is set, the built-in <<converterScan>> cannot be used in tests as it does not pick up this property.
144+
However, setting the property `generateConverterScan` to `true` will create an alternative inside the project.
145+
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:
142146

143-
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:
147+
- Unlike its <<testExtensions>> counterpart, this version is perfectly suited to be used in production code.
148+
- In a test, the developer will still have to provide a `ConfigurableConversionService` themselves, e.g.:
144149
====
145-
[source, java, linenums]
150+
[source,java,linenums]
146151
[subs="verbatim,attributes"]
147152
----
148-
import java.lang.String;
149-
import java.util.Locale;
150-
import javax.annotation.Generated;
151-
import org.springframework.context.annotation.Lazy;
152-
import org.springframework.core.convert.ConversionService;
153-
import org.springframework.stereotype.Component;
154-
155-
@Generated(
156-
value = "org.mapstruct.extensions.spring.converter.ConversionServiceAdapterGenerator",
157-
date = "2021-06-25T18:51:21.585Z"
158-
)
159-
@Component
160-
public class ConversionServiceAdapter {
161-
private final ConversionService conversionService;
162-
163-
public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
164-
this.conversionService = conversionService;
153+
@ExtendWith(SpringExtension.class)
154+
public class ConversionServiceAdapterIntegrationTest {
155+
@Configuration
156+
@ConverterScan
157+
static class AdditionalBeanConfiguration {
158+
@Bean
159+
ConfigurableConversionService myConversionService() {
160+
return new DefaultConversionService();
161+
}
165162
}
166163
167-
public Locale mapStringToLocale(final String source) {
168-
return (Locale) conversionService.convert(source, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Locale.class));
169-
}
164+
@Autowired
165+
@Qualifier("myConversionService")
166+
private ConversionService conversionService;
170167
}
171168
----
172169
====
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
[[externalConversions]]
2+
== External Conversions
3+
Spring ships with a variety of https://github.com/spring-projects/spring-framework/tree/main/spring-core/src/main/java/org/springframework/core/convert/support[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`:
4+
====
5+
[source, java, linenums]
6+
[subs="verbatim,attributes"]
7+
----
8+
import org.mapstruct.MapperConfig;
9+
import org.mapstruct.extensions.spring.ExternalConversion;
10+
import org.mapstruct.extensions.spring.SpringMapperConfig;
11+
12+
import java.util.Locale;
13+
14+
@MapperConfig(componentModel = "spring")
15+
@SpringMapperConfig(
16+
externalConversions = @ExternalConversion(sourceType = String.class, targetType = Locale.class))
17+
public interface MapstructConfig {}
18+
----
19+
====
20+
21+
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:
22+
====
23+
[source, java, linenums]
24+
[subs="verbatim,attributes"]
25+
----
26+
import java.lang.String;
27+
import java.util.Locale;
28+
import javax.annotation.Generated;
29+
import org.springframework.context.annotation.Lazy;
30+
import org.springframework.core.convert.ConversionService;
31+
import org.springframework.stereotype.Component;
32+
33+
@Generated(
34+
value = "org.mapstruct.extensions.spring.converter.ConversionServiceAdapterGenerator",
35+
date = "2021-06-25T18:51:21.585Z"
36+
)
37+
@Component
38+
public class ConversionServiceAdapter {
39+
private final ConversionService conversionService;
40+
41+
public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
42+
this.conversionService = conversionService;
43+
}
44+
45+
public Locale mapStringToLocale(final String source) {
46+
return (Locale) conversionService.convert(source, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Locale.class));
47+
}
48+
}
49+
----
50+
====
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[[testExtensions]]
2+
== Test Extensions
3+
4+
[[converterScan]]
5+
=== ConverterScan
6+
7+
In a production environment, Spring will take care of initializing the `ConversionService` and registering all generated Mappers inside it.
8+
However, in integration tests, the developer will typically have to take care of all that themselves.
9+
In order to simplify this task, the `test-extensions` module provides a `@ConverterScan` annotation which can be used much like Spring's own `@ComponentScan`.
10+
It will perform the same scanning task and also provide a `ConversionService` with all found Mappers already registered inside.
11+
This is sufficient for most tests.
12+
In its simplest form, the annotation can be used like this:
13+
14+
====
15+
[source,java,linenums]
16+
[subs="verbatim,attributes"]
17+
----
18+
@ExtendWith(SpringExtension.class)
19+
class ConversionServiceAdapterIntegrationTest {
20+
@Configuration
21+
@ConverterScan(basePackageClasses = MapperSpringConfig.class)
22+
static class ScanConfiguration {}
23+
24+
@Autowired
25+
private ConversionService conversionService;
26+
27+
[...]
28+
}
29+
----
30+
====

docs/src/docs/asciidoc/mapstruct-spring-extensions-reference-guide.asciidoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ include::chapter-1-introduction.asciidoc[]
2323
include::chapter-2-set-up.asciidoc[]
2424

2525
include::chapter-3-mapper-as-converter.asciidoc[]
26+
27+
include::chapter-4-external-conversions.asciidoc[]
28+
29+
include::chapter-5-test-extensions.asciidoc[]

examples/classname/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ dependencies {
33
implementation projects.examples.model
44
implementation projects.annotations
55

6+
testImplementation projects.testExtensions
67
testImplementation libs.assertj
78
testImplementation libs.bundles.junit.jupiter
89
implementation libs.jsr250

0 commit comments

Comments
 (0)