Skip to content

关于SpringAOP的一个细节

分类:

我们知道SpringAOP只对方法进行增强,并且只提供运行时增强。最近发现了一个诡异的点:Spring可以保留加载时注解,这里总结出来以警示后人。

公司有个AOP切面是这么写的 :

java
@Component
@Aspect
public class UserIdInjectorHandler {

    @Pointcut("@within(annotations.EnableUserIdInject)")
    public void addAdvice(){
    }

    @Before( "addAdvice()" )
    public void interceptor( JoinPoint joinPoint ) throws Throwable {}

包含@EnableUserIdInject注解的类的每个public方法都被增强了。 而这个注解是这么定义的:

java
@Retention( RetentionPolicy.CLASS )
@Target( ElementType.TYPE )
public @interface EnableUserIdInject {
}

这是一个运行时不可见的注解。那问题就来了,这个运行时不可见的注解是这么在SpringAOP中生效的呢? 使用:

java
@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()方法匹配切点。
java
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。
java
	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寻找注解
java
	public ResolvedType[] getAnnotationTypes() {
		if (annotations == null) {
			annotations = annotationFinder.getAnnotations(getBaseClass(), getWorld());
		}
		return annotations;
	}
  • 6.getAnnotations()。显然又读了一遍这个类文件,便且保留了运行时与加载时的注解并返回了这些注解。
java
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对象。
java
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的一堆注解都是在编译时进行增强,在加载时与运行时都是看不到的。 当然,一切都得从实际出发,过于学院派与想当然是不可取的。

注:转载请标明来源与作者。有意见或者建议请留言。

实践、认识、再实践、再认识,这种形式,循环往复以至无穷,而实践和认识之每一循环的内容,都比较地进到了高一级的程度。