Spring核心——设计模式与IoC

全文共 3208 个字

“Spring”——每一个Javaer开发者都绕不开的字眼,从21世纪第一个十年国内异常活跃的SSH框架,到现在以Spring Boot作为入口粘合了各种应用。Spring现在已经完成了从web入口到微服务架构再到数据处理整个生态,看着现在https://spring.io/projects上长长的项目清单,一脸懵逼的自问到这些到底是啥?可以干嘛?

一切都从IoC开始

早期的Spring并没有这么多亮瞎眼的项目,仅仅是围绕着core、context、beans以及MVC提供了一个简单好用搭建网站级应用的工具。那个时候完全是一个与J2EE的繁杂多样对抗简单便捷的小清新。Srping之父Rod的一本《J2EE Development without EJB》宣告J2EE那么名堂完全没多大用处。经过这么多年的发展,事实也证明除了Servlet、JDBC以及JSP似乎其他东西可有可无。后来Vertx、WebFlux等Reactive机制框架的出现,以及前后端分离开发的盛行,似乎Servlet也可有可无了、jsp也快消失了。所以现在Oracle干脆把J2EE这个烫手山芋直接丢给开源社区了。

Rod的轮子理论造就了Spring的2大核心概念——IoC(Inversion of Control)和beans。Spring IoC和Beans的概念度娘、谷哥一搜一大把,在此就不重复介绍了。个人认为IoC和Beans最基本的实现思想来自于设计模式的几大原则,它之所以这么好用并且深入人心就是体现了设计模式的精髓。

依赖倒转原则:Spring的介绍Framework文档的开篇就提到反向依赖注入(DI——dependency injection ),其目标是让调用者不要主动去使用被调用者,而是让被调用者向调用者提供服务。IoC和beans的配合完美实现了这个过程,一个@component注解添加一个bean到Ioc容器,一个@autowired注解Ioc容器会找到对应的类注入进来。

接口隔离原则:Ioc不仅仅根据class类型注入bean,他还会根据接口类型自动装配注入一个bean。

里氏代换原则:在接口隔离的原则的基础上我们可以利用XML配置文件来制定装配的服务。例如javax.sql.DataSource是Java里提供数据库链接服务的接口,世面上有各种各样开源或闭源的工具实现了DataSource接口,例如c3p0和druid。我们想要切换他们仅仅需要像下面这样添加或删除一个bean(当然先要引入Jar包):

<!-- c3p0 -->
<bean id="ds" class="com.mchange.v2.c3p0.ComboPooledDataSource">
     <property name="driverClass" value="com.mysql.jdbc.Driver"/>
     <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/c3p0jdbctemplate"/>
     <property name="user" value="admin"/>
     <property name="password" value="123456"/>
</bean>
<!-- druid -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
     <property name="url" value="jdbc:mysql://localhost:3306/c3p0jdbctemplate" />
	 <property name="username" value="admin"/>
	 <property name="password" value="123456"/>
</bean>

聚合复用原则:SpringFramework号称非侵入式框架,我们在使用的过程中也很少有继承的情况,基本上所有的特性都是通过注解(Annotation)来实现,需要某一项服务也是将其注入后使用。虽然我们在开发的过程中为了实现一些高级功能会继承重写某些方法后,然后再将我们的新类添加到Ioc中,但是Spring本身并不太鼓励这样去实现。

除了前面4项原则,迪米特法则开闭原则并没有太直观的体现。对于迪米特法则来说Ioc机制本身就实现了调用者与被调用者之间不会直接发生依赖关系(new创建)。而开闭原则,Spring框架本身那么多构建类都是按照这个原则开发的——新功能用新的类实现,而非增加原有方法。

Beans

配置

现在我们知道Spring的2大核心是IoC和Beans。IoC字面翻译叫“控制反转”,这个“反转”过程实现的思想其实蛮简单的:就是先有一个容器(container),我们把实现各种功能的bean(一个类的实例)一股脑向容器里面扔,至于最后这些bean被谁用了通过配置和注解来确定。

