GeezerVison is showing github.com — Add annotations to Generated code · Issue #1574 · mapstruct/mapstruct
Add annotations to Generated code #1574DescriptionI just want to share an idea... I think it would be great to be able to add any annotations you want to generated classes/methods.
Something like this:
@Annotated({
@Component,
@Qualifier("fooMapper")
})
@Mapper
public interface FooMapper {
@Annotated({
@Cacheable(value = "cache", key = "mappedFoo #foo.id" )
})
Foo map(Foo foo);
}
So, the content of @Annotated would be added to generated code like this:
@Component,
@Qualifier("fooMapper")
public class FooMapperImpl implements FooMapper {
@Cacheable(value = "cache", key = "mappedFoo #foo.id" )
Foo map(Foo foo) {
...
}
}
With this functionality we wouldn't need @Mapper(componentModel = "spring") for example, and many annotation-based issues would gone. Reacted by xqdd, Martin Weise, Michael Brem, Falko Modler, Michael Kroll, Marco Martins, payne911, Sean Flanigan, Ivan Vakhrushev, LukeDunsmore and 4 more Activitysjaakd commented on Aug 5, 2018 Interesting. I'm not an expert on Spring but to my understanding some of the annotations end up on a referenced mapper instance. Like '@Inject' for CDI. How would that work with your idea? vadim-shb commented on Aug 5, 2018 I'm not sure if I've understood your question correctly, so please rephrase it then, or add an example...
In context of Spring @Inject would work just fine with the example above (if package with FooMapperImpl.java is scanned for beans by Spring):
@Component
public class SomeBean {
@Inject
@Qualifier("fooMapper")
FooMapper mapper
...
}
sjaakd commented on Aug 5, 2018 When you use a mapper, ('@Mapper#use') MapStruct will generate that code when the component model is Spring. Don't know if it also evaluates the annotations on the used mapper vadim-shb commented on Aug 5, 2018 Ok, I've never used @Mapper#use, and after quick peek I still not see where I would like to use it, but it was probably a mistake talking that
we wouldn't need @Mapper(componentModel = "spring")
But it still might be useful in different situations... Leave it here to discuss... filiphr commented on Aug 5, 2018 @vadim-shb the request is interesting.
Regarding:
With this functionality we wouldn't need @Mapper(componentModel = "spring") for example, and many annotation-based issues would gone.
This is wrong. If you use the default componentModel than when you use other mappers with Mapper#uses MapStruct will use the Mappers factory to create the used mapper or invoke it's default constructor. This is wrong.
Having said that, the other part for annotating the methods might be interesting. However, I am not entirely sure about it. Your example doesn't really make sense to me, why would you want to cache the DTOs? Are you seeing a performance hit when the mapping is invoked? vadim-shb commented on Aug 5, 2018
Your example doesn't really make sense to me
It makes no sense for me either. I made it up quickly just to show that this functionality should be implemented for method annotations as well as for class annotations.
It's general functionality and it may be used in lot's of ways by different frameworks.
Let's made up a better example with Spring:
public interface FooMapper {
Foo map(Foo foo);
}
@Annotated({
@Component,
@Lazy,
@Profile("prod")
})
@Mapper
public interface FooMapperProd extends FooMapper {
Foo map(Foo foo);
}
@Annotated({
@Component,
@Profile("qa")
})
@Mapper
public interface FooMapperQa extends FooMapper {
@Annotated({
@Loggable
})
Foo map(Foo foo);
}
@Service
public class SomeBean {
@Inject
FooMapper mapper;
...
}
I don't know an easy way to do such configuration now. Reacted by Martin Macko, Johan Steffens, Michel Erard, Bogdan Mariesan, Nick Buller, guleryuz, Baptiste Pernet-Mugnier, radhakrishnanch, XS, Bragolgirith and 1 more LinkedList commented on Oct 15, 2018 mjustin commented on Jul 16, 2021 I just ran across a completely non-Spring situation where this could be useful. I marked one of my mapper methods as @Deprecated, since I hope to remove it in a future release, and want it documented in the code. However, since the generated method is neither @Deprecated nor has @SuppressWarnings("deprecation"), I get a compilation warning for the deprecation:
/path/to/project/build/generated/sources/annotationProcessor/java/main/com/example/ExampleMapperImpl.java:90: warning: [deprecation] map(ExampleTarget) in ExampleMapper has been deprecated
public ExampleTarget map(ExampleSource source) {
Granted, this could (and perhaps should) be solved by a deprecation-specific MapStruct feature. However, it could also be solved by being able to specify that I want to add either of these annotations to the generated method. Reacted by Janusz, Henk Norman, payne911, Pavel Stepanov and Daniel Hammer famod commented on Sep 3, 2021 My team just ran into this "missing feature": We use componentModel=cdi and mostly mapping interfaces.
We now wanted to add cdi interceptor binding annotations to the generated MapperImpl classes, but there is no way to do that.
As a workaround, we'll have to migrate to abstract mapping classes. Reacted by Michael Kroll, Chris and mjustin payne911 commented on Feb 23, 2022 My team would love such a feature as well.
We are trying to exclude the generated classes from the unit-test code coverage stats in our pipeline via a custom annotation. It would be great to be able to propagate that annotation to the implementation of the mappers.
But maybe another way to approach this would be for MapStruct to have its own @Generated class and to change the Retention Policy from SOURCE to either RUNTIME or CLASS, as explained there. That is how Lombok does it. Zegveld commented on Feb 23, 2022 Zegveld commented on Feb 27, 2022 I've been looking into a way to implement this.
I did not find a way to create an annotation that would allow the following notation:
@AnnotateWith( {
@Deprecated,
@AnnotationWithParameters( param1="value", param2="otherValue" )
} )
What I did find that might work is the following, although the autocomplete behavior of the IDE would be lost:
@AnnotateWith( Deprecated.class )
@AnnotateWith( type=AnnotationWithParameters.class, parameters = {
@AnnotateWith.Parameter ( key = "param1", value = "value" ),
@AnnotateWith.Parameter ( key = "param2", value = "otherValue" )
} )
wdyt, @sjaakd and @filiphr ? Would this be the way to go, or is there perhaps a better way to support the initial idea? filiphr commented on Feb 28, 2022 I've missed the comment about the @Deprecated from @mjustin:
I just ran across a completely non-Spring situation where this could be useful. I marked one of my mapper methods as @Deprecated, since I hope to remove it in a future release, and want it documented in the code.
I've created #2773 for this.
Regarding the @Generated from @payne911:
But maybe another way to approach this would be for MapStruct to have its own @Generated class and to change the Retention Policy from SOURCE to either RUNTIME or CLASS, as explained there. That is how Lombok does it.
I do not agree with this because I do not agree that the code generated by MapStruct should not be tested and be unverified. We strive for generating code that looks like it has been written by a person, which means that it shouldn't have warnings. The generated code usually is simple, but there can be complex things due to the different configuration options we have, you should most definitely test that the mappings that are generated are mapping your data as you would expect.
Regarding the proposal from @Zegveld.
@AnnotateWith( {
@Deprecated,
@AnnotationWithParameters( param1="value", param2="otherValue" )
} )
That would be ideal, unfortunately Java doesn't allow doing that. Lombok has their experimental onX for supporting that, but that only works for them since they are changing the byte code of the class itself.
What I did find that might work is the following, although the autocomplete behavior of the IDE would be lost:
@AnnotateWith( Deprecated.class )
@AnnotateWith( type=AnnotationWithParameters.class, parameters = {
@AnnotateWith.Parameter ( key = "param1", value = "value" ),
@AnnotateWith.Parameter ( key = "param2", value = "otherValue" )
} )
This is actually not that bad of an idea. We will need to provide parameters for the different parameter types of an annotation but I think it can work. People can also use their own composed meta annotations and use use @AnnotateWith( MyCustomAnnotation.class ). We will need to be a bit careful with the component model annotations that we are already adding, but it is doable. Zegveld commented on Mar 6, 2022
We will need to be a bit careful with the component model annotations that we are already adding, but it is doable.
Concerning that, I was thinking that perhaps the @Named annotation on the mapper could be transferred over as the name for the mapper in the annotation of the component model.
For example:
@Named( "MyOwnMapper" )
@Mapper( componentModel = "spring" )
interface Mapper {
would result in:
@Component( "MyOwnMapper" )
class MapperImpl implements Mapper {
Perhaps the above could be an issue by itself.
Concerning the @AnnotateWith and @AnnotateWith.Parameter I think that we will get the following classes:
@Repeatable( AnnotateWiths.class )
@Retention( SOURCE )
@Target( { TYPE, METHOD } )
public @interface AnnotateWith {
Class<? extends Annotation> value();
Parameter[] parameters() default {};
@interface Parameter {
String key();
String stringValue() default ""; // used for having the value as a string.
String objectValue() default ""; // used when the value is not a string.
}
}
Example:
@AnnotateWith( MyCustomAnnotation.class )
@AnnotateWith( value=MyCustomAnnotationWithParameters.class, parameters = {
@AnnotateWith.Parameter ( key = "stringParam", stringValue = "value" ),
@AnnotateWith.Parameter ( key = "classParam", objectValue = "java.lang.String.class" ),
@AnnotateWith.Parameter ( key = "intParam", objectValue = "1234" )
} )
would result in:
@MyCustomAnnotation
@MyCustomAnnotationWithParameters( stringParam = "value", classParam = java.lang.String.class, intParam = 1234 )
How extended to we want to protect the user against mistakes with @AnnotateWith usage?
There are several things that can go wrong, that cause the generated mapper code to not compile:
- The annotation is not allowed at that location.
- The parameter does not exist on the annotation.
- The parameter value is of the wrong type.
filiphr commented on Mar 6, 2022
Concerning that, I was thinking that perhaps the @Named annotation on the mapper could be transferred over as the name for the mapper in the annotation of the component model.
We can't do that, because @Named is used for qualifying methods / mappers. If a mapper is annotated with @Named then it won't be automatically used for mapping custom types.
Concerning the @AnnotateWith and @AnnotateWith.Parameter I think that we will get the following classes:
I don't like the stringValue and objectValue, I think that we need to have the actual values that are supported for an annotation.
e.g.
@Repeatable( AnnotateWiths.class )
@Retention( CLASS )
@Target( { TYPE, METHOD, ANNOTATION_TYPE } )
public @interface AnnotateWith {
Class<? extends Annotation> value();
Parameter[] parameters() default {};
@interface Parameter {
String key();
short[] shorts() default {};
byte[] bytes() default {};
int[] ints() default {};
long[] longs() default {};
float[] floats() default {};
double[] doubles() default {};
char[] chars() default {};
boolean[] booleans() default {};
String[] strings() default {};
Class<?>[] classes() default {};
}
}
This makes it easy to use statics and so on. This is similar to the JUnit 5 ValueSource. An annotation also allows having enums, but for those we will have to use the strings and determine the enum value on our own.
Also the ElementType should be:
TYPE
METHOD
ANNOTATION_TYPE - in order to allow meta annotations
With the the example would look like:
@AnnotateWith( MyCustomAnnotation.class )
@AnnotateWith( value=MyCustomAnnotationWithParameters.class, parameters = {
@AnnotateWith.Parameter ( key = "stringParam", strings = "value" ),
@AnnotateWith.Parameter ( key = "classParam", classes = String.class ),
@AnnotateWith.Parameter ( key = "intParam", ints = 1234 )
} )
How extended to we want to protect the user against mistakes with @AnnotateWith usage?
We want to provide as much information to the user as possible. We can easily detect certain things so we can easily
There are several things that can go wrong, that cause the generated mapper code to not compile:
- The annotation is not allowed at that location.
We can have a compile error pointing out the mistake.
- The parameter does not exist on the annotation.
Again, compile error pointing out the mistake.
- The parameter value is of the wrong type.
Also a compile error pointing out that that it isn't supported. added a commit that references this issue on Aug 20, 2022#1574: Support for annotating the generated code with custom annotations NadChel commented on May 22, 2025 Can I ask what is the rationale behind this design decision involving class literals? As opposed to what the OP suggested
@Annotated(@Cacheable(value="cache", key="mappedFoo #foo.id" ))
It appears more straightforward and flexible than
@AnnotatedWith(value=Cacheable.class, elements={@Element(strings="cache"), @Element(name=key, strings="mappedFoo #foo.id")})
I'm sure there was reasoning behind it that I don't yet realize filiphr commented on May 25, 2025 @NadChel, the proposed option is not valid Java. I think that we couldn't provide an interface where you can just add any annotated element in it. If you can provide us with a valid java code that can be used as
@Annotated(@Cacheable(value="cache", key="mappedFoo #foo.id" ))
we are more than welcome to adjust it. Please keep in mind, that if you refer to the way that Lombok does this using the onX, that's not valid Java and we have no plans to go down the path of Lombok to implement such a feature outside of using the public Java API
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|