前景回顾

当我们把写好的业务代码交给Spring之后,Spring都会做些什么呢?

仔细想象一下,再稍微抽象一下,Spring所做的几乎全部都是:

“bean的实例化,bean的依赖装配,bean的初始化,bean的方法调用,bean的销毁回收”。

那问题来了,Spring为什么能够准确无误的完成这波对bean的操作呢?答案很简单,就是:

“Spring掌握了有关bean的足够多的信息”。

这就是本系列文章第一篇“帝国的基石”的核心思想。Spring通过bean定义的概念收集到了bean的全部信息。

这件事也说明,当我们拥有了一个事物的大量有效信息之后,就可以做出一些非常有价值的操作。如大数据分析,用户画像等。

紧接着就是第二个问题,Spring应该采用什么样的方式来收集bean的信息呢?

这就是本系列文章第二篇“bean定义上梁山”主要讲的内容。

首先是统一了编程模型,只要是围绕Spring的开发,包括框架自身的开发,最后大都转化为bean定义的注册。

为了满足不同的场景,Spring提供了两大类的bean定义注册方式:

实现指定接口,采用写代码的方式来注册,这是非常灵活的动态注册,根据不同的条件注册不同的bean,主要用于第三方组件和Spring的整合。

标上指定注解,采用注解扫描的方式来注册,这相当于一种静态的注册,非常不灵活,但特别简单易用,主要用于普通业务代码的开发。

Spring设计的这一切,看起来确实完美,用起来也确实很爽,但实现起来呢,也确实的非常麻烦。

尤其是在全部采用注解和Java配置的时候,那才叫一个繁琐,看看源码便知一二。

所以本篇及接下来的几篇都会写一些和实现细节相关的内容,俗称“干货”,哈哈。

最容易想到的实现方案

一个bean其实就是一个类,所以bean的信息就是类的信息。

那一个类都有哪些信息呢,闭着眼睛都能说出来,共四大类信息:

类型信息,类名,父类,实现的接口,访问控制/修饰符

字段信息,字段名,字段类型,访问控制/修饰符

方法信息,方法名,返回类型,参数类型,访问控制/修饰符

注解信息,类上的注解,字段上的注解,方法上的注解/方法参数上的注解

注:还有内部类/外部类这些信息,也是非常重要的。

看到这里脑海中应该立马蹦出两个字,没错,就是反射

但是,Spring并没有采用反射来获取这些信息,个人认为可能有以下两个大的原因:

性能损耗问题:

要想使用反射,JVM必须先加载类,然后生成对应的Class<?>对象,最后缓存起来。

实际的工程可能会注册较多的bean,但是真正运行时不一定都会用得到。

所以JVM加载过多的类,不仅会耗费较多的时间,还会占用较多的内存,而且加载的类很多可能都不用。

信息完整度问题:

JDK在1.8版本中新增加了一些和反射相关的API,比如和方法参数名称相关的。此时才能使用反射获取相对完善的信息。

但Spring很早就提供了对注解的支持,所以当时的反射并不完善,也可能是通过反射获取到的信息并不能完全符合要求。

总之,Spring没有选择反射。

那如何获取类的这些信息呢?答案应该只剩一种,就是直接从字节码文件中获取。

采用先进的生产力

源码经过编译变成字节码,所以源码中有的信息,在字节码中肯定都有。只不过换了一种存在的形式。

Java源码遵循Java语法规范,生成的字节码遵循JVM中的字节码规范。

字节码文件的结构确实有些复杂,应用程序想要直接从字节码中读出需要的信息也确实有些困难。

小平同志曾说过,“科学技术是第一生产力”。所以要解决复杂的问题,必须要有比较可靠的技术才行。

对于复杂的字节码来说,先进的生产力就是ASM了。ASM是一个小巧快速的Java字节码操作框架。

它既可以读字节码文件,也可以写字节码文件。Spring框架主要用它来读取字节码。

