I want constraint validation in the method to happen before around advice aspect execution, but I see the opposite happening. The aspect is triggered without validation.
I have the following RestController class:
package com.pkg;
@RestController
@Validated
public class RestController {
@GetMapping("/v1/{id}")
public Object getIDInformation(
@PathVariable("id")
@Pattern(regexp = "^[0-9]*$", message = "Non numeric id")
@Size(min = 9, max = 10, message = "Invalid id")
String id,
HttpServletRequest httpRequest,
SomeClass someObject
)
{
return service.getIDInformation(Long.parseLong(id), someObject);
}
}
Then I have the following around aspect advice in a different class:
@Around(
"execution(* com.pkg.RestController.getIDInformation(..)) && " +
"args(id,httpRequest,..)"
)
public Object aspectMethod(ProceedingJoinPoint pjp, String id, HttpServletRequest httpRequest)
throws Throwable
{
SomeClass someObject = changedValue;
Object[] targetMethodArgs = pjp.getArgs();
if (!valid(id)) {
//throw Exception
}
else {
// Make use of HttpServletRequest httpRequest (not shown here) to modify
// SomeClass someObject argument in the target method
for (int i = 0; i < targetMethodArgs.length; i++) {
if (targetMethodArgs[i] instanceof SomeClass) {
targetMethodArgs[i] = someObject;
}
}
}
return pjp.proceed(targetMethodArgs);
}
If a request is made to GET handler method, the constraint validation for id path variable has to occur first before the around advice can execute. Is there any way I can achieve this?
I am not a Spring user, so I cannot tell you
What I did find out, however, is that once the list of advisors for an advised bean has been set, it is simply applied in the order of elements in the list. You can influence aspect precedence via @Order
or implementing @Ordered
, but I have no idea if that approach can somehow be applied to method validation advisors.
Because I was curious, I created a proof-of-concept, hacky workaround. Here is my MCVE replicating your original situation:
Service, controller and helper classes:
package de.scrum_master.spring.q71219717;
public class SomeClass {
private final String suffix;
public SomeClass(String suffix) {
this.suffix = suffix;
}
public String getSuffix() {
return suffix;
}
}
package de.scrum_master.spring.q71219717;
import org.springframework.stereotype.Component;
@Component
public class MyService {
public String getIDInformation(long id, SomeClass someObject) {
return id + "-" + someObject.getSuffix();
}
}
package de.scrum_master.spring.q71219717;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
@RestController
@Validated
public class MyRestController {
@Autowired
MyService service;
@GetMapping("/v1/{id}")
public Object getIDInformation(
@PathVariable("id")
@Pattern(regexp = "^[0-9]*$", message = "Non-numeric ID ${validatedValue}")
@Size(min = 9, max = 10, message = "ID ${validatedValue} must be {min}-{max} numbers long")
String id,
HttpServletRequest httpRequest,
SomeClass someObject
)
{
return service.getIDInformation(Long.parseLong(id), someObject);
}
}
Aspect:
My dummy for the missing valid(String id)
method in your example simply returns true
if the ID contains a '0'
character.
package de.scrum_master.spring.q71219717;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Aspect
@Component
public class MyRestControllerAspect {
@Around(
"execution(* de.scrum_master.spring.q71219717.MyRestController.getIDInformation(..)) && " +
"args(id, httpRequest, ..)"
)
public Object aspectMethod(ProceedingJoinPoint pjp, String id, HttpServletRequest httpRequest)
throws Throwable
{
System.out.println(pjp + " -> " + id);
SomeClass changedValue = new SomeClass("ASPECT");
SomeClass someObject = changedValue;
Object[] targetMethodArgs = pjp.getArgs();
if (!valid(id)) {
throw new IllegalArgumentException("invalid ID " + id);
}
else {
// Make use of HttpServletRequest httpRequest (not shown here) to modify
// SomeClass someObject argument in the target method
for (int i = 0; i < targetMethodArgs.length; i++) {
if (targetMethodArgs[i] instanceof SomeClass) {
targetMethodArgs[i] = someObject;
}
}
}
return pjp.proceed(targetMethodArgs);
}
private boolean valid(String id) {
return id.contains("0");
}
}
Driver application:
package de.scrum_master.spring.q71219717;
import org.springframework.aop.framework.Advised;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.MethodValidationInterceptor;
import java.util.Arrays;
@SpringBootApplication
@Configuration
public class DemoApplication {
public static void main(String[] args) throws Throwable {
try (ConfigurableApplicationContext appContext = SpringApplication.run(DemoApplication.class, args)) {
doStuff(appContext);
}
}
private static void doStuff(ConfigurableApplicationContext appContext) {
MyRestController restController = appContext.getBean(MyRestController.class);
//reorderAdvisorsMethodValidationFirst(restController);
printIDInfo(restController, "1234567890", "Valid @Pattern, valid @Size, valid for aspect (contains '0')");
printIDInfo(restController, "123456789", "Valid @Pattern, valid @Size, invalid for aspect (does not contain '0')");
printIDInfo(restController, "123", "Valid @Pattern, invalid @Size, invalid for aspect (does not contain '0')");
printIDInfo(restController, "250", "Valid @Pattern, invalid @Size, valid for aspect (contains '0')");
printIDInfo(restController, "x", "Invalid @Pattern, invalid @Size, invalid for aspect (does not contain '0')");
printIDInfo(restController, "A0", "Invalid @Pattern, invalid @Size, valid for aspect (contains '0')");
}
private static void printIDInfo(MyRestController restController, String id, String infoMessage) {
try {
System.out.println(infoMessage);
System.out.println("ID info: " + restController.getIDInformation(id, null, new SomeClass("ABC")));
}
catch (Exception e) {
System.out.println(e);
}
System.out.println("----------");
}
public static void reorderAdvisorsMethodValidationFirst(Object targetBean) {
if (!(targetBean instanceof Advised))
return;
Advised advisedBean = (Advised) targetBean;
Arrays.stream(advisedBean.getAdvisors())
.filter(advisor -> !(advisor.getAdvice() instanceof MethodValidationInterceptor))
.forEach(advisor -> {
advisedBean.removeAdvisor(advisor);
advisedBean.addAdvisor(advisor);
});
}
}
Please note the one helper method call I commented out. When running the application like this, the console log says:
Valid @Pattern, valid @Size, valid for aspect (contains '0')
execution(Object de.scrum_master.spring.q71219717.MyRestController.getIDInformation(String,HttpServletRequest,SomeClass)) -> 1234567890
ID info: 1234567890-ASPECT
----------
Valid @Pattern, valid @Size, invalid for aspect (does not contain '0')
execution(Object de.scrum_master.spring.q71219717.MyRestController.getIDInformation(String,HttpServletRequest,SomeClass)) -> 123456789
java.lang.IllegalArgumentException: invalid ID 123456789
----------
Valid @Pattern, invalid @Size, invalid for aspect (does not contain '0')
execution(Object de.scrum_master.spring.q71219717.MyRestController.getIDInformation(String,HttpServletRequest,SomeClass)) -> 123
java.lang.IllegalArgumentException: invalid ID 123
----------
Valid @Pattern, invalid @Size, valid for aspect (contains '0')
execution(Object de.scrum_master.spring.q71219717.MyRestController.getIDInformation(String,HttpServletRequest,SomeClass)) -> 250
javax.validation.ConstraintViolationException: getIDInformation.id: ID 250 must be 9-10 numbers long
----------
Invalid @Pattern, invalid @Size, invalid for aspect (does not contain '0')
execution(Object de.scrum_master.spring.q71219717.MyRestController.getIDInformation(String,HttpServletRequest,SomeClass)) -> x
java.lang.IllegalArgumentException: invalid ID x
----------
Invalid @Pattern, invalid @Size, valid for aspect (contains '0')
execution(Object de.scrum_master.spring.q71219717.MyRestController.getIDInformation(String,HttpServletRequest,SomeClass)) -> A0
javax.validation.ConstraintViolationException: getIDInformation.id: Non-numeric ID A0, getIDInformation.id: ID A0 must be 9-10 numbers long
As you can see from the logged execution
joinpoints and ensuing IllegalArgumentException
s, the aspect kicks in before method argument validation, just as you described.
Now, let us uncomment (i.e. activate)
reorderAdvisorsMethodValidationFirst(restController);
What the method does, is
MethodValidationInterceptor
adviceThe effect is that now the method validation interceptors take precedence over other advice types for the target bean. The console log consequently changes to:
Valid @Pattern, valid @Size, valid for aspect (contains '0')
execution(Object de.scrum_master.spring.q71219717.MyRestController.getIDInformation(String,HttpServletRequest,SomeClass)) -> 1234567890
ID info: 1234567890-ASPECT
----------
Valid @Pattern, valid @Size, invalid for aspect (does not contain '0')
execution(Object de.scrum_master.spring.q71219717.MyRestController.getIDInformation(String,HttpServletRequest,SomeClass)) -> 123456789
java.lang.IllegalArgumentException: invalid ID 123456789
----------
Valid @Pattern, invalid @Size, invalid for aspect (does not contain '0')
javax.validation.ConstraintViolationException: getIDInformation.id: ID 123 must be 9-10 numbers long
----------
Valid @Pattern, invalid @Size, valid for aspect (contains '0')
javax.validation.ConstraintViolationException: getIDInformation.id: ID 250 must be 9-10 numbers long
----------
Invalid @Pattern, invalid @Size, invalid for aspect (does not contain '0')
javax.validation.ConstraintViolationException: getIDInformation.id: Non-numeric ID x, getIDInformation.id: ID x must be 9-10 numbers long
----------
Invalid @Pattern, invalid @Size, valid for aspect (contains '0')
javax.validation.ConstraintViolationException: getIDInformation.id: ID A0 must be 9-10 numbers long, getIDInformation.id: Non-numeric ID A0
See? Now the aspect only kicks in in the first two cases, after method parameter validation has been passed successfully.
Some Spring AOP internals:
CglibAopProxy.DynamicAdvisedInterceptor.intercept
calls this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass)
.this.advised
is of type AdvisedSupport
, which is a public type, but unfortunately CglibAopProxy.DynamicAdvisedInterceptor
is a private static inner class of CglibAopProxy
and only used internally.AdvisedSupport
instance and e.g. call its setAdvisorChainFactory
method. If that was possible, you could just inject a factory returning a list of advisors in an order different from the default one (a DefaultAdvisorChainFactory
).Maybe some Spring pros here know of a canonical way to influence the internal advisor chain order by configuring Spring in order to wire the application the way you want it to, but I really do not know. I am just an AOP (mostly AspectJ) expert who sometimes looks into more specific Spring AOP questions.
OK, I refactored the original solution into something more generic, using a BeanPostProcessor
. The post-processor will
Advised
(i.e. is a Spring proxy with advisors),@Validated
annotations,The advantage is that there is no more need to manually fetch bean instances from the application context and call reorderAdvisorsMethodValidationFirst(..)
on them one by one. Spring takes care of post-processing each bean, which is how it is supposed to be. Sorry for coming up with this solution only in iteration #2, but like I said, I am a Spring noob.
Updated, simplified driver application:
package de.scrum_master.spring.q71219717;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
@SpringBootApplication
@Configuration
public class DemoApplication {
private static MyRestController restController;
public static void main(String[] args) throws Throwable {
try (ConfigurableApplicationContext appContext = SpringApplication.run(DemoApplication.class, args)) {
doStuff(appContext);
}
}
private static void doStuff(ConfigurableApplicationContext appContext) {
restController = appContext.getBean(MyRestController.class);
printIDInfo("1234567890", "Valid @Pattern, valid @Size, valid for aspect (contains '0')");
printIDInfo("123456789", "Valid @Pattern, valid @Size, invalid for aspect (does not contain '0')");
printIDInfo("123", "Valid @Pattern, invalid @Size, invalid for aspect (does not contain '0')");
printIDInfo("250", "Valid @Pattern, invalid @Size, valid for aspect (contains '0')");
printIDInfo("x", "Invalid @Pattern, invalid @Size, invalid for aspect (does not contain '0')");
printIDInfo("A0", "Invalid @Pattern, invalid @Size, valid for aspect (contains '0')");
}
private static void printIDInfo(String id, String infoMessage) {
try {
System.out.println(infoMessage);
System.out.println("ID info: " + restController.getIDInformation(id, null, new SomeClass("ABC")));
}
catch (Exception e) {
System.out.println(e);
}
System.out.println("----------");
}
}
Bean post-processor:
package de.scrum_master.spring.q71219717;
import org.springframework.aop.framework.Advised;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.MethodValidationInterceptor;
import java.util.Arrays;
@Component
public class MethodValidationFirstBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof Advised) {
Advised advisedBean = (Advised) bean;
if (advisedBean.getTargetSource().getTargetClass().isAnnotationPresent(Validated.class)) {
System.out.println("Reordering advisors to \"method validation first\" for bean " + beanName);
reorderAdvisorsMethodValidationFirst(advisedBean);
}
}
return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
}
public void reorderAdvisorsMethodValidationFirst(Advised advisedBean) {
Arrays.stream(advisedBean.getAdvisors())
.filter(advisor -> !(advisor.getAdvice() instanceof MethodValidationInterceptor))
.forEach(advisor -> {
advisedBean.removeAdvisor(advisor);
advisedBean.addAdvisor(advisor);
});
}
}
The console logs with and without the active post-processor remain the same as in the original solution.