详解Spring IOC
Spring IOC其实很简单
有部分Java开发者对IOC(Inversion Of Control)和DI(Dependency Injection)的概念有些混淆,认为二者是对等对,实际上我在之前对著作中已经说过了,IOC其实有两种方式,一种就是DI,而另一种是DL,即Dependency Lookup(依赖查找),前者是当前软件实体被动接受其依赖对其他组件被IOC容器注入,而后者则是当前软件实体主动去某个服务注册地查找其依赖对那些服务,概念之间对关系图如下图所示可能更贴切些。 IOC相关概念示意图.png我们通常提到的Spring IOC,实际上是指Spring框架提供的IOC容器实现(IOC Container),而使用Spring IOC容器的一个典型的代码片段就是:
public class App{
public static void main(String [] args){
Application context = new FileSystemXmlApplicationContext(". . .");
// ...
MockService service = context.getBean(MockService.class);
service.doSomething();
}
}
任何一个使用Spring框架构建的独立的Java应用(Standalone Java Application),通常都会存在一行类似于“context.getBean(...);”的代码,实际上,这行代码做的就是DL的工作,而构建的任何一种IOC容器背后(比如BeanFactory或者ApplicationContext)发生的事情,则更多是DI的过程(也可能有部分DL的逻辑用于对接遗留系统)。
Spring的IOC容器中发生的事情其实也很简单,总结下来即两个阶段:
- 采摘和收集“咖啡豆”(bean)
- 研磨和烹饪咖啡
我们还是回过头来继续说Spring IOC容器的依赖注入流程吧!Spring IOC容器的依赖注入工作可以分为两个阶段:
阶段一:收集和注册
第一阶段可以认为是构建和收集bean定义的阶段,在这个阶段中,我们可以通过XML或者Java代码的方式定义一些bean,然后通过手动组装或者让容器基于某些机制自动扫描的形式,将这些bean定义收集到IOC容器中。
假设我们以XML配置的形式来收集并注册单一bean,一般形式如下:
<bean id="mockService" class="..MockServiceImpl">
...
</bean>
如果嫌逐个收集bean定义麻烦,想批量地收集并注册到IOC容器中,我们也可以通过XML Schema形式的配置进行批量扫描并采集和注册:
<context:component-scan base-package="com.keevol">
注意基于JavaConfig形式的收集和注册,不管是单一还是批量,后面我们都会单独提及。
阶段二:分析和组装
当第一阶段工作完成后,我们可以先暂且认为IOC容器中充斥着一个个独立到bean,它们之间没有任何关系。但实际上,它们之间是有依赖关系到,所以,IOC容器在第二阶段要干到事情就是分析这些已经在IOC容器之中的bean,然后根据它们之间的依赖关系先后组装它们。如果IOC容器发现某个bean依赖另一个bean,它就会将这另一个bean注入给依赖它的那个bean,直到所有到bean的依赖都注入完成,所有bean都“整装待发”,整个IOC容器都工作即算完成。
至于分析和组装的依据,Spring框架最早是通过XML配置文件的形式来描述bean与bean之间都关系都,随着Java业界研发技术和理念都转变,基于Java代码和Annotation元信息的描述方式也日渐兴盛(比如@Autowired和@Inject),但不管使用哪种方式,都只是为了简化绑定逻辑描述的各种“表象”,最终都是为本阶段都最终目的服务。
注意:很多Java开发者一定认为spring的XML配置文件是一种配置(configuration),但本质上,这些配置文件更应该是一种代码形式,XML在这里其实可以看作一种DSL,它用来表述的是bean与bean之间的依赖绑定关系,大家还记得没有IOC容器的年代要自己写代码新建(new)对象并配置(set)依赖的吧?
了解一点JavaConfig
Java 5的推出,加上当年基于纯Java Annotation的依赖注入框架Guice的出现,使得Spring框架及其社区也“顺应民意”,推出并持续完善了基于Java代码和Annotation元信息的依赖关系绑定描述方式,即JavaConfig项目。
基于JavaCOnfig方式的依赖关系绑定描述基本上映射了最早的基于XML的配置方式,比如:
(1)表达形式层面
基于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:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/shcema/beans/spring-beans.xsd
http://www.springframework.org/shcema/context
http://www.springframework.org/shcema/context/spring-context.xsd">
<!-- bean定义 -->
</bean>
而基于JavaConfig的配置方式是这样店:
@Configuration
public class MockConfiguration{
//bean定义
}
任何一个标注了@Configuration的Java类定义都是一个JavaConfig配置类。
(2)注册bean定义层面
基于XML的配置形式是这样的:
<bean id="mockService" class="..MockServiceImpl">
...
</bean>
而基于JavaConfig的配置形式是这样的:
@Configuration
public class MockConfiguration{
@Bean
public MockService mockService(){
return new MockServiceImpl();
}
}
任何一个标注了@Bean的方法,其返回值将作为一个bean定义注册到Spring的IOC容器,方法名将默认成为该bean定义的id。
(3)表达依赖关系注入关系层面
为了表达bean与bean之间的依赖关系,在XML形式中一般是这样的:
<bean id="mockService" class="..MockServiceImpl">
<property name="dependencyService" ref="dependencyService"/>
</bean>
<bean id="dependencyService" class="DependencyServiceImpl"/>
而在JavaConfig中则是这样的:
@Configuration
public class MockConfiguration {
@Bean
public MockService mockService(){
return new MockServiceImpl(denpendcyService());
}
@Bean
public DependencyService dependencyService(){
return new DependencyServiceImpl();
}
}
如果一个bean的定义依赖其他bean,则直接调用对应JavaConfig类中依赖bean的创建方法就可以了。
注意:在JavaConfig形式的依赖注入过程中,我们使用方法调用的形式注入依赖,如果这个方法返回的对象实例只被一个bean依赖注入,那也还好,如果多于一个bean需要依赖这个方法调用返回的对象实例,那是不是意味着我们就会创建多个同一类型的对象实例?
从代码表述的逻辑来看,直觉上应该是会创建多个同一类型的对象实例,但实际上最终结果却不是这样,依赖注入的都是同一Singleton的对象实例,那这是如何做到的?
笔者一开始以为Spring框架会通过解析JavaConfig的代码结构,然后通过解析器转换加上反射等方式完成这一目的,但实际上Spring的参考文档对象的问题,一旦拥有拦截逻辑的子类发现当前方法没有对应的类型实例时才会去请求父类的同一方法来初始化对象实例,否则直接返回之前的对象实例。
所以,原来Spring IOC容器中有的特性(features)在JavaConfig中都可以表达,只是换了一种形式而已,而且,通过声明相应的Java Annotation反而“内聚”一处,变得更加简洁明了了。
高曝光率的Annotation
至于@Configuration,我想前面已经提及了,这里不再赘述,下面我们看几个其他比较常见的Annotation,便于为后面更好地理解SpringBoot框架的奥秘做准备。
1.@ComponentScan
@ComponentScan对应XML配置形式中的<context:component-scan>元素,用于配合一些元信息Java Annotation的bean定义类批量采集到Spring的IOC容器中。
我们可以通过basePackages等属性来细粒度地定制@ComponentScan自动扫描的范围,如果不指定,则默认Spring框架实现从声明@ComponentScan所在类的package进行扫描。
2.@PropertySource与@PropertySources
@PropertySource用于某些地方加载*.properties文件内容,并降其中的属性加载到IOC容器中,便于填充一些bean定义属性到占位符(placeholder),当然,这需要PropertySourcesPlaceholderConfigurer的配合。
如果我们使用Java 8或者更高版本开发,那么,我们可以并行声明多个@PropertySource:
@Configuration
@PropertySource("classpath:1.properties")
@PropertySource("classpath:2.properties")
@PropertySource("...")
public class XConfiguration{
...
}
如果我们使用低于Java 8版本到Java开发Spring应用,又想声明多个@PropertySource,则需要借助@PropertySources的帮助了:
@PropertySources({
@PropertySource("classpath:1.properties")
@PropertySource("classpath:2.properties")
...
})
public class XConfiguration{
...
}
3.@Import与@ImportResource
在XML形式的配置中,我们通过<import resource="XXX.xml"/>的形式将多个分开到容器配置合到一个配置中,在JavaConfig形式的配置中,我们则使用@Import这个Annotation完成同样目的:
@Configuration
@Import(MockConfiguration.class)
public class XConfiguration {
...
}
@Import只负责引入JavaConfig形式定义到IOC容器配置,如果有一些遗留到配置或者遗留系统需要以XML形式来配置(比如dubbo框架),我们依然可以通过@ImportResource将它们一起合并到当前JavaConfig配置的容器中:
@Configuration
@Import(MockConfiguration.class)
@ImportResource("...")
public class XConfiguration {
...
}
一、bean作用域:理解Bean生命周期
在bean准备就绪给我们使用之前,容器默默的执行了很多启动步骤。Spring提供了BeanFactory作为Spring容器,平常开发中我们常用其派生类ApplicationContext。尽管BeanFactory和ApplicationContext容器的注册方式不太一样(使用BeanFactory要显式地调用其addBeanPostProcessor()方法进行注册)以及加载bean的时机不一样(BeanFactory默认采用延迟加载)等,但对于bean的生命周期管控基本一致。
Bean的完整生命周期可以认为是从容器创建初始化Bean开始,直到Spring容器关闭。事实上,在Bean被初始化创建之前,容器级别的接口方法也会对Bean产生影响,因此这里把两个事件也加进来了。 spring ioc之bean周期.png接口分类
在以上接口方法中,可以分为几类:
- 容器级生命周期接口方法:BeanFactoryPostProcessor、BeanPostProcessor和InstantiationAwareBeanPostProcessor。它们都是容器初始化bean时对暴露的扩展点(容器扩展点),一般称它们的实现类为“后置处理器”。InstantiationAwareBeanPostProcessor是BeanPostProcessor的子接口,可以在Bean生命周期的另外三个时期提供扩展的回调接口,即实例化之前(调用postProcessBeforeInstantiation方法)、给bean注入对应属性后(调用postProcessPropertyValues方法)和实例化Bean之后(调用postProcessAfterInstantiantion方法)。
- Bean级生命周期接口方法:BeanNameAware、BeanFactoryAware、InitializingBean和DiposableBean。bean继承接口然后在bean内部实现方法,因此配置文件中的init-method和destory-method指定的方法。
- Bean自身的调用方法:通过配置文件中init-method和destory-method指定的方法。
流程概述
- [容器级别]BeanFactoryPostProcessor接口:可以注册bean,如果spring配置文件注册了该接口的实现类,可以在spring的bean创建之前,修改bean的定义属性。例如可以把bean的scope从singleton改为prototype。
- [容器级别]InstantiationAwareBeanPost接口:初始化Bean之前的预处理。
- 初始化Bean
- [容器级别]InstantiationAwareBeanPost接口:如果spring配置文件注册了该接口的实现类,Spring将调用它们的postProcessPropertyValues方法;
- 注入:Spring根据定义信息(beanDifinition)将值和引用bean注入到bean对应的属性中。
- [Bean级别]感知类名:如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBeanName()方法;
- [Bean级别]感知容器:如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法,将bean所在的应用上下文的引用传入进来。注:BeanFactory容器查找实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入。
- [容器级]BeanPostProcessor:如果bean实现了BeanPostProcessor接口,Spring将调用它们的postProcessBeforeInitialization()方法;
- [容器级别]InitializingBean:如果bean实现了InitializingBean接口,Spring将调用它们的postprocessAfterInitialialization()方法;
- [Bean自身方法]init-method:如果bean定义中使用指定了init-method初始化方法,该方法也会被调用;
- [容器级]BeanPostProcessor:如果bean实现了BeanPostProcessor接口,Spring将会调用它们的postProcessBeforeInitialization()方法;
-
[容器级别/后处理器]InstantiationAwareBeanPostProcessor:如果bean实现了InstantiationAwareBeanPostProcessor接口,Spring将调用它们的postProcessAfterInitialization()方法;
此时,bean已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文,直到该应用上下文被销毁。 - [Bean级]DisposableBean:如果bean实现了DisposableBean接口,Spring将调用它的destory()接口方法。
- [Bean自身方法]destory-method:如果bean定义中使用destory-method声明了销毁方法,该方法也会被调用。
二、容器扩展点:FactoryBean
FactoryBean接口是Spring IOC容器实例逻辑的扩展点。什么叫扩展点?还记得前面说得bean生命周期吗?Spring处理Bean生命周期的事件就是扩展点。
区分FactoryBean和BeanFactory
BeanFactory直译是生产Bean的工厂,在Spring中就是容器,常用的ApplicationContext就是它的一个继承类。我们也可以直接使用BeanFactory示例:
BeanFactory factory = new XmlBeanFactory(new FileSystemResource("c:/piratebean.xml"));
而FactoryBean顾名思义就是一个bean,但这个bean和普通bean有点不一样。
区分FactoryBean和普通Bean
Spring中有两种类型的Bean,一种是普通Bean,另一种是工厂Bean即FactoryBean,这两种Bean都被容器管理。工厂Bean是实现了org.springframework.beans.factory.FactoryBean<T>接口的Bean,从ApplicationContext的getBean()方法获取的对象不是该类的一个实例,而是该类的getObject()方法所返回的对象。
当我们需要获取FactoryBean实例本身而不是它所产生的bean,则要使用&符号。
比如,现有FactoryBean,id为“playerBean”,在容器上调用getBean("playerBean")将返回FactoryBean产生的bean。调用getBean("&playerBean")将返回FactoryBean它本身的实例。
使用场景
当需要在类中撰写复杂的初始化程序,此时使用java编码比使用XML配置更容易表达。我们常接触的代理类就可以用这个接口实现。在Spring框架内部,有很多地方有FactoryBean的实现类,它们在很多应用如(Spring的AOP、ORM、事务管理)及与其它第三框架(ehCache)集成时都有体现。
示例
package twm.spring.LifecycleTest;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
public class PlayerFactory implements FactoryBean<PlayerActionInterface>, InitializingBean,
DisposableBean {
/*被代理的PlayerActionInterface实现对象,通过XML注入*/
private PlayerActionInterface target;
public PlayerActionInterface getTarget() {
return target;
}
public void setTarget(PlayerActionInterface target) {
this.target = target;
}
/*本类生成的代理对象*/
private PlayerActionInterface proxyObj;
/*在容器设置bean必须的属性之后执行初始化工作*/
public void afterPropertiesSet() throws Exception {
System.out.println("afterPropertiesSet");
proxyObj = (PlayerActionInterface)Proxy.newProxyInstance(this.getClass().getClassLoader(),
new Class[] { Class.forName("twm.spring.LifecycleTest.PlayerActionInterface") },
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
System.out.println("method:" + method.getName());
System.out.println("观察进攻及防守队员跑位");
Object result = method.invoke(target, args);
System.out.println("无球跑动");
return result;
}
});
}
public void destroy() throws Exception {
System.out.println("distory");
}
@Override
public PlayerActionInterface getObject() throws Exception {
System.out.println("getObject");
return proxyObj;
}
@Override
public Class<?> getObjectType() {
return proxyObj == null ? Object.class : proxyObj.getClass();
}
@Override
public boolean isSingleton() {
return true;
}
}
可以看到在PlayerFactory这个类,我们实现了FactoryBean接口的三个方法。其中getObject()方法返回的是代理对象proxyObj。这个对象是在bean初始化回调接口InitializingBean的实现方法afterPropertiesSet()里创建的。使用了Proxy.newProxyInstance给原来的类创建了一个代理类,该代理类在方法调用的前后都加上了动作。
容器扩展点:后置处理器BeanPostProcessor
综述
先回顾bean生命周期的这张图,看看BeanPostProcessor调用位置。 spring ioc之bean周期.png通过上图看到BeanPostProcessor(Bean后置处理器)两个方法在bean生命周期的位置,即:在Spring容器完成Bean实例化和属性设置后,并且在bean调用初始化方法之前或之后。因此BeanPostProcessor(Bean后置处理器)常用在:对bean内部的值进行修改;实现Bean的动态代理等。
可以定义一个或者多个BeanPostProcessor接口的实现,然后注册到容器中。那么该容器里管控的所有Bean在调用初始化方法之前或之后,都会调用BeanPostProcessor接口中对应的方法。
InstantiationAwareBeanPostProcessor是BeanPostProcessor的子接口。从最上面的的生命周期图,我们知道它在Bean生命周期的另外三个时期提供扩展的回调接口。其使用方法与BeanPostProcessor接口类似,只是回调时机不同。
BeanPostProcessor接口有两个方法:
public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
容器调用接口定义的方法时会将该受管Bean的实例和名字通过参数传入方法,经过处理后通过方法的返回值返回给容器。注意,不能返回null,如果返回的是null那么我们通过getBean方法将得不到目标。
BeanPostProcessor不允许标记为延迟加载。因为如果这样做,Spring容器将不会注册它们,自定义逻辑也就无法得到应用。假如你在<beans/>元素的定义中使用'default-lazy-init'属性,那就必须将每个BeanPostProcessor显式标记为'lazy-init="false"'。
示例
package twm.spring.LifecycleTest;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class beanPostProcessorImpl implements BeanPostProcessor{
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if(bean instanceof footballPlayer){
((footballPlayer) bean).setName("Messi");
}
return bean;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
}
配置文件中beans.xml加上,如果定义了多个BeanPostProcessor,可以在xml通过order来通过order属性来指定执行的顺序。
<bean id="cluo" class="twm.spring.LifecycleTest.footballPlayer">
<property name="name" value="C.罗纳尔多"></property>
</bean>
容器扩展点:后置处理器BeanFactoryPostProcessor
BeanPostProcessor(Bean后置处理器)常用在对bean内部的值进行修改;实现Bean的动态代理等。BeanFactoryPostProcessor和BeanPostProcessor都是spring初始化bean时对外暴露的扩展点。但它们有什么区别呢?
由Bean生命周期的图可知:BeanFactoryPostProcessor是生命周期中最早被调用的,远远早于BeanPostProcessor。它在spring容器加载了bean的定义文件之后,在bean实例化之前执行的。也就是说,Spring允许BeanFactoryPostProcessor在容器创建bean之前读取bean配置元数据,并可进行修改。例如增加bean的属性和值,重新设置bean是否作为自动装配的后选者,重设bean的依赖项等等。
在spring配置文件中可以同时配置多个BeanFactoryPostProcessor,并通过在xml中注册时设置'order'属性来控制各个BeanFactoryPostProcessor的执行次序。
@FunctionalInterface
public interface BeanFactoryPostProcessor {
/**
* Modify the application context's internal bean factory after its standard
* initialization. All bean definitions will have been loaded, but no beans
* will have been instantiated yet. This allows for overriding or adding
* properties even to eager-initializing beans.
* @param beanFactory the bean factory used by the application context
* @throws org.springframework.beans.BeansException in case of errors
*/
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
}
接口只有一个方法postProcessBeanFactory。该方法的参数是ConfigurableListableBeanFactory类型,实际开发中,我们常使用它的getBeanDefinition()方法获取某个bean的元数据定义:BeanDefinition。它有这些方法:
BeanDefinition
示例一
配置文件中定义了一个bean:
<bean id="messi" class="twm.spring.LifecycleTest.footballPlayer">
<property name="name" value="Messi"></property>
<property name="team" value="Barcelona"></property>
</bean>
创建类BeanFactoryPostProcessorImpl,实现接口FactoryPostProcessor:
public class beanFactoryPostProcessorImpl implements BeanFactoryPostProcessor{
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
System.out.println("beanFactoryPostProcessorImpl");
BeanDefinition bdefine=beanFactory.getBeanDefinition("messi");
System.out.println(bdefine.getPropertyValues().toString());
MutablePropertyValues pv = bdefine.getPropertyValues();
if (pv.contains("team")) {
PropertyValue ppv= pv.getPropertyValue("name");
TypedStringValue obj=(TypedStringValue)ppv.getValue();
if(obj.getValue().equals("Messi")){
pv.addPropertyValue("team", "阿根延");
}
}
bdefine.setScope(BeanDefinition.SCOPE_PROTOTYPE);
}
}
容器扩展点:动态注册BeanDefinitionRegistryPostProcessor
BeanDefinitionRegistryPostProcessor继承自BeanFactoryPostProcessor,是一种比较特殊的BeanFactoryPostProcessor。BeanDefinitionRegistryPostProcessor中定义的postProcessBeanDefinitionRegistry(BeanDeinitionRegistry registry)方法可以让我们实现自定义的注册bean定义的逻辑。下面的示例中就新定义了一个名为hello,类型为Hello的bean定义。
public class CustomBeanDefinitionRegistry implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
RootBeanDefinition helloBean = new RootBeanDefinition(Hello.class);
//新增Bean定义
registry.registerBeanDefinition("hello", helloBean);
}
}
下面配置可以让CustomBeanDefinitionRegistry起效:
@Import(CustomBeanDefinitionRegistry.class)
public class Application{
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}
如果需要給我修改意见的发送邮箱:erghjmncq6643981@163.com
资料参考:《SpringBoot揭秘-快速构建微服务体系》
转发博客,请注明,谢谢。