SpringBoot 的 LoadTimeWeaving 的 AOP

普通的 SpringBoot 的默认 AOP 模式只能适用于 public 的方法,且内部调用不生效(需要使用AopContext.currentProxy()获取代理类),且对于 Spring 内部的类/第三方类且没有放入 Spring 管理(直接 new 来使用)的那种无效。

参考:
详解 Spring AOP LoadTimeWeaving

Spring AOP and AspectJ Load-Time Weaving: Around advice will be invoked twice for private methods

Java 对类加载信息追踪

原理

在类加载期通过字节码编辑技术将切面织入目标类,这种方式叫做 LTW(Load Time Weaving)。
使用 JDK5 新增的 java.lang.instrument 包,在类加载时对字节码进行转换,从而实现 AOP 功能。

SpringBoot 启用

@EnableSpringConfigured 主要是为了不编写 aspectOf 静态方法。

@EnableLoadTimeWeaving(aspectjWeaving = EnableLoadTimeWeaving.AspectJWeaving.AUTODETECT)
@EnableSpringConfigured

JVM 启动参数

-javaagent:C:/Users/chenxy/.m2/repository/org/aspectj/aspectjweaver/1.9.7/aspectjweaver-1.9.7.jar
-javaagent:C:/Users/chenxy/.m2/repository/org/springframework/spring-instrument/5.3.8/spring-instrument-5.3.8.jar

问题

如何只代理 aop.xml 的
如何只处理 aop.xml 的,其他的保持原样?内部调用不需要AOP的类怎么处理
发现没有在 aop.xml 的,内部调用还是没有被代理,但是为什么启动时会打印很多日志,是在哪里排除掉的

如何代理所有的

多次代理问题
代理时会对本身方法代理一次、还会对 Spring 代理后的方法再代理一次,会发现调用一个方法,执行了两次。比如 ControllerAop 使用 Spring 原生方式进行代理,ControllerAspect 使用 LTW 方式代理,那么 LTW 会拦截代理类一次、再拦截原生方法一次。
CGLIB 是基于类继承的,那么意味着 LTW 对子类也会进行代理 ? 是的
Aspect 如果指定了切点,那么其子类也会被代理,也就是如果调用子类方法,子类方法再调用了父类方法,会发现切面被执行了两次。

[Aspect] 切面 --------------------
[Aspect] 类: org.example.rest.DemoController$$EnhancerBySpringCGLIB$$7f17068d
[AOP] 前置通知:执行切面
[Aspect] 切面 --------------------
[Aspect] 类: org.example.rest.DemoController
[Aspect] 切面完毕- ---------------
[AOP] 后置通知:执行切面
[Aspect] 切面完毕- ---------------

Aspect 日志

启动 SpringBoot 会有如下大量的这种日志
image

最终是这个类处理的:org.springframework.aop.aspectj.AspectJWeaverMessageHandler
在这个类要求打印的日志:org.aspectj.weaver.tools.WeavingAdaptor#weaveClass(java.lang.String, byte[], boolean),第三个参数名称是 mustWeave,注释是 if true then this class must get woven (used for concrete aspects generated from XML),也就是 LTW 必须是从 XML 得到的 Aspect ?

加载 aop.xml 文件在这
org.aspectj.weaver.loadtime.ClassLoaderWeavingAdaptor#parseDefinitions

spring 默认也有自己的 aop.xml 文件:jar:file:/C:/Users/chenxy/.m2/repository/org/springframework/spring-aspects/5.3.20/spring-aspects-5.3.20.jar!/META-INF/aop.xml
image
看这这几个 AOP 类,好像 Spring 很多默认的 AOP 也是不能自动改为 LTW 的

打印类加载信息

-XX:+TraceClassLoading

那么应该是后续类的加载都经过了这个处理,打印日志不代表要进行 AOP
image

日志打印这么多的原因是 aop.xml 的这个配置 options

<weaver options="-XnoInline -Xset:weaveJavaxPackages=true -Xlint:ignore -verbose -XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler">

实际场景1

项目使用的是比较早的若依框架开发,代码生成器生成的每个 Controller 都继承了 BaseController,里面有一个这样的配置

