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