mapstruct / mapstruct-spring-extensions Goto Github PK
View Code? Open in Web Editor NEWHelpful additions to MapStruct when using the Spring Framework.
License: Apache License 2.0
Helpful additions to MapStruct when using the Spring Framework.
License: Apache License 2.0
Adapter method will not be generated for below converter even though `mapstruct` able generate the implementation class.
@Mapper(config = MapperConfiguration.class)
interface MyConverter extends Converter<Foo, FooDto> {
}
To be able to generate the method for adapter class, have to declare convert
method again as below.
@Mapper(config = MapperConfiguration.class)
interface MyConverter extends Converter<Foo, FooDto> {
FooDto convert(Foo source);
}
Originally posted by @myatmin in #53 (comment)
Hi guys, i'm trying to create a composed mapper that throws almost the same problem than the #21. Heres is my mappers:
@Mapper(config = MapStructConfiguration.class)
public interface PaymentTypeToPaymentTypeRecord extends Converter<PaymentType, PaymentTypeRecord> {
@Override
PaymentTypeRecord convert(final PaymentType paymentType);
}
and the second:
@Mapper(config = MapStructConfiguration.class)
public interface PaymentSubtypeToPaymentSubtypeRecord extends Converter<PaymentSubtype, PaymentSubtypeRecord> {
@Override
@Mapping(source = "paymentType", target = "paymentType")
PaymentSubtypeRecord convert(final PaymentSubtype paymentSubtype);
}
and the generated code:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2021-07-29T00:13:30-0300",
comments = "version: 1.4.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.0.2.jar, environment: Java 16.0.1 (Oracle Corporation)"
)
@Component
public class PaymentSubtypeToPaymentSubtypeRecordImpl implements PaymentSubtypeToPaymentSubtypeRecord {
@Autowired
private ConversionServiceAdapter conversionServiceAdapter;
@Override
public PaymentSubtypeRecord convert(PaymentSubtype paymentSubtype) {
if ( paymentSubtype == null ) {
return null;
}
PaymentTypeRecord paymentType = null;
Integer id = null;
String name = null;
String icon = null;
paymentType = conversionServiceAdapter.mapPaymentTypeToPaymentTypeRecord( paymentSubtype.getPaymentType() );
id = paymentSubtype.getId();
name = paymentSubtype.getName();
icon = paymentSubtype.getIcon();
PaymentSubtypeRecord paymentSubtypeRecord = new PaymentSubtypeRecord( id, name, icon, paymentType );
return paymentSubtypeRecord;
}
}
and the exception:
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2021-07-29 00:21:37.357 ERROR 1293694 --- [ restartedMain] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
Field conversionServiceAdapter in mapper.PaymentSubtypeToPaymentSubtypeRecordImpl required a bean of type 'org.mapstruct.extensions.spring.converter.ConversionServiceAdapter' that could not be found.
can you guys help me with that? i appreciate every kind of help. thanks
The Generated
annotation can be in different packages, depending on Java versions. We should cater for this in a fashion similar to the MapStruct core.
Do you have any plan to support multiple conversion methods in the same mapper?
after maven package,the jar not include any class file
FIrst attempt: Update dependencies - there is a new major version of the OWASP plugin.
Good extension, however according to https://github.com/mapstruct/mapstruct-spring-extensions/blob/main/gradle/libs.versions.toml
this is build for SpingBoot 2.7 and should support SpringBoot 3 to avoid a lot of CVE-complaints and to support the latest
released version of SpringBoot
I am trying to achieve the following but not sure if this is what this extensions is meant for. I am a bit confused.
I will like to inject a custom service in the CarMapper interface extending the Spring Converter.
It seems injection is not possible with CarMapper interface but only with abstract classes.
Is it possible to achieve this while keeping the integration with the Spring ConversionService?
@Mapper(componentModel = "spring")
public interface CarMapper extends Converter<Car, CarDto> {
@Autowired
private MyCustomService myCustomService;//This is not working
@Mapping(target = "title", expression = "java(getLocaleTitle(car)))
CarDto convert(Car car);
default String getLocaleTitle(Car car) {
return myCustomService.translate(car.getTitle());
}
}
Usage:
@Autowired
private ConversionService conversionService;
...
Car car = ...;
CarDto carDto = conversionService.convert(car, CarDto.class);
Hi,
I am eager to move my mappers into the Conversion Service and have found an issue. it is related to an issue I have raised with the mapstruct project (mapstruct/mapstruct#2352).
I get the following exception
Caused by: java.lang.ClassCastException: com.squareup.javapoet.ParameterizedTypeName cannot be cast to com.squareup.javapoet.ClassName
at org.mapstruct.extensions.spring.converter.ConverterMapperProcessor.toFromToMapping (ConverterMapperProcessor.java:100)
at java.util.stream.ReferencePipeline$3$1.accept (ReferencePipeline.java:193)
at java.util.stream.ReferencePipeline$3$1.accept (ReferencePipeline.java:193)
at java.util.stream.ReferencePipeline$2$1.accept (ReferencePipeline.java:175)
at java.util.stream.ReferencePipeline$3$1.accept (ReferencePipeline.java:193)
at java.util.stream.ReferencePipeline$2$1.accept (ReferencePipeline.java:175)
at java.util.stream.ReferencePipeline$2$1.accept (ReferencePipeline.java:175)
at java.util.Iterator.forEachRemaining (Iterator.java:116)
at java.util.Spliterators$IteratorSpliterator.forEachRemaining (Spliterators.java:1801)
at java.util.stream.AbstractPipeline.copyInto (AbstractPipeline.java:482)
at java.util.stream.AbstractPipeline.wrapAndCopyInto (AbstractPipeline.java:472)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential (ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate (AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.collect (ReferencePipeline.java:499)
at org.mapstruct.extensions.spring.converter.ConverterMapperProcessor.processMapperAnnotation (ConverterMapperProcessor.java:87)
at org.mapstruct.extensions.spring.converter.ConverterMapperProcessor.lambda$process$1 (ConverterMapperProcessor.java:70)
at java.util.stream.ForEachOps$ForEachOp$OfRef.accept (ForEachOps.java:184)
at java.util.stream.ReferencePipeline$2$1.accept (ReferencePipeline.java:175)
at java.util.Iterator.forEachRemaining (Iterator.java:116)
at java.util.Spliterators$IteratorSpliterator.forEachRemaining (Spliterators.java:1801)
at java.util.stream.AbstractPipeline.copyInto (AbstractPipeline.java:482)
at java.util.stream.AbstractPipeline.wrapAndCopyInto (AbstractPipeline.java:472)
at java.util.stream.ForEachOps$ForEachOp.evaluateSequential (ForEachOps.java:151)
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential (ForEachOps.java:174)
at java.util.stream.AbstractPipeline.evaluate (AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.forEach (ReferencePipeline.java:418)
at org.mapstruct.extensions.spring.converter.ConverterMapperProcessor.process (ConverterMapperProcessor.java:68)
at com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor (JavacProcessingEnvironment.java:794)
The set up I have is the following...
@Value
public class TheDto {
@NonNull
String id;
}
@Value
public class TheModel {
@NonNull
private final String id;
}
public class TheModels extends ArrayList<TheModel> {
private static final long serialVersionUID = 1L;
}
@Mapper(uses = TheModelMapper.class, imports = TheModel.class)
@SpringMapperConfig
public interface TheModelsMapper extends Converter<TheModels, List<TheDto>> {
@Override
List<TheDto> convert(TheModels theModels);
}
@Mapper
public interface TheModelMapper {
TheDto convert(TheModel theModel);
}
The issue is the class extending the ArrayList. My other modules have all worked without issue (though interaction with Eclipse (STS) is not quite there yet - tends to build the default ConversionAdapter alot instead of the renamed one. Have to clean and rebuild all for it to be correct - But I can live with that :) )
In my current project I've started to use mapstruts together with mapstruct-spring-extension and when I've started to save jpa objects generated by convertes I've realized that dependent object in relations like one-to-many are not saved. Reason for that is that child object doesn't contain reference to it's parent. Solution for this issue is provided by mapstructs and described in one of examples. I've struggled to achieve same behavior in together with spring-extension, but without success.
Mappers interfaces needs to extend Converter interface from spring, where there is one argument method convert and in order to achieve effect from https://github.com/mapstruct/mapstruct-examples/tree/main/mapstruct-jpa-child-parent somehow we need to pass @context param. Is there any way of solving this problem ?
When an application has multiple conversion service beans, there needs to be a way to specify the name of the ConversionService bean.
Already looked through existing issues but couldn't find anything. (Although this helped me with some other common issues like componentmodel=spring, central config, Injecting the Interface instead of the Adapter, ...)
I created an MVP to show an issue I had in one of my applications.
Given a Mapper like this:
@Mapper(config = MapperSpringConfig.class)
public abstract class ExampleMapper implements Converter<ExampleDTO, Example> {
/*
this breaks
*/
@Autowired
ExampleService exampleService;
/*
this works
*/
// @Autowired
// ExampleServiceWithoutConversionService exampleServiceWithoutConversionService;
@Override
public abstract Example convert(@Nullable ExampleDTO exampleDTO);
}
With Services:
// ExampleService.java
@Service
public class ExampleService {
@Autowired
ConversionService conversionService;
// methods that do something ...
}
// ExampleServiceWithoutConversionService.java
@Service
public class ExampleServiceWithoutConversionService {
// methods that do something ...
}
Central Config:
// MapperSpringConfig.java
@MapperConfig(componentModel = "spring", uses = MyAdapter.class)
@SpringMapperConfig(
conversionServiceAdapterPackage ="com.example.springmapstructissue",
conversionServiceAdapterClassName ="MyAdapter"
)
public interface MapperSpringConfig {
}
When running the application like this it results in the following error:
Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [com.example.springmapstructissue.ExampleDTO] to type [com.example.springmapstructissue.Example]
at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:322) ~[spring-core-6.0.11.jar:6.0.11]
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:195) ~[spring-core-6.0.11.jar:6.0.11]
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:175) ~[spring-core-6.0.11.jar:6.0.11]
at com.example.springmapstructissue.SpringMapstructIssueApplication.lambda$commandLineRunner$0(SpringMapstructIssueApplication.java:23) ~[classes/:na]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:771) ~[spring-boot-3.1.3.jar:3.1.3]
... 5 common frames omitted
But when changing to the other Mapper (ExampleServiceWithoutConversionService.java in this example) then everything works. So something gets broken when injecting a Service into a Mapper that uses ConversionService.
(PS: In the real application this Service also injects a lot of other things like jpa repositories and does more, but not relevant for this problem as this does not result in the issue mentioned - so I also don't think its a Spring Context problem because injecting Repositories and other Services works but something seems special about ConversionService)
Full source of this example available on my Github: https://github.com/SimonFischer04/spring-mapstruct-issue
@MapperConfig(componentModel = "spring")
@SpringMapperConfig(
externalConversions = @ExternalConversion(sourceType = String.class, targetType = Locale.class))
public interface MapstructConfig {}
!ENTRY org.eclipse.jdt.apt.pluggable.core 4 1 2022-08-30 02:26:34.062
!MESSAGE Exception thrown by Java annotation processor org.mapstruct.extensions.spring.converter.ConverterMapperProcessor@2c971632
!STACK 0
java.lang.Exception: java.lang.ClassCastException: class org.eclipse.jdt.internal.compiler.apt.model.AnnotationValueImpl cannot be cast to class javax.lang.model.element.AnnotationMirror (org.eclipse.jdt.internal.compiler.apt.model.AnnotationValueImpl is in unnamed module of loader org.eclipse.osgi.internal.loader.EquinoxClassLoader @61817efb; javax.lang.model.element.AnnotationMirror is in module java.compiler of loader 'platform')
at org.eclipse.jdt.internal.compiler.apt.dispatch.RoundDispatcher.handleProcessor(RoundDispatcher.java:172)
at org.eclipse.jdt.internal.compiler.apt.dispatch.RoundDispatcher.round(RoundDispatcher.java:124)
at org.eclipse.jdt.internal.compiler.apt.dispatch.BaseAnnotationProcessorManager.processAnnotations(BaseAnnotationProcessorManager.java:172)
at org.eclipse.jdt.internal.apt.pluggable.core.dispatch.IdeAnnotationProcessorManager.processAnnotations(IdeAnnotationProcessorManager.java:138)
at org.eclipse.jdt.internal.compiler.Compiler.processAnnotations(Compiler.java:953)
at org.eclipse.jdt.internal.compiler.Compiler.compile(Compiler.java:450)
at org.eclipse.jdt.internal.compiler.Compiler.compile(Compiler.java:426)
at org.eclipse.jdt.internal.core.builder.AbstractImageBuilder.compile(AbstractImageBuilder.java:379)
at org.eclipse.jdt.internal.core.builder.IncrementalImageBuilder.compile(IncrementalImageBuilder.java:371)
at org.eclipse.jdt.internal.core.builder.AbstractImageBuilder.compile(AbstractImageBuilder.java:311)
at org.eclipse.jdt.internal.core.builder.IncrementalImageBuilder.incrementalBuildLoop(IncrementalImageBuilder.java:190)
at org.eclipse.jdt.internal.core.builder.IncrementalImageBuilder.build(IncrementalImageBuilder.java:147)
at org.eclipse.jdt.internal.core.builder.JavaBuilder.buildDeltas(JavaBuilder.java:290)
at org.eclipse.jdt.internal.core.builder.JavaBuilder.build(JavaBuilder.java:213)
at org.eclipse.core.internal.events.BuildManager$2.run(BuildManager.java:1024)
at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:45)
at org.eclipse.core.internal.events.BuildManager.basicBuild(BuildManager.java:254)
at org.eclipse.core.internal.events.BuildManager.basicBuild(BuildManager.java:311)
at org.eclipse.core.internal.events.BuildManager$1.run(BuildManager.java:400)
at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:45)
at org.eclipse.core.internal.events.BuildManager.basicBuild(BuildManager.java:403)
at org.eclipse.core.internal.events.BuildManager.basicBuildLoop(BuildManager.java:514)
at org.eclipse.core.internal.events.BuildManager.basicBuildLoop(BuildManager.java:462)
at org.eclipse.core.internal.events.BuildManager.build(BuildManager.java:544)
at org.eclipse.core.internal.events.AutoBuildJob.doBuild(AutoBuildJob.java:161)
at org.eclipse.core.internal.events.AutoBuildJob.run(AutoBuildJob.java:255)
at org.eclipse.core.internal.jobs.Worker.run(Worker.java:63)
Caused by: java.lang.ClassCastException: class org.eclipse.jdt.internal.compiler.apt.model.AnnotationValueImpl cannot be cast to class javax.lang.model.element.AnnotationMirror (org.eclipse.jdt.internal.compiler.apt.model.AnnotationValueImpl is in unnamed module of loader org.eclipse.osgi.internal.loader.EquinoxClassLoader @61817efb; javax.lang.model.element.AnnotationMirror is in module java.compiler of loader 'platform')
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
at org.mapstruct.extensions.spring.converter.ConverterMapperProcessor.toSourceTargetTypeNamePairs(ConverterMapperProcessor.java:104)
at java.base/java.util.Optional.map(Optional.java:260)
at org.mapstruct.extensions.spring.converter.ConverterMapperProcessor.getExternalConversionMappings(ConverterMapperProcessor.java:95)
at org.mapstruct.extensions.spring.converter.ConverterMapperProcessor.buildDescriptor(ConverterMapperProcessor.java:77)
at org.mapstruct.extensions.spring.converter.ConverterMapperProcessor.process(ConverterMapperProcessor.java:63)
at org.eclipse.jdt.internal.compiler.apt.dispatch.RoundDispatcher.handleProcessor(RoundDispatcher.java:142)
... 26 more
Hi, I started using your development in my application. Faced the following problem.
If the generated mapper uses 'ConversionServiceAdapter' as a dependency then spring not add the mapper to the ConversionService. There is an implicit circular dependency between mapper and 'ConversionServiceAdapter' due to ConversionService. It prevents all converters from being added to the ConversionService.
For example, the generated class might be like this:
`@Component
public class TestConverter implements Converter<S, T> {
private ConversionServiceAdapter serviceAdapter;
public TestConverter(@Lazy ConversionServiceAdapter serviceAdapter) {
this.serviceAdapter = serviceAdapter;
}
@Override
public T convert(S source) {
return T;
}
}`
All mappers are created without race for ConversionServiceAdapter, all mappers are added to ConversionService. Otherwise, the mapper that is using the ConversionServiceAdapter will not be added as a converter.
ConverterNotFoundException will be thrown.
Applicable for version 0.0.2.
P.S. If I can help, I can do a PR. I would like to get a fix faster
Instead of
@Autowired
private ConversionService conversionService;
we prefer
private final ConversionService conversionService;
public ConversionServiceAdapter(final ConversionService conversionService) {
this.conversionService = conversionService;
}
Hi,
Minor issue...
When I define my Conversion Service to be a different name the @generated annotation has the default value.
@SpringMapperConfig(conversionServiceAdapterClassName ="AvaloqApiMapper")
public @interface AvaloqApiMapperConfig { }
and the generated is
@Generated(
value = "org.mapstruct.extensions.spring.converter.ConversionServiceAdapterGenerator",
date = "2021-02-24T11:03:43.603282300Z"
)
@Component
public class AvaloqApiMapper {
...
}
cheers,
Andrew
I created a project named mapstruct-spring-plus
to simplify the use of mapstruct. In this project, @AutoMap
and @AutoMapField
annotations are used to automatically generate Mapper interface files. If I want to merge the code into mapstruct.org
,what should I do? Project address: https://github.com/ZhaoRd/mapstruct-spring-plus.
Use code
@Data
@AutoMap(targetType = Car.class)
public class CarDto {
private String make;
@AutoMapField(target = "carType")
private String type;
}
@Data
public class Car {
private String make;
private String carType;
}
@ExtendWith(SpringExtension.class)
@ContextConfiguration(
classes = {AutoMapTests.AutoMapTestConfiguration.class})
public class AutoMapTests {
@Autowired
private IObjectMapper mapper;
@Test
public void testDtoToEntity() {
var dto = new CarDto();
dto.setMake("M1");
dto.setType("OTHER");
Car entity = mapper.map(dto, Car.class);
assertThat(entity).isNotNull();
assertThat(entity.getMake()).isEqualTo("M1");
assertThat(entity.getCarType()).isEqualTo("OTHER");
}
@ComponentScan("io.github.zhaord.mapstruct.plus")
@Configuration
@Component
static class AutoMapTestConfiguration {
}
}
Something of the form e.g. public interface WheelsMapper extends Converter<Wheels, List<WheelsDto>>
or public interface WheelsListMapper extends Converter<List<WheelsDto>, Wheels>
.
Related to #22
We're trying to integrate mapstruct-spring-extensions into a project and hit a little bit of a roadblock.
I was under the impression that, when defining a mapper, extending Converter
and adding a @Mapper
annotation would be enough for ConversionService
to pick this mapper up automatically. However, this isn't happening in my Spring Boot project with Spring Boot Starter Web.
This is how I defined a Mapper:
@Mapper
interface FooMapper : Converter<Foo, FooDto> {
override fun convert(source: Foo): FooDto
}
I've built an MCVE so that you can verify my test setup.
FooMapperImpl
is being generated as expected and so is ConversionServiceAdapter
, and yet, ConversionService
fails to convert from Foo
to FooDto
.
Am I missing something here? Is there some configuration I need to do after all? I feel like I exhausted the available documentation on this issue. Thanks in advance!
edit: It doesn't seem to be a problem with Spring. I've just tested it, and Converters that aren't generated are detected automatically.
Hi.
I'm trying to define a mapper convert a Blob to byte array, but the compilation of the project fails with the following error:
class com.squareup.javapoet.ArrayTypeName cannot be cast to class com.squareup.javapoet.ClassName (com.squareup.javapoet.ArrayTypeName and com.squareup.javapoet.ClassName are in unnamed module of loader java.net.URLClassLoader @712cfb63)
Just for reference, the implementation of the mapper is the following:
import org.mapstruct.MapperConfig;
import org.mapstruct.extensions.spring.converter.ConversionServiceAdapter;
/**
* @author Cosimo Damiano Prete
* @since 03/02/2022
*/
@MapperConfig(componentModel = "spring", uses = ConversionServiceAdapter.class)
interface SpringMapperConfig {}
import org.mapstruct.Mapper;
import org.springframework.core.convert.converter.Converter;
import javax.validation.constraints.NotNull;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.sql.Blob;
import java.sql.SQLException;
/**
* @author Cosimo Damiano Prete
* @since 05/02/2022
*/
@Mapper(uses = SpringMapperConfig.class)
interface BlobToByteArrayMapper extends Converter<Blob, byte[]> {
@Override
default byte[] convert(@NotNull Blob blob) {
try (BufferedInputStream bis = new BufferedInputStream(blob.getBinaryStream());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(baos)) {
byte[] buffer = new byte[4096];
boolean keepReading;
do {
int bytesRead = bis.read(buffer);
keepReading = bytesRead != -1;
if(keepReading) {
bos.write(buffer, 0, bytesRead);
}
} while (keepReading);
return baos.toByteArray();
} catch (SQLException | IOException e) {
throw new RuntimeException(e);
}
}
}
Maven compiler setup:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.compiler.release>${java.version}</maven.compiler.release>
<maven.compiler.parameters>true</maven.compiler.parameters>
</properties>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final</version>
</path>
<path>
<groupId>org.mapstruct.extensions.spring</groupId>
<artifactId>mapstruct-spring-extensions</artifactId>
<version>0.1.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
Problem:
Setting up a valid Spring context is quite difficult in tests. Most often the ConversionService provided via WebMvcAutoConfiguration
is being used. When it is not present, for example when testing service & persistence layer interaction, can get quite ugly to get all plumbing done. Something we've had to do:
@SpringBootTest({WebMvcAutoConfiguration.class, Test.Config.class})
class Test {
@Configuration
@ComponentScan(basePackages = {"com.example.mappers", "com.example.conversionserviceadapterpackage", "com.example.etc"})
static class Config {
}
Without this, instantiating Spring context fails in ConversionServiceAdapter
due to missing ConversionService
bean.
Duplicating this to several test cases gets old quite quick. It is hard to make it portable/re-usable due to how different test context configuration options work: for example, setup with @DataJpaTest looks quite different.
Suggestion:
If detecting that ConversionService
bean is missing AND ConversionServiceAdapter
base package gets component scanned, should provide a fallback bean with available Converters (at least the ones detected as MapStruct Mappers).
Can be opt-in via a property in SpringMapperConfig
or a separate annotation to be placed on test classes.
OR
Provide an example reference documentation on how to conveniently set up a valid Spring context for the ConversionServiceAdapter
when Spring web/reactive-web is not present.
Configure maven-publish, javadoc and signing.
When Integrating with @Context
in @AfterMapping
or @BeforeMapping
is not working
Below Example is working as expected,
@Mapper(
config = MapperSpringConfig.class,
builder = @Builder(disableBuilder = true),
uses = EntityManager.class)
public interface NewPostRequestToPostEntityMapper extends Converter<NewPostRequest, PostEntity> {
@Mapping(target = "tags", ignore = true)
PostEntity convert(NewPostRequest newPostRequest);
@AfterMapping
default void afterMapping(NewPostRequest newPostRequest, @MappingTarget PostEntity postEntity) {
newPostRequest
.tags()
.forEach(tagsRequest -> postEntity.addTag(getTagEntity(null, tagsRequest)));
}
}
But the moment I add @Context EntityManager entityManager
as third parameter afterMapping method is not generating. I expect it to be working like normal Mapstruct.
@Mapper(
config = MapperSpringConfig.class,
builder = @Builder(disableBuilder = true),
uses = EntityManager.class)
public interface NewPostRequestToPostEntityMapper extends Converter<NewPostRequest, PostEntity> {
@Mapping(target = "tags", ignore = true)
PostEntity convert(NewPostRequest newPostRequest);
@AfterMapping
default void afterMapping(
NewPostRequest newPostRequest,
@MappingTarget PostEntity postEntity,
@Context EntityManager entityManager) {
newPostRequest
.tags()
.forEach(
tagsRequest -> postEntity.addTag(getTagEntity(entityManager, tagsRequest)));
}
}
Sample code can be found here to reproduce the issue
My project is using the Spring Extension 0.1.0 and my project is constantly rebuilding.
In the gradle log :
Full recompilation is required because org.mapstruct.extensions.spring.converter.ConverterMapperProcessor is not incremental. Analysis took 0.304 secs.
Is it possible to have ConverterMapperProcessor
incremental ?
Hi there,
Unable to use test-extensions within my org as it has dependency on vulnerable version of spring-beans CVE-2022-22965. Is there any plan to upgrade in the near future?
Many thanks! :)
environment
Java and Maven information
C:Usersqolom>java -version
java version "20.0.1" 2023-04-18
Java(TM) SE Runtime Environment (build 20.0.1+9-29)
Java HotSpot(TM) 64-Bit Server VM (build 20.0.1+9-29, mixed mode, sharing)
C:Usersqolom>mvn -v
Apache Maven 3.9.2 (c9616018c7a021c1c39be70fb2843d6f5f9b8a1c)
Maven home: D:Serverapache-maven-3.9.2
Java version: 20.0.1, vendor: Oracle Corporation, runtime: C:Program FilesJavajdk-20
Default locale: zh_CN, platform encoding: UTF-8
OS name: "windows 11", version: "10.0", arch: "amd64", family: "windows"
pom information
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-parent -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/>
<!-- lookup parent from repository -->
</parent>
<properties>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<lombok.version>1.18.28</lombok.version>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok-mapstruct-binding -->
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
<mapstruct.version>1.5.5.Final</mapstruct.version>
<!-- https://mvnrepository.com/artifact/org.mapstruct.extensions.spring/mapstruct-spring-annotations -->
<mapstruct-spring-annotations.version>1.0.1</mapstruct-spring-annotations.version>
<!-- https://mvnrepository.com/artifact/org.mapstruct.extensions.spring/mapstruct-spring-extensions -->
<mapstruct-spring-extensions.version>1.0.1</mapstruct-spring-extensions.version>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-dependencies -->
<spring-cloud-dependencies.version>2022.0.3</spring-cloud-dependencies.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct.extensions.spring</groupId>
<artifactId>mapstruct-spring-extensions</artifactId>
<version>${mapstruct-spring-extensions.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct.extensions.spring</groupId>
<artifactId>mapstruct-spring-annotations</artifactId>
<version>${mapstruct-spring-annotations.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-all</artifactId>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-common -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-common</artifactId>
</dependency>
</dependencies>
</project>
Related logs
"C:\Program Files\Java\jdk-20\bin\java.exe" -Dvisualvm.id=8147785106100 -Dmaven.multiModuleProjectDirectory=D:\workspace\cloud-workspace\cloud-common -Djansi.passthrough=true -Xms512m -Xmx1024m -Dfile.encoding=UTF-8 -Dmaven.home=D:\Server\apache-maven-3.9.2 -Dclassworlds.conf=D:\Server\apache-maven-3.9.2\bin\m2.conf "-Dmaven.ext.class.path=D:\Software\IntelliJ IDEA 2023.1.2\plugins\maven\lib\maven-event-listener.jar" "-javaagent:D:\Software\IntelliJ IDEA 2023.1.2\lib\idea_rt.jar=58531:D:\Software\IntelliJ IDEA 2023.1.2\bin" -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath D:\Server\apache-maven-3.9.2\boot\plexus-classworlds-2.7.0.jar;D:\Server\apache-maven-3.9.2\boot\plexus-classworlds.license org.codehaus.classworlds.Launcher -Didea.version=2023.1.2 --update-snapshots -s D:\Server\apache-maven-3.9.2\conf\settings.xml clean package install
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] cloud-common [pom]
[INFO] cloud-common-client [jar]
[INFO] cloud-common-server [jar]
[INFO]
[INFO] -------------------< com.qolome.cloud:cloud-common >--------------------
[INFO] Building cloud-common 2.1.3-jre20 [1/3]
[INFO] from pom.xml
[INFO] --------------------------------[ pom ]---------------------------------
[INFO]
[INFO] --- clean:3.2.0:clean (default-clean) @ cloud-common ---
[INFO]
[INFO] >>> source:3.2.1:jar (default) > generate-sources @ cloud-common >>>
[INFO]
[INFO] <<< source:3.2.1:jar (default) < generate-sources @ cloud-common <<<
[INFO]
[INFO]
[INFO] --- source:3.2.1:jar (default) @ cloud-common ---
[INFO]
[INFO] --- javadoc:3.5.0:jar (attach-javadocs) @ cloud-common ---
[INFO] Not executing Javadoc as the project is not a Java classpath-capable package
[INFO]
[INFO] >>> source:3.2.1:jar (default) > generate-sources @ cloud-common >>>
[INFO]
[INFO] <<< source:3.2.1:jar (default) < generate-sources @ cloud-common <<<
[INFO]
[INFO]
[INFO] --- source:3.2.1:jar (default) @ cloud-common ---
[INFO]
[INFO] --- javadoc:3.5.0:jar (attach-javadocs) @ cloud-common ---
[INFO] Not executing Javadoc as the project is not a Java classpath-capable package
[INFO]
[INFO] --- install:3.1.1:install (default-install) @ cloud-common ---
[INFO] Installing D:\workspace\cloud-workspace\cloud-common\pom.xml to D:\Server\Repository\com\qolome\cloud\cloud-common\2.1.3-jre20\cloud-common-2.1.3-jre20.pom
[INFO]
[INFO] ----------------< com.qolome.cloud:cloud-common-client >----------------
[INFO] Building cloud-common-client 2.1.3-jre20 [2/3]
[INFO] from cloud-common-client\pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- clean:3.2.0:clean (default-clean) @ cloud-common-client ---
[INFO] Deleting D:\workspace\cloud-workspace\cloud-common\cloud-common-client\target
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ cloud-common-client ---
[INFO] skip non existing resourceDirectory D:\workspace\cloud-workspace\cloud-common\cloud-common-client\src\main\resources
[INFO] skip non existing resourceDirectory D:\workspace\cloud-workspace\cloud-common\cloud-common-client\src\main\resources
[INFO]
[INFO] --- compiler:3.11.0:compile (default-compile) @ cloud-common-client ---
[INFO] Changes detected - recompiling the module! :source
[INFO] Compiling 25 source files with javac [debug release 20] to target\classes
[INFO]
[INFO] >>> source:3.2.1:jar (default) > generate-sources @ cloud-common-client >>>
[INFO]
[INFO] <<< source:3.2.1:jar (default) < generate-sources @ cloud-common-client <<<
[INFO]
[INFO]
[INFO] --- source:3.2.1:jar (default) @ cloud-common-client ---
[INFO] Building jar: D:\workspace\cloud-workspace\cloud-common\cloud-common-client\target\cloud-common-client-2.1.3-jre20-sources.jar
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ cloud-common-client ---
[INFO] skip non existing resourceDirectory D:\workspace\cloud-workspace\cloud-common\cloud-common-client\src\test\resources
[INFO]
[INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ cloud-common-client ---
[INFO] No sources to compile
[INFO]
[INFO] --- surefire:3.0.0:test (default-test) @ cloud-common-client ---
[INFO] No tests to run.
[INFO]
[INFO] --- jar:3.3.0:jar (default-jar) @ cloud-common-client ---
[INFO] Building jar: D:\workspace\cloud-workspace\cloud-common\cloud-common-client\target\cloud-common-client-2.1.3-jre20.jar
[INFO]
[INFO] --- javadoc:3.5.0:jar (attach-javadocs) @ cloud-common-client ---
[INFO] No previous run data found, generating javadoc.
[INFO] Building jar: D:\workspace\cloud-workspace\cloud-common\cloud-common-client\target\cloud-common-client-2.1.3-jre20-javadoc.jar
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ cloud-common-client ---
[INFO] skip non existing resourceDirectory D:\workspace\cloud-workspace\cloud-common\cloud-common-client\src\main\resources
[INFO] skip non existing resourceDirectory D:\workspace\cloud-workspace\cloud-common\cloud-common-client\src\main\resources
[INFO]
[INFO] --- compiler:3.11.0:compile (default-compile) @ cloud-common-client ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] >>> source:3.2.1:jar (default) > generate-sources @ cloud-common-client >>>
[INFO]
[INFO] <<< source:3.2.1:jar (default) < generate-sources @ cloud-common-client <<<
[INFO]
[INFO]
[INFO] --- source:3.2.1:jar (default) @ cloud-common-client ---
[WARNING] artifact com.qolome.cloud:cloud-common-client:java-source:sources:2.1.3-jre20 already attached, replace previous instance
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ cloud-common-client ---
[INFO] skip non existing resourceDirectory D:\workspace\cloud-workspace\cloud-common\cloud-common-client\src\test\resources
[INFO]
[INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ cloud-common-client ---
[INFO] No sources to compile
[INFO]
[INFO] --- surefire:3.0.0:test (default-test) @ cloud-common-client ---
[INFO] No tests to run.
[INFO] Skipping execution of surefire because it has already been run for this configuration
[INFO]
[INFO] --- jar:3.3.0:jar (default-jar) @ cloud-common-client ---
[INFO]
[INFO] --- javadoc:3.5.0:jar (attach-javadocs) @ cloud-common-client ---
[INFO] Configuration changed, re-generating javadoc.
[INFO] Building jar: D:\workspace\cloud-workspace\cloud-common\cloud-common-client\target\cloud-common-client-2.1.3-jre20-javadoc.jar
[WARNING] artifact com.qolome.cloud:cloud-common-client:javadoc:javadoc:2.1.3-jre20 already attached, replace previous instance
[INFO]
[INFO] --- install:3.1.1:install (default-install) @ cloud-common-client ---
[INFO] Installing D:\workspace\cloud-workspace\cloud-common\cloud-common-client\pom.xml to D:\Server\Repository\com\qolome\cloud\cloud-common-client\2.1.3-jre20\cloud-common-client-2.1.3-jre20.pom
[INFO] Installing D:\workspace\cloud-workspace\cloud-common\cloud-common-client\target\cloud-common-client-2.1.3-jre20.jar to D:\Server\Repository\com\qolome\cloud\cloud-common-client\2.1.3-jre20\cloud-common-client-2.1.3-jre20.jar
[INFO] Installing D:\workspace\cloud-workspace\cloud-common\cloud-common-client\target\cloud-common-client-2.1.3-jre20-sources.jar to D:\Server\Repository\com\qolome\cloud\cloud-common-client\2.1.3-jre20\cloud-common-client-2.1.3-jre20-sources.jar
[INFO] Installing D:\workspace\cloud-workspace\cloud-common\cloud-common-client\target\cloud-common-client-2.1.3-jre20-javadoc.jar to D:\Server\Repository\com\qolome\cloud\cloud-common-client\2.1.3-jre20\cloud-common-client-2.1.3-jre20-javadoc.jar
[INFO]
[INFO] ----------------< com.qolome.cloud:cloud-common-server >----------------
[INFO] Building cloud-common-server 2.1.3-jre20 [3/3]
[INFO] from cloud-common-server\pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- clean:3.2.0:clean (default-clean) @ cloud-common-server ---
[INFO] Deleting D:\workspace\cloud-workspace\cloud-common\cloud-common-server\target
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ cloud-common-server ---
[INFO] Copying 0 resource from src\main\resources to target\classes
[INFO] Copying 1 resource from src\main\resources to target\classes
[INFO]
[INFO] --- compiler:3.11.0:compile (default-compile) @ cloud-common-server ---
[INFO] Changes detected - recompiling the module! :dependency
[INFO] Compiling 9 source files with javac [debug release 20] to target\classes
[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] ๆๅก้
็ฝฎๆไปถไธๆญฃ็กฎ, ๆๆ้ ๅค็็จๅบๅฏน่ฑกjavax.annotation.processing.Processor: Provider org.mapstruct.extensions.spring.converter.ConverterMapperProcessor could not be instantiatedๆถๆๅบๅผๅธธ้่ฏฏ
[INFO] 1 error
[INFO] -------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary for cloud-common 2.1.3-jre20:
[INFO]
[INFO] cloud-common ....................................... SUCCESS [ 0.663 s]
[INFO] cloud-common-client ................................ SUCCESS [ 4.449 s]
[INFO] cloud-common-server ................................ FAILURE [ 0.586 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.914 s
[INFO] Finished at: 2023-06-17T16:27:29+08:00
[INFO] ------------------------------------------------------------------------
[WARNING]
[WARNING] Plugin validation issues were detected in 2 plugin(s)
[WARNING]
[WARNING] * org.apache.maven.plugins:maven-source-plugin:3.2.1
[WARNING] * org.apache.maven.plugins:maven-javadoc-plugin:3.5.0
[WARNING]
[WARNING] For more or less details, use 'maven.plugin.validation' property with one of the values (case insensitive): [BRIEF, DEFAULT, VERBOSE]
[WARNING]
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile (default-compile) on project cloud-common-server: Compilation failure
[ERROR] ๆๅก้
็ฝฎๆไปถไธๆญฃ็กฎ, ๆๆ้ ๅค็็จๅบๅฏน่ฑกjavax.annotation.processing.Processor: Provider org.mapstruct.extensions.spring.converter.ConverterMapperProcessor could not be instantiatedๆถๆๅบๅผๅธธ้่ฏฏ
[ERROR]
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
[ERROR]
[ERROR] After correcting the problems, you can resume the build with the command
[ERROR] mvn <args> -rf :cloud-common-server
Process finished with exit code 1
I have introed mapstruct-spring-extensions
and config as the docs told.
mapstruct version is 1.4.2.Final
<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>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
and SpringMapperConfig
@MapperConfig(componentModel = "spring", uses = ConversionServiceAdapter.class)
@SpringMapperConfig(conversionServiceAdapterPackage = "com.example.converter")
public interface MapStructSpringConfig {
}
converter interface (for some reason, changed type name and field name)
@Mapper(uses = ConversionServiceAdapter.class)
interface CarStructMapper extends Converter<Car, CarDTO> {
@Mappings({
@Mapping(target = "seatCount", source = "seat.total"),
@Mapping(target = "product", source = "model"),
})
CarDTO convert(Car source);
}
used converter adapter in my class as bellow:
@Autowired
private ConversionServiceAdapter conversionServiceAdapter;
// source is an instance of Car
conversionServiceAdapter.mapCarToCarDTO(source)
but I got an exception
No converter found capable of converting from type [com.example.Car] to type [com.example.CarDTO]
What should I do to fix it. Thanks
Hi everyone,
I'd like to define multiple Spring Converter<S, T> implementations (with different generic parameters) in a single @Mapper annotated interface. However, due to type erasure, it is currently not possible to directly declare the same interface multiple times.
Would it be possible to generate additional implementation classes that delegate back to the additional Mapping methods? Currently, we need to manually define a delegate mapper.
Current approach:
@Mapper
public interface MyMapper extends Converter<BarTo, FooTo> {
@Override
@Mapping(source = "attribA", target = "attribB")
FooTo convert(BarTo source);
@InheritInverseConfiguration
BarTo convertInverse(FooTo source);
// Manual delegating mapper required
@Mapper
abstract class MyMapperDelegate implements Converter<FooTo, BarTo> {
@Autowired
private MyMapper mapper;
@Override
public BarTo convert(FooTo source) {
return this.mapper.convertInverse(source);
}
}
// Generated Mapper Impl
@Component
public class MyMapper$MyMapperDelegateImpl extends MyMapperDelegate {}
}
It would be nice if additional Implementation classes could be generated with a simple annotation such as e.g. @GenerateDelegate
.
Idea for a future approach:
@Mapper
public interface MyMapper extends Converter<BarTo, FooTo> {
@Override
@Mapping(source = "attribA", target = "attribB")
FooTo convert(BarTo source);
// Any SAM interface with matching structure
@GenerateDelegate(class = org.springframework.core.convert.converter.Converter.class)
@InheritInverseConfiguration
BarTo convertInverse(FooTo source);
// Generated Mapper Impl
@Component
public class MyMapperDelegateImpl implements Converter<FooTo, BarTo> {
@Autowired
private MyMapper mapper;
@Override
public BarTo convert(FooTo source) {
return this.mapper.convertInverse(source);
}
}
}
I'm asking this here since it would be especially useful together with the Spring Converter Interface, and would potentially also require an entry in the ConversionServiceAdapter.
See discussion in mapstruct/mapstruct#1528.
My code like this:
public interface CarMapper extends Converter<Car, CarDTO> { @Override @Mappings( @Mapping(source = "wheels",target = "carWheels") ) CarDTO convert(Car source); }
how can i get a Inversed method "Car convert(CarDTO source);"
I configured the mapstruct spring extension according the guide, but when doing a mvn package I get following, error, the build continues anyway and is successful and functional at the end. If I do a first build, make some changes, and then a second one, no error message appears, since the conversionServiceadapter already created by the first build.
So the whole thing works, maybe it is even expected to react like that, but I'd still like not to see an error anywhere in my builds :-).
Errormessage:
[INFO] --- apt-maven-plugin:1.1.3:process (default) @ test ---
/tmp/test/test/src/main/java/org/test/test/config/MapStructConfig.java:5: error: package org.test.test.dto.spring.adapter does not exist
import org.test.test.dto.spring.adapter.ConversionServiceAdapter;
^
/tmp/test/test/src/main/java/org/test/test/config/MapStructConfig.java:8: error: cannot find symbol
@MapperConfig(componentModel = "spring", uses = ConversionServiceAdapter.class)
^
symbol: class ConversionServiceAdapter
pom.xml
<properties>
<java.version>11</java.version>
<org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
<org.mapstruct.extensions.spring.version>0.1.2</org.mapstruct.extensions.spring.version>
<m2e.apt.activation>jdt_apt</m2e.apt.activation>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<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>
<configuration>
<source>${java.version}</source> <!-- depending on your project -->
<target>${java.version}</target> <!-- depending on your project -->
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.mapstruct.extensions.spring</groupId>
<artifactId>mapstruct-spring-extensions</artifactId>
<version>${org.mapstruct.extensions.spring.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
mapstructconfig
@MapperConfig(componentModel = "spring", uses = ConversionServiceAdapter.class)
@SpringMapperConfig(conversionServiceAdapterPackage ="org.test.test.dto.spring.adapter")
public interface MapStructConfig {
}
If it is true . We should make a of java version requisite on reference documentation.
We've got a Processor that recognises all Mappers extending Spring's Converter interface. What we'd like to do with this information is to generate a class that contains one method per Mapper which delegates to an injected ConversionService.
In the spirit of the core's @AnnotateWith
.
This would solve #3 as well.
I've seen the previous issues in the subject, but event though I inject ConversionService
to my code and use the convert()
function, I get the error. I've couldn't manage to understand the solution suggested in this comment, but anyway this is not ideal.
Any idea why this can happen? I'm really getting stuck because of this.
I also have another converter on the classpath, if that matters.
Let say I create two classes Source
:
public class Source {
public byte fieldA;
public byte fieldB;
public byte fieldC;
public byte fieldD;
}
and Target
:
public class Target {
public byte fieldE;
public byte fieldF;
}
Then I create a new mapper for the two classes:
@Mapper(config = MapperTestConfig.class)
public interface SourceToTargetConverter extends Converter<Source, Target> {
@Mapping(target = "fieldE", source = "source")
@Mapping(
target = "fieldF",
source = "source",
qualifiedByName = {"cAndD", "toF"})
@Override
Target convert(Source source);
}
And then I create mappers for each of the two operations:
@Mapper
public interface FieldsAPlusBToFieldEMapper extends Converter<Source, Byte> {
@Override
default Byte convert(Source source) {
return (byte) (source.fieldA + source.fieldB);
}
}
and:
@Named("cAndD")
@Mapper
public interface FieldsCPlusDToFieldFMapper extends Converter<Source, Byte> {
@Named("toF")
@Override
default Byte convert(Source source) {
return (byte) (source.fieldC + source.fieldD);
}
}
Finally, I create the @MapperConfig
:
@MapperConfig(componentModel = MappingConstants.ComponentModel.SPRING)
@SpringMapperConfig
public interface MapperTestConfig {}
Result: Two methods with the same are passed down to ConversionServiceAdapter
, thus creating a compiler error:
@Component
public class ConversionServiceAdapter {
private final ConversionService conversionService;
public ConversionServiceAdapter(@Lazy final ConversionService conversionService) {
this.conversionService = conversionService;
}
public Byte mapSourceToByte(final Source source) {
return (Byte) conversionService.convert(source, TypeDescriptor.valueOf(Source.class), TypeDescriptor.valueOf(Byte.class));
}
public Byte mapSourceToByte(final Source source) {
return (Byte) conversionService.convert(source, TypeDescriptor.valueOf(Source.class), TypeDescriptor.valueOf(Byte.class));
}
public Target mapSourceToTarget(final Source source) {
return (Target) conversionService.convert(source, TypeDescriptor.valueOf(Source.class), TypeDescriptor.valueOf(Target.class));
}
}
Stacktrace (redacted):
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.10.1:compile (default-compile) on project [name]: Compilation failure: Compilation failure:
[ERROR] [generated-annotation-path]/ConversionServiceAdapter.java:[30,15] method mapSourceToByte(Source) is already defined in class ConversionServiceAdapter
[ERROR] [source-file-path]/FieldsCPlusDToFieldFMapper.java:[9,8] No implementation was created for FieldsCPlusDToFieldFMapper due to having a problem in the erroneous element java.util.ArrayList. Hint: this often means that some other annotation processor was supposed to process the erroneous element. You can also enable MapStruct verbose mode by setting -Amapstruct.verbose=true as a compilation argument.
[ERROR] [source-file-path]/SourceToTargetConverter.java:[8,8] No implementation was created for SourceToTargetConverter due to having a problem in the erroneous element java.util.ArrayList. Hint: this often means that some other annotation processor was supposed to process the erroneous element. You can also enable MapStruct verbose mode by setting -Amapstruct.verbose=true as a compilation argument.
[ERROR] [source-file-path]/FieldsAPlusBToFieldEMapper.java:[7,8] No implementation was created for FieldsAPlusBToFieldEMapper due to having a problem in the erroneous element java.util.ArrayList. Hint: this often means that some other annotation processor was supposed to process the erroneous element. You can also enable MapStruct verbose mode by setting -Amapstruct.verbose=true as a compilation argument.
Expected:
The enqine should generate two methods with different signatures, each resolving the qualifications specified with (at)Named
.
@Component
public class ConversionServiceAdapter {
// fields and constructor. Might use more than one ConversionService to qualify different methods.
public Byte mapSourceToByte(final Source source) {
return (Byte) conversionService.convert(source, TypeDescriptor.valueOf(Source.class), TypeDescriptor.valueOf(Byte.class));
}
// Method resolved with (at)Named = "cAndD" and "toF".
public Byte mapSourceToByteCandDtoF(final Source source) {
// logic for qualified mapping, maybe using another ConversionService.
}
public Target mapSourceToTarget(final Source source) {
return (Target) conversionService.convert(source, TypeDescriptor.valueOf(Source.class), TypeDescriptor.valueOf(Target.class));
}
}
and then use them in the converter:
@Component
public class SourceToTargetConverterImpl implements SourceToTargetConverter {
@Autowired
private ConversionServiceAdapter conversionServiceAdapter;
@Override
public Target convert(Source source) {
if ( source == null ) {
return null;
}
Target target = new Target();
target.fieldE = conversionServiceAdapter.mapSourceToByte( source );
target.fieldF = conversionServiceAdapter.mapSourceToBytecAndDtoF( source );
return target;
}
}
We're only using it for generating the annotations. This can be done by referring to the strings and removing the dependency from the core code.
With following environment `compiler: Eclipse JDT (IDE) 1.4.200.v20220802-0458, environment: Java 17.0.4.1 (Eclipse Adoptium)`, adapter class will NOT be generating method for all of the converters, but it only generate the class.
[Error - 4:53:17 AM] Sep 30, 2022, 4:53:17 AM Exception thrown by Java annotation processor org.mapstruct.extensions.spring.converter.ConverterMapperProcessor@67b39ba
java.lang.ClassCastException: class org.eclipse.jdt.internal.compiler.apt.model.AnnotationValueImpl cannot be cast to class javax.lang.model.element.AnnotationMirror (org.eclipse.jdt.internal.compiler.apt.model.AnnotationValueImpl is in unnamed module of loader org.eclipse.osgi.internal.loader.EquinoxClassLoader @6f270916; javax.lang.model.element.AnnotationMirror is in module java.compiler of loader 'platform')
java.lang.Exception: java.lang.ClassCastException: class org.eclipse.jdt.internal.compiler.apt.model.AnnotationValueImpl cannot be cast to class javax.lang.model.element.AnnotationMirror (org.eclipse.jdt.internal.compiler.apt.model.AnnotationValueImpl is in unnamed module of loader org.eclipse.osgi.internal.loader.EquinoxClassLoader @6f270916; javax.lang.model.element.AnnotationMirror is in module java.compiler of loader 'platform')
at org.eclipse.jdt.internal.compiler.apt.dispatch.RoundDispatcher.handleProcessor(RoundDispatcher.java:172)
at org.eclipse.jdt.internal.compiler.apt.dispatch.RoundDispatcher.round(RoundDispatcher.java:124)
at org.eclipse.jdt.internal.compiler.apt.dispatch.BaseAnnotationProcessorManager.processAnnotations(BaseAnnotationProcessorManager.java:172)
at org.eclipse.jdt.internal.apt.pluggable.core.dispatch.IdeAnnotationProcessorManager.processAnnotations(IdeAnnotationProcessorManager.java:138)
at org.eclipse.jdt.internal.compiler.Compiler.processAnnotations(Compiler.java:953)
at org.eclipse.jdt.internal.compiler.Compiler.compile(Compiler.java:450)
at org.eclipse.jdt.internal.compiler.Compiler.compile(Compiler.java:426)
at org.eclipse.jdt.internal.core.builder.AbstractImageBuilder.compile(AbstractImageBuilder.java:379)
at org.eclipse.jdt.internal.core.builder.BatchImageBuilder.compile(BatchImageBuilder.java:214)
at org.eclipse.jdt.internal.core.builder.AbstractImageBuilder.compile(AbstractImageBuilder.java:311)
at org.eclipse.jdt.internal.core.builder.BatchImageBuilder.build(BatchImageBuilder.java:79)
at org.eclipse.jdt.internal.core.builder.JavaBuilder.buildAll(JavaBuilder.java:273)
at org.eclipse.jdt.internal.core.builder.JavaBuilder.build(JavaBuilder.java:188)
at org.eclipse.core.internal.events.BuildManager$2.run(BuildManager.java:1024)
at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:45)
at org.eclipse.core.internal.events.BuildManager.basicBuild(BuildManager.java:254)
at org.eclipse.core.internal.events.BuildManager.basicBuild(BuildManager.java:311)
at org.eclipse.core.internal.events.BuildManager$1.run(BuildManager.java:400)
at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:45)
at org.eclipse.core.internal.events.BuildManager.basicBuild(BuildManager.java:403)
at org.eclipse.core.internal.events.BuildManager.basicBuildLoop(BuildManager.java:514)
at org.eclipse.core.internal.events.BuildManager.basicBuildLoop(BuildManager.java:462)
at org.eclipse.core.internal.events.BuildManager.build(BuildManager.java:544)
at org.eclipse.core.internal.resources.Workspace.buildInternal(Workspace.java:524)
at org.eclipse.core.internal.resources.Workspace.build(Workspace.java:420)
at org.eclipse.jdt.ls.core.internal.handlers.BuildWorkspaceHandler.buildProjects(BuildWorkspaceHandler.java:112)
at org.eclipse.jdt.ls.core.internal.handlers.JDTLanguageServer.lambda$28(JDTLanguageServer.java:885)
at org.eclipse.jdt.ls.core.internal.handlers.JDTLanguageServer.lambda$55(JDTLanguageServer.java:1070)
at java.base/java.util.concurrent.CompletableFuture$UniApply.tryFire(Unknown Source)
at java.base/java.util.concurrent.CompletableFuture$Completion.exec(Unknown Source)
at java.base/java.util.concurrent.ForkJoinTask.doExec(Unknown Source)
at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(Unknown Source)
at java.base/java.util.concurrent.ForkJoinPool.scan(Unknown Source)
at java.base/java.util.concurrent.ForkJoinPool.runWorker(Unknown Source)
at java.base/java.util.concurrent.ForkJoinWorkerThread.run(Unknown Source)
Caused by: java.lang.ClassCastException: class org.eclipse.jdt.internal.compiler.apt.model.AnnotationValueImpl cannot be cast to class javax.lang.model.element.AnnotationMirror (org.eclipse.jdt.internal.compiler.apt.model.AnnotationValueImpl is in unnamed module of loader org.eclipse.osgi.internal.loader.EquinoxClassLoader @6f270916; javax.lang.model.element.AnnotationMirror is in module java.compiler of loader 'platform')
at java.base/java.util.stream.ReferencePipeline$3$1.accept(Unknown Source)
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.copyInto(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.evaluate(Unknown Source)
at java.base/java.util.stream.ReferencePipeline.collect(Unknown Source)
at org.mapstruct.extensions.spring.converter.ConverterMapperProcessor.toSourceTargetTypeNamePairs(ConverterMapperProcessor.java:104)
at java.base/java.util.Optional.map(Unknown Source)
at org.mapstruct.extensions.spring.converter.ConverterMapperProcessor.getExternalConversionMappings(ConverterMapperProcessor.java:95)
at org.mapstruct.extensions.spring.converter.ConverterMapperProcessor.buildDescriptor(ConverterMapperProcessor.java:77)
at org.mapstruct.extensions.spring.converter.ConverterMapperProcessor.process(ConverterMapperProcessor.java:63)
at org.eclipse.jdt.internal.compiler.apt.dispatch.RoundDispatcher.handleProcessor(RoundDispatcher.java:142)
... 34 more
Originally posted by @myatmin in #53 (comment)
Thanks. So combining various default starters can actually result in multiple ConversionService
s being in the ApplicationContext
. I guess at the very least that would call for an option to specify a bean name at the injection point inside the adapter class. Of course, it still wouldn't solve the auto-registration of the individual Converter
s. In any case, this has brought up something to work on for the extension project.
Originally posted by @Chessray in mapstruct/mapstruct#3336 (reply in thread)
If it is true . We should make a of java version requisite on reference documentation.
I am new to MapStruct and am interested in the idea presented by the Spring extension. The doc is non-existent though. It is basically a link to GitHub (with no doc), a Stack Overflow question, and some examples. The noob users are therefore left to synthesize this information into something useful - which is problematic and probably driving users away.
Would be nice to have at least a getting started guide in your readme that explains:
Thanks!
codecov.io still seems to show the results for the default Gradle project. There's no mention of the classes currently in the project.
Because org.springframework.core.convert.converter.Converter only provides conversion from S to T, if you need to reverse conversion, is there any good way?
It seems that mapping lists is not really possible, the generated code is not able to understand the type of the list:
public List<MyTarget> mapListToList(final List<MySource> source) {
return conversionService.convert(source,List.class)
}
I have also the same generated method 2 times if I have the conversion in the both directions. the compiler end up saying "name clash: mapListToList.... and mapListToList have the same type erasure"
For lists, another method should be used in the interface ConversionService (see this stackoverflow):
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType)
It would be nice if the documentation could say something about this
@Chessray I'm experiencing a similar name collision. When defining mappers for classes with the same name but different packages, the generated name in the ConversionServiceAdapter is the same:
@Mapper(config = SharedConfig.class)
public interface MyMapper extends Converter<Foo, com.contoso.Bar> {
com.contoso.Bar convert(Foo source);
}
@Mapper(config = SharedConfig.class)
public interface MyMapper extends Converter<Foo, com.fabrikam.Bar> {
com.fabrikam.Bar convert(Foo source);
}
// With classes
class Foo {
}
package com.contoso;
class Bar {
}
package com.fabrikam;
class Bar {
}
Is there a simple way around this, or does that require changes to the name generator as well?
Originally posted by @pw-lehre in #86 (comment)
The ConversionServiceAdapter
generated by ConversionServiceAdapterGenerator
contains a "date" value in its @Generated
annotation, despite mapstruct.suppressGeneratorTimestamp=true
being configured:
@Generated(
value = "org.mapstruct.extensions.spring.converter.ConversionServiceAdapterGenerator",
date = "2023-07-05T15:30:39.534896439Z"
)
@Component
public class ConversionServiceAdapter {
// ...
}
It would be nice if mapstruct-spring-extensions
would support this setting (or add a simular one) just like mapstruct
does and render without the date
attribute:
@Generated(
value = "org.mapstruct.extensions.spring.converter.ConversionServiceAdapterGenerator"
)
@Component
public class ConversionServiceAdapter {
// ...
}
I'm mainly thinking "reproducible builds"/"optimizing caching" here, so it would be nice that if my code didn't change, the generated output would stay the same. ๐
Spring provides a rich set of standard converters that go beyond MapStruct's built-in functionalities (e.g. UUID to String). It would be beneficial if we could utilize these in order to reduce the burden on the MapStruct user who has to reimplement these in every project.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.