0

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.

dingalapadum
  • 2,077
  • 2
  • 21
  • 31
  • I think what you should do is accept the `JoinPoint` as parameter, obtain a `Signature` from that (it would be a `MethodSignature`), and then inspect the method you get from the signature with Spring's `AnnotatedElementUtils` to obtain the metadata of your annotations. – M. Prokhorov Dec 20 '18 at 18:24
  • @M.Prokhorov Thx! I am using `JoinPoint`s already. The only solution I could think of was reflection. But `AnnotatedElementUtils`sounds promising! I'll have a look at it. – dingalapadum Dec 20 '18 at 18:34
  • I think what you'd be looking for there is `getMergedAnnotationAttributes` with right parameters (such as `RequestMapping.class` annotation type). If that helps, I can later make an answer out of my comments. – M. Prokhorov Dec 20 '18 at 18:38
  • @M.Prokhorov I guess you mean `AnnotatedElementUtils.findMergedAnnotationAttributes`? I have to figure out how to use `AnnotatedElement`s and all these things.. But I'll give it a try... thx – dingalapadum Dec 20 '18 at 19:01
  • @M.Prokhorov the more I look into this, it feels like I could just use reflection... – dingalapadum Dec 20 '18 at 19:14
  • Java's annotated element is anything in Java that can be annotated, including methods. That Util already uses reflection, point is - it uses it in the same way Spring itself does, and the more you write yourself, the more possibility for bugs there is. – M. Prokhorov Dec 20 '18 at 19:49
  • @M.Prokhorov fair enough. But, the thing is, I'm not finding it easier or more handy or anything with the AnnotatedElementUtils. I still have the issue that I basically need to "if get.. , if post..., if put.." at some point. I'm not seeing how I can avoid that with AnnotatedElementUtils - if you can tell me how that works, I'm happy to use it. But right now it seems easier to get the annotation, convert them toString and match "*Mapping" to 'select' the correct annotation, and from there move on to getting the value of that annotation...it's horrible I know, but I'm not seeing a cleaner way. – dingalapadum Dec 20 '18 at 19:55
  • Interesting question. FYI, I have an idea which does not involve reflection as discussed above, but am busy right now. If you can wait until tonight (in my current South-East Asian time zone) or tomorrow latest, I might be able to prepare something for you. But I will only do so if you tell me that you don't go with the suggested approach. I just want to be sure that my time will be well-spent. – kriegaex Dec 21 '18 at 07:41
  • @kriegaex I was hoping to get *your* attention ;). Unfortunately, we have a release today and I finished implementing this yesterday. I ended up creating pointcuts and advices for each http-verb and one common pointcut. The good thing is that now we have pretty finegrained control, and I can also log the verb easily. The bad is that it is not very DRY code... I still would be very interested in seeing what the ‚proper‘ way to achieve this is.. maybe I‘ll refactor it if I find the time. but don‘t bother if it is too much effort. – dingalapadum Dec 21 '18 at 11:19
  • You cannot at the same time filter to a _disjunction_ of `@annotation` pointcuts __and__ bind a variable to the value of the matched annotation. I would suggest you to create multiple advices that each match a different annotation, extract the value of the annotation if applicable, and delegate the rest of the functionality to a common method. Although for a different question, you can see a similar approach in this [answer](https://stackoverflow.com/a/34426950/2699901). – Nándor Előd Fekete Dec 21 '18 at 23:39
  • @NándorElődFekete thanks for your suggestion. Funnily enough, if you read the comment right before your comment, you can see that this is what I actually did (multiple advices). But it is good to see somebody supporting this idea. – dingalapadum Dec 22 '18 at 23:27

2 Answers2

2

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.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • Thanks a lot! So `metaMapping` will also capture `GetMapping` because this is just an alias for the `RequestMapping(method=GET)`? And if so: when I call `requestMapping.value()` in `metaMapping` will I get the value which was passed to the `GetMapping`? If so, then I would accept this answer. If not, I guess the 'accepted answer' would be the one saying 'to make advices for each of the `*mapping`s' (which is what I ended up doing). Anyway, I'm glad and thankful to see this idea - I really didn't think about it, so +1. If getting the value is possible, I'll mark it as accepted. – dingalapadum Dec 22 '18 at 23:19
  • Did you actually run the code? It would have answered your questions. Answer 1: Yes, it captures `GetMapping`, as can clearly be seen from the log output. Answer 2: No, `requestMapping.value()` would not print the value of `GetMapping` because it contains the meta-annotation `RequestMapping`. You need to modify the code a bit in order to get the actual value and that involves reflection and makes things a bit uglier, see my updated answer. – kriegaex Dec 23 '18 at 00:03
  • Please also see update #2. I think I found the best way to merge my own approach with _@M. Prokhorov_'s suggestion to use Spring's annotation utility classes. Thanks to him as well. – kriegaex Dec 23 '18 at 00:44
  • Yes, I should have run the code myself. Sorry not doing that. I wasn't at work over the weekend and was quite busy with x-mas and family business, that's way I didn't try it on my private machine. I apologize for that. Anyway, yes after seeing your solution I thought it could be combined with Prokhorov's answer. But if I'm thinking through this correctly, I guess I'd still need to be a bit careful with getting the `value` this way, in case some other annotation also had a value field. But I think this is pretty much as good as it gets. Thank you a lot for your efforts and happy holidays. – dingalapadum Dec 24 '18 at 12:37
  • Why would other annotations' `value`s bother you? I am gathering `getMergedAnnotationAttributes(, RequestMapping.class)` only, so you should be fine. – kriegaex Dec 24 '18 at 14:45
  • Oh, right! My bad! Looks solid. I will refactor this when I get back to work! Thank you once again. – dingalapadum Dec 24 '18 at 16:00
1

You can accept a JoinPoint in any Spring aspect, and from that you can extract a call Signature (which, in your case, should always be a MethodSignature). Then you can work with that signature to get a method that was called.

Once you got the method, you can then use Spring's meta-annotation API to obtain all relevant attributes you want from the mapping annotation.

Example code:

@PutMapping(path = "/example", consumes = "application/json")
void exampleWebMethod(JsonObject json) {
  /* implementation */
}

/**
 * Your aspect. I used simplified pointcut definition, but yours should work too.
 */
@Before("@annotation(org.springframework.web.bind.annotation.PutMapping)")
public void beforeRestMethods(JoinPoint jp) {
    MethodSignature sgn = (MethodSignature) jp.getSignature();
    Method method = sgn.getMethod();

    AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(
            method,
            RequestMapping.class
    );

    // and a simple test that this works.
    assertEquals(new String[] {"/example"}, attributes.getStringArray("path"));
    assertEquals(new String[] {"application/json"}, attributes.getStringArray("consumes"));

    // notice that this also works, because PutMapping is itself annotated with
    // @RequestMethod(method = PUT), and Spring's programming model lets you discover that

    assertEquals(new RequestMethod[] {RequestMethod.PUT}, (Object[]) attributes.get("method"));
}

If you really want, you can also have Spring sythnesize the annotation for you, like this:

RequestMapping mapping = AnnotatedElementUtils.getMergedAnnotation(
        method,
        RequestMapping.class
);

Spring will then create a proxy that implements the annotation interface, which will allow calling methods on it as if that was the actual annotation obtained from the method, but with support of Spring's meta-annotations.

M. Prokhorov
  • 3,894
  • 25
  • 39
  • Yes, at some point I had something similar to this. But I am not a huge fan of the `attributes.getStringArray("path")`. I'd prefer something like `RequestMapping.value()` or `RequestMapping.path()`. As I have it now, with multiple pointcuts for each http-verb, I am able to have `postMapping.value()[0]`, which also isn't the prettiest thing (because of indexOutOfBounds check and so on) but at least it's the proper way to access a field... you see what I mean? It might be just a matter of taste, but a typo in `.path()`is caught by the compiler... `"path"`, well... you guess you get the point – dingalapadum Dec 21 '18 at 16:17
  • I don't really see a huge point in additionally wrapping the attributes in something else, but you can have Spring synth a proxy of the annotation for you which you'd be able to call methods on. I added this in my answer. But, this has downsides in generating more garbage in forms of field accessors and a proxy itself on top of attributes from the previous version of the answer. – M. Prokhorov Dec 21 '18 at 17:51
  • Ok. I see. It wasn‘t the wrapping which I was looking for. IMHO this whole approach (which btw is sinilar to what I did), feels cluttered with boilerplate. I mean - we get called because of the PutMapping annotation, but now we go over the joinpont, signature, method (+casting) to then access the annotation (which was the reason why we got called in the first place!) to finally get to the value that we actually want. Maybe this reallyis how it‘s supposed to be, but I was hoping for something more direct and elegant. Anyway, +1 because I think it is a legitimate approach. – dingalapadum Dec 21 '18 at 18:28
  • There isn't any more direct approach than this which would allow you to also obtain something from the annotation by calling methods on it. Spring does all that I described itself to provide you with an annotation argument to your aspects. Unfortunately, annotation interface is not extendable, and cannot extend other interfaces, so meta-annotation is as close to annotation inheritance as it gets, at least for Java 8/10, and this is the only way known to me to work with several annotation types using common code. – M. Prokhorov Dec 21 '18 at 19:46