Spring核心组件原理解析

前言

尽管希腊哲学家赫拉克利特(Heraclitus)并不作为一名软件开发人员而闻名,但他似乎深谙此道。他的一句话经常被引用:“唯一不变的就是变化”,这句话抓住了软件开发的真谛。

我们现在开发应用的方式和1年前、5年前、10年前都是不同的,更别提15年前了,当时RodJohnson的图书 Expert One-on-One J2EE Design and Development 介绍了Spring框架的初始形态。当时,最常见的应用形式是基于浏览器的Web应用,后端由关系型数据库作为支撑。尽管这种形式的开发依然有它的价值,Spring也为这种应用提供了良好的支持,但是我们现在感兴趣的还包括如何开发面向云的由微服务组成的应用,这些应用会将数据保存到各种类型的数据库中。

另外一个崭新的关注点是反应式编程,它致力于通过非阻塞操作提供更好的扩展性并提升性能。随着软件开发的发展,Spring框架也在不断变化,以解决现代应用开发中的问题,其中就包括微服务和反应式编程。Spring还通过引入Spring Boot简化自己的开发模型。

Spring 的核心

任何实际的应用程序都是由很多组件组成的,每个组件负责整个应用功能的一部分,这些组件需要与其他的应用元素进行协调以完成自己的任务。当应用程序运行时,需要以某种方式创建并引入这些组件。

Spring Framework 总共有十几个组件,但真正核心的组件只有三个:Spring Core,Spring Context 和 Spring Bean,它们奠定了 Spring 的基础并撑起了 Spring 的框架结构。Spring 的其它功能特性例如 Web、AOP、JDBC 等都是在其基础上发展实现的。

Spring之中最重要的当属Bean了,Spring实际上就是面向Bean的编程,Bean对于Spring的意义就好比Object对于OOP的意义一样。那么,三个核心组件之间是如何协同工作的呢?如果把Bean比作一场演出中的演员,那么Context就是这场演出的舞台,Core就是演出的道具,至于演出的节目,就是Spring的一系列特色功能了。

我们知道Bean包裹的是Object,而Object中必然有数据,Context就是给这些数据提供生存环境,发现每个Bean之间的关系,为他们建立并维护好这种关系。这样来说,Context就是一个Bean关系的集合,这个关系集合就是我们所说的IOC容器。那么Core又有什么作用呢?Core就是发现、建立和维护每个Bean之间的关系所需的一系列工具,就是我们经常说的Util。

Bean 组件

Bean组件在Spring的org.springframework.beans包下,主要完成了Bean的创建、Bean的定义以及Bean的解析三件事。

SpringBean的创建是典型的工厂模式,其工厂的继承层次关系如图所示:

Spring 使用工厂模式来管理程序中使用的对象(Bean),Bean 工厂最上层的接口为 BeanFactory,简单来看,工厂就是根据需要返回相应的 Bean 实例。

public interface BeanFactory { 

    //...         

    Object getBean(String name); 

}

在工厂模式中,在工厂的实现类中生成 Bean 返回给调用客户端,这就要求客户端提供生成自己所需类实例的工厂类,增加客户负担。Spring 结合控制反转和依赖注入为客户端提供所需的实例,简化了客户端的操作。具体的实现方式大致如下。

public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory 

        implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable { 

    /** Map of bean definition objects, keyed by bean name */ 

    private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>; 

    public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition){     

        //... 

    } 

}

beanDefinitionMap 作为具体的 Bean 容器,Spring 创建的对象实例保存其中。客户端需要时,使用工厂的 getBean 方法去试图得到相应的实例,如果实例已存在,则返回该实例;如果实例不存在,则首先产生相应实例并通过 registerBeanDefinition 方法将其保存在 beanDefinitionMap 中(Lazy Initialization),然后返回该实例给客户端。

Spring Bean 工厂的继承关系

beanDefinitionMap 并不直接保存实例本身,而是将实例封装在 BeanDefinition 对象后进行保存。BeanDefinition 包含了实例的所有信息,其简化版的定义如下。

