品Spring:实现bean定义时采用的“先进生产力”
前景回顾
当我们把写好的业务代码交给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定义时采用的“先进生产力”的更多相关文章
- 品Spring:bean定义上梁山
认真阅读,收获满满,向智慧又迈进一步... 技术不枯燥,先来点闲聊 先说点好事高兴一下.前段时间看新闻说,我国正式的空间站建设已在进行当中.下半年,长征五号B运载火箭将在海南文昌航天发射场择机将空间站 ...
- 品Spring:bean工厂后处理器的调用规则
上一篇文章介绍了对@Configuration类的处理逻辑,这些逻辑都写在ConfigurationClassPostProcessor类中. 这个类不仅是一个“bean工厂后处理器”,还是一个“be ...
- Spring的Bean定义
以下内容引用自http://wiki.jikexueyuan.com/project/spring/bean-definition.html: Bean定义 被称作bean的对象是构成应用程序的支柱也 ...
- Spring中 bean定义的parent属性机制的实现分析
在XML中配置bean元素的时候,我们常常要用到parent属性,这个用起来很方便就可以让一个bean获得parent的所有属性 在spring中,这种机制是如何实现的? 对于这种情况 tra ...
- Spring XML Bean 定义的加载和注册
前言 本篇文章主要介绍 Spring IoC 容器怎么加载 bean 的定义元信息. 下图是一个大致的流程图: 第一次画图,画的有点烂.
- 品Spring:负责bean定义注册的两个“排头兵”
别看Spring现在玩的这么花,其实它的“筹码”就两个,“容器”和“bean定义”. 只有先把bean定义注册到容器里,后续的一切可能才有可能成为可能. 所以在进阶的路上如果要想走的顺畅些,彻底搞清楚 ...
- 品Spring:SpringBoot轻松取胜bean定义注册的“第一阶段”
上一篇文章强调了bean定义注册占Spring应用的半壁江山.而且详细介绍了两个重量级的注册bean定义的类. 今天就以SpringBoot为例,来看看整个SpringBoot应用的bean定义是如何 ...
- 品Spring:SpringBoot发起bean定义注册的“二次攻坚战”
上一篇文章整体非常轻松,因为在容器启动前,只注册了一个bean定义,就是SpringBoot的主类. OK,今天接着从容器的启动入手,找出剩余所有的bean定义的注册过程. 具体细节肯定会颇为复杂,同 ...
- 品Spring:详细解说bean后处理器
一个小小的里程碑 首先感谢能看到本文的朋友,感谢你的一路陪伴. 如果每篇都认真看的话,会发现本系列以bean定义作为切入点,先是详细解说了什么是bean定义,接着又强调了bean定义为什么如此重要. ...
随机推荐
- 一个最简单的通过自定义注解形式实现AOP的例子
1.首先实现AOP实例的第一步即声明切面类,两种方式(1.基于注解形式@Aspect,2.基于xml配置,一般都通过注解来声明切面类) 2.切入点表达式大致也有两种,一种是直接根据方法的签名来匹配各种 ...
- Kafka到底有几个Offset?——Kafka核心之偏移量机制
Kafka是由LinkIn开源的实时数据处理框架,目前已经更新到2.3版本.不同于一般的消息中间件,Kafka通过数据持久化和磁盘读写获得了极高的吞吐量,并可以不依赖Storm,SparkStre ...
- python 36 进程池、线程池
目录 1. 死锁与递归锁 2. 信号量Semaphor 3. GIL全局解释器锁:(Cpython) 4. IO.计算密集型对比 4.1 计算密集型: 4.2 IO密集型 5. GIL与Lock锁的区 ...
- switch语句(下)(转载)
之前我们介绍了在switch语句中使用整数类型和枚举类型的情况.这一部分继续介绍使用string类型的情况.string类型是switch语句接受的唯一一种引用类型参数. 下面来看一段C#代码. 代码 ...
- C#开发学习人工智能的第一步
前言 作为一个软件开发者,我们除了要学会复制,黏贴,还要学会调用API和优秀的开源类库. 也许,有人说C#做不了人工智能,如果你相信了,那只能说明你的思想还是狭隘的. 做不了人工智能的不是C#这种语言 ...
- spss数据分析可以被人工智能替换吗
作为一名需要对课题进行研究的大学生,我在日常学习中经常需要用到spss,虽然老师上课已经初步教了我如何用这个软件,然而,在使用过程中我还是遇到了许多问题.具体来说,就是这个软件在很多地方都不够与时俱进 ...
- 洛谷 P1640 【连续攻击游戏】
question bank :luogu question Number :1640 title :Continuous attacking game link :https://www.luogu. ...
- [python] - profilers性能分析器
1. 性能分析器: profile, hotshot, cProfile 2. 作用: 测试函数的执行时间 每次脚本执行的总时间
- CodeForces - 1150 D Three Religions
题目传送门 题解: id[ i ][ j ] 代表的是在第j个位置之后的第i个字符的位置在哪里. dp[ i ][ j ][ k ] 代表的是 第一个串匹配到第i个位置, 第二个串匹配到第j个位置, ...
- 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 ...