我们知道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的一堆注解都是在编译时进行增强,在加载时与运行时都是看不到的。
当然,一切都得从实际出发,过于学院派与想当然是不可取的。

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

标签: spring

仅有一条评论

  1. 哇噻

    李哥牛皮,确实看不懂

添加新评论