由AnnotatedElementUtils延伸的一些所思所想

in 编程
关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9

这篇博客的两个主题:

为什么要分享AnnotatedElementUtils这个类呢,这个类看起来就是一个工具类,听起来很像apache的StringUtils,CollectionUtils。

原因是,它包含着spring对java注解的另类理解,和运用。

java的是怎样支撑注解的?

Class<TestAnnotation> clazz = TestAnnotation.class;

// 获取类注解
MyClassAnnotation myClassAnnotation = clazz.getAnnotation(MyClassAnnotation.class);

// 获得构造方法注解
Constructor<TestAnnotation> cons = clazz.getConstructor(new Class[] {});
MyConstructorAnnotation Constructor = cons.getAnnotation(MyConstructorAnnotation.class);

// 获得方法注解
Method method = clazz.getMethod("setId", new Class[] { String.class });
MyMethodAnnotation myMethodAnnotation = method.getAnnotation(MyMethodAnnotation.class);

// 获得字段注解
Field field = clazz.getDeclaredField("id");
MyFieldAnnotation myFieldAnnotation = field.getAnnotation(MyFieldAnnotation.class);

以及@Inherited,它可以将父类的注解,带到继承体系上的子类中去。

这套注解体系有什么问题?

面向对象语言之所以被冠以“面向对象”这样的名字,是因为它具有多态的能力。有了多态的能力,我们才有了面向接口编程的能力,有了这个能力,依赖反转才有立足点;所有的设计模式才有立足点(工厂模式,装饰器模式,策略模式...)。可以说多态是java这样的强类型,面向对象语言的灵魂。

那么多态这种能力是怎么来的?

父类与接口。弱类型的语言其实天然就支持多态,但强类型的语言则不是。而java在语言层面支持了"父类与接口",体现在java程序可以自动的向上转型,并且可以安全的向下转型。向上,向下转型这两件事,就实现了所谓的“多态”语义。

我们再向问题的本质进一步,看看java是怎么实现上下转型的?

当把class文件加载进内存(方法区)时,方法在真正运行之前就有一个确定的调用版本,且该版本在运行期不可变的一类,将会被解析,符号引用将被替换为实在的内存地址,成为该方法的入口地址。静态方法,私有方法,构造器,父类方法符合这个要求。这类方法也被称为非虚方法。

public class Test {
    public void test() {
        // 实例和方法都是确定的(Human的静态方法run)
        Human.run();
    }
}

而虚方法和静态分派则是:

// 实例不确定,方法也不确定。
// 此处唯一能确定的是,方法的重载版本。可见这个方法的版本是无参数的,它确定了执行器在调用run时,
// 一定不会去调用一个带任何参数的版本的run方法。这就是静态分派。
public class Test {
    public void test(Human human) {
        human.run();
    }
}

上面提到了重载,而静态分派就是用以确定重载版本的,下面我要说的是覆写。覆写会导致不同实例的覆写版本,方法体不一样,所以虚拟机只能在运行期通过对象的实际类型来决定调用哪个版本的覆写方法。这被称为动态分派。

public class Test {
    public void test() {
    	System.out.println("i'm Test");
    }
}

public class SubTest extends Test {
    public void test() {
    	System.out.println("i'm SubTest");
    }
}

public class TestTest {
    public static void main(String[] args) {
    	Test t = new Test();
        t.test();
        
        t = new SubTest();
        t.test();
    }
}

发现没有,覆写是多态的原理!动态分派是覆写的原理!那么,动态分派也就是多态的原理,进而,动态分派也就是java是面向对象语言的根本原理,或者说根本原因!

而目前,java的注解,并不支持动态分派,就是说它并不支持覆写!这就是目前java这套注解体系的一个重要的问题,它使得注解不易使用。

举例:

在深入探究Spring解决方案前,还有一个问题有待解决

在阐述AnnotatedElementUtils前,我要引出今天这次分享的第二个主题:源码阅读方法论。