上面提到了配置,在2.5版本之前配置只能通过XML文件实现,之后引入了annotation配置的方式,然后3.x版本之后可以完全使用Java代码来实现配置而无需XML文件。配置文件的格式和作用其实也不复杂,就是告诉容器我要扔进去什么bean。扔进去的bean当然需要初始化一些数据了,丢一个光秃秃没有任何数据的实例到容器中貌似也没多大用处,所以XML文件中就提供了一些标签来标记如何初始化数据:

<?xml version="1.0" encoding="UTF-8"?>
<!-- 省略xmlns -->
<beans>
    <bean id="otherBean" class="myProject.OtherBean" />
    <bean id="myBean" class="myProject.MyClass">
        <!-- 通过setOtherBean方法设置OtherBean的实例 -->
        <property name="otherBean" ref="otherBean"/>
        <!-- 通过setValue方法设置数值 -->
        <property name="value" value="myValue"/>
    </bean>
</beans>

参数

下面是Bean相关的参数,它们可以用XML<bean>标签来配置,也可以用@bean传递一个参数来设定:

class 标记当前Bean加载的类
name Bean的别名和名称。
scope Bean的范围,默认是单例。
constructor 构造函数注入<constructor-arg />
properties 属性注入<property>
autowiring auto注入模式
lazy 懒加载模式
initialization 制定初始化类时执行的方法
destruction 制定类销毁时要执行的方法

Spring Framework的官网用了一个小节专门介绍bean的命名方式,既可以用id来标识,又可以用name来标识,第一次看还挺晕乎的。

<bean id="myBeanId" name=“myAlias1,myAlias2” />

其实注意一下四点即可:

  1. id和name均可以标识一个bean,但是id必须是全局一对一的,而一个bean可以用多个name,用,号分割。
  2. 如果不给bean制定id,那么容器会为他自动生成一个唯一的序列号。
  3. name可以配合<alias>标签使用来转换别名。

个人感觉使用spring到现在name出现场景并不多,也很少看到哪个开源项目通过name的方式向外暴露服务。

创建模式与Scope

Bean只是一个和IoC容器相对应的概念:IoC容器存放并管理bean,bean是IoC机制的最小工作单元。往后的AOP等功能都是建立在Bean的基础上拓展开来的——要使用Spring这些功能首先得是一个Ioc容器中的Bean。Bean实际上就是一个Java类的实例,只不过实例化工作交给了Ioc容器而已。

Bean的实例化有3种方式——构造方法创建、静态工厂、动态工厂。每一个Bean对应的Scope实际上就2个参数——singletonprototype(实际上还有其他参数可以使用,这里说只有2个具体原因见后面Scope的说明)。

单例构造创建

90%的Bean都是直接通过这种方法方法来创建的。这也是我们最常见的配置方式:

<bean id="myBean" class="myProject.MyClass" />

当以上面这样的方式配置一个bean时,Ioc容器会直接调用构造方法来创建一个类实例(当然在定义类时必须提供一个公开的构造方法)。由于默认情况下bean的scope参数是singleton,所以创建出来bean在不指定scope的状态下都是一个单例。

某些时候我们会在类当中再用static 来设定一个嵌入类:

package myProject;
class MyClass {
	static class MyNestClass{
		public MyNestClass(){}
	}
}

可以通过“$”符号关联的方式创建这个Bean:

<bean id="myBean" class="myProject.MyClass$MyNestClass" />

静态工厂创建

静态工厂创建bean和静态工厂模式的概念一样,就是指定一个工厂类,然后通过一个静态方法返回一个新的bean。

XML配置:

<bean id="myFactory"
    class="myProject.MyFactory"
    factory-method="createInstance"/>

工厂类:

class MyFactory {
    static class MyClass{};
    private static MyClass myClass = new MyClass();
    private MyFactory() {}

    public static MyClass createInstance() {
        return myClass;
    }
}

动态工厂创建

动态工厂在设计模式上叫“抽象工厂”,spring官网将其自称为实例工厂(instance factory)。这里叫“动态工厂”是想对他们加以区分。虽然“实例工厂”并不是教科书似的抽象工厂,但是目的就是实现工厂动态创建。动态工厂与静态工厂最大的区别就是会先将工厂本身设置成一个bean(实例化),然后再通过这个工厂bean来创建“产品bean”。看下面的例子:

<bean id="myLocator" class="myProject.MyLocator">
    <!-- 自身就是一个实例化的bean,可以设定任何bean的配置 -->
</bean>

<!-- 绑定bean与一个动态工厂 -->
<bean id="instanceFactory"
    factory-bean="myLocator"
    factory-method="createInstance"/>
class MyFactory {
    static class MyClass{};
    public MyClass createInstance() {
        return new MyClass();
    }
}

一个工厂可以同时用于创建多个bean方法:

<bean id="myLocator" class="myProject.MyFactory" />

<bean id="serverOne"
    factory-bean="myLocator"
    factory-method="createClassOne"/>

<bean id="serverTwo"
    factory-bean="myLocator"
    factory-method="createClassTwo"/>
class MyFactory {
    static class MyServerOne{};
    static class MyServerTwo{};
    
    public MyServerOne createClassOne() {
        return new MyServerOne();
    }
    public MyServerTwo createClassTwo() {
        return new MyServerTwo();
    }
}

为什么需要实例化方法

可能你会想,Spring实例化提供一个简单的bean创建实例就好了,干嘛还要整静态工厂、抽象工厂之类的东西?

实际上我个人认为Spring的架构大神们是想通过一套简单的机制帮你实现设计模式中的所有创建模式——静态工厂、抽象工厂、单例模式、建造者模式和原型模式。因为IoC的最大任务之一就是代替我们创建各种Bean(类实例),而类实例的创建无非就是这几种创建模式。

这里仅仅介绍了2种工厂模式,下面将结合Bean的Scope属性介绍其他模式的思路。

Scope

scope直译过来叫范围、界限、广度。不过按照字面意思理解Bean的Scopd属性肯定要跑偏的。Scope数据涉及2个层面的含义。

首先在实现层面,对于设计模式来说,Scope就只有2种模式——singleton模式和prototype模式。

其次在应用层面,除了上面2个,Scope还提供了requestsessionapplicationwebsocket。从字面上看就知道实际上这些Scope参数仅仅是指定了一个bean的适用范围。

request为例,要启用他需要保证应用的“上下文”是web模式,例如XmlWebApplicationContext,其他情况下会抛出异常。然后"scope=request"的工作方式就是外部发起一个请求后,web层(servlet)启用一个线程来响应这个请求。到了业务层面我们需要指定一些bean来处理这个请求,当这些bean设定为request时,那么它仅仅用于这一次请求就抛弃。下一次请求出现时会创建一个新的实例。

所以不管是requestsessionapplication还是websocket,实际上都是通过prototype模式创建的实例,也就是设计模式中的原型模式,虽然并不一定是教科书般的标准,但是在整个容器中他实现了原型的特性。

此外singleton模式和 Gang of Four (GoF)中定义的通过ClassLoad实现的单例模式也有很大的区别,但是对于Ioc容器而言,任何bean在一个容器中绝对是一个单例,现在所有的资源都通过容器来管理依赖关系,那么最终的效果也是一个单例。

建造者模式

到目前为止,还有一个创建模式未出场——建造者模式。建造者模式实际上就是通过一个标准的方法组装一个复杂的对象。

标准的建造者模式先得有一个Director提供外部访问接口,外部调用者要创建一个复杂对象时向接口传递指定参数,然后Director根据参数调用Builder提供的各种方法,这些方法再用concrete去构建最终的Product。

实际上把复杂对象创建的过程看成各个bean依赖构造的过程即可实现模式,例如:

<!-- cpu部件 -->
<bean id="amdCpu" class="myProject.cpu.Amd"/>
<bean id="intelCpu" class="myProject.cpu.Intel"/>
<!-- 显卡部件 -->
<bean id="amdGraphics" class="myProject.graphics.Amd"/>
<bean id="nvdiaGraphics" class="myProject.graphics.Nvdia"/>

<!-- 组装电脑1 -->
<bean id="myComputer" class="myProject.computer.MyComputer">
     <property name="cpu" ref="amdCpu"/>
     <property name="graphics" ref="nvdiaGraphics"/>
</bean>

<!-- 组装电脑2 -->
<bean id="yourComputer" class="myProject.computer.YourComputer">
     <property name="cpu" ref="intelCpu"/>
     <property name="graphics" ref="amdGraphics"/>
</bean>