Skip to content

Commit

Permalink
87 provide a conversionservice bean if missing (#91)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
Chessray authored Aug 28, 2023
1 parent 18a43e3 commit 12b1fb5
Show file tree
Hide file tree
Showing 67 changed files with 2,292 additions and 515 deletions.
5 changes: 0 additions & 5 deletions annotations/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ dependencies {
implementation libs.spring.core
}

java {
withJavadocJar()
withSourcesJar()
}

publishing {
publications {
mavenJava(MavenPublication) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@
*/
boolean lazyAnnotatedConversionServiceBean() default true;

/**
* Indicates whether to generate a {@code ConverterScan} when using {@link
* #conversionServiceBeanName()}. Has no effect when {@link #conversionServiceBeanName()} is not
* set.
*
* @return {@code true} - Generate {@code ConverterScan}, {@code false} - otherwise
*/
boolean generateConverterScan() default false;

/**
* Additional {@link ExternalConversion conversions} which should be made available through the
* generated Adapter.
Expand Down
5 changes: 4 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,15 @@ subprojects {
apply plugin: "jacoco"

group = 'org.mapstruct.extensions.spring'
version = '1.0.3-SNAPSHOT'
version = '1.1.0-SNAPSHOT'

java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(11))
}

withJavadocJar()
withSourcesJar()
}

testing {
Expand Down
97 changes: 47 additions & 50 deletions docs/src/docs/asciidoc/chapter-3-mapper-as-converter.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
== Mappers as Converters

MapStruct Mappers nicely match Spring's https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#core-convert-Converter-API[Converter] idea:

====
[source, java, linenums]
[source,java,linenums]
[subs="verbatim,attributes"]
----
@Mapper(componentModel = "spring")
Expand All @@ -17,7 +18,7 @@ public interface CarMapper extends Converter<Car, CarDto> {
This allows using the Mapper indirectly via the `ConversionService`:

====
[source, java, linenums]
[source,java,linenums]
[subs="verbatim,attributes"]
----
...
Expand All @@ -29,12 +30,17 @@ This allows using the Mapper indirectly via the `ConversionService`:
----
====

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.
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.

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:
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:

====
[source, java, linenums]
[source,java,linenums]
[subs="verbatim,attributes"]
----
@Mapper
Expand All @@ -49,7 +55,7 @@ public interface SeatConfigurationMapper extends Converter<SeatConfiguration, Se
The generated Adapter class will look like this:

====
[source, java, linenums]
[source,java,linenums]
[subs="verbatim,attributes"]
----
@Component
Expand All @@ -73,8 +79,9 @@ public class ConversionServiceAdapter {
====

Since this class' methods match the signature that MapStruct expects, we can now add it to the CarMapper:

====
[source, java, linenums]
[source,java,linenums]
[subs="verbatim,attributes"]
----
@Mapper(uses = ConversionServiceAdapter.class)
Expand All @@ -87,9 +94,14 @@ public interface CarMapper extends Converter<Car, CarDto> {

[[mappersAsConvertersCustomNames]]
=== 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 https://mapstruct.org/documentation/stable/reference/html/#shared-configurations[shared configuration] if you use this:

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:

====
[source, java, linenums]
[source,java,linenums]
[subs="verbatim,attributes"]
----
import org.mapstruct.MapperConfig;
Expand All @@ -104,11 +116,15 @@ 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.
====

[[customConversionService]]
=== 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.

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.

====
[source, java, linenums]
[source,java,linenums]
[subs="verbatim,attributes"]
----
import org.mapstruct.MapperConfig;
Expand All @@ -120,53 +136,34 @@ public interface MapperSpringConfig {
}
----
====
[[externalConversions]]
=== External Conversions
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`:
====
[source, java, linenums]
[subs="verbatim,attributes"]
----
import org.mapstruct.MapperConfig;
import org.mapstruct.extensions.spring.ExternalConversion;
import org.mapstruct.extensions.spring.SpringMapperConfig;

import java.util.Locale;
[[generateConverterScan]]
==== Modified ConverterScan

@MapperConfig(componentModel = "spring")
@SpringMapperConfig(
externalConversions = @ExternalConversion(sourceType = String.class, targetType = Locale.class))
public interface MapstructConfig {}
----
====
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:

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:
- Unlike its <<testExtensions>> 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.:
====
[source, java, linenums]
[source,java,linenums]
[subs="verbatim,attributes"]
----
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;
@ExtendWith(SpringExtension.class)
public class ConversionServiceAdapterIntegrationTest {
@Configuration
@ConverterScan
static class AdditionalBeanConfiguration {
@Bean
ConfigurableConversionService myConversionService() {
return new DefaultConversionService();
}
}
public Locale mapStringToLocale(final String source) {
return (Locale) conversionService.convert(source, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Locale.class));
}
@Autowired
@Qualifier("myConversionService")
private ConversionService conversionService;
}
----
====
50 changes: 50 additions & 0 deletions docs/src/docs/asciidoc/chapter-4-external-conversions.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[[externalConversions]]
== External Conversions
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`:
====
[source, java, linenums]
[subs="verbatim,attributes"]
----
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:
====
[source, java, linenums]
[subs="verbatim,attributes"]
----
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));
}
}
----
====
30 changes: 30 additions & 0 deletions docs/src/docs/asciidoc/chapter-5-test-extensions.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[[testExtensions]]
== Test Extensions

[[converterScan]]
=== 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:

====
[source,java,linenums]
[subs="verbatim,attributes"]
----
@ExtendWith(SpringExtension.class)
class ConversionServiceAdapterIntegrationTest {
@Configuration
@ConverterScan(basePackageClasses = MapperSpringConfig.class)
static class ScanConfiguration {}
@Autowired
private ConversionService conversionService;
[...]
}
----
====
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@ include::chapter-1-introduction.asciidoc[]
include::chapter-2-set-up.asciidoc[]

include::chapter-3-mapper-as-converter.asciidoc[]

include::chapter-4-external-conversions.asciidoc[]

include::chapter-5-test-extensions.asciidoc[]
1 change: 1 addition & 0 deletions examples/classname/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ dependencies {
implementation projects.examples.model
implementation projects.annotations

testImplementation projects.testExtensions
testImplementation libs.assertj
testImplementation libs.bundles.junit.jupiter
implementation libs.jsr250
Expand Down
Loading

0 comments on commit 12b1fb5

Please sign in to comment.