如何在 GET 方法中的约束验证后执行 RestController GET 方法上的 @Around 建议(Spring AOP)?
How to make @Around advice (Spring AOP) on RestController GET Method execute after constraint validation in the GET method?
我希望方法中的约束验证发生在围绕建议方面执行之前,但我看到相反的情况发生。该方面未经验证即被触发。
我有以下 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);
}
}
然后我在另一个 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);
}
如果向 GET 处理程序方法发出请求,则必须先对 id 路径变量进行约束验证,然后才能执行周围建议。有什么办法可以实现吗?
前言
我不是Spring用户,所以我不能告诉你
- 如果有任何方法可以影响任何建议 bean 的顾问程序应用顺序,non-invasive 方式,
- 确切地 Spring 在连接应用程序时创建与建议 bean 关联的顾问列表。
然而,我确实发现,一旦设置了建议 bean 的顾问列表,它就会按照列表中元素的顺序简单地应用。您可以通过 @Order
或实施 @Ordered
影响方面优先级,但我不知道这种方法是否可以以某种方式应用于方法验证顾问。
概念验证,版本 1
因为好奇,我创建了一个 proof-of-concept,hacky 解决方法。这是我 MCVE 复制您的原始情况:
服务、控制器和助手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);
}
}
看点:
如果 ID 包含 '0'
字符,我的示例中缺少 valid(String id)
方法的虚拟对象只是 returns true
。
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");
}
}
驱动申请:
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);
});
}
}
请注意我注释掉的一个辅助方法调用。当 运行 应用程序像这样时,控制台日志显示:
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
正如您从记录的 execution
连接点和随后的 IllegalArgumentException
中看到的那样,正如您所描述的,方面在方法参数验证之前开始。
现在,让我们取消注释(即激活)
reorderAdvisorsMethodValidationFirst(restController);
该方法的作用是
- 检查目标对象是否是建议的 Spring bean,
- 如果是这样,只需简单地重新排序顾问列表
- 暂时删除每个没有
MethodValidationInterceptor
建议的顾问
- 然后立即再次将其追加到列表末尾。
效果是现在方法验证拦截器优先于目标 bean 的其他建议类型。控制台日志因此更改为:
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
看到了吗?现在该方面仅在前两种情况下启动,在方法参数验证成功通过后。
一些Spring AOP内部:
- 方法
CglibAopProxy.DynamicAdvisedInterceptor.intercept
调用 this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass)
。
this.advised
是AdvisedSupport
类型,是public类型,可惜CglibAopProxy.DynamicAdvisedInterceptor
是[=33的private static inner class =] 且仅在内部使用。
- 所以没有好的方法来获取
AdvisedSupport
实例,例如调用它的 setAdvisorChainFactory
方法。如果可能的话,您可以注入一个工厂,以不同于默认顺序的顺序返回顾问列表(DefaultAdvisorChainFactory
)。
也许这里的某些 Spring 专业人士知道通过配置 Spring 影响内部顾问链顺序的规范方法,以便按照您希望的方式连接应用程序,但我确实知道不知道。我只是一名 AOP(主要是 AspectJ)专家,有时会研究更具体的 Spring AOP 问题。
概念验证,版本 2
好的,我使用 BeanPostProcessor
将原始解决方案重构为更通用的东西。 post-processor 将
- 为每个 Spring 个实例化 bean 自动调用,
- 检查创建的 bean 是否是
Advised
(即是一个带有顾问的 Spring 代理),
- 建议 bean 的过滤器 class 带有
@Validated
注释,
- re-order 顾问喜欢我原来的解决方案。
优点是不再需要手动从应用程序上下文中获取 bean 实例并对其一一调用 reorderAdvisorsMethodValidationFirst(..)
。 Spring 照顾 post-processing 每个 bean,这是它应该的样子。很抱歉只在第 2 次迭代中提出这个解决方案,但就像我说的,我是一个 Spring 菜鸟。
更新、简化的驱动程序应用程序:
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("----------");
}
}
豆子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);
});
}
}
包含和不包含活动 post-processor 的控制台日志与原始解决方案中的相同。
我希望方法中的约束验证发生在围绕建议方面执行之前,但我看到相反的情况发生。该方面未经验证即被触发。
我有以下 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);
}
}
然后我在另一个 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);
}
如果向 GET 处理程序方法发出请求,则必须先对 id 路径变量进行约束验证,然后才能执行周围建议。有什么办法可以实现吗?
前言
我不是Spring用户,所以我不能告诉你
- 如果有任何方法可以影响任何建议 bean 的顾问程序应用顺序,non-invasive 方式,
- 确切地 Spring 在连接应用程序时创建与建议 bean 关联的顾问列表。
然而,我确实发现,一旦设置了建议 bean 的顾问列表,它就会按照列表中元素的顺序简单地应用。您可以通过 @Order
或实施 @Ordered
影响方面优先级,但我不知道这种方法是否可以以某种方式应用于方法验证顾问。
概念验证,版本 1
因为好奇,我创建了一个 proof-of-concept,hacky 解决方法。这是我 MCVE 复制您的原始情况:
服务、控制器和助手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);
}
}
看点:
如果 ID 包含 '0'
字符,我的示例中缺少 valid(String id)
方法的虚拟对象只是 returns true
。
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");
}
}
驱动申请:
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);
});
}
}
请注意我注释掉的一个辅助方法调用。当 运行 应用程序像这样时,控制台日志显示:
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
正如您从记录的 execution
连接点和随后的 IllegalArgumentException
中看到的那样,正如您所描述的,方面在方法参数验证之前开始。
现在,让我们取消注释(即激活)
reorderAdvisorsMethodValidationFirst(restController);
该方法的作用是
- 检查目标对象是否是建议的 Spring bean,
- 如果是这样,只需简单地重新排序顾问列表
- 暂时删除每个没有
MethodValidationInterceptor
建议的顾问 - 然后立即再次将其追加到列表末尾。
- 暂时删除每个没有
效果是现在方法验证拦截器优先于目标 bean 的其他建议类型。控制台日志因此更改为:
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
看到了吗?现在该方面仅在前两种情况下启动,在方法参数验证成功通过后。
一些Spring AOP内部:
- 方法
CglibAopProxy.DynamicAdvisedInterceptor.intercept
调用this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass)
。 this.advised
是AdvisedSupport
类型,是public类型,可惜CglibAopProxy.DynamicAdvisedInterceptor
是[=33的private static inner class =] 且仅在内部使用。- 所以没有好的方法来获取
AdvisedSupport
实例,例如调用它的setAdvisorChainFactory
方法。如果可能的话,您可以注入一个工厂,以不同于默认顺序的顺序返回顾问列表(DefaultAdvisorChainFactory
)。
也许这里的某些 Spring 专业人士知道通过配置 Spring 影响内部顾问链顺序的规范方法,以便按照您希望的方式连接应用程序,但我确实知道不知道。我只是一名 AOP(主要是 AspectJ)专家,有时会研究更具体的 Spring AOP 问题。
概念验证,版本 2
好的,我使用 BeanPostProcessor
将原始解决方案重构为更通用的东西。 post-processor 将
- 为每个 Spring 个实例化 bean 自动调用,
- 检查创建的 bean 是否是
Advised
(即是一个带有顾问的 Spring 代理), - 建议 bean 的过滤器 class 带有
@Validated
注释, - re-order 顾问喜欢我原来的解决方案。
优点是不再需要手动从应用程序上下文中获取 bean 实例并对其一一调用 reorderAdvisorsMethodValidationFirst(..)
。 Spring 照顾 post-processing 每个 bean,这是它应该的样子。很抱歉只在第 2 次迭代中提出这个解决方案,但就像我说的,我是一个 Spring 菜鸟。
更新、简化的驱动程序应用程序:
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("----------");
}
}
豆子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);
});
}
}
包含和不包含活动 post-processor 的控制台日志与原始解决方案中的相同。