public class BeanDefinition { 

    private Object bean; 

    private Class<?> beanClass; 

    private String beanClassName; 

    // Bean 属性字段的初始化值 

    private BeanPropertyValues beanPropertyValues; 

    //... 

}

Spring Bean 工厂生产 Bean 时

先将实例的类型参数保存到 beanClass 和 beanClassName,将需要初始化的字段名和值保存到 beanPropertyValues 中,这个过程 Spring 通过控制反转来实现,本文第二小节将予以简要说明

生成 bean 实例,并利用反射机制将需要初始化的字段值写入 bean 实例,将实例保存在 bean 中,完成 BeanDefinition 的构建。

    假设我们已经完成了步骤 1) 的操作,之后的过程用代码表述如下所示。

public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition){ 

    //生成 bean 实例,并完成初始化 

    Object bean = createBean(beanDefinition); 

    //将 bean 实例保存在 beanDefinition 中 

    beanDefinition.setBean(bean); 

    //将 beanDefinition 实例保存在 Spring 容器中 

    beanDefinitionMap.put(beanName, beanDefinition); 

protected Object createBean(BeanDefinition beanDefinition) { 

    try{ 

        Object bean = beanDefinition.getBeanClass().newInstance(); 

        try { 

            setBeanPropertyValues(bean, beanDefinition); 

        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException e) { 

            e.printStackTrace(); 

        } 

        return bean; 

    }catch(InstantiationException e){ 

        e.printStackTrace(); 

    }catch(IllegalAccessException e){ 

        e.printStackTrace(); 

    } 

    return null; 

protected void setBeanPropertyValues(Object bean, BeanDefinition beanDefinition) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException{

    for(PropertyValue pv : beanDefinition.getBeanPropertyValues().getBeanPropertyValues()){ 

        Field beanbeanFiled = bean.getClass().getDeclaredField(pv.getName()); 

        beanFiled.setAccessible(true); 

        beanFiled.set(bean, pv.getValue()); 

    } 

}

Context 组件

前面说到,Context组件的作用是给Spring提供一个运行时的环境,用以保存各个对象的状态,我们来看一下与Context相关的类结构图。

从图中可以看出,Context类结构的顶级父类是ApplicationContext,它除了能标识一个应用环境的基本信息以外,还继承了5个接口,这5个接口主要是扩展了Context的功能。ApplicationContext的子类主要包含两个方向,图中已作说明。再往下就是构建Context的文件类型,接着就是访问Context的方式。

一般地,传统的程序设计中,无论是使用工厂创建实例,或是直接创建实例,实例调用者都要先主动创建实例,而后才能使用。控制反转(Inverse of Control) 将实例的创建过程交由容器实现,调用者将控制权交出,是所谓控制反转。

依赖注入(Dependence Injection) 在控制反转的基础上更进一步。如果没有依赖注入,容器创建实例并保存后,调用者需要使用 getBean(String beanName) 才能获取到实例。使用依赖注入时,容器会将 Bean 实例自动注入到完成相应配置的调用者,供其进一步使用。

Context 组件借助上述的控制反转和依赖注入,协助实现了 Spring 的 Ioc 容器。下面我们以一个 Service 类作为所需的 Bean 实例进行说明。实际应用中,我们会需要 Spring 管理很多 Bean 实例。

public class SampleService { 

    private String service; 

        public String getService() { 

        return service; 

    } 

    public void setService(String service) { 

this.service= service; 

    } 

}

在程序运行过程中,需要一个 SampleService ,我们不让调用者 new 一个实例,而是在配置文件中表明该 SampleService 的实例交由 Spring 容器进行管理,并指定其初始化参数。配置文件即资源,其内容如下。

<?xml version="1.0" encoding="UTF-8"?> 

<beans> 

<bean name="sampleService " class="com.service.SampleService "> 

<property name="service" value="This is a service"></property> 

</bean> 

</beans>

Spring Core 组件提供 ResourceLoader 接口,便于读入 xml 文件或其他资源文件。其核心功能代码应该提供如下方法。

public class ResourceLoader { 

    public Resource getResource(String location){ 

        URL resource = this.getClass().getClassLoader().getResource(location); 

        return new UrlResource(resource); 

    } 

// UrlResource 的功能代码 

public class UrlResource implements Resource { 

    private final URL url; 

    public UrlResource(URL url){ 

this.url = url; 

    } 

    @Override 

    public InputStream getInputStream() throws IOException { 

        URLConnection urlurlConnection = url.openConnection(); 

        urlConnection.connect(); 

        return urlConnection.getInputStream(); 

    } 

}

即加载资源文件,并以数据流的形式返回。Context 根据资源中的定义,生成相应的 bean 并保存在容器中,bean 的名字是 sampleService ,供程序进一步使用。这样就完成了控制反转的工作。接下来就需要把 sampleService 注入到需要使用它的地方,亦即完成依赖注入操作。

现在假设 SampleController 中使用 SampleService 的对象,Spring 提供三种依赖注入的方式,构造器注入、setter 注入和注解注入。

public class SampleController { 

    /** 

    * 3\. 注解注入 

    **/ 

    /* @Autowired */ 

    private SampleService sampleService; 

    /** 

    * 1\. 构造器注入 

    **/ 

    public SampleController(SampleService sampleService){ 

this.sampleService = sampleService; 

    } 

    //无参构造函数 

    public SampleController(){} 

    // 类的核心功能 

    public void process(){ 

        System.out.println(sampleService.getService()); 

    } 

    /** 

    * 2\. setter 注入 

    **/ 

    /*public void setService(SampleService service) { 

this.service= service; 

    }*/ 

}

三种注入方式在配置文件中对应不同的配置方式,在前面 xml 文件的基础上,我们可以分别实现这三种注入方式。需要注意的是,这里 SampleController 也是使用 Spring 的 Ioc 容器生成管理的。

<?xml version="1.0" encoding="UTF-8"?> 

<beans> 

<bean name="sampleService " class="com.service.SampleService "> 

<property name="service" value="This is a service"></property> 

</bean> 

<!-- 1\. 构造器注入方式为SampleContorller 的 bean 注入 SampleService --> 

<bean name="sampleContorller" class="com.controller.SampleContorller"> 

<!-- index 是构造方法中相应参数的顺序 --> 

<constructor-arg index="0" ref="sampleService"></constructor-arg> 

</bean> 

<!-- 2\. setter 注入方式为SampleContorller 的 bean 注入 SampleService --> 

<!-- 

<bean name="sampleContorller" class="com.controller.SampleContorller"> 

<property name="sampleService " ref="sampleService"></property> 

</bean> 

--> 

<!-- 3\. 注解注入方式为SampleContorller 的 bean 注入 SampleService --> 

<!-- 

<bean name="sampleContorller" class="com.controller.SampleContorller"> 

<!-- 不需要配置,Spring 自动按照类型注入相应的 bean --> 

</bean> 

--> 

</beans>

Core组件

Core组件一个重要的组成部分就是定义了资源的访问方式。Core组价把所有的资源都抽象成一个接口,这样,对于资源使用者来说,不需要考虑文件的类型。对资源提供者来说,也不需要考虑如何将资源包装起来交给别人使用(Core组件内所有的资源都可以通过InputStream类来获取)。另外,Core组件内资源的加载都是由ResourceLoader接口完成的,只要实现这个接口就可以加载所有的资源。

那么,Context和Resource是如何建立关系的呢?通过前面Context的介绍我们知道,Context组件里面的类或者接口最终都实现了ResourcePatternResolver接口,ResourcePatternResolver接口的作用就是加载、解析和描述资源。这个接口相当于Resource里面的一个接头人,它把Resource里的资源加载、解析和定义整合到一起,便于其他组件使用。

需要这本电子书版的关注我,转发、私信关键词“实战”免费领取,因文章限制还有更多免费资料。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341