我问过好多朋友,也在各社区搜索过,如何阅读开源代码这件事情。得到的答案往往是一些“放之四海而皆准”的指导性建议,始终没有得到一个切实可行的方法论,后来我自己总结,摸索了一套。

首先问一个问题:当我们说“读源码”时,我们究竟是要做一件什么事情?

以前我对这个问题的回答是:读懂它的逻辑,或叫流程。这个答案背后的含义是,我在乎的是代码中的判断,分支。但是我经常在读源码时有很强的挫败感,因为我很努力的去读,却发现,我读懂了一个方法,而这个类有好几十个方法,我对这个类还是不理解,方法和方法间的关系还是不明朗,类的抽象还是很模糊。

也就是在这个阶段,我请教过很多朋友,以及论坛,甚至每当遇到新的程序员朋友时,我都会问对方这个问题——怎么阅读源码。

直到后来,我看到《人月神话》中有这样一句话:让我看你的流程图不让我看表,我会仍然搞不明白。给我看你的表,一般我就不用看你的流程图了,表能让人一目了然。

这里的表指的是数据,以及数据的结构,例如一个类的成员变量就是它的表;我们写业务的时候,mysql中的数据就是表。《人月神话》的这句话让我突然一惊,难道我一直以来在理解代码的时候,所关注的点是错的,我不应该关注逻辑,而应该关注表?

验证这个道理的最好办法,就是运用它,实验它!

在这里我可以告诉大家,它是对的!我的方法论就是建立在它之上的。

源码阅读方法论——原则

源码阅读方法论——技巧

源码阅读方法论——步骤

带着方法论,探索Spring

到这里,今天的主角AnnotatedElementUtils就要登场了,它虽然名字叫做utils,但它可不是一个工具类那么简单,它蕴含着spring对注解这种语法的思考。

我是通过阅读《Spring Boot 编程思想》这本书了解到AnnotatedElementUtils的,书中没有详细展开介绍它,但是通过书中的描述,我知道了spring对注解的处理,是不同于java反射的语义的。我们就来读一读,看看有什么奥秘。我的阅读目的就是:spring怎样让注解实现属性覆写?

先展示一下AnnotatedElementUtils的作用

@TestA
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface TestRoot {
}

@TestB
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface TestA {
    @AliasFor(value = "c1", annotation = TestC.class)
    String bb() default "testA";
}

@TestC
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface TestB {
    @AliasFor(value = "c2", annotation = TestC.class)
    String cc() default "testB";
}

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface TestC {
    @AliasFor(value = "c2")
    String c1() default "testC";

    @AliasFor(value = "c1")
    String c2() default "testC";
}

在这里插入图片描述

从AnnotatedElementUtils开始构建依赖图

AnnotatedElementUtils这个类没有表,显然它只是某些其他类的代理,既然没有表,按照我们的方法论,它就没有什么太多可理解的了,我们读一读它的注释:

AnnotatedElementUtils(类)

从注释里我们知道,它有一堆get和find方法,find方法的语义看起来更接近我所提出的问题,所以我选择了findMergedAnnotationAttributes来作为切入点。迅速的阅读一下这个方方法,找出它当中依赖了些什么。

(下面给出依赖图)

在这里插入图片描述

从底层开始阅读

在这个依赖图中,最底层的类是:AttributeMethods,RepeatableContainers的实现类,AnnotationsScanner的实现类,AnnotationFilter,这几个底层类是比较简单的,所以今天我不讲他们,我讲AnnotationTypeMapping,按照正常的顺序,你应该是先去读它们的。

看了AnnotationTypeMapping的表,你的脑袋里是否已经有了它的概念了呢?

AnnotationTypeMapping提供了三个关键功能方法,分别是

这三个方法就形成了spring获取注解属性的基础能力。

回到开篇——spring是如何赋予注解覆写能力的?