@InitBinder
public void initBinder(WebDataBinder binder)
{
  // Date 类型转换
  binder.registerCustomEditor(Date.class, new PropertyEditorSupport()
  {
    @Override
    public void setAsText(String text)
    {
      setValue(DateUtils.parseDate(text));
    }
  });
}

上面的参数对于没有使用 @RequestBody 的查询实体可以自动转换 Data 与 String。
现在测试环境的数据库字段被改了(有些数据库字段命名存在问题,开会时直接改了以前的字段),那么相关的实体、VO、DTO等都要改,但是这个版本比较紧急,没有时间和前端对并进行测试,那么只能使用别名处理,对于 @RequestBody 和 @RequestBody 有其扩展的地方(@JsonProperty),对于实体的查询参数就没有办法了。
比如说参考如下链接:SpringBoot 参数别名实现,它处理调用了 ConfigurableWebBindingInitializer#initBinder 的初始化,但是 DefaultDataBinderFactory#initBinder(DefaultDataBinderFactory#createBinder时调用),调用链路如下

initBinder:66, InitBinderDataBinderFactory (org.springframework.web.method.annotation)
createBinder:60, DefaultDataBinderFactory (org.springframework.web.bind.support)
resolveArgument:168, ModelAttributeMethodProcessor (org.springframework.web.method.annotation)
resolveArgument:122, HandlerMethodArgumentResolverComposite (org.springframework.web.method.support)
getMethodArgumentValues:179, InvocableHandlerMethod (org.springframework.web.method.support)
invokeForRequest:146, InvocableHandlerMethod (org.springframework.web.method.support)

每次都是通过这个工厂创建新的实例,且在创建的时候还调用了自己的 initBinder 对实例进行处理,而这个工厂又不是注入到 Spring 的单例,这个时候就很难搞了。

还有一个问题,那就是本身 ServletRequestDataBinder 提供了一个接口 addBindValues 解析参数名称和实体名称的映射关系,但是它又是 protected 的,只能反射调用。

这个时候问题就来了

  1. 因为是 new 的,无法使用传统 AOP
  2. protected 的,也不能 AOP

那么最简单的方法就是像前面链接那样继承 ServletModelAttributeMethodProcessor,在 bindRequestParameters 生成代理类,对参数中的 binder 进行代理,由于其 addBindValue 的逻辑在方法 bind 中,因此需要代理这个方法,原始调用逻辑代码 copy 一份,加上自己的别名方法,其他方法不进行代理。

实际上这样也有问题,因为一般来说接口定义基本不会更改,但是实现的具体细节可能会更改,那么你复制代码进行处理的话,可能框架升级就会出现问题。

如果使用 LoadTimeWeaving 形式的 AOP,由于在类加载时就修改了字节码,对于 new 也可以 AOP,那么我们可以简单的在 ExtendedServletRequestDataBinder#addBindValues 其后添加逻辑。

原始逻辑
可以发现由于代理方法中有自己逻辑,且调用的方法不是 public 的,还需要反射调用

