第4章 IoC容器
主要内容
- IoC概念所包含的设计思想
- Java反射技术
- BeanFactory、ApplicationContext及WebApplicationContext接口
- Bean的生命周期
文章目录
4.1 IoC概述
IoC
(Inverse of Control,控制反转)是 Spring
容器的内核,AOP和声明式事务也是在此基础上出现的,涉及代码解耦、设计模式、代码优化等问题的考量。
4.1.1 通过实例理解IoC的概念
这里举了一个电影场景的例子(虽然有点中二[捂脸]),主要是利用Java代码描述剧本、电影角色、演员三者间的关系如何关联,这三者分别对应:
- 剧本:MoAttack(电影《墨攻》)
- 电影角色:Geli(主角人物革离)
- 演员:LiuDeHua(演员刘德华)
传统的表示方式:
public class MoAttack {
/**
* 电影中的桥段: 城门叩问
*/
public void cityGateAsk() {
// 演员直接入侵了脚本
LiuDeHua ldh = new LiuDeHua();
ldh.responseAsk("I am Geli.");
}
}
剧本在创作的时候应该围绕故事人物进行,而不是考虑演员,这时可以引入导演,让导演负责剧本、角色、饰演者之间的关系。
通过引入导演,使得剧本和具体饰演者解耦。对应到软件中,导演就像是一台装配器,安排演员表演具体的角色。
-
IoC:中文名叫控制反转,控制是指控制对象的创建和依赖关系,反转可以理解为转移,将控制权转移给 IoC 容器,对应前面的例子,「控制」是指选择GeLi角色扮演者的控制权,「反转」是指这种控制权从剧本中反转移交到导演手中。以此来映射
Spring
容器对Bean
对象的控制。 -
DI:依赖注入,Spring 把众多对象独立创建,然后根据依赖关系,把被依赖的对象赋值给依赖对象的成员属性;spring容器通过调用set方法或构造器来实现对象之间的依赖关系。
IoC是设计思想、原理,DI是实现的一种具体方法。
- Spring出现前的问题:"对象自给自足,丰衣足食"
传统编程中,我们需要调用其他类的非静态方法时,通常的做法是new一个对象,再调用方法。这样的话,一个类中又引用其他类,耦合度非常高。如果日后需要调整、修改时,则需要修改源代码。而且某些对象会被重复创建,比如:Service层调用DAO层方法时重复创建,没有必要,即使是用单例模式解决了对象重复问题,但也引入了新的问题——项目大时,多个DAO则每个DAO书写重复的单例模式代码,十分繁琐。
- Spring的解决方式:“你们不用动手,我为你们服务”
将上面对象的创建和对象之间的依赖关系交给 IoC 容器同一管理,容器创建对象。举个栗子,当 Car 对象需要 Tyre 对象时,IoC 容器会创建 Tyre 对象直接注入到 Car 对象中,不需要 Car 对象自己去获取(或者自己造轮子)。
Car对象一般称为被注入对象,Tyre对象称为依赖对象
4.1.2 IoC的类型
IoC
主要有3种注入方法:构造函数注入、属性注入(setter注入) 和 接口注入。Spring支持构造函数和属性注入。下面例子中,将 MoAttack
称为调用类,GeLi
为被调用类依赖的对象。这里并未使用Spring的方式来演示。
- 构造函数注入
通过调用类的构造函数,将依赖的对象通过构造函数变量传入,以此达到IoC。
public class MoAttack {
public GeLi geli;
/**
* 注入革离这个角色的具体扮演者(刘德华)
*/
public MoAttack(GeLi geli) {
this.geli = geli;
}
public void cityGateAsk() {
geli.responseAsk("墨者革离");
}
}
可以看到 MoAttack
构造函数不关心由谁来扮演 GeLi
,只要传入的扮演者能按照剧本完成表演即可。具体的演员由导演(IoC容器)来决定:
public class Director {
public void direct() {
// ①
GeLi geli = new LiuDeHua();
// ②
MoAttack moAttack = new MoAttack(geli );
moAttack.cityGateAsk();
}
}
在①处导演安排刘德华饰演革离,并在②处将刘德华版的革离 “注入” 到剧本中。
- 属性注入
又称为setter注入,即通过对象的setter方法来传入依赖的对象:
public class MoAttack {
public GeLi geli;
/**
* 注入革离这个角色的具体扮演者(刘德华)
*/
public void setGeli(GeLi geli) {
this.geli = geli;
}
public void cityGateAsk() {
geli.responseAsk("墨者革离");
}
}
用法和上面使用构造器注入用法一样。
- 接口注入
本质是将调用类中实现依赖注入的方法抽取到一个接口中,调用类实现这个接口并重写相应的方法。先声明一个接口:
public interface ActorArrangeble {
void injectGeli(GeLi geli);
}
调用类实现接口:
public class MoAttack implements ActorArrangeble {
public GeLi geli;
/**
* 注入革离这个角色的具体扮演者(刘德华)
*/
public void injectGeli(GeLi geli) {
this.geli = geli;
}
public void cityGateAsk() {
geli.responseAsk("墨者革离");
}
}
在使用时,导演通过接口的依赖注入方法来完成注入工作:
public class Director {
public void direct() {
// 这里都没有用到那个接口...
MoAttack moAttack = new MoAttack();
GeLi geli = new LiuDeHua();
moAttack.injectGeli(geli);
moAttack.cityGateAsk();
}
}
通过接口注入需要额外声明一个接口,增加了类的数目,而且效果和属性注入并无本质区别,书中不提倡采用这种注入方式。
4.1.3 通过容器完成依赖关系的注入
虽然 MoAttack
和 LiuDeHua
通过上面的方式实现了解耦,但是它们之间依赖关系的建立工作依然存在在代码中,只是将这种控制权限转移到 Director
(相当于IoC容器)上了,它帮助完成类的初始化和装配工作,让开发者从繁琐的底层实现类实例化和依赖关系装配等工作中解脱出来,只需专注于更有意义的业务上。
Spring容器就是这样的角色,通过配置文件或注解来描述类之间的依赖关系,自动完成类的初始化和依赖注入工作。下面使用Spring配置文件对上面例子进行配置,文件名beans.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">
<!-- ① 对象定义 -->
<bean id="geli" class="LiuDeHua"/>
<!-- ② 通过 geli-ref 建立依赖关系 -->
<bean id="moAttack" class="com.sanata.ioc.MoAttack" p:geli-ref="geli"/>
<!-- 构造器注入
<bean id="moAttack" class="com.sanata.ioc.MoAttack">
<constructor-arg index="0" ref="geli"> ref里的是bean的id
</bean>
-->
<!-- setter注入
<bean id="moAttack" class="com.sanata.ioc.MoAttack">
<property name="geli" ref="geli"> ref里的是bean的id
</bean>
-->
</beans>
配置好后,通过 new XmlBeanFactory("beans.xml")
等方式即可启动容器。容器启动时,Spring会根据配置文件的描述信息,自动实例化 Bean
并完成依赖关系装配,然后放入到IoC容器中,后续可以直接使用。
Spring能完成这些自动化操作大部分要归功于Java的反射技术
4.2 相关Java基础知识
Class文件在类加载器加载后,会在JVM中形成一份描述 Class的元信息对象,该对象包含Class的结构信息:构造器,属性和方法等。Java允许用户使用程序化方式(反射机制)来操作、获取这些信息。
4.2.1 简单实例
先定义一个汽车类:
package com.smart.reflect;
/**
* @author Jason
* @version V1.0
* @Date 2020/1/4 15:33
*/
public class Car {
private String brand;
private String color;
private int maxSpeed;
public Car() {
}
public Car(String brand, String color, int maxSpeed) {
this.brand = brand;
this.color = color;
this.maxSpeed = maxSpeed;
}
public void introduce() {
System.out.println("Car{" +
"brand='" + brand + '\'' +
", color='" + color + '\'' +
", maxSpeed=" + maxSpeed +
'}');
}
// 省略 getter 和 setter 方法
...
}
一般情况下,我们会使用构造器来创建对象:
Car car = new Car();
car.setBrand("二手奥拓");
// 或者
Car car = new Car("二手奥拓", "黑色", 200);
如果使用Java反射机制来获取对象:
package com.smart.reflect;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
/**
* @author Jason
* @version V1.0
* @Date 2020/1/4 15:33
*/
public class ReflectTest {
public static Car initByDefaultConst() throws Exception {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class<?> clazz = loader.loadClass("com.smart.reflect.Car");
// 获取对象的构造器
Constructor<?> constructor = clazz.getDeclaredConstructor((Class<?>[]) null);
Car car = (Car) constructor.newInstance();
// 获取方法并传入参数
Method setBrand = clazz.getMethod("setBrand", String.class);
setBrand.invoke(car, "二手奥拓");
Method setColor = clazz.getMethod("setColor", String.class);
setColor.invoke(car, "黑色");
Method setMaxSpeed = clazz.getMethod("setMaxSpeed", int.class);
setMaxSpeed.invoke(car, 200);
return car;
}
public static void main(String[] args) throws Exception {
Car car = initByDefaultConst();
car.introduce();
}
}
运行代码:
Car{brand='二手奥拓', color='黑色', maxSpeed=200}
可以看到使用反射可以达到和构造器创建对象一样的效果。在 initByDefaultConst()
方法中,类的全限定名称 com.smart.reflect.Car
,setter方法名 setBrand
,setColor
,setMaxSpeed
,都可以提取到配置文件中,在初始化加载实例时就可以使用反射来获取到对象了。
4.2.2 类加载器 ClassLoader
类装载器 ClassLoader
是一个重要的Java运行时系统组件,负责在运行时查找和装入Class字节码文件,并构造出类在JVM内部表示的对象。JVM在运行时会产生3个 ClassLoader
:
名字 | 装载内容 | 父级 | 编写语言 |
---|---|---|---|
根加载器 | 负责装载JRE核心类库rt.jar等 | 无 | C++语言编写 |
ExtClassLoader |
装载JRE扩展目录ext的JAR包 | 根加载器 | Java编写 |
AppClassLoader |
装载Classpath 下的类包 |
ExtClassLoader |
Java编写 |
默认情况下使用 AppClassLoader
装载应用程序的类。通过代码可以测试他们的层级关系:
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("current loader: " + loader);
System.out.println("parent loader: " + loader.getParent());
System.out.println("grandparent loader: " + loader.getParent().getParent());
}
}
运行后打印:
current loader: sun.misc.Launcher$AppClassLoader@18b4aac2
parent loader: sun.misc.Launcher$ExtClassLoader@6fdb1f78
grandparent loader: null
JVM装载类时使用"全盘负责委托机制"
- 全盘负责:当ClassLoader装载类时,该类所依赖及引用的类也由同一个装载器载入,除非显示指定另一个装载器;
- 委托机制:委托父装载器寻找类,只有找不到时才从自己的类路径中查找并装载目标类。
1. 类装载器的工作机制
- 装载:查找和导入Class文件
- 链接:执行校验、准备和解析步骤,其中解析可选
1) 校验:检查载入Class文件数据的正确性
2)准备:给类的静态变量分配存储空间
3)解析:将符号引用转换成直接引用 - 初始化:对类的静态变量、静态代码块执行初始化工作
类文件被装载后,在JVM内会有一个对应的 java.lang.Class 类描述对象,该类的所有实例都拥有指向这个类描述对象的引用,而类描述对象又拥有指向 ClassLoader 的引用:
数组、枚举、基本类型等,甚至 void 也拥有对应的 Class 对象。Class 对象没有public的构造方法,JVM是通过 defineClass() 自动构造的。
2. ClassLoader的重要方法
ClassLoader是位于 java.lang 包下的一个抽象类,重要方法如下:
方法 | 作用 |
---|---|
Class loadClass(String name) | name参数指定类装载器需要装载类的名字,必须使用全限定类名,如 com.smart.beans.Car。 |
Class defineClass(String name, byte[] b, int off, int len) | 将类文件的字节数组转换成JVM内部的 java.lang.Class 对象, name为全限定名 |
Class findSystemClass(String name) | 从本地文件系统载入 Class 文件。如果本地文件系统不存在该 Class 文件则抛出 ClassNotFoundException,JVM默认使用该方法装载 |
Class findLoadedClass(String name) | 查看 ClassLoader 是否已装载某个类,已装入则返回 Class,否则返回 null |
Class getParent() | 获取父装载器,除根装载器是C++编写无法获取 |
此外,用户可以自定编写类装载器以实现一些特殊需求。
4.2.3 Java反射机制
通过反射机制可以从 Class 对象中获取构造函数、成员变量、方法类等类的反射对象,并以编程的方式通过这些反射对象对目标类对象进行操作。这些反射对象都在 java.reflect 包中。主要的3个反射类:
类名 | 获取方法和作用 |
---|---|
Constructor | 类构造器的反射类,主要方法是 newInstance(Object... params) ,可以创建一个对象类的实例,相当于 new 关键字,获取方法有:Class#getConstructors():获取所有构造器对象数组 Class#getConstructor(Class… parameterTypes): 获取拥有特定入参类型的构造器对象 |
Method | 类方法的反射类,主要方法是 invoke(Object obj, Object... args) ,obj 是需要操作的目标对象,args是方法入参。还有其他方法· Class getReturnType():获取方法的返回值类型。 Class[] getReturnTypes():获取方法的入参类型数组 Class[] getExceptionTypes():获取方法的异常类型数组 Annotation[] getParameterAnnotations():获取方法的注解信息 获取方法有: Class#getDeclaredMethods():获取所有方法对象数组 Class#getDeclaredMethod(String name, Class… parameterTypes): 获取特定签名的方法对象, name 是方法名, Class... 是入参类型列表 |
Field | 类的成员变量的反射类,主要方法: set(Object obj, Object value), obj表示操作的目标对象, value为成员变量要设置的值 获取方法有: Class#getDeclaredFields():获取所有成员变量对象数组 Class#getDeclaredField(String name): 获取特定签名的方法对象, name 是方法名 |
这些反射类都有一个setAccessible(boolean access)
方法,用于设置访问private 或protected属性或方法的权限,如果没有设置直接操作会抛出IllegalAccessException
异常。