在spring中,注解之间具有多种关系,并且存在层级概念。使用者输入“别名关系”,spring则将这种关系深化,最终落到“惯例关系”与“最低阶关系”上,从而赋予低阶注解属性影响高阶注解属性的能力,实现低阶对高阶的覆写,就像子类对父类的覆写一样。并且,值得注意的是,spring并没有真的去修改高阶注解的属性值,而是通过类似指针的方式,将获取高阶注解属性值的操作指向它的低阶镜像,从而在外部看来,像是高阶属性被低阶属性覆写。

这种能力可以为我们带来什么优势?

在这里插入图片描述

以spring的@Service注解为例,它被@Component注解元标注,并且其value属性被标识了是@Component的value属性的别名。spring在为我们提供@Service注解的时候,并不需要专门去写一个注解处理器来将被@Service标注的类注册成Bean,spring只需要一个@Component的注解处理器就可以,因为它可以从任何被@Service标注的类上获取到@Component,并且获取到被覆写的value值。这是不是很像向上转型,很像多态?

对于广大的互联网开发人员来说,我们的基础工作栈之一就是spring,当我们在spring应用中开发时,何不使用spring已经搭建好的脚手架呢,当我们需要开发一些注解处理器的时候,完全可以使用spring封装好的AnnotatedElementUtils。

题外话

大家有没有注意到MirrorSet的resolve方法有问题?

问题出在:“如果所有属性都是默认值,则result = -1”(参看前文对MirrorSet的resolve方法的注释截图)。

-1表示的是它在某组镜像属性中没有找到有效属性,如果没有找到有效属性,那么某个高层注解的“最低阶属性”就不可能定位到这组镜像上来。

举个例子说明它会导致的问题:

@TestB
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface TestA {    
    @AliasFor(value = "b1", annotation = TestB.class)    
    String a1() default "testA";
}

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface TestB {    
    @AliasFor(value = "b2")    
    String b1() default "testB";    
    
    @AliasFor(value = "b1")    
    String b2() default "testB";
}

@TestA
public class Test {    
    public static void main(String[] args) {
        // 这里你得到的实例b有两个key,b1和b2,值都是"testA"
        AnnotationAttributes b = AnnotatedElementUtils.findMergedAnnotationAttributes(Test.class, TestB.class, false, true);    
    }
}

但是当你将TestA修改成这样,使得a1和a2成为镜像属性时,得到的结果就比较奇怪了

@TestB
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface TestA {    
    @AliasFor(value = "b1", annotation = TestB.class)    
    String a1() default "testA";    
    
    @AliasFor(value = "b2", annotation = TestB.class)    
    String a2() default "testA";
}

@TestA
public class Test {    
    public static void main(String[] args) {
        // 这里你得到的实例b有两个key,b1和b2,值都是"testB"    
        AnnotationAttributes b = AnnotatedElementUtils.findMergedAnnotationAttributes(Test.class, TestB.class, false, true);    
    }
}

注解TestB中的属性并没有被TestA中的属性覆盖,但TestA确实是TestB的低层级属性,它理应具有覆写上层属性的能力,当TestA中的属性没有形成镜像时,它确实表现出了这种能力,但当TestA中的属性形成镜像时,这种能力消失了(这个bug在spring-framework5.2.x版本下存在,将可能于5.2.3版本修复)。

比较幸运,我们发现了一个spring的bug。也从侧面证明了,我们的源码阅读方法论是有效的。给spring提一个PR,我们就能收到几个感谢。

结语

AnnotatedElementUtils的能力其实并不是一个AnnotationTypeMapping可以概括的,还有其他一些类在整个逻辑中发挥重要作用,我会继续更新博客,慢慢将完整的AnnotatedElementUtils展现出来,而面对今天的AnnotationTypeMapping,你在看了表的注释后,有一个概括性的认识就可以了。

希望我的方法能对大家有所帮助,也期望大家和我分享你们的方法,让我们取长补短,最后能得出一套高效的方法论。

关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9
扫一扫关注公众号添加购物返利助手,领红包
Comments are closed.

推荐使用阿里云服务器

超多优惠券

服务器最低一折,一年不到100!

朕已阅去看看