@Configuration
public class WebContextConfiguration implements WebMvcConfigurer {
  @Override
  public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
    argumentResolvers.add(renamingProcessor());
  }

  @Bean
  protected RenamingProcessor renamingProcessor() {
    return new RenamingProcessor(true);
  }

  public static class RenamingProcessor extends ServletModelAttributeMethodProcessor {

    /**
     * A map caching annotation definitions of command objects (@JsonProperty-to-fieldname mappings)
     */
    private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<>();

    public RenamingProcessor(boolean annotationNotRequired) {
      super(annotationNotRequired);
    }

    @Override
    protected void bindRequestParameters(WebDataBinder binder, @NonNull NativeWebRequest nativeWebRequest) {
      // 要处理的实体类
      Object target = binder.getTarget();
      if (target == null) {
        super.bindRequestParameters(binder, nativeWebRequest);
      }

      // 获取别名
      assert target != null;
      Map<String, String> mapping = getFieldMapping(target.getClass());
      if (CollectionUtil.isEmpty(mapping)) {
        super.bindRequestParameters(binder, nativeWebRequest);
      }

      // 填充别名信息
      if (binder instanceof ServletRequestDataBinder) {
        // 使用代理填充别名信息
        binder = proxyBinder((ServletRequestDataBinder) binder, mapping);
      }

      // 绑定参数和值
      super.bindRequestParameters(binder, nativeWebRequest);
    }

    /**
     * 代理绑定器
     *
     * @param binder    绑定器
     * @param renameMapping 别名 Map,key 是别名,val 是字段名
     * @return 代理后的绑定器
     */
    private WebDataBinder proxyBinder(ServletRequestDataBinder binder, Map<String, String> renameMapping) {

      ProxyFactory proxyFactory = new ProxyFactory();
      proxyFactory.setTarget(binder);
      DefaultPointcutAdvisor defaultPointcutAdvisor = new DefaultPointcutAdvisor();
      defaultPointcutAdvisor.setAdvice((MethodInterceptor) methodInvocation -> {
        // 找到绑定方法
        Method expectMethod = ReflectionUtils.findMethod(ServletRequestDataBinder.class, "bind", ServletRequest.class);
        // 没有,ignore
        if (expectMethod == null || ObjectUtil.notEqual(methodInvocation.getMethod(), expectMethod)) {
          // 直接调用
          return methodInvocation.proceed();
        }

        ServletRequest request = (ServletRequest) methodInvocation.getArguments()[0];

        // 原始逻辑
        MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
        MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
        if (multipartRequest != null) {
          invokeBindMultipart(binder, multipartRequest.getMultiFileMap(), mpvs);
        } else if (StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.MULTIPART_FORM_DATA_VALUE)) {
          HttpServletRequest httpServletRequest = WebUtils.getNativeRequest(request, HttpServletRequest.class);
          if (httpServletRequest != null && HttpMethod.POST.matches(httpServletRequest.getMethod())) {
            StandardServletPartUtils.bindParts(httpServletRequest, mpvs, binder.isBindEmptyMultipartFiles());
          }
        }

        invokeAddBindValues(binder, mpvs, request);
        // 自定义的填充逻辑
        customAddBindValues(mpvs, renameMapping);
        invokeDoBind(binder, mpvs);
        return null;
      });

      proxyFactory.addAdvisor(defaultPointcutAdvisor);
      return (ServletRequestDataBinder) proxyFactory.getProxy();
    }

    /**
     * 自定义处理别名信息
     *
     * @param mpvs      mpvs
     * @param renameMapping 别名数据
     */
    private void customAddBindValues(MutablePropertyValues mpvs, Map<String, String> renameMapping) {
      for (Map.Entry<String, String> entry : renameMapping.entrySet()) {
        String from = entry.getKey();
        String to = entry.getValue();
        if (mpvs.contains(from)) {
          // 设置field name 的值,使spring能注入
          mpvs.add(to, Objects.requireNonNull(mpvs.getPropertyValue(from)).getValue());
        }
      }
    }

    private void invokeBindMultipart(ServletRequestDataBinder binder, MultiValueMap<String, MultipartFile> multiFileMap, MutablePropertyValues mpvs) {
      Method bindMultipart = ReflectionUtils.findMethod(ServletRequestDataBinder.class, "bindMultipart", Map.class, MutablePropertyValues.class);
      if (bindMultipart == null) {
        return;
      }
      bindMultipart.setAccessible(true);
      ReflectionUtils.invokeMethod(bindMultipart, binder, multiFileMap, mpvs);
    }

    private void invokeDoBind(ServletRequestDataBinder binder, MutablePropertyValues mpvs) {
      Method doBind = ReflectionUtils.findMethod(ServletRequestDataBinder.class, "doBind", MutablePropertyValues.class);
      if (doBind == null) {
        return;
      }
      doBind.setAccessible(true);
      ReflectionUtils.invokeMethod(doBind, binder, mpvs);
    }

    private void invokeAddBindValues(ServletRequestDataBinder binder, MutablePropertyValues mpvs, ServletRequest request) {
      Method addBindValues = ReflectionUtils.findMethod(ServletRequestDataBinder.class, "addBindValues", MutablePropertyValues.class, ServletRequest.class);
      if (addBindValues == null) {
        return;
      }
      addBindValues.setAccessible(true);
      ReflectionUtils.invokeMethod(addBindValues, binder, mpvs, request);
    }

    /**
     * 获取 @JsonProperty 注解配置
     *
     * @param targetClass 处理的目标类型
     * @return Class 为 key,Class 内的配置信息为 value
     */
    private Map<String, String> getFieldMapping(Class<?> targetClass) {
      if (targetClass == Object.class) {
        return Collections.emptyMap();
      }

      // 已处理过
      if (replaceMap.containsKey(targetClass)) {
        return replaceMap.get(targetClass);
      }

      Map<String, String> renameMap = new HashMap<>();
      Field[] fields = targetClass.getDeclaredFields();
      for (Field field : fields) {
        JsonProperty anno = field.getAnnotation(JsonProperty.class);
        if (Objects.nonNull(anno) && StrUtil.isNotEmpty(anno.value())) {
          // aliasName --> fieldName
          renameMap.put(anno.value(), field.getName());
        }
      }

      // 递归获取全部Field
      renameMap.putAll(getFieldMapping(targetClass.getSuperclass()));

      if (renameMap.isEmpty()) {
        renameMap = Collections.emptyMap();
      }
      replaceMap.put(targetClass, renameMap);
      return renameMap;
    }
  }
}

