框架源码系列四:手写Spring-配置(为什么要提供配置的方法、选择什么样的配置方式、配置方式的工作过程是怎样的、分步骤一个一个的去分析和设计)
一、为什么要提供配置的方法
经过前面的手写Spring IOC、手写Spring DI、手写Spring AOP,我们知道要创建一个bean对象,需要用户先定义好bean,然后注册到bean工厂才能创建一个bean对象。代码如下:
static PreBuildBeanFactory bf = new PreBuildBeanFactory();
GenericBeanDefinition bd = new GenericBeanDefinition(); bd.setBeanClass(ABean.class); List<Object> args = new ArrayList<>(); args.add("abean01"); args.add(new BeanReference("cbean")); bd.setConstructorArgumentValues(args); bf.registerBeanDefinition("abean", bd); bd = new GenericBeanDefinition(); bd.setBeanClass(CBean.class); args = new ArrayList<>(); args.add("cbean01"); bd.setConstructorArgumentValues(args); bf.registerBeanDefinition("cbean", bd);
那么如果我们上面的过程换成配置的方式会是什么样的呢?
<bean id="abean" class="com.study.spring.samples.ABean"> <constructor-arg type="String" value="abean01"></constructor-arg> <constructor-arg ref="cbean"></constructor-arg> </bean> <bean id="cbean" class="com.study.spring.samples.CBean"> <constructor-arg type="String" value="cbean01"></constructor-arg> </bean>
经过上面的创建bean对象的过程由ava代码转为xml配置的方式,可以看出使用配置有如下优势:
1. 因为我们是写框架的,提供配置的方式,别人使用更简单,改动更加灵活
2. 要新增修改东西时不需要改代码
二、选择什么样的配置方式
用过Spring的朋友都知道,配置方式有两种:
1. xml
2. 注解
三、配置方式的工作过程是怎样的
四、分步骤一个一个的去分析和设计
1. 定义xml标准和注解标准
首先我们需要理清楚定义xml标准、定义注解标准的目的是什么,定义它们的目的是让用户可以使用它们去配置bean定义和标注bean定义。那么
问题1:bean定义需要指定些什么信息呢?
需要指定的信息我们可以从之前写的BeanDefinition里面看到
可以看到bean定义需要上面的这些信息
如果使用xml配置的方式,我们需要为上面的这些信息定义一个DTD(Document Type Definition)文件或者XSD(XML Schemas Definition)文件,具体实现利用了Spring提供的可扩展Schema机制实现,实现方式查看我前面的文章:
然后用户根据提供的DTD文件定义bean定义需要的信息即可:
<bean id="abean" class="com.study.spring.samples.ABean" init-method="init" destroy-method="destroy" scope="prototype" > <constructor-arg type="String" value="abean01"></constructor-arg> <constructor-arg ref="cbean"></constructor-arg> <property name="name" value="leSmall"></property> <property name="age" value="18"></property> </bean>
问题2:如果使用注解的方式,需要定义一些什么注解?
从需要的bean定义信息里面我们可能需要做如下的步骤:
1) 指定类
2)指定beanName
3)指定scope
4)指定工厂bean
5)指定工厂方法
6)指定init method
7)指定销毁方法
8)指定构造参数依赖
9)指定属性依赖
前面的1)到 7)我们可以定义一个注解@Component,里面持有1)到 7)需要的bean定义的信息,在创建bean定义的时候通过反射获取这些信息
package com.dn.spring.context.config.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import com.study.spring.beans.BeanDefinition; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Component { String value() default ""; String name() default ""; String scope() default BeanDefinition.SCOPE_SINGLETION; String factoryMethodName() default ""; String factoryBeanName() default ""; String initMethodName() default ""; String destroyMethodName() default ""; }
注意:注解 Component里面没有定义 1)指定类 需要的元素,因为通过在类上加上@Component就能获取到类的名称了
前面的8)到 9)我们可以定义三个注解@Autowired、@Qualifier、@Value,其中@Autowired用来指定属性依赖的bean依赖或者有多个构造函数时指定使用哪个构造函数;@Qualifier用来指定bean依赖的具体bean,比如一个类有多个bean,可以指定具体的一个bean,如@Qualifier("cbean01") CBean cb;@Value用来指定属性依赖的非bean依赖。
@Autowired代码:
package com.study.spring.context.config.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Autowired { /** * Declares whether the annotated dependency is required. * <p> * Defaults to {@code true}. */ boolean required() default true; }
@Qualifier代码:
package com.study.spring.context.config.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Qualifier { String value() default ""; }
@Value代码:
package com.study.spring.context.config.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Value { String value(); }
4个注解的使用示例代码:
package com.study.spring.samples; import com.study.spring.context.config.annotation.Autowired; import com.study.spring.context.config.annotation.Component; import com.study.spring.context.config.annotation.Qualifier; import com.study.spring.context.config.annotation.Value; @Component(initMethodName = "init", destroyMethodName = "destroy") public class ABean { private String name; private CBean cb; @Autowired private DBean dbean; @Autowired public ABean(@Value("mike") String name, @Qualifier("cbean01") CBean cb) { super(); this.name = name; this.cb = cb; System.out.println("调用了含有CBean参数的构造方法"); } public ABean(String name, CCBean cb) { super(); this.name = name; this.cb = cb; System.out.println("调用了含有CCBean参数的构造方法"); } public ABean(CBean cb) { super(); this.cb = cb; } }
2. 用户怎么指定配置的xml文件的位置?用户怎么指定要扫描的包?
同样的我们需要分析用户指定xml配置文件的位置和指定扫描的包的目的是什么,目的是让提供方去加载xml文件和扫描包下的类,那么
问题1:怎么指定?
我们需要为用户提供一种方式
问题2:下面这些事情在哪里做好?
这里所做的事情是解析xml配置和反射获取bean定义注解、创建bean定义、向bean工厂注册bean定义,他不是bean工厂的事,我们应该单独定义接口和类完成这些事
无论是XmlApplicationContext还是AnnotationApplicationContext都要使用BeanFactory和BeanDefinitionRegistry,所以可以进一步优化类图
现在用户要使用我们的框架需要知道下面的接口和类:
那么让用户只需要知道ApplicationContext接口及其子类是否对用户更简单?
是的,这里我们可以用到前面学习的设计模式——外观模式,即提供一个新的外观,用户只需要知道要调用哪些接口和类,而不需要知道具体的实现。正确的做法是把上面类图中的BeanFactory和ApplicationContext合并到一起
3.怎么加载xml文件的配置?怎么扫描用户指定包下的类?
3.1 加载用户指定的xml配置文件
思考1:xml的来源会有多种吗?
会
那么它们的加载方式一样吗?
不一样
对于xml解析来说,从加载过程它希望获得什么?
xml解析希望获得流InputStream
我们希望能加载不同来源的xml,向解析xml配置提供统一的使用接口,那么该如何来设计接口和类呢?
让解析xml配置面向接口编程,不同的xml来源都实现该接口,提供不同的实现去加载xml配置文件最终都生产出一个InputStream
问题:这里我们定义不同的Resource类对应不同的xml来源,谁去负责分辨创建他们的对象?因为用户给定的是一个一个的字符串(这对他们来说是最简单的方式)
这个分辨字符串 ,创建对应的Resource对象的工作就是加载xml配置文件,这个事情有ApplicationContext来做。
这里就需要使用前面学过的设计模式工厂模式了:根据不同的字符串创建不同的对象
给ApplicationContext定义一个加载xml配置文件的行为ResourceLoader::
问题:怎么分辨字符串?
定义一套规则
工厂根据不同的前缀来区分,创建不同的Resource对象
3.2 注解的方式如何扫描
扫描过程如下:
到指定的包目录下找出所有的类文件(包含子孙包下的)
根据上面的扫描过程分析,我们需要定义一个正则表达式的匹配器来看扫描到的路径是否匹配用户指定的扫描包的路径:
思考:如果要扫描的是com.study下所有service包下的类,现在满足吗?
com.study/**/service/* 这种写法是ant path表达式
这时就需要在PathMatcher匹配器的基础上扩展一个ant path表达式的匹配器了:
思考:扫到了指定包下的class文件,我们最终需要的是什么?
我们最终需要的是类名,因为要拿类名去获取bean定义信息、创建bean定义、注册bean定义到bean工厂
那么这里我们需要设计什么样的接口和类呢?
在加载xml文件的时候存放扫描到的类可以吗?
不可以
思考:扫描的事情是由AnnotationApplicationContext这个类来做还是外包给其他的类来做?
外包给ClassPathBeandefinitionScanner这个类来做
说明:
扫描外包给ClassPathBeandefinitionScanner这个类来做,ClassPathBeandefinitionScanner里面的scan方法扫描完成以后得到bean定义信息,然后创建bean定义,把bean定义通过-registry : BeanDefinitionRegistry注册到bean工厂
思考:在哪里启动扫描调用ClassPathBeandefinitionScanner的scan方法?
在AnnotationApplicationContext的构造方法里面调用scan方法
4. 提供方解析xml配置文件、提供方反射获取bean定义注解
问题1:加载和扫描的输出是什么?
加载和扫描的输出是Resource
说明:
上面这张图红色部分要做的事情就是解析xml、反射获取注解,然后得到Resource读取bean定义信息,把bean定义信息注册到bean工厂里面去创建bean对象,由此我们可以做下面的设计:
说明:
BeanDefinitionReader负责解析xml、反射获取注解得到Resource,然后AbstractBeanDefinitionReader从Resource里面获取bean定义信息、创建bean定义、注册bean定义到bean工厂,具体的实现交给XmlBeanDefinitionReader和AnnotationBeanDefinitionReader这两个读取器来做
那么谁应该持有BeanDefinitionReader呢?请看下面完整的类图:
下面根据上面完整的类图开始写代码:
先定义接口,再定义抽象类、最后再定义具体类,把架子搭起来,然后再开始写具体的业务逻辑
完整代码获取地址:
https://github.com/leeSmall/FrameSourceCodeStudy/tree/master/spring-v4