Sunday, August 19, 2018

Spring magic: Dynamically set URI prefix for group of controllers

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
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.
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:

  1. 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.
  2. 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. 
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

1 comment:

  1. Thank you for sharing this. Don't we still have any intrinsic alternatives with Spring?

    ReplyDelete

Spring magic: Dynamically set URI prefix for group of controllers

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 d...