LTW 后

@Slf4j
@Aspect
public class ExtendedServletRequestDataBinderAspect {

    public static ExtendedServletRequestDataBinderAspect aspectOf() {
        return new ExtendedServletRequestDataBinderAspect();
    }


    @Pointcut(value = "execution(* org.springframework.web.servlet.mvc.method.annotation.ExtendedServletRequestDataBinder.addBindValues(..))")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object advise(ProceedingJoinPoint pjp) throws Throwable {
        Object proceed = pjp.proceed(pjp.getArgs());
        if (pjp.getArgs() == null || !(pjp.getArgs()[0] instanceof MutablePropertyValues)) {
            throw error(null);
        }

        Object target = getTarget(pjp);
        addBindValues(target, ((MutablePropertyValues) pjp.getArgs()[0]));

        return proceed;
    }

    private void addBindValues(Object target, MutablePropertyValues mpvs) {
        Map<String, String> renameMapping = getFieldMapping(target.getClass());
        customAddBindValues(mpvs, renameMapping);
    }

    private void customAddBindValues(MutablePropertyValues mpvs, Map<String, String> renameMapping) {
        for (Map.Entry<String, String> entry : renameMapping.entrySet()) {
            String from = entry.getKey();
            String to = entry.getValue();
            if (mpvs.contains(from)) {
                // 设置field name 的值,使spring能注入
                mpvs.add(to, Objects.requireNonNull(mpvs.getPropertyValue(from)).getValue());
            }
        }
    }

    private Map<String, String> getFieldMapping(Class<?> targetClass) {
        if (targetClass == Object.class) {
            return Collections.emptyMap();
        }

        Map<String, String> renameMap = new HashMap<>();
        Field[] fields = targetClass.getDeclaredFields();
        for (Field field : fields) {
            JsonProperty anno = field.getAnnotation(JsonProperty.class);
            if (Objects.nonNull(anno) && StrUtil.isNotEmpty(anno.value())) {
                // aliasName --> fieldName
                renameMap.put(anno.value(), field.getName());
            }
        }

        // 递归获取全部Field
        renameMap.putAll(getFieldMapping(targetClass.getSuperclass()));

        if (renameMap.isEmpty()) {
            renameMap = Collections.emptyMap();
        }
        return renameMap;
    }


    private Object getTarget(ProceedingJoinPoint pjp) {
        Object target = pjp.getTarget();
        if (target == null) {
            throw error(null);
        }

        try {
            Method method = target.getClass().getMethod("getTarget");
            return method.invoke(target);
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            log.error("反射调用错误", e);
            throw error(e);
        }
    }

    private RuntimeException error(Exception e) {
        return new RuntimeException("unsupport spring framework version", e);
    }

}

实际场景2

对参数进行编解码/加解密,默认应用的id是自增的,当将这个id暴露给外部时,特别是 web 应用,就容易暴露一些敏感信息,这个时候就可以对id进行处理。
实体的 json 好处理,本身各个 json 框架有提供对应的扩展点。
但是 RequestParam、PathVariable、直接返回id的就不好处理了,需要对对应的方法进行处理。
简单就如下处理,三个接口处理(未校验,主要是未校验是否存在重复调用的问题,这个时候应该可以和 SpringSecurity 的一些操作一样进行打标标记已处理过?)
PathVariableMethodArgumentResolverAspect
RequestParamMethodArgumentResolverAspect
RequestResponseBodyMethodProcessorAspect

