理解IoC container
什么是IoC Container?
IoC(Inversion of control)是一种设计思想,可以将设计好的对象交给容器去控制(包括对象的整个生命周期、以及对象之间的组装)。-
它解决了什么问题?
传统应用程序中,为了实现业务逻辑,肯定需要多个对象来协作完成任务,例如:- 有一个用户信息类
- 有一个用户类,这个用户类会依赖于用户信息类
- 然后用一个客户端类去创建用户类和用户信息类,并且将用户信息类主动注入到用户类中
有了IoC Container之后,在客户端类中就不需要再去主动去创建对象和注入依赖了,而是通过配置文件或者注解的形式表现出来,然后IoC Container通过解析配置文件和注解去完成模块的创建和模块间的依赖注入。这样可以使代码之间的耦合度更低。
IoC原型(参考JinhaoPlus的博客)
- 如果不采用IoC,传统程序依赖注入的方式
class MainModule{
private DependModuleA moduleA; // 存在依赖关系
public DependModuleA getModuleA() {
return moduleA;
}
public void setModuleA(DependModuleA moduleA) {
this.moduleA = moduleA;
}
}
class DependModuleAImpl {
public void funcFromModuleA() {
System.out.println("This is func from Module A");
}
}
public class SimpleIOCDemo {
public static void main(String[] args) throws ClassNotFoundException {
MainModule mainModule = new MainModule();
// 依赖注入
mainModule.setModuleA(new DependModuleAImpl());
mainModule.getModuleA().funcFromModuleA();
}
}
- 自己定义一个简易的IoC
定义一个properties配置文件(xxx.properties)描述依赖关系
moduleA=DependModuleAImpl
mainModule=MainModule
mainModule.moduleA=moduleA
class SimpleIoCContainer{
private Properties properties = new Properties();
// 存储已经加载过的bean,String为bean的名称
private Map<String, Object> moduleMap = new HashMap<>();
public SimpleIoCContainer(String configurFile) {
try { // 加载配置文件
properties.load(new FileInputStream(configurFile));
} catch (Exception e) {
e.printStackTrace();
}
}
public Object getBean(String moduleName) throws ClassNotFoundException {
Object instanceObj;
// 如果不为空,说明这个bean已经创建过,返回已经创建过的bean
if(moduleMap.get(moduleName)!=null){
System.out.println("return old bean");
return moduleMap.get(moduleName);
}
System.out.println("create new bean");
String fullClassName = properties.getProperty(moduleName);
if(fullClassName == null)
throw new ClassNotFoundException();
else{
// 通过properties的值获取类对象
Class<? extends Object> clazz = Class.forName(fullClassName);
try {
// 通过类对象创建一个实例对象
instanceObj = clazz.newInstance();
// 检查每个属性是否有对其他bean的依赖
instanceObj = buildAttachedModules(moduleName,instanceObj);
// bean创建完成,put到map中
moduleMap.put(moduleName, instanceObj);
return instanceObj;
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return null;
}
/*
* 实现依赖注入
*/
private Object buildAttachedModules(String modulename , Object instanceObj) {
Set<String> propertiesKeys = properties.stringPropertyNames();
Field[] fields = instanceObj.getClass().getDeclaredFields();
for (String key : propertiesKeys) {
// 如果存在,说明可能存在对其他的bean对象的依赖
if(key.contains(modulename)&&!key.equals(modulename)){
try {
Class<? extends Object> clazz = Class.forName(properties.getPropert(key));
// 存在依赖其他bean的字段属性
for (Field field : fields) {
if(field.getType().isAssignableFrom(clazz))
// 依赖注入
field.set(instanceObj, moduleMap.get(properties.getPropert(key)));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
return instanceObj;
}
}
public class SimpleIOCDemo {
public static void main(String[] args) throws ClassNotFoundException {
SimpleIOCContainer container = new SimpleIOCContainer();
// spring默认是预加载的(容器创建时,就开始执行bean的创建和依赖注)
// 这里实现是在getBean方法执行时才会加载(懒加载方式)
DependModuleAImpl moduleA = container.getBean("moduleA");
MainModule mainModule = container.getBean("mainModule");
}
}
Spring IoC容器的工作过程
-
容器的启动阶段
- 和自己写的ioc原型类似,首先要找到并加载配置文件或者注解等元信息
- 容器对加载的配置信息进行解析和分析
- 将解析处理后的信息装配成BeanDefinition(bean在Spring ioc内部表示的一种数据结构,用来描述一个bean实例所具有的属性值、构造函数参数值、具体实现的信息),相到于一个生成bean的“内部图纸”
- 将BeanDefinition注册到容器中
- 对BeanDefinition进行一些后处理
-
Bean实例化阶段
- 容器通过对BeanDefinition的解析进行实例化对象
- 如果有依赖,就装配依赖(注:最后创建的实例是经过spring包装的BeanWrapper实例,而不是简单的bean实例,为了提供统一访问bean属性的接口,其可以方便的进行属性设置,而不用通过反射的方式)
- 可以通过定义BeanPostProcessor处理器对已经装配好的实例进行一些处理(可选)
- 可以设置init-method和destroy-method来对bean的初始化和销毁过程设置毁掉函数(可选)
-
Bean完整的生命周期(下面以ApplicationContext作为容器为例)
- Spring对Bean进行实例化,相当于new
- 为bean设置对象属性(依赖注入)
- 注入Aware接口
- 如果bean实现了BeanNameAware接口,将Spring的id传递给setBeanName方法并执行,目的是提供接口通过Bean来访问Bean的id
- 如果bean实现了BeanFactory接口,Spring把BeanFactory容器实例传递给setBeanFaoctory方法并执行,目的是通过bean获取Spring容器实例,使用场景-》bean通过spring容器发布事件
- 如果Bean实现了ApplicationContextAware接口,Spring容器将调用setApplictionContext(Application ctx)方法,把应用上下文作为参数传入。
它与BeanFactory的区别在于,前者在spring容器调用时会把自己作为参数传入,而不需要程序员去指定参数传入,而且前者提供的功能更多。
- 经过上面的步骤后,bean对象就被正确的构造,但是还可以通过一下方法进行自定义处理 ,实现BeanPostProcessor接口
- postProcessBeforeInitialzation( Object bean, String beanName ) 当前正在初始化的bean对象会被传递进来,我们就可以对这个bean作任何处理。 这个函数会先于InitialzationBean执行,因此称为前置处理。
- postProcessAfterInitialzation( Object bean, String beanName ) ,Spring会在InitialzationBean完成后执行
- 如果bean实现了InitializingBean接口,spring将调用afterPropertiesSet()方法。如果设置了属性init-method,在这之后,还会执行init-method属性指定的自定义方法。
- bean将一直驻留在应用上下文中(如果bean的作用域为prototype类型,就会把bean实例交给客户端,不驻留在容器中),直到应用上下文被销毁
- 如果bean实现了DispostbleBean接口,销毁前将执行destroy方法
- 如果这个Bean配置了destroy-method属性,会自动调用这个自定义的销毁方法
BeanFactoty作为容器的几点不同
- 不会调用ApplicationContextAware接口的setApplicationContext()方法
- postProcessBeforeInitialzation()方法和postProcessAfterInitialization()方法不会自动调用,必须自己通过代码手动注册
- BeanFactory容器启动的时候,不会去实例化所有Bean,包括所有scope为singleton且非懒加载的Bean也是一样,而是在调用的时候去实例化(默认为懒加载)
基于xml配置文件的方式声明bean、依赖注入(主要使用场景:主要用于第三方类库)
- 常见的配置方式如下:
<?xml version="1.0" encoding="UTF-8"?>
<!-- xmlns表示默认的命名空间,不使用前缀的标签,都代表是这个命名空间下的标签 -->
<!-- xmlns:xsi表示使用xsi作为前缀的命名空间 -->
<!-- xsi:schemaLocation定义命名空间和对应的XSD文档(可以对文档格式合法性进行验证)的位置关系 -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 可以导入另外一个xml配置文件,进行组合 -->
<import resource="another.xml"/>
<!-- 在使用注解和xml配置文件混合配置的时候使用 -->
<context:annotation-config />
<!-- 最常见声明bean的方式,id为bean的名称,class为bean的classpath -->
<bean id="a" class="com.wulei.A">
<!-- 对名为a的bean声明一个别名 -->
<alias name="a" alias="aAlias" />
<!-- 这里在声明一个bean的同时,还指定了这个bean的bPropertie属性值依赖于名为a的bean -->
<!-- spring默认是预加载的,可以设置lazy-init属性为懒加载 -->
<!-- init-method和destroy-mehod用于指定自定义的初始化和销毁方法 -->
<bean id="b" class="com.wulei.B" lazy-init="true" init-method="" destroy-method="">
<property name="bPropertie" ref="a" />
</bean>
<bean id="b1" class="com.wulei.B">
<property name="bPropertie" ref="a" />
</bean>
<bean id="b2" class="com.wulei.B">
<property name="bPropertie" ref="a" />
</bean>
<!-- 如果设置bean的作用域为prototype,那么就不再默认是单例模式-->
<bean id="b3" class="com.wulei.B" scope="prototype">
</bean>
<!--
public class C {
public Foo(A a, List<B> bList, int i,String name s) {
// ...
}
}
对构造函数参数的依赖注入声明方式
-->
<bean id="c" class="com.wulei.C">
<constructor-arg ref="a" />
<constructor-arg>
<list>
<ref bean="b1" />
<ref bean="b2" />
</list>
</constructor-arg>
<constructor-arg type="int" value="1" />
<constructor-arg name="s" value="oneString" />
</bean>
</beans>
- 如何让这个配置文件被容器加载?
// 还可以使用FileSystemXmlApplicationContext
ApplicationContext context = new ClassPathXmlApplicationContext("xxx.xml");
基于注解的方式声明bean、依赖注入(主要使用场景:Bean的实现类是当前项目开发的)
- @Autowired,此注解可以声明依赖注入,使用方式如下
public class A {
private final B b;
// 对属性字段使用
@Autowired
private C c;
private D d;
private E e;
// 对构造函数使用
@Autowired
public MovieRecommender(B b) {
this.b = b;
}
// 对setter方法使用
@Autowired
public void setD(D d) {
this.d = d;
}
// 对一个方法使用
@Autowired
public void wireE(E e) {
this.E = e;
}
}
- @Bean,它是一个方法级别的注解,声明当前方法的返回值为一个Bean。同时它必须使用在有@Configuration或@Component注解的类中,@Configuration注解让容器知道这个类相当于一个xml配置文件,@Bean有一些属性可以定义,例如:init-method、destroy-method、name等。使用方式如下,
/*
@Configuration包含了@Component,他们的区别在于如下场景。
如果使用@Component,那么在userInfo中,country()会生成一个新的实例,
而使用@Configuration则不会出现这种情况,会使用同一个Country实例。
@Component
public class MyBeanConfig {
@Bean
public Country country(){
return new Country();
}
@Bean
public UserInfo userInfo(){
return new UserInfo(country());
}
}
*/
@Configuration
public class AppContext {
// 该方法必须返回一个对象
@Bean
public Course course() {
return new Course();
}
}
@Component、@Repository(用于标注数据访问层组件)、@Service(用于标注业务层组件)、@Controller(用于标注控制层组件),这四个注解表明一个类声明为一个bean。
@Qualifier,在依赖注入时,如果有多个候选的Bean,可以指定具体某一个Bean来作为注入的类,使用方式如下
@Autowired
public void setOffice(@Qualifier("office")Office office)
{
this.office =office;
}
//或则如下
@Autowired
@Qualifier("office")
private Office office;
- @ComponentScan,自动扫描包明下所有使用@Component @Service @Repository @Controller 的类,并注册为Bean。需要将@ComponentScan添加在包含有@Configuration的类中,其中basePackages属性
可以指定扫描的路径。
// 相当于xml文件中的<context:component-scan base-package="org.example"/>
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
...
}
- 这种情况如何实例化一个容器?
// AppContext类必须是要被@Configuration声明过的
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppContext.class)