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