ASM框架是采用访问者模式设计出来的,如果不熟悉这个设计模式的可以阅读本公众号上一篇文章“趣说访问者模式”。

该模式的核心思想就是,访问者按照一定的规则顺序进行访问,期间会自动获取到相关信息,把有用的信息保存下来即可。

下面介绍一下ASM的具体使用方式,可以看看作为了解,说不定以后会用到。哈哈。

ASM定义了ClassVisitor来获取类型信息,AnnotationVisitor来获取注解信息,FieldVisitor来获取字段信息,MethodVisitor来获取方法信息。

先准备好产生字节码的素材,其实就是一个类啦,这个类仅作测试使用,不用考虑是否合理,如下:

@Configuration("ddd")
@ComponentScan(basePackages = {"a.b.c", "x.y.z"},
scopedProxy = ScopedProxyMode.DEFAULT,
includeFilters = {@Filter(classes = Integer.class)})
@Ann0(ann1 = @Ann1(name = "ann1Name"))
public class D<@Null T extends Number> extends C<@Valid Long, @NotNull Date> implements A, B {     protected Long lon = Long.MAX_VALUE;     private String str;     @Autowired(required = false)
    private Date date;     @Resource(name = "aaa", lookup = "bbb")
    private Map<@NotNull String, @Null Object> map;     @Bean(name = {"cc", "dd"}, initMethod = "init")
    public String getStr(@NotNull String sssss, @Null int iiiii, double dddd, @Valid long llll) throws Exception {
        return sssss;
    }     @Override
    public double getDouble(double d) {
        return d;
    }
}

这个类里面包含了较为全面的信息,泛型、父类、实现的接口、字段、方法、注解等。

按照ASM规定的访问顺序,首先访问类型信息,使用ClassVisitor的visit方法,如下:

@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
    log("---ClassVisitor-visit---");
    log("version", version);
    log("access", access);
    log("name", name);
    log("signature", signature);
    log("superName", superName);
    log("interfaces", Arrays.toString(interfaces));
}

这个方法会由ASM框架调用,方法参数的值是框架传进来的,我们要做的只是在方法内部把这些参数值保存下来就行了。

然后可以按照自己的需求去解析和使用,我这里只是简单输出一下。如下:

//版本信息,52表示的是JDK1.8
version = 52
//访问控制信息,表示的是public class
access = 33
//类型的名称
name = org/cnt/ts/asm/D
//类型的签名,依次为,本类的泛型、父类、父类的泛型、实现的接口
signature = <T:Ljava/lang/Number;>Lorg/cnt/ts/asm/C<Ljava/lang/Long;Ljava/util/Date;>;Lorg/cnt/ts/asm/A;Lorg/cnt/ts/asm/B;
//父类型的名称
superName = org/cnt/ts/asm/C
//实现的接口
interfaces = [org/cnt/ts/asm/A, org/cnt/ts/asm/B]

现在我们已经获取到了这些信息,虽然我们并不知道它是如何在字节码中存着的,这就是访问者模式的好处。

类型名称都是以斜线“/”分割,是因为斜线是路径分隔符,可以非常方便的拼出完整路径,从磁盘上读取.class文件的内容。

还有以大写“L”开头后跟一个类型名称的,这个大写L表示的是“对象”的意思,后跟的就是对象的类型名称,说白了就是类、接口、枚举、注解等这些。

接着访问的是类型上标的注解,使用ClassVisitor的visitAnnotation方法,如下:

@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
    log("---ClassVisitor-visitAnnotation---");
    log("descriptor", descriptor);
    log("visible", visible);
    return new _AnnotationVisitor();
}

需要说明的是,这个方法只能访问到注解的类型信息,注解的属性信息需要使用AnnotationVisitor去访问,也就是这个方法的返回类型。

类上标有@Configuration("ddd"),所以输出结果如下:

//类型描述/名称
descriptor = Lorg/springframework/context/annotation/Configuration;
//这个是可见性,表明在运行时可以获取到注解的信息
visible = true

然后使用AnnotationVisitor去访问显式设置过的注解属性信息,使用visit方法访问基本的信息,如下:

@Override
public void visit(String name, Object value) {
    log("---AnnotationVisitor-visit---");
    log("name", name);
    log("value", value);
}

实际上我们是把ddd设置给了注解的value属性,所以结果如下:

//属性名称,是value
name = value
//属性值,是ddd
value = ddd

至此,@Configuration注解已经访问完毕。

然后再访问@ComponentScan注解,同样使用ClassVisitor的visitAnnotation方法,和上面的那个一样。

得到的结果如下:

descriptor = Lorg/springframework/context/annotation/ComponentScan;
visible = true

然后使用AnnotationVisitor去访问设置过的注解属性信息,使用visitArray方法访问数组类型的信息,如下:

@Override
public AnnotationVisitor visitArray(String name) {
    log("---AnnotationVisitor-visitArray---");
    log("name", name);
    return new _AnnotationVisitor();
}

这个方法只能访问到数组类型属性的名称,结果如下:

name = basePackages

属性的值还是使用基本的visit方法去访问,因为数组的值是多个,所以visit方法会多次调用,按顺序依次获取数组的每个元素值。

因数组有两个值,所以方法调用两次,结果如下:

name = null
value = a.b.c name = null
value = x.y.z

因为数组的值没有名称,所以name总是null。value的值就是数组的元素值,按先后顺序保存在一起即可。

然后由于注解的下一个属性是枚举类型的,所以使用visitEnum方法来访问,如下:

@Override
public void visitEnum(String name, String descriptor, String value) {
    log("---AnnotationVisitor-visitEnum---");
    log("name", name);
    log("descriptor", descriptor);
    log("value", value);
}

结果如下:

//注解的属性名称,是scopedProxy
name = scopedProxy
//枚举类型,是ScopedProxyMode
descriptor = Lorg/springframework/context/annotation/ScopedProxyMode;
//属性的值,是我们设置的DEFAULT
value = DEFAULT

然后继续访问数组类型的属性,使用visitArray方法访问。

得到的结果如下:

name = includeFilters

接下来该获取数组的元素了,由于这个数组元素的类型也是一个注解,所有使用visitAnnotation方法访问,如下:

@Override
public AnnotationVisitor visitAnnotation(String name, String descriptor) {
    log("---AnnotationVisitor-visitAnnotation---");
    log("name", name);
    log("descriptor", descriptor);
    return new _AnnotationVisitor();
}

得到的结果如下:

name = null
//注解类型名称
descriptor = Lorg/springframework/context/annotation/ComponentScan$Filter;

可以看到这个注解是@ComponentScan内部的@Filter注解。这个注解本身是作为数组元素的值,所以name是null,因为数组元素是没有名称的。

然后再访问@Filter这个注解的属性,得到属性名称如下:

name = classes

属性值是一个数组,它只有一个元素,如下:

name = null
value = Ljava/lang/Integer;

,代码较多,不再贴了,只给出结果的解析。

下面是map类型的那个字段的结果,如下:

//访问控制,private
access = 2
//字段名称
name = map
//字段类型
descriptor = Ljava/util/Map;
//字段类型签名,包括泛型信息
signature = Ljava/util/Map<Ljava/lang/String;Ljava/lang/Object;>;
value = null

该字段上标了注解,结果如下:

descriptor = Ljavax/annotation/Resource;
visible = true

并且设置了注解的两个属性,结果如下:

name = name
value = aaa name = lookup
value = bbb

由于编译器会生成默认的无参构造函数,所以会有如下:

//访问控制,public
access = 1
//对应于构造函数名称
name = <init>
//方法没有参数,返回类型是void
descriptor = ()V
signature = null
exceptions = null

这有一个定义的方法结果,如下:

//public
access = 1
//方法名称
name = getStr
//方法参数四个,分别是,String、int、double、long,返回类型是String
descriptor = (Ljava/lang/String;IDJ)Ljava/lang/String;
signature = null
//抛出Exception异常
exceptions = [java/lang/Exception]

参数里面的大写字母I表示int,D表示double,J表示long,都是基本数据类,要记住不是包装类型。

方法的四个参数名称,依次分别是:

//参数名称
name = sssss
//参数访问修饰,0表示没有修饰
access = 0 name = iiiii
access = 0 name = dddd
access = 0 name = llll
access = 0

由于方法上标有注解,结果如下:

descriptor = Lorg/springframework/context/annotation/Bean;
visible = true

数组类型的属性名称,如下:

name = name

属性值有两个,如下:

name = null
value = cc name = null
value = dd

简单类型的属性值,如下:

name = initMethod
value = init

由于方法的其中三个参数上也标了注解,结果如下:

//参数位置,第0个参数
parameter = 0
//注解类型名称,@NotNull
descriptor = Ljavax/validation/constraints/NotNull;
//可见性,运行时可见
visible = true parameter = 1
descriptor = Ljavax/validation/constraints/Null;
visible = true parameter = 3
descriptor = Ljavax/validation/Valid;
visible = true

以上这些只是部分的输出结果。完整示例代码参见文章末尾,可以自己运行一下仔细研究研究。

结尾总结

在业务开发中直接使用ASM的情况肯定较少,一般在框架开发或组件开发时可能会用到。

ASM的使用并不是特别难,多做测试即可发现规律。

我在测试时发现两个值得注意的事情:

只能访问到显式设置注解属性的那些值,对于注解的默认属性值是访问不到的。

要想获取到注解的默认值,需要去访问注解自己的字节码文件,而不是使用注解的类的字节码文件。

只能访问到类型自己定义的信息,从父类型继承的信息也是访问不到的。

也就是说,字节码中只包括在源码文件中出现的信息,字节码本身不处理继承问题。

因此,JVM在加载一个类型时,要加载它的父类型,并处理继承问题。

完整示例代码:
https://github.com/coding-new-talking/taste-spring.git

(END)

作者是工作超过10年的码农,现在任架构师。喜欢研究技术,崇尚简单快乐。追求以通俗易懂的语言解说技术,希望所有的读者都能看懂并记住。下面是公众号和知识星球的二维码,欢迎关注!

       

品Spring:实现bean定义时采用的“先进生产力”的更多相关文章

  1. 品Spring:bean定义上梁山

    认真阅读,收获满满,向智慧又迈进一步... 技术不枯燥,先来点闲聊 先说点好事高兴一下.前段时间看新闻说,我国正式的空间站建设已在进行当中.下半年,长征五号B运载火箭将在海南文昌航天发射场择机将空间站 ...

  2. 品Spring:bean工厂后处理器的调用规则

    上一篇文章介绍了对@Configuration类的处理逻辑,这些逻辑都写在ConfigurationClassPostProcessor类中. 这个类不仅是一个“bean工厂后处理器”,还是一个“be ...

  3. Spring的Bean定义

    以下内容引用自http://wiki.jikexueyuan.com/project/spring/bean-definition.html: Bean定义 被称作bean的对象是构成应用程序的支柱也 ...

  4. Spring中 bean定义的parent属性机制的实现分析

    在XML中配置bean元素的时候,我们常常要用到parent属性,这个用起来很方便就可以让一个bean获得parent的所有属性 在spring中,这种机制是如何实现的?     对于这种情况 tra ...

  5. Spring XML Bean 定义的加载和注册

    前言 本篇文章主要介绍 Spring IoC 容器怎么加载 bean 的定义元信息. 下图是一个大致的流程图: 第一次画图,画的有点烂.

  6. 品Spring:负责bean定义注册的两个“排头兵”

    别看Spring现在玩的这么花,其实它的“筹码”就两个,“容器”和“bean定义”. 只有先把bean定义注册到容器里,后续的一切可能才有可能成为可能. 所以在进阶的路上如果要想走的顺畅些,彻底搞清楚 ...

  7. 品Spring:SpringBoot轻松取胜bean定义注册的“第一阶段”

    上一篇文章强调了bean定义注册占Spring应用的半壁江山.而且详细介绍了两个重量级的注册bean定义的类. 今天就以SpringBoot为例,来看看整个SpringBoot应用的bean定义是如何 ...

  8. 品Spring:SpringBoot发起bean定义注册的“二次攻坚战”

    上一篇文章整体非常轻松,因为在容器启动前,只注册了一个bean定义,就是SpringBoot的主类. OK,今天接着从容器的启动入手,找出剩余所有的bean定义的注册过程. 具体细节肯定会颇为复杂,同 ...

  9. 品Spring:详细解说bean后处理器

    一个小小的里程碑 首先感谢能看到本文的朋友,感谢你的一路陪伴. 如果每篇都认真看的话,会发现本系列以bean定义作为切入点,先是详细解说了什么是bean定义,接着又强调了bean定义为什么如此重要. ...

