Comments (6)
Solution idea:
- Include qualifying elements in generated method names for adapter class.
- Clone
@Named
or respective qualifier annotation from called methods onto generated methods so that MapStruct Processor can "see" them. - Make a note of this in the core documentation on qualifiers as in this scenario the called Mapper must not be included in the
uses
attribute. - Consider separate
ConversionService
bean for every qualified method? Or add a separate annotation that allows the developer to specify a non-default bean name on the called method?
@gigena-git It's definitely intriguing, and as you can see there are a few things to consider. I can't guarantee a quick solution. Happy for you to take a head start in this yourself if you wish. 🙂
from mapstruct-spring-extensions.
Things to note here:
- The
ConversionService
only allows for a singleConverter
per source and target type. - We'd have to keep in mind the
qualifiedBy
attribute as well. - I agree that generating method signatures should be relatively straightforward.
- Method bodies are a different beast. As you already noted yourself, we'd have to somehow cater for several
ConversionService
beans in the adapter bean.
To sum things up, this is definitely a non-trivial issue. I'll see if I can give it some thought over the next couple of days/weeks.
from mapstruct-spring-extensions.
Hello @Chessray, thanks for your answer!
Yes, I overlooked the qualifiedBy
attribute as I don't use it, but considering it makes sense.
If there's any additional information, use cases, insights, or help I can provide from my end, please let me know.
Thanks, Maximiliano.
from mapstruct-spring-extensions.
I agree with the approach for method name generation. Even though I'm still getting acquainted with how the MapStruct processor works, I can visualize a way in which it could be implemented.
I didn't understand the third bullet point. Why the called Mapper should not be included in the uses
attribute?
About the fourth point, I think that giving each method a different ConversionService
, while easy to implement it is not efficient and might not be a trivial issue when dealing with code that declares multiple mappers. Let's say for instance that we declare the following methods across various Converters/Mappers.
@Named("toUppercase")
String convert(String);
////////////////////////////////
String convert(String);
////////////////////////////////
@QualifiedInt // custom qualifier
int convert(int);
////////////////////////////////
@QualifiedByte // custom qualifier
byte convert(byte);
////////////////////////////////
@QualifiedLong // custom qualifier
long convert(long)
////////////////////////////////
@QualifiedBoolean // custom qualifier
boolean convert(boolean)
////////////////////////////////
@QualifiedCharToChar // custom qualifier
char convert(char)
////////////////////////////////
@QualifiedCharToString // custom qualifier
String convert(char)
If the processor gives every single qualified method it's own conversion service, the adapter would end up with 7 instances. However, with the given methods, only 2 are needed - as the only the String convert(String)
methods are colliding.
The best approach - if possible - would be to determine the number of ConversionService
instances. Something like this:
- Start at the point where the processor has the list of all the converters - and it's annotations - that have been declared throughout the codebase.
- Determine the minimum number of
ConversionService
instances that are needed. I don't have at the moment a particular algorithm for this, but I think it can be done in one loop through the converters. - Instantiate the computed number of conversion services, and keep them on an array.
- Create a
Map<String, ConversionService>
object. - Assign the converters to each of the conversion services, making sure that no service is assigned two converters with the same source and target.
a. Loop through all the converters. For each, extract source, target,@Named
, and@Qualifier
annotations.
b. Generate a String from the annotation names. Concatenate and camelCase if multiple exist.
c. Check if the map contains the annotations' string as key. If it does, get the conversion service from the map determine if it can convert from the converter's source to the converter's target.
I. If it can't, add the converter to the conversion service and continue.
II. If it can throw an exception, as adding the converter to the service would put two converters of the same source and target.
d. Start with the zeroth conversion service of the array. If the conversion service cannot convert from the converter's source to the converter's target, add the converter. If it can move to the next conversion service and repeat this step until it can't.
e. Add the conversion service to the map with the annotations' string as key and continue. - Return the Map - this is what will be injected into
ConversionServiceAdapter
.
The implementation could look something like this:
// Step 1 it could be a bean declared in the configuration class, or it could be a call from a Post Processor.
@Bean
Map<String, ConversionService> conversionServiceMapBean(List<Converter> converters) {
// Step 2 get the minimum number of conversion services needed.
// Step 3
ConversionService[] csArr = new ConversionService[numberOfMinimumConversionServices];
// Step 4
Map<String, ConversionService> conversionServiceMap = new HashMap<>();
// Step 5
for(Converter c : converters) {
// Step 5a
Class<?> source = c.getSource();
Class<?> target = c.getTarget();
// Extract annotations
Class<?> clazz = c.getClass();
Annotation[] anns = clazz.getAnnotations();
Method m = clazz.getMethod("convert", ActionItemDTO.class);
Annotation[] mAnns = m.getAnnotations();
// Generate String from annotations
String key = "";
for(Annotation ann : anns) {
if(ann.annotationType().equals(Named.class){
key += ann.annotationType().value();
} if(ann.annotationType().equals(Qualifier.class)) {
key += ann.getClass().getSimpleName();
}
}
for(Annotation mAnn : mAnns) {
if(mAnn.annotationType().equals(Named.class){
key += mAnn.annotationType().value();
} if(mAnn.annotationType().equals(Qualifier.class)) {
key += mAnn.getClass().getSimpleName();
}
}
key = (key != "") ? key : "primary";
// Step 5b
if(conversionServiceMap.containsKey(key)) {
ConversionService conversionService = conversionServiceMap.get(key);
// Step 5c
if(!conversionService.canConvert(source, target) {
// Step 5 c i
conversionService.addConverter(c);
continue;
} else {
// Step 5 c ii
throw new Exception("Cannot add converter to conversion service. A converter with the same source and target already exists.");
}
}
// Step 5d
for(int i = 0; i < csArr.length; i++) {
ConversionService conversionService = csArr[i];
if(!conversionService.canConvert(c.getSource(), c.getTarget())) {
conversionService.addConverter(c);
// Step 5e
conversionServiceMap.put(key, conversionService);
break;
}
}
}
// Step 6
return conversionServiceMap;
}
This is a brief description of what the Mapper builder should do.
- Now the constructor will receive a
Map<String, ConversionService>
instead of aConversionService
. - For each Source/Target pair, the annotations that were extracted from the converter, will be passed to a variable that will be used to get the conversion service from the map. The default one will be assigned the "primary" key.
Now, inside ConversionServiceAdapter
, the end result should look something like this:
@Component
public class ConversionServiceAdapter {
private final Map<String, ConversionService> conversionServiceMap;
// New Constructor
public ConversionServiceAdapter(final @Lazy Map<String, ConversionService> conversionServiceMap) {
this.conversionServiceMap = conversionServiceMap;
}
// Method with no annotations, assigned with @Primary on the initialization of the Map.
public Byte mapSourceToByte(final Source source) {
String key = "primary"; // This line should be generated from the processor.
ConversionService conversionService = conversionServiceMap.get(key);
return (Byte) conversionService.convert(source, TypeDescriptor.valueOf(Source.class), TypeDescriptor.valueOf(Byte.class));
}
// Method name resolved with @Named = "cAndD" and "toF".
public Byte mapSourceToByteCandDtoF(final Source source) {
String key = "cAndDtoF"; // This line should be generated from the processor.
ConversionService conversionService = conversionServiceMap.get(key);
return (Byte) conversionService.convert(source, TypeDescriptor.valueOf(Source.class), TypeDescriptor.valueOf(Byte.class));
}
public Target mapSourceToTarget(final Source source) {
String key = "primary";
ConversionService conversionService = conversionServiceMap.get(key);
return (Target) conversionService.convert(source, TypeDescriptor.valueOf(Source.class), TypeDescriptor.valueOf(Target.class));
}
}
I still have to look how would a mapper implementation call the methods with modified qualified names.
from mapstruct-spring-extensions.
I didn't understand the third bullet point. Why the called Mapper should not be included in the
uses
attribute?
The whole point of this module is the decoupling of Mappers. The idea is that we clone the annotations onto the generated methods so only the Adapter class shows up in the uses
clause. The calling Mapper will then simply use the method in the Adapter class like it does normally. Were the called Mapper in the uses
clause as well, we'd end up with ambiguity.
About the fourth point, I think that giving each method a different
ConversionService
, while easy to implement it is not efficient and might not be a trivial issue when dealing with code that declares multiple mappers.
I merely threw some quick ideas around. If there is a way to determine this for all cases automatically, then I'm certainly on board with that.
from mapstruct-spring-extensions.
After giving this some thought, it feels like we'd be going down a nearly bottomless rabbit hole without much gain. The idea as described so far would require several ConversionService
s initialized by some additional configuration code. I definitely want to avoid generating that kind of thing. Spring provides several different service implementations, and users might want to add their own. This is different from the testing context where the default service implementation covers pretty much all scenarios.
So if we always leave the service initialization to the user, there seems to be little gain in pursuing this idea. What we could rather think about is suppressing the method generation for certain cases so at least the generated Adapter passes compilation. This seems like one of the situations where you want to just use MapStruct directly and not let the Mapper extend and/or implement the Converter
interface.
from mapstruct-spring-extensions.
Related Issues (20)
- DelegatingConverter and CycleAvoidingMappingContext HOT 4
- Allow inherited DelegatingConverter to be processed HOT 1
- Not auto register converters after manually create a ConversionService bean. HOT 14
- Support SpringBoot3 HOT 1
- maven package jar but no class file in jar HOT 2
- Java version requisite is 11 ?
- Java version requisite is 11 ? HOT 2
- how can i use "Inverse mappings" HOT 1
- Problem of defining parent-child JPA mapping. HOT 3
- maven Compilation failed [JDK20 MAVEN3.9.2 ] javax.annotation.processing.Processor: Provider org.mapstruct.extensions.spring.converter.ConverterMapperProcessor could not be instantiated HOT 2
- ConversionServiceAdapterGenerator does not respect mapstruct.suppressGeneratorTimestamp
- Combining various default starters can result in multiple `ConversionService`s being in the `ApplicationContext`.
- How to inject Spring CustomComponent in Mapper interface? HOT 2
- Provide a ConversionService bean if missing HOT 7
- Generate additional delegating mappers for @InheritInverseConfiguration HOT 5
- Name collision with same class name in different packages HOT 2
- 'No converter found' when injection ConversionService into Spring Service. / indirect reference to ConversionService HOT 6
- spring-beans vulnerability CVE-2022-22965 HOT 4
- Provide mechanism for adding annotations to generated code HOT 2
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from mapstruct-spring-extensions.