Search code examples
javaspringreflectionannotationsspring-annotations

Replacing an annotation with another annotation during compile time in Spring?


I am using Swagger annotations over my controller parameters. So, I end up with annotations like @ApiParam(name="default name", value="this is a default value"). I think this is quite verbose. I would like to change it to something like @Foo. I want to know if there's a way to replace @Foo with @ApiParam during compile time. Also, since I am using Spring, I think I have to consider the annotation processing order in Spring, as well. I mean I shouldn't replace @ApiParam with @Foo after Swagger or Spring picks it up. Is there any way to do this?

In simpler words, I have the same annotation with the same parameters used 5 times. Basically, I want to replace them with some custom annotation.

I know I have to show what I have already tried, but I have no clue where to even start.

Also, the question is not related to Swagger, it is just an example. I want to replace one annotation with another during compile time, so that the one picked up by Spring won't be the one I have put on the source code, but the one I have replaced.


Solution

  • If I understand what you are asking for, this is possible without compile-time annotation processing. It's not pretty and it might be more complexity than it's worth, but here's one way to do it.

    Here's a custom annotation I made that is used for my shorthand @ApiParam.

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    public @interface GameIdParam {
        String name() default "My Game ID";
    
        String value() default "The integer ID of a particular game";
    }
    

    You can define whatever properties in @ApiParam that you wish to override. Then you can use Springfox's Extension Framework to implement a custom handler for the new annotation.

    import com.google.common.base.Optional;
    import io.swagger.annotations.ApiParam;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.annotation.AnnotationUtils;
    import org.springframework.stereotype.Component;
    import springfox.documentation.schema.Example;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spi.schema.EnumTypeDeterminer;
    import springfox.documentation.spi.service.contexts.ParameterContext;
    import springfox.documentation.spring.web.DescriptionResolver;
    import springfox.documentation.swagger.readers.parameter.ApiParamParameterBuilder;
    
    import java.util.function.Predicate;
    
    import static java.util.Optional.ofNullable;
    import static springfox.documentation.swagger.common.SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER;
    import static springfox.documentation.swagger.common.SwaggerPluginSupport.pluginDoesApply;
    import static springfox.documentation.swagger.readers.parameter.Examples.examples;
    
    @Component
    public class ShorthandAnnotationPlugin extends ApiParamParameterBuilder {
        private final DescriptionResolver descriptions;
        private final EnumTypeDeterminer enumTypeDeterminer;
    
        @Autowired
        public ShorthandAnnotationPlugin(
                DescriptionResolver descriptions,
                EnumTypeDeterminer enumTypeDeterminer) {
            super(descriptions, enumTypeDeterminer);
            this.descriptions = descriptions;
            this.enumTypeDeterminer = enumTypeDeterminer;
        }
    
        @Override
        public void apply(ParameterContext context) {
            Optional<GameIdParam> gameIdParam = context.resolvedMethodParameter().findAnnotation(GameIdParam.class);
    
            if (gameIdParam.isPresent()) {
                GameIdParam annotation = gameIdParam.get();
    
                // Instantiate an ApiParam so we can take default values for attributes we didn't override.
                ApiParam parentAnnotation = AnnotationUtils.synthesizeAnnotation(ApiParam.class);
    
                context.parameterBuilder().name(ofNullable(annotation.name())
                        .filter(((Predicate<String>) String::isEmpty).negate()).orElse(null))
                        .description(ofNullable(descriptions.resolve(annotation.value()))
                                .filter(((Predicate<String>) String::isEmpty).negate()).orElse(null))
                        .parameterAccess(ofNullable(parentAnnotation.access())
                                .filter(((Predicate<String>) String::isEmpty).negate())
                                .orElse(null))
                        .defaultValue(ofNullable(parentAnnotation.defaultValue())
                                .filter(((Predicate<String>) String::isEmpty).negate())
                                .orElse(null))
                        .allowMultiple(parentAnnotation.allowMultiple())
                        .allowEmptyValue(parentAnnotation.allowEmptyValue())
                        .required(parentAnnotation.required())
                        .scalarExample(new Example(parentAnnotation.example()))
                        .complexExamples(examples(parentAnnotation.examples()))
                        .hidden(parentAnnotation.hidden())
                        .collectionFormat(parentAnnotation.collectionFormat())
                        .order(SWAGGER_PLUGIN_ORDER);
            }
        }
    
        @Override
        public boolean supports(DocumentationType documentationType) {
            return pluginDoesApply(documentationType);
        }
    }
    

    I used Springfox's ApiParamParameterBuilder as an example.

    Now, I can use my @GameIdParam

    @PostMapping("/{gameId}/info")
    public String play(@GameIdParam @PathVariable int gameId) // ...
    

    This pattern could be generalized to work with a series of custom shorthand annotations. It's not pretty and it introduces another level of indirection that people who know Springfox Swagger won't be familiar with.

    Hope that helps! Good luck!