5

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
2
  • If you need a different mappings (completely different url mappings) altogether, you may need to create different controllers. Having all of them in a single controller is not a good design practice. Commented Aug 17, 2018 at 15:29
  • 1
    @Jawa : I want to create different controllers, some of them under /api, some under something different, like /dashboard. Commented Aug 17, 2018 at 15:37

1 Answer 1

3

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";
    }

}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.