0

Using spring boot 2 on java 11, I want to make a custom annotation for each REST API version (eg: "/api/v1/") that can be joined with subsequent URI components as below:

@APIv1("/users/")    // this annotation should prepend "/api/v1/{argument}"
public class UserController {
    @GetMapping("/info")
    public String info() {return "This should be returned at /api/v1/users/info/";}

    /* More methods with mappings */
}

The problem is I don't know how to define that @APIv1 annotation. From what I've searched, I referenced https://stackoverflow.com/a/51182494/ to write the following:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
@RequestMapping("/api/v1")
@interface APIv1 {
    @AliasFor(annotation = RestController.class)
    String value() default "";
}

But this cannot handle arguments. Doing the same as above will route to /api/v1/info/ whether the argument is given or not. It's better than nothing since I can switch the method annotation to @GetMapping("/users/info"), but I was wondering if there was a way to combine the constant with an argument to reduce repetition across method annotations within the same controller class.

2 Answers2

0

In @APIv1 you defined:

@RequestMapping("/api/v1")

So it is working as you told it to.

Bill Mair
  • 1,073
  • 6
  • 15
  • Yes, that's the best I was able to get so far, but I was wondering if there was a different way so that I can concatenate it with the argument given to my custom annotation. Both are compile-time constant string literals so I thought there might be a way – aPatchyDev Feb 18 '23 at 14:03
0

After trying around with different solutions proposed in similar questions, I settled on using my custom annotation to hold the prefix path separately and join them in WebMvcConfigurer

Usage in controllers:

@APIv1("/users")
public class UserController {
    @GetMapping("/info")
    public String info() {return "This should be returned at /api/v1/users/info/";}

    /* More methods with mappings */
}

where the custom annotations are defined as follows (in a separate package, ie: com.example.myannotations):

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PrefixMapping {
    /**
     * The prefix to prepend to path mappings
     * */
    String value() default "";
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@RestController
@RequestMapping
@PrefixMapping("/api/v1")    // Prefix to add when using this annotation
public @interface APIv1 {
    /**
     * Alias for {@link RequestMapping#value}.
     */
    @AliasFor(annotation = RequestMapping.class)
    String value() default "";
}

And define a WebMvcConfigurer that will search for annotations on startup

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false) {
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                return true;
            }
        };
        provider.addIncludeFilter(new AnnotationTypeFilter(PrefixMapping.class));

        String pkgName = "com.example.myannotations";    // Package name where the custom annotations are defined in
        provider.findCandidateComponents(pkgName).forEach(bean -> {
            try {
                String className = bean.getBeanClassName();
                Class<? extends Annotation> clz = (Class<? extends Annotation>) Class.forName(className);

                String prefix = clz.getDeclaredAnnotation(PrefixMapping.class).value();
                configurer.addPathPrefix(prefix, HandlerTypePredicate.forAnnotation(clz));
            } catch (ClassNotFoundException | ClassCastException e) {
                e.printStackTrace(System.err);
            }
        });
    }

}

Now if I want an annotation with different prefix, I can define the annotation without changing anything else.

One thing that troubled me when writing the WebConfig class was getting the Class object for the annotation class from the BeanDefinition returned by findCandidateComponents(pkgName) even though there were supposed to be functions for getting them available.

Functions I tried                                       -> resulting Class<?>::getName() or null

- ClassUtils.getUserClass(bean.getClass())              -> org.springframework.context.annotation.ScannedGenericBeanDefinition
- AopUtils.getTargetClass(bean.getClass())              -> java.lang.Class    - ScannedGenericBeanDefinition if bean instead of bean.class()
- AopProxyUtils.ultimateTargetClass(bean.getClass())    -> java.lang.Class    - ScannedGenericBeanDefinition if bean instead of bean.class()
- AopProxyUtils.proxiedUserInterfaces(bean.getClass())  -> [java.io.Serializable, java.lang.reflect.GenericDeclaration, java.lang.reflect.Type, java.lang.reflect.AnnotatedElement]    - [org.springframework.beans.factory.annotation.AnnotatedBeanDefinition] if bean instead of bean.getClass()
- bean.getResolvableType().resolve()                    -> null
- bean.getResolvableType().toClass()                    -> java.lang.Object
- bean.getResolvableType().getRawClass()                -> null