Overview
Say you have a big Spring or Spring Boot project with a lot of controller, which you want do separate in different groups with different prefixes.
Annotation
Create annotation which is metaannotated with @Controler or @RestController, It will be used as a marker for controller for which we want to change prefix. In our example this annotation is @ApiController
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package co.jware.uri.demo.annotation; | |
import org.springframework.web.bind.annotation.RestController; | |
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
@RestController | |
@Target(ElementType.TYPE) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface ApiController { | |
} |
Controller
Annotate your controllers with marker annotation (@ApiContgroller) and put @RequestMapping annotation on path level with path attribute set. This attribute will be changed by the postprocessor.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package co.jware.uri.demo.controllers; | |
import co.jware.uri.demo.annotation.ApiController; | |
import org.springframework.web.bind.annotation.GetMapping; | |
import org.springframework.web.bind.annotation.RequestMapping; | |
@ApiController | |
@RequestMapping("/demo") | |
public class DemoController { | |
@GetMapping | |
public String handle() { | |
return "demo"; | |
} | |
} |
Postprocessor
Spring uses postprocessors to enhance beans. We use postprocessor which selects beans on existence of our marker annotation (@ApiController); then checks for @RequestMapping annotation and copies all its attributes in a map. Now we have all attributes at disposiotion and can change it as we like. In our our case we change only value and path attributes by appending configured prefix. This prefix is taken from the environment, but other sources can be implemente.
The postprocessor is able to process only one marker annotation, but this can be enhanced in future versions.
Two things are very important in postprocessor implementation:- Annotation Data in Class - java.la ng.Class has method called annotationData(),which gives acces to allannotation information. It has to be accessedvia reflection since it is not part of the public API.
- Syntesised annotations Spring framework has special way of annotation processing to support some non trivial features as @AliasFor support.We utilise this mechanism to create clone with changed attributes and to substitute annotatios at runtime.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package co.jware.uri.demo.spring; | |
import co.jware.uri.demo.annotation.ApiController; | |
import lombok.extern.slf4j.Slf4j; | |
import org.springframework.beans.BeansException; | |
import org.springframework.beans.factory.config.BeanPostProcessor; | |
import org.springframework.context.EnvironmentAware; | |
import org.springframework.core.annotation.AnnotationUtils; | |
import org.springframework.core.env.Environment; | |
import org.springframework.web.bind.annotation.RequestMapping; | |
import java.lang.annotation.Annotation; | |
import java.lang.reflect.Field; | |
import java.lang.reflect.Method; | |
import java.util.Arrays; | |
import java.util.Map; | |
@Slf4j | |
public class ApiControllerAnnotationBeanPostProcessor implements BeanPostProcessor, EnvironmentAware { | |
private static final String ANNOTATION_METHOD = "annotationData"; | |
private static final String DECLARED_ANNOTATIONS = "declaredAnnotations"; | |
private Environment env; | |
@Override | |
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { | |
ApiController annotation = AnnotationUtils.findAnnotation(bean.getClass(), ApiController.class); | |
if (null != annotation) { | |
RequestMapping requestMapping = AnnotationUtils.findAnnotation(bean.getClass(), RequestMapping.class); | |
if (null != requestMapping) { | |
Map<String, Object> attributes = AnnotationUtils.getAnnotationAttributes(requestMapping); | |
String apiPrefix = env.getProperty("api.prefix", "/api"); | |
String[] values = (String[]) attributes.get("path"); | |
values = Arrays.stream(values) | |
.filter(s -> !s.startsWith(apiPrefix)) | |
.map(s -> apiPrefix + s).toArray(String[]::new); | |
if (values.length > 0) { | |
attributes.put("value", values); | |
attributes.put("path", values); | |
RequestMapping target = AnnotationUtils.synthesizeAnnotation(attributes, | |
RequestMapping.class, null); | |
changeAnnotationValue(bean.getClass(), RequestMapping.class, target); | |
// changeAnnotationValueMethodHandles(bean.getClass(), RequestMapping.class, target); | |
} | |
} | |
} | |
return bean; | |
} | |
@SuppressWarnings("unchecked") | |
private static void changeAnnotationValue(Class<?> targetClass, Class<? extends Annotation> targetAnnotation, Annotation targetValue) { | |
try { | |
Method method = Class.class.getDeclaredMethod(ANNOTATION_METHOD); | |
method.setAccessible(true); | |
Object annotationData = method.invoke(targetClass); | |
Field annotations = annotationData.getClass().getDeclaredField(DECLARED_ANNOTATIONS); | |
annotations.setAccessible(true); | |
Map<Class<? extends Annotation>, Annotation> map = (Map<Class<? extends Annotation>, Annotation>) annotations.get(annotationData); | |
map.put(targetAnnotation, targetValue); | |
} catch (Exception e) { | |
log.error("Error changing annotation", e); | |
} | |
} | |
@Override | |
public void setEnvironment(Environment environment) { | |
env = environment; | |
} | |
} |
Project
Source code for example project can be found on GitHub