简体   繁体   中英

Extend class level RequestMapping with custom annotation

I would like to create a custom annotation in my Spring Boot application which always adds a prefix to my class level RequestMapping path .

My Controller:

import com.sagemcom.smartvillage.smartvision.common.MyApi;
import org.springframework.web.bind.annotation.GetMapping;

@MyApi("/users")
public class UserController {

    @GetMapping("/stackoverflow")
    public String get() {
        return "Best users";
    }

}

My custom annotation

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
@RequestMapping(path = "/api")
public @interface MyApi {

    @AliasFor(annotation = RequestMapping.class)
    String value();

}

GOAL: a mapping like this in the end: /api/users/stackoverflow

Notes:

  • server.servlet.context-path is not an option because I want to create several of these
  • I'm using Spring Boot version 2.0.4

I was not able to find an elegant solution for the issue. However, this worked:

Slightly modified annotation, because altering behavior of value turned out to be more difficult.

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
@RequestMapping
public @interface MyApi {

    @AliasFor(annotation = RequestMapping.class, attribute = "path")
    String apiPath();

}

Bean Annotation Processor

import com.sagemcom.smartvillage.smartvision.common.MyApi;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;

@Component
public class MyApiProcessor implements BeanPostProcessor {

    private static final String ANNOTATIONS = "annotations";
    private static final String ANNOTATION_DATA = "annotationData";

    public Object postProcessBeforeInitialization(@NonNull final Object bean, String beanName) throws BeansException {
        MyApi myApi = bean.getClass().getAnnotation(MyApi.class);
        if (myApi != null) {
            MyApi alteredMyApi = new MyApi() {

                @Override
                public Class<? extends Annotation> annotationType() {
                    return MyApi.class;
                }

                @Override
                public String apiPath() {
                    return "/api" + myApi.apiPath();
                }

            };
            alterAnnotationOn(bean.getClass(), MyApi.class, alteredMyApi);
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(@NonNull Object bean, String beanName) throws BeansException {
        return bean;
    }

    @SuppressWarnings("unchecked")
    private static void alterAnnotationOn(Class clazzToLookFor, Class<? extends Annotation> annotationToAlter, Annotation annotationValue) {
        try {
            // In JDK8 Class has a private method called annotationData().
            // We first need to invoke it to obtain a reference to AnnotationData class which is a private class
            Method method = Class.class.getDeclaredMethod(ANNOTATION_DATA, null);
            method.setAccessible(true);
            // Since AnnotationData is a private class we cannot create a direct reference to it. We will have to manage with just Object
            Object annotationData = method.invoke(clazzToLookFor);
            // We now look for the map called "annotations" within AnnotationData object.
            Field annotations = annotationData.getClass().getDeclaredField(ANNOTATIONS);
            annotations.setAccessible(true);
            Map<Class<? extends Annotation>, Annotation> map = (Map<Class<? extends Annotation>, Annotation>) annotations.get(annotationData);
            map.put(annotationToAlter, annotationValue);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

Controller:

import com.sagemcom.smartvillage.smartvision.common.MyApi;
import org.springframework.web.bind.annotation.GetMapping;

@MyApi(apiPath = "/users")
public class UserController {

    @GetMapping("/stackoverflow")
    public String get() {
        return "Best users";
    }

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM