如何在 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.advisedAdvisedSupport类型,是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 的控制台日志与原始解决方案中的相同。