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
原理
在类加载期通过字节码编辑技术将切面织入目标类,这种方式叫做 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 会有如下大量的这种日志
最终是这个类处理的: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
看这这几个 AOP 类,好像 Spring 很多默认的 AOP 也是不能自动改为 LTW 的
打印类加载信息
-XX:+TraceClassLoading
那么应该是后续类的加载都经过了这个处理,打印日志不代表要进行 AOP
日志打印这么多的原因是 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 的,只能反射调用。
这个时候问题就来了
- 因为是 new 的,无法使用传统 AOP
- 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);
}
}