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