当咱们把写好的业务代码交给Spring以后,Spring都会作些什么呢?java
仔细想象一下,再稍微抽象一下,Spring所作的几乎所有都是:git
“bean的实例化,bean的依赖装配,bean的初始化,bean的方法调用,bean的销毁回收”。github
那问题来了,Spring为何可以准确无误的完成这波对bean的操做呢?答案很简单,就是:spring
“Spring掌握了有关bean的足够多的信息”。编程
这就是本系列文章第一篇“帝国的基石”的核心思想。Spring经过bean定义的概念收集到了bean的所有信息。设计模式
这件事也说明,当咱们拥有了一个事物的大量有效信息以后,就能够作出一些很是有价值的操做。如大数据分析,用户画像等。数组
紧接着就是第二个问题,Spring应该采用什么样的方式来收集bean的信息呢?缓存
这就是本系列文章第二篇“bean定义上梁山”主要讲的内容。框架
首先是统一了编程模型,只要是围绕Spring的开发,包括框架自身的开发,最后大都转化为bean定义的注册。ide
为了知足不一样的场景,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)