Search code examples
javaannotationsaopaspectjspring-aop

Get parameters of different annotations in a single Pointcut


I need to log whenever a RESTendpoint gets called. I'm trying to do this with spring AOP.

Among other things I need to long what endpoint was called. I.e I need to read out the value of the Mapping annotation.

I want to solve this in a generic way. I.e "Give me the value of the Mapping whatever the exact mapping is".

So what I was doing for now is basically what was proposed in this answer: https://stackoverflow.com/a/26945251/2995907

@Pointcut("@annotation(getMapping)")
    public void getMappingAnnotations(GetMapping getMapping){ }

Then I pass getMapping to my advice and get out the value of that.

To be able to select whatever mapping I encounter I was following the accepted answer from this question: Spring Aspectj @Before all rest method

@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping) " +
    "|| @annotation(org.springframework.web.bind.annotation.GetMapping)" +
    "|| @annotation(org.springframework.web.bind.annotation.PostMapping)" +
    "|| @annotation(org.springframework.web.bind.annotation.PathVariable)" +
    "|| @annotation(org.springframework.web.bind.annotation.PutMapping)" +
    "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)"
)
public void mappingAnnotations() {}

I'd like to just write something like

public void mappingAnnotations(RequestMapping requestMapping) {}

and then get the value out of it, since all the Mappings are aliases for RequestMapping. Unfortunately, this did not work. Until now it looks like I have to do a separate pointcut for every kind of mapping and then having a method for each of them (which would be very similar - not very DRY) or quite an ugly if-else-block (maybe I could make it a switch with some fiddeling).

So the question is how I could solve this in a clean way. I just want to log any kind of mapping and get the corresponding path-argument which the annotation carries.


