品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 Numberextends C<@Valid Long, @NotNull Dateimplements AB {

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

 

       

posted on 2019-09-27 08:07  编程新说(李新杰)  阅读(932)  评论(1编辑  收藏  举报