关于SpringAOP的一个细节
我们知道SpringAOP只对方法进行增强,并且只提供运行时增强。最近发现了一个诡异的点:Spring可以保留加载时注解,这里总结出来以警示后人。
公司有个AOP切面是这么写的 :
@Component
@Aspect
public class UserIdInjectorHandler {
@Pointcut("@within(annotations.EnableUserIdInject)")
public void addAdvice(){
}
@Before( "addAdvice()" )
public void interceptor( JoinPoint joinPoint ) throws Throwable {}
包含@EnableUserIdInject注解的类的每个public方法都被增强了。
而这个注解是这么定义的:
@Retention( RetentionPolicy.CLASS )
@Target( ElementType.TYPE )
public @interface EnableUserIdInject {
}
这是一个运行时不可见的注解。那问题就来了,这个运行时不可见的注解是这么在SpringAOP中生效的呢?
使用:
@RestController
@EnableUserIdInject
public class TestController {
@InjectUserId
private Integer userId;
@GetMapping("/test")
public String test(){
return userId;
}
}
一个方向是自定义classloader在Spring容器中使用的类加载器是不一样的,他保留了不该存在与运行时的注解。
显然这这个思路是不对的。经过验证,标注该注解的类在runtime是找不到该注解的,并且这个类的类加载器依然是:Launcher.AppClassLoader。这个思路是违反java语言规范的,同时还是反双亲委派模型的。
另一个方向是Spring提供了加载时的增强。
上面也提到了,TestController 这个类的类加载器依然是:Launcher.AppClassLoader。最大的问题是:如果提供了加载时增强,那么Spring的文档就不再具有参考意义。这是合作中最糟糕的一个部分,他会导致合作双方的不信任,因为你没有按照你承诺的方式来实现,并且这种改变没有让他人知晓。这会让他人面向玄学编程。
上面两种都是不对的。但是你有俩个很重要的线索:TestController类对象无法获取该注解;切面在运行时生效了。从这个思路往下走,你可以推断出这个类文件比如被读取了两次。最后总结如下:
- 1.在TestController初始化完成后,调用BenPostProcessor处理该对象。
- 2.AnnotationAwareAspectJAutoProxyCreator.postProcessAfterInitialization(bean,beanName)用以产生代理对象。
3.AopUtils.findAdvisorsThatCanApply()找到合适的切面,本质是使用AopUtils.canApply()方法匹配切点。
public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) { Assert.notNull(pc, "Pointcut must not be null"); if (!pc.getClassFilter().matches(targetClass)) { return false; } //上面已经把切点不匹配的情况过滤掉了 }
4.找到对应的切点实现,进行匹配Pointcut.fastMatch(info);info是ReflectionFastMatchInfo对象,这里的Pointcut是WithinAnnotationPointcut。
public FuzzyBoolean fastMatches(AnnotatedElement annotated) { //这里的hasAnnotation()是关键 if (annotated.hasAnnotation(annotationType) && annotationValues == null) { return FuzzyBoolean.YES; } else { // could be inherited, but we don't know that until we are // resolved, and we're not yet... return FuzzyBoolean.MAYBE; } }
5.检查info中是否有切点注解,一开始是没有的annotations==null,使用annotationFinder寻找注解
public ResolvedType[] getAnnotationTypes() { if (annotations == null) { annotations = annotationFinder.getAnnotations(getBaseClass(), getWorld()); } return annotations; }
6.getAnnotations()。显然又读了一遍这个类文件,便且保留了运行时与加载时的注解并返回了这些注解。
public ResolvedType[] getAnnotations(Class forClass, World inWorld) { // here we really want both the runtime visible AND the class visible // annotations so we bail out to Bcel and then chuck away the JavaClass so that we // don't hog memory. ............... }
7.ClassParser.parse()重新从流中解析出完整的JavaClass对象。
public JavaClass parse() throws IOException, ClassFormatException { ZipFile zip = null; try { if (fileOwned) { if (is_zip) { zip = new ZipFile(zip_file); final ZipEntry entry = zip.getEntry(file_name); if (entry == null) { throw new IOException("File " + file_name + " not found"); } dataInputStream = new DataInputStream(new BufferedInputStream(zip.getInputStream(entry), BUFSIZE)); } else { dataInputStream = new DataInputStream(new BufferedInputStream(new FileInputStream( file_name), BUFSIZE)); } } /****************** Read headers ********************************/ // Check magic tag of class file readID(); // Get compiler version readVersion(); /****************** Read constant pool and related **************/ // Read constant pool entries readConstantPool(); // Get class information readClassInfo(); // Get interface information, i.e., implemented interfaces readInterfaces(); /****************** Read class fields and methods ***************/ // Read class fields, i.e., the variables of the class readFields(); // Read class methods, i.e., the functions in the class readMethods(); // Read class attributes readAttributes(); } ......... }
注:
1.上述是个大概流程,细节还得自己看源码。
2.切点与切面都是有缓存的。
3.aspectjweaver提供的切点表达式解析功能的具体实现。
4.Spring是如何处理@EnableXXXXXX注解的,以及该如何优雅得实现自己得starter?这篇文章说明了@EnableXXX注解的优雅实现。
5.上面UserIdInjectorHandler实现将请求的对象域中是会导致并发问题的。
总结起来就以下几点需要内化的:
1.语义一致、行为一致。最终Spring并没有违反其在文档中的约定,提供运行时的方法增强。而@EnableUserIdInject注解在运行时不可见导致其在运行时我们是难以观察到的,更优的方式是RetentionPolicy.CLASS(这是默认策略)改为RetentionPolicy.RUNTIME。
2.选择最小生命周期。从这个角度来说,RetentionPolicy.CLASS策略的选择是正确的,最小的生命周期意味着最小的副作用影响范围,比如,@EnableUserIdInject就可以避免我们在运行时不规范的使用它。像Lombok的一堆注解都是在编译时进行增强,在加载时与运行时都是看不到的。
当然,一切都得从实际出发,过于学院派与想当然是不可取的。
注:转载请标明来源与作者。有意见或者建议请留言。
李哥牛皮,确实看不懂