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

           

  • 相关阅读:
    MVC3 模板页页预留Section
    LINQ表达式总结笔记
    分布式事务管理器(MSDTC)的事务处理异常的排错
    ado。net的事物BeginTransaction demo
    TransactionScope类使用场景和方法介绍
    Linq中使用Left Join
    FullText Search in ASP.NET using Lucene.NET
    EF的BeginTransaction 用法
    mvc4 @Html.Partial,@Html.RenderPartial
    Android监听EditText内容变化
  • 原文地址:https://www.cnblogs.com/lixinjie/p/taste-spring-003.html
Copyright © 2011-2022 走看看