Solution

  • I would have given the same answer as Nándor under usual circumstances. AspectJ bindings to parameters from different branches of || are ambiguous because both branches could match, so this is a no-go.

    With regard to @RequestMapping, all the other @*Mapping annotations are syntactic sugar and documented to be composed annotations acting as shortcuts, see e.g. @GetMapping:

    Specifically, @GetMapping is a composed annotation that acts as a shortcut for @RequestMapping(method = RequestMethod.GET).

    I.e. the type GetMapping itself is annotated by @RequestMapping(method = RequestMethod.GET). The same applies to the other composed (syntactic sugar) annotations. We can utilise this circumstance for our aspect.

    AspectJ has a syntax for finding an annotated annotation (also nested), see e.g. my answer here. We can use that syntax in this case in order to generically match all annotations annotated by @RequestMapping.

    This still leaves us with two cases, i.e. direct annotation and syntactic sugar annotation, but it simplifies the code a bit anyway. I came up with this pure Java + AspectJ sample application, only imported the spring-web JAR in order to have access to the annotations. I do not use Spring otherwise, but the pointcuts and advice would look the same in Spring AOP, you can even eliminate the && execution(* *(..)) part from the first pointcut because Spring AOP does not know anything but execution pointcuts anyway (but AspectJ does and would also match call(), for instance).

    Driver application:

    package de.scrum_master.app;
    
    import org.springframework.web.bind.annotation.*;
    import static org.springframework.web.bind.annotation.RequestMethod.*;
    
    public class Application {
      @GetMapping public void get() {}
      @PostMapping public void post() {}
      @RequestMapping(method = HEAD) public void head() {}
      @RequestMapping(method = OPTIONS) public void options() {}
      @PutMapping public void put() {}
      @PatchMapping public void patch() {}
      @DeleteMapping @Deprecated public void delete() {}
      @RequestMapping(method = TRACE) public void trace() {}
      @RequestMapping(method = { GET, POST, HEAD}) public void mixed() {}
    
      public static void main(String[] args) {
        Application application = new Application();
        application.get();
        application.post();
        application.head();
        application.options();
        application.put();
        application.patch();
        application.delete();
        application.trace();
        application.mixed();
      }
    }
    

    Please note how I mixed different annotation types and how I also added another annotation @Deprecated to one method in order to have a negative test case for an annotation we are not interested in.

    Aspect:

    package de.scrum_master.aspect;
    
    import java.lang.annotation.Annotation;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    @Aspect
    public class RequestMappingAspect {
    
      @Before("@annotation(requestMapping) && execution(* *(..))")
      public void genericMapping(JoinPoint thisJoinPoint, RequestMapping requestMapping) {
        System.out.println(thisJoinPoint);
        for (RequestMethod method : requestMapping.method())
          System.out.println("  " + method);
      }
    
      @Before("execution(@(@org.springframework.web.bind.annotation.RequestMapping *) * *(..))")
      public void metaMapping(JoinPoint thisJoinPoint) {
        System.out.println(thisJoinPoint);
        for (Annotation annotation : ((MethodSignature) thisJoinPoint.getSignature()).getMethod().getAnnotations()) {
          RequestMapping requestMapping = annotation.annotationType().getAnnotation(RequestMapping.class);
          if (requestMapping == null)
            continue;
          for (RequestMethod method : requestMapping.method())
            System.out.println("  " + method);
        }
      }
    
    }
    

    Console log:

    execution(void de.scrum_master.app.Application.get())
      GET
    execution(void de.scrum_master.app.Application.post())
      POST
    execution(void de.scrum_master.app.Application.head())
      HEAD
    execution(void de.scrum_master.app.Application.options())
      OPTIONS
    execution(void de.scrum_master.app.Application.put())
      PUT
    execution(void de.scrum_master.app.Application.patch())
      PATCH
    execution(void de.scrum_master.app.Application.delete())
      DELETE
    execution(void de.scrum_master.app.Application.trace())
      TRACE
    execution(void de.scrum_master.app.Application.mixed())
      GET
      POST
      HEAD
    

    It is not perfect with regard to DRY, but we can only go as far as possible. I still think it is compact, readable and maintainable without having to list every single annotation type to be matched.

    What do you think?


    Update:

    If you want to get the values for "syntactic sugar" request mapping annotations, the whole code looks like this:

    package de.scrum_master.app;
    
    import org.springframework.web.bind.annotation.*;
    import static org.springframework.web.bind.annotation.RequestMethod.*;
    
    public class Application {
      @GetMapping public void get() {}
      @PostMapping(value = "foo") public void post() {}
      @RequestMapping(value = {"foo", "bar"}, method = HEAD) public void head() {}
      @RequestMapping(value = "foo", method = OPTIONS) public void options() {}
      @PutMapping(value = "foo") public void put() {}
      @PatchMapping(value = "foo") public void patch() {}
      @DeleteMapping(value = {"foo", "bar"}) @Deprecated public void delete() {}
      @RequestMapping(value = "foo", method = TRACE) public void trace() {}
      @RequestMapping(value = "foo", method = { GET, POST, HEAD}) public void mixed() {}
    
      public static void main(String[] args) {
        Application application = new Application();
        application.get();
        application.post();
        application.head();
        application.options();
        application.put();
        application.patch();
        application.delete();
        application.trace();
        application.mixed();
      }
    }
    
    package de.scrum_master.aspect;
    
    import java.lang.annotation.Annotation;
    import java.lang.reflect.InvocationTargetException;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    @Aspect
    public class RequestMappingAspect {
    
      @Before("@annotation(requestMapping) && execution(* *(..))")
      public void genericMapping(JoinPoint thisJoinPoint, RequestMapping requestMapping) {
        System.out.println(thisJoinPoint);
        for (String value : requestMapping.value())
          System.out.println("  value = " + value);
        for (RequestMethod method : requestMapping.method())
          System.out.println("  method = " + method);
      }
    
      @Before("execution(@(@org.springframework.web.bind.annotation.RequestMapping *) * *(..))")
      public void metaMapping(JoinPoint thisJoinPoint) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
        System.out.println(thisJoinPoint);
        for (Annotation annotation : ((MethodSignature) thisJoinPoint.getSignature()).getMethod().getAnnotations()) {
          RequestMapping requestMapping = annotation.annotationType().getAnnotation(RequestMapping.class);
          if (requestMapping == null)
            continue;
          for (String value : (String[]) annotation.annotationType().getDeclaredMethod("value").invoke(annotation))
            System.out.println("  value = " + value);
          for (RequestMethod method : requestMapping.method())
            System.out.println("  method = " + method);
        }
      }
    
    }
    

    The console log then looks like this:

    execution(void de.scrum_master.app.Application.get())
      method = GET
    execution(void de.scrum_master.app.Application.post())
      value = foo
      method = POST
    execution(void de.scrum_master.app.Application.head())
      value = foo
      value = bar
      method = HEAD
    execution(void de.scrum_master.app.Application.options())
      value = foo
      method = OPTIONS
    execution(void de.scrum_master.app.Application.put())
      value = foo
      method = PUT
    execution(void de.scrum_master.app.Application.patch())
      value = foo
      method = PATCH
    execution(void de.scrum_master.app.Application.delete())
      value = foo
      value = bar
      method = DELETE
    execution(void de.scrum_master.app.Application.trace())
      value = foo
      method = TRACE
    execution(void de.scrum_master.app.Application.mixed())
      value = foo
      method = GET
      method = POST
      method = HEAD
    

    Update 2:

    If you want to hide the reflection stuff by using Spring's AnnotatedElementUtils and AnnotationAttributes as originally suggested by @M. Prokhorov, you can utilise the fact that with getMergedAnnotationAttributes you can actually get one-stop shopping for both the original RequestMapping annotation and syntax sugar ones like GetMapping, getting both method and value information in a single, merged attribute object. This even enables you to eliminate the two different cases for getting the information and thus merge the two advices into one like this:

    package de.scrum_master.aspect;
    
    import static org.springframework.core.annotation.AnnotatedElementUtils.getMergedAnnotationAttributes;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.core.annotation.AnnotationAttributes;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    /**
     * See https://stackoverflow.com/a/53892842/1082681
     */
    @Aspect
    public class RequestMappingAspect {
      @Before(
        "execution(@org.springframework.web.bind.annotation.RequestMapping * *(..)) ||" +
        "execution(@(@org.springframework.web.bind.annotation.RequestMapping *) * *(..))"
      )
      public void metaMapping(JoinPoint thisJoinPoint) {
        System.out.println(thisJoinPoint);
          AnnotationAttributes annotationAttributes = getMergedAnnotationAttributes(
            ((MethodSignature) thisJoinPoint.getSignature()).getMethod(),
            RequestMapping.class
          );
          for (String value : (String[]) annotationAttributes.get("value"))
            System.out.println("  value = " + value);
          for (RequestMethod method : (RequestMethod[]) annotationAttributes.get("method"))
            System.out.println("  method = " + method);
      }
    }
    

    There you have it: DRY as you originally wished for, fairly readable and maintainable aspect code and access to all (meta) annotation information in an easy way.