文档章节

框架源码系列四:手写Spring-配置(为什么要提供配置的方法、选择什么样的配置方式、配置方式的工作过程是怎样的、分步骤一个一个的去分析和设计)

o
 osc_y0xqgfqd
发布于 2019/03/17 10:44
字数 2581
阅读 10
收藏 0

行业解决方案、产品招募中!想赚钱就来传!>>>

一、为什么要提供配置的方法

经过前面的手写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机制实现,实现方式查看我前面的文章:

dubbo系列三:dubbo源码分析(dubbo框架是如何跟spring进行整合的,消费者获取的代理实例是如何创建的呢、生产者怎么把内容注册到注册中心的,然后消费者是如何获取这个信息的、dubbo负载均衡策略)

然后用户根据提供的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

o
粉丝 0
博文 500
码字总数 0
作品 0
私信 提问
加载中
请先登录后再评论。
Netty那点事(三)Channel与Pipeline

Channel是理解和使用Netty的核心。Channel的涉及内容较多,这里我使用由浅入深的介绍方法。在这篇文章中,我们主要介绍Channel部分中Pipeline实现机制。为了避免枯燥,借用一下《盗梦空间》的...

黄亿华
2013/11/24
2W
22
记一次失败的Perl + Nginx + FastCGI 配置过程

这两天心血来潮,不知道为什么和 Perl + Nginx + FastCGI 配置 耗上了。但是失败了,记录如下: 1)安装Nginx 1.4.3 ,我的是WINDOWS 7 系统,修改配置文件如下: location ~ .(pl|cgi|perl)?...

通吃岛-低手哥
2013/10/27
1.6K
7
访问安全控制解决方案

本文是《轻量级 Java Web 框架架构设计》的系列博文。 今天想和大家简单的分享一下,在 Smart 中是如何做到访问安全控制的。也就是说,当没有登录或 Session 过期时所做的操作,会自动退回到...

黄勇
2013/11/03
3.5K
6
用vertx实现高吞吐量的站点计数器

工具:vertx,redis,mongodb,log4j 源代码地址:https://github.com/jianglibo/visitrank 先看架构图: 如果你不熟悉vertx,请先google一下。我这里将vertx当作一个容器,上面所有的圆圈要...

jianglibo
2014/04/03
4.1K
3
Flappy Bird(安卓版)逆向分析(一)

更改每过一关的增长分数 反编译的步骤就不介绍了,我们直接来看反编译得到的文件夹 方法1:在smali目录下,我们看到org/andengine/,可以知晓游戏是由andengine引擎开发的。打开/res/raw/at...

enimey
2014/03/04
6K
18

没有更多内容

加载失败,请刷新页面

加载更多

听说你还不会jwt和swagger-饭我都不吃了带着实践项目我就来了

前言 哈喽,大家好,我是asong,这是我的第八篇原创文章。听说你们还不会jwt、swagger,所以我带来一个入门级别的小项目。实现用户登陆、修改密码的操作。使用GIN(后台回复Golang梦工厂:g...

sunsong2020
今天
3
0
python合并excel的多个sheet

简介 因为,每天都会有的大量excel报表汇总处理任务,所以写了一个脚本来处理。 就是找出每一个excel中特定的sheet,把这些sheet的特定列读取出来合并到一个sheet中。 因为每一个sheet的数据...

trayvon
30分钟前
7
0
如何设计一个幂等接口

什么叫幂等接口 幂等性,就是只多次操作的结果是一致的。这里可能有人会有疑问。 问:为什么要多次操作结果都一致呢?比如我查询数据,每次查出来的都一样,即使我修改了每次查出来的也都要一...

贪挽懒月
昨天
0
0
华为阿里下班时间曝光:所有的光鲜,都有加班的味道

点击上方 Z先生点记,加为星标 第一时间收到 Python 技术干货! 来源:GitHubPorn 声明:本文由HR人力资源成长俱乐部整理发布。素材来源程序员之家。如需转载,请务必注明以上信息,侵权必究...

zeroing1
08/06
0
0
Proguard 常用规则

入口 为了决定哪些代码要被保留哪些代码要出丢弃和混淆,必须指定入口点。这些入口点通常是 main方法,activity,service等。 在压缩阶段,Proguard从这些入口点开始递归确定哪些类或类成员要...

佛系编码
38分钟前
8
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部