随机推荐

  1. 一个最简单的通过自定义注解形式实现AOP的例子

    1.首先实现AOP实例的第一步即声明切面类,两种方式(1.基于注解形式@Aspect,2.基于xml配置,一般都通过注解来声明切面类) 2.切入点表达式大致也有两种,一种是直接根据方法的签名来匹配各种 ...

  2. Kafka到底有几个Offset?——Kafka核心之偏移量机制

    ​ Kafka是由LinkIn开源的实时数据处理框架,目前已经更新到2.3版本.不同于一般的消息中间件,Kafka通过数据持久化和磁盘读写获得了极高的吞吐量,并可以不依赖Storm,SparkStre ...

  3. python 36 进程池、线程池

    目录 1. 死锁与递归锁 2. 信号量Semaphor 3. GIL全局解释器锁:(Cpython) 4. IO.计算密集型对比 4.1 计算密集型: 4.2 IO密集型 5. GIL与Lock锁的区 ...

  4. switch语句(下)(转载)

    之前我们介绍了在switch语句中使用整数类型和枚举类型的情况.这一部分继续介绍使用string类型的情况.string类型是switch语句接受的唯一一种引用类型参数. 下面来看一段C#代码. 代码 ...

  5. C#开发学习人工智能的第一步

    前言 作为一个软件开发者,我们除了要学会复制,黏贴,还要学会调用API和优秀的开源类库. 也许,有人说C#做不了人工智能,如果你相信了,那只能说明你的思想还是狭隘的. 做不了人工智能的不是C#这种语言 ...

  6. spss数据分析可以被人工智能替换吗

    作为一名需要对课题进行研究的大学生,我在日常学习中经常需要用到spss,虽然老师上课已经初步教了我如何用这个软件,然而,在使用过程中我还是遇到了许多问题.具体来说,就是这个软件在很多地方都不够与时俱进 ...

  7. 洛谷 P1640 【连续攻击游戏】

    question bank :luogu question Number :1640 title :Continuous attacking game link :https://www.luogu. ...

  8. [python] - profilers性能分析器

    1. 性能分析器: profile, hotshot, cProfile 2. 作用: 测试函数的执行时间 每次脚本执行的总时间

  9. CodeForces - 1150 D Three Religions

    题目传送门 题解: id[ i ][ j ] 代表的是在第j个位置之后的第i个字符的位置在哪里. dp[ i ][ j ][ k ] 代表的是 第一个串匹配到第i个位置, 第二个串匹配到第j个位置, ...

  10. Codeforces 734D. Anton and Chess(模拟)

    Anton likes to play chess. Also, he likes to do programming. That is why he decided to write the pro ...