@Aspect
public class PathVariableMethodArgumentResolverAspect {
  private static final NumberCipher NUMBER_CIPHER = NumberCipher.DEFAULT;

  @Pointcut(value = "execution(* org.springframework.web.servlet.mvc.method.annotation.PathVariableMethodArgumentResolver.resolveName(..))")
  public void pointCut() {
  }

  @Around("pointCut()")
  public Object advise(ProceedingJoinPoint pjp) throws Throwable {
    Object proceed = pjp.proceed();

    MethodParameter annotatedParameter = null;
    for (int i = 0; i < pjp.getArgs().length; i++) {
      for (Object arg : pjp.getArgs()) {
        if (arg instanceof MethodParameter) {
          if (((MethodParameter) arg).getParameterType() == Long.class) {
            if (((MethodParameter) arg).hasParameterAnnotation(LongCodec.class)) {
              annotatedParameter = (MethodParameter) arg;
              break;
            }
          }
        }
      }
    }

    if (annotatedParameter == null) {
      return proceed;
    }

    if (proceed == null) {
      return null;
    }

    if (proceed.getClass() == Long.class) {
      return NUMBER_CIPHER.decryptLong((Long) proceed);
    }

    try {
      long result = Long.parseLong(proceed.toString());
      NUMBER_CIPHER.decryptLong(result);
    } catch (NumberFormatException e) {
      return proceed;
    }

    return proceed;
  }
}
@Aspect
public class RequestParamMethodArgumentResolverAspect {

  private static final NumberCipher NUMBER_CIPHER = NumberCipher.DEFAULT;

  @Pointcut(value = "execution(* org.springframework.web.method.annotation.RequestParamMethodArgumentResolver.resolveName(..))")
  public void pointCut() {
  }

  @Around("pointCut()")
  public Object advise(ProceedingJoinPoint pjp) throws Throwable {
    Object proceed = pjp.proceed();

    MethodParameter annotatedParameter = null;
    for (int i = 0; i < pjp.getArgs().length; i++) {
      for (Object arg : pjp.getArgs()) {
        if (arg instanceof MethodParameter) {
          if (((MethodParameter) arg).getParameterType() == Long.class) {
            if (((MethodParameter) arg).hasParameterAnnotation(LongCodec.class)) {
              annotatedParameter = (MethodParameter) arg;
              break;
            }
          }
        }
      }
    }

    if (annotatedParameter == null) {
      return proceed;
    }

    if (proceed == null) {
      return null;
    }

    if (proceed.getClass() == Long.class) {
      return NUMBER_CIPHER.decryptLong((Long) proceed);
    }

    try {
      long result = Long.parseLong(proceed.toString());
      NUMBER_CIPHER.decryptLong(result);
    } catch (NumberFormatException e) {
      return proceed;
    }

    return proceed;
  }
}
@Aspect
public class RequestResponseBodyMethodProcessorAspect {

  private final WrapperResultConfig wrapperResultConfig = new WrapperResultConfig();

  private static final NumberCipher NUMBER_CIPHER = NumberCipher.DEFAULT;

  @Pointcut(value = "execution(* org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(..))")
  public void pointCut() {

  }

  @Around("pointCut()")
  public Object advise(ProceedingJoinPoint pjp) throws Throwable {
    Object returnValue = null;
    returnValue = pjp.getArgs()[0];
    MethodParameter parameter = (MethodParameter) pjp.getArgs()[1];
    if (returnValue == null || parameter.getParameterType() != Long.class || !Objects.requireNonNull(parameter.getMethod()).isAnnotationPresent((LongCodec.class))) {
      return pjp.proceed();
    }

    Long longVal = (Long) returnValue;
    long aLong = NUMBER_CIPHER.decryptLong(longVal);
    if(!wrapperResultConfig.supportsReturnType(parameter)){
      return aLong;
    }

    Object[] args = new Object[] {new HttpWrapper<>().data(aLong), pjp.getArgs()[1], pjp.getArgs()[2], pjp.getArgs()[3]};
    return pjp.proceed(args);
  }

}
posted @ 2023-11-20 22:00  YangDanMua  阅读(15)  评论(0编辑  收藏  举报