文章目录
- Spring的起源和背景
- 理解依赖注入
- Spring容器
- 理解Spring容器中的Bean
- 管理容器中的Bean及其依赖注入
- 自动装配
- 使用Java类进行配置管理
- 使用静态工厂、实例工厂创建Bean实例
- 抽象Bean与子Bean
- 容器中的工厂Bean
- 管理Bean的生命周期
- 几种特殊的依赖注入
- Spring的简化配置
- SpEL的功能和用法
前言
上面的目录内容本文章不一定都会写到,本次的文章将非常简略,主写主要内容,次要内容需要读者有一定的学习基础才可以看懂以及掌握,本文章比较偏向于理论复习,注意是理论,本文章代码不多,代码比较多的是高效率复习文章系列,该文章后续也会有Spring高效率复习文章。
Spring简介
简介
Spring是一个非常实用的框架,它提取了大量的再实际开发中需要重复解决的步骤,将这些步骤抽象成一个框架。
以Spring为核心还衍生出了一系列框架,如Spring Web Flow、Spring Security、Spring Data、SpringBoot、Spring Cloud等。
Spring是企业级应用开发的“一站式”选择,Spring贯穿表现出、业务层、持久层。Spring并且以高度的开放性与其他框架无缝整合。
总结起来,Spring具有如下优点。
- 低入侵式设计,代码的污染极小。
- 独立于各种应用服务器,基于Spring框架的应用,可以真正实现“Write Once,Run Anywhere”的承诺——写一次,在任何地方运行。
- Spring的IOC容器降低了业务对象替换的复杂性,提高了组件之间的解耦。
- Spring的ORM和DAO提供了与第三方持久层框架的良好整合,并简化了底层数据库访问。
- Spring的高度开放性,并不强制应用完全依赖于Spring,开发者可以自由选用Spring框架的部分或全部功能。
下图显示了Spring框架的组成结构图。
正如上图所看到的,当使用Spring框架时,必须使用Spring Core Container(即Spring容器),它代表了Spring框架的核心机制。Spring Core Container 主要由org.springframework.core、org.springframework.beans.org.springframework.context和org.springframework.expression4个包及其子包组成,主要提供SpringIOC容器支持。其中,org.springframework.expression及其子包是Spring3.0新增的,它提供了SpringExpression Language支持。
Spring入门
此处就不说怎么安装下载了,此处也并没有进行实机演示,文字描述和代码就行。
使用Spring管理Bean
Spring核心容器就是一个超级大工厂,可以生产所有的对象,包括第三方框架等其他对象(SqlSessionFactory、数据源等等)都会被当成Spring核心容器管理的对象——Spring把容器中的一切对象统称为Bean。
我们在初学JavaSE时,都听说过这样的说法,如果一个类,提供了成员变量以及对应的setter和getter方法和无参构造器等等,那么这个对象就是遵循了JavaBean的规范,此处所说的JavaBean和Spring的Bean本质上大同小异。
例如下面程序先定义一个简单的类。
public class Axe{
public String chop(){
return "使用斧头砍柴";
}
}
我们再定义一个Person类,而Person类需要调用Axe的方法,这就叫做依赖,Person类依赖Axe类。
public class Person{
private Axe axe;
//设置注入所需的setter方法
public void setAxe(Axe axe){
this.axe = axe;
}
public void useAxe(){
System.out.println("我打算去砍点柴火");
//调用axe的chop()方法
//表明Person对象依赖axe对象
System.out.println(axe.chop());
}
}
我们将这两个对象交给Spring核心容器去管理,通过XML配置文件让Spring将两个对象管理为Bean。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置名为person的Bean,其实现类是Person -->
<bean id = "person" class="Person">
<!-- 控制调用setAxe()方法,将容器中的axe Bean作为传入参数 -->
<property name = "axe" ref = "axe"/>
</bean>
<!-- 配置名为axe的Bean -->
<bean id = "axe" class = "Axe"/>
配置文件中的<bean …/>元素默认以反射方式来调用该类无参的构造器,以如下元素为例:
<bean id = "person" class = "Person">
Spring框架解析该<bean …/>元素后将可以得到两个字符串,其中idStr的值为"person"(解析该bean的id属性得到的值),classStr的值为"Person"(解析该bean的class属性得到的值)。
也就是说,Spring底层会执行如下形式的代码:
String idStr = ...;//解析<bean .../>元素的id属性得到该字符串的值为"Person"
//解析<bean .../>元素的class属性得到该字符串值为"Person"
String classStr = ...;
Class clazz = Class.forName(classStr);//全类名反射创建对象
Object obj = clazz.newInstance();//创建实例
//container代表Spring容器
//我们可以先把这个容器理解为一个Map集合
//保存对象实例的时候是put(key,value)->(String,Object)
//取值的时候是get(key)
container.put(idStr,obj);//obj是classStr反射得到后的对象实例
其实和之前的JavaWeb内容一模一样<bean>的id是存放到容器里的key值,class是保存Bean对象的全类名,通过全类名来反射创建对象实例然后保存到容器中的value值。如果依赖注入,则使用<proerty …/>来设置,name属性设置本对象中要赋值注入的属性,ref表示要注入的实际对象,该ref通常是另一个Bean标签的id,通过这个引用Bean标签的id来创建一个实例对象,然后将这个实例对象赋值到name属性所表示的对象属性上。
每个<bean…/>元素默认驱动Spring调用该类无参构造器来创建实例,并将该实例作为Spring容器中的Bean。
上面还包含了一个<property…/>子元素,它驱动Spring在底层以反射方式执行一次setter方法。其中,<property…/>的name属性值决定执行哪个setter方法,而value或ref决定执行setter方法的传入参数。
- 如果传入参数是基本类型及其包装类、String等类型,则使用value属性指定传入参数。
- 如果以容器中其他Bean作为传入参数,则使用ref数学指定传入参数。
Bean一旦被创建出来,Spring就会立即根据<propert…/>子元素来执行一次setter方法,也就是说,<bean…/>元素驱动Spring调用构造器创建对象;<propert…/>子元素驱动Spring执行setter方法,这两步是先后执行的,中间几乎没有任何间隔。
以上面配置文件中的如下配置为例:
<bean id = "person" class="Person">
<!-- 控制调用setAxe()方法,将容器中的axe Bean作为传入参数 -->
<property name = "axe" ref = "axe"/>
</bean>
上面配置中<proeprt…/>元素的name属性值为axe,该元素将驱动Spring以反射方式执行 person Bean的setAxe()方法;ref属性值为axe,该属性值指定以容器中名为axe的Bean作为执行setter方法的传入参数。
也就是说,Spring底层会执行以下形式的代码:
String nameStr = ...;//解析子元素的name属性得到字符串"axe"
String refStr = ...;//解析子元素的ref属性得到字符串"axe"
String setterName = "set" + nameStr.subString(0,1).toUpperCase()
+nameStr.substring(1);//生成将要调用的setter方法名
//获取Spring容器中名为refStr的Bean,该Bean将会作为传入参数
Object paramBean = container.get(refSr);
//此处的clazz是前一段反射代码通过<bean.../>元素的class属性得到的Class对象
Method setter = clazz.getMethod(setterName,paramBean.getClass());
//此处的obj参数是前一段反射代码为<bean.../>元素创建的对象
setter.invoke(obj,paramBean);
我们来大概解读一下上面的代码:
这两个变量,第一个用于获取set方法,refStr用于获取引用的类实例,用来赋值nameStr所指向的实际执行方法参数。
这个字符串是获取实际要执行的set方法,首先以固定的一个set字符串来作为前缀,然后调用nameStr(此时是<property…/>的name属性的值)变量的截取字符串方法,截取第一个字符,并且将其转换为大写,获取一个子串,此时相当于set+A,后面的API是截取从索引x开始的所有字符串,所以就是set+A+xe;此时setterName=setAxe,刚好对应了类中的setter方法。
此处是为了获取<property…/>子元素的ref的值所代表的实体类对象,该对象的作用是为前面的setAxe方法赋值,作为形参。可以看到调用核心容器来获取该实例对象,就像在Map里通过key值取value值一样。
这里是获取setAxe方法的步骤,首先Method是大方法,clazz是前面Person类,然后getMethod是获取方法,第一个参数是方法名,第二个参数是形参的类型。
这里相当于Person.getMethod(“setAxe”,Axe.class);
我们获取了实际的方法了,也就是setter引用所指向的对象(万物皆对象,Class也是对象,这是一个很哲学的思考题),obj是前面创建的Person类的实例,invoke表示调用执行方法,第一个参数表示执行对象是谁,obj就是Person类对象,第二个参数表示要传入的参数是什么,paramBean是在核心容器中取出的Axe实例对象,注意这里是实际的Axe实例对象。
经过上面的操作等同于:
Axe axe = new Axe();
Person person = new Person();
person.setAxe(person,axe);//为person的axe实体属性赋值
每个<property…/>元素默认驱动Spring调用一次setter方法。
有了上面的配置文件后,我们就可以用Spring容器来访问容器中的Bean了,ApplicationContext是Spring容器最常用的接口,该接口有如下两个实现类。
- ClassPathXmlApplicationContext:在类加载路径下搜索配置文件,并根据配置文件来创建Spring容器。
- FileSystemXmlApplicationContext:在文件系统的相对路径或绝对路径下搜索配置文件,并根据配置文件来创建Spring容器。
对于Java项目而言,类加载路径总是稳定的,因此,通常总是使用ClassPathXmlApplicationContext创建Spring容器。下面是本例的示例代码。
public class BeanTest{
public static void main(String[]args)throws Excetion{
//创建Spring容器
var ctx = new ClassPathXmlApplicationContext("beans.xml");
//获取id为person的Bean
var p = ctx.getBean("person",Person.class);
}
}
getBean这里,可以只写个key值,但是这样需要强制类型转换,如果指定了泛型类型,则无须强制类型转换。
- Object getBean(String id):根据容器中Bean的id来获取指定Bean,获取Bean之后需要进行强制类型转换。
- T getBean(String name,Class<T>requiredType):根据容器中Bean的id来获取指定Bean,但该方法带一个泛型参数,因此获取Bean之后无须进行强制类型转换。
调用方法后可以看到结果:
我打算去砍点柴火
使用斧头砍柴
上面就完成了Spring的一个入门操作了。
Spring的核心机制:依赖注入
程序并没有给axe成员变量设置值,但是实际调用的时候依然正常执行,这说明axe成员变量并不是程序主动设置的,而是由Spring容器负责设置的——开发者主要为axe成员变量提供一个setter方法,并通过<property…/>元素驱动Spring容器调用该setter方法为Person对象的axe成员变量设置值。
像这种A调用B的方法,也可以称为A依赖B,Spring把这种互相调用的关系称为依赖关系。假如A组件调用了B组件的方法,即可称A组件依赖B组件。
Spring的核心功能有两个:
- Spring容器作为超级大工厂,负责创建、管理所有的Java对象,这些Java对象被称为Bean。
- Spring容器管理容器中Bean之间的依赖关系,Spring使用一种被称为“依赖注入”的方法来管理Bean之间的依赖关系。
这种依赖注入和直接创建对象以及工厂模式对比的话,首先实现了无硬编码的方式进行了解耦,其实根本无须关心自己依赖的实体对象的创建过程,只调用。
理解注入依赖。
Spring的注入依赖早期时被称为控制反转,意思是Java的对象的控制权(赋值等等)从程序员手中交给了程序负责,后来也被称为依赖注入,这两者是同一回事。
当某个Java对象(调用者)需要调用另一个Java对象(被依赖对象)的方法时,在传统模式下通常有如下两种做法:
- 原始做法:调用者主动创建被依赖对象,然后调用被依赖对象的方法。
- 简单工厂模式:调用者先找到被依赖对象的工厂,然后主动通过工厂获取被依赖对象,最后调用被依赖对方的方法。
使用简单工厂模式的坏处是:
- 调用组件需要主动通过工厂去获取被依赖对象,这就会带来调用组件与被依赖对象工厂的耦合。
- 程序需要额外维护一个工厂类,增加了编程的复杂度。
使用了Spring框架后,调用者无须主动获取被依赖对象,而是只要被动接收Spring容器为调用者的成员变量赋值即可。使用Spring框架后,调用者获取被依赖对象的方式从主动获取变成了被动接收——于是有了“依赖注入”的方式/概念。
打个比喻,人需要复杂,原始做法等于自己造了个斧子,简单工厂模式相当于去工厂买了个斧子,或者就是找地方买了个斧子,而依赖注入则只有等着给斧子用就行了,无须关心斧子的来头,只用即可。
依赖注入通常有如下三种方式:
- 设值注入:IOC容器使用成员变量的setter方法注入被依赖对象。
- 构造注入:IOC容器使用构造器注入被依赖对象。
- 接口注入:调用者实现特定接口,并实现接口中的方法,而IOC容器会自动检测并调用这个特定的方法,从而完成依赖注入。
设值注入
设置注入指的是IOC容器通过成员变量的setter方法注入被依赖对象。这种方式简单、直观,因此在Spring的依赖中被大量使用。
当我们程序中需要将A依赖的B换一个B的时候,在Spring中操作很简单,将原来的对象id所代表的全类名更换为新的全类名即可。
构造注入
构造注入是指利用构造器来完成依赖注入,驱动Spring在底层以反射方式执行带参数的构造器,在执行带参数的构造器时,就可利用构造器参数对成员变量执行初始化——这就是构造注入的本质。
<bean…/>元素默认总是驱动Spring调用无参构造器来创建对象,可以使用<constructor-arg…/>子元素,每个子元素代表一个构造器参数,如果包含N个,则驱动Spring调用带N个参数的构造器来创建对象。
下面只简单演示下XML中配置变量。
<bean id="person" class="Person">
<constructor-arg ref="Axe"/>
</bean>
其实变化就是把<property…/>元素去掉换成了更简单的构造器参数引用。
上面的代码相当于驱动Spring执行如下代码:
String idStr= ...;//元素id
String refStr = ...;//子元素ref
Object paramBean= container.get(refStr);//和前面一致
Object obj = new Person(paramBean);//传入参数进行构造器初始化操作
//container代表Spring容器
container.put(idStr,obj);
其实本质就是在保存创建的Person实例到容器前,就通过构造器完成了依赖注入,而非后续的依赖注入,创建时顺带就完成了依赖注入再存到容器里。
Spring虽然有自动类型转换,但是遇到字符串和整数类型时,Spring只能解析为字符串,所以此时可以通过type来指定,比如<constructor-arg value = “23” type = “int”/>,Spring明确知道此处配置了一个int类型的参数。前面的value也可以指定type属性,用于确定该属性值的数据类型。
两种注入方式的对比。
设值注入更直观和可读性更强,也很简单,对于很复杂的依赖关系,使用构造器注入则导致构造器过于臃肿,而且通过setter方法设定依赖关系显得更加直观、自然。
构造注入可以决定依赖关系注入的顺序,并且依赖关系只能在构造器中设定,只有组件的创建者才能改变组件的依赖关系,对于调用者而言完全透明,因为没有setter方法,所有的依赖关系全部在构造器内设定。因此也无须担心后续代码对依赖关系产生破坏,更符合高内聚的原则。
使用Spring容器
P283