目录
1. 前言
不知道为什么,虽然工作了两年有余,Spring相关的资料也看了不少,但如果被面试问到这道题还是会一脸懵逼,被克的死死的(可能是因为大学学Java web和struts的时候没认真学,导致我不清楚没有Spring的世界是什么样的,有机会可以考察一波)。最近的一次机会就是因为Spring相关回答的很差结果挂了(不讲武德不预约时间搞偷袭),痛定思痛决定开一篇自己总结的Spring专题,最老生常谈的IOC理解放到第一篇。
文章前面也有帮助理解、记录的作用,最后一节是话术总结。
参考范围:《学透Spring》(主要)、某视频、一些博客。
2. 介绍IOC是什么
Spring最关键的两个思想是IOC-控制反转和AOP-依赖注入,控制反转使你从繁琐的对象交易中解脱出来,专注于对象本身,更进一步突出“面向对象”。
IOC的作用是将业务对象(也就是Bean)和关于组件的配置元数据(比如依赖关系)输入Spring容器中,容器就能为我们组装出一个可用的系统。
IOC是通过DI-依赖注入实现的(还有DL-依赖查找,但已废弃或没人用),它的含义是把底层类作为参数传递给上层类,实现上层对下层的“控制”。
在实际的工作中,这种思想也有利于团队合作和单元测试,几个类之间只需要定义好接口就可以同时开发了,不会受到制约。
上面的例子是基于构造函数方式的注入。其他常见的还有:
- Setter注入
- 接口注入
- 注解注入
注入方式有的地方说只有两种,或基本的注入方式是两种。接口注入因为很不常见所以暂不考究,注解注入其实可以放到自动织入的范畴,但也无所谓看怎么划分的了。
因为使用了依赖注入,在初始化的时候就会有很多new对象(上图右边框框),而IOC容器就解决了这个问题,IOC容器可以自动对代码进行初始化,程序员只需要维护一个configuration,比如xml文件,或配置类(通过使用@Configuration、@Bean、@ComponentScan等注解)。
整个bean的生命周期,从创建到使用到销毁的过程全部都是由容器来管理。
2.1 补充
上面说了依赖注入有两种方式, 类似的还有Bean的配置方式是三种:
- 基于XML文件的配置
- 基于注解的配置
- 基于Java类的配置
其实依赖注入和Bean的装配是一回事,只是上面分类的维度不同。
依赖注入的本质就是装配,装配是依赖注入的具体行为。
2.1.1 依赖注入的方式
2.1.1.1 基于构造方法
所谓基于构造方法的注入,就是通过构造方法来注入依赖。首先
public class Hello {
private String name;
public Hello(String name) {
this.name = name;
}
public String hello() {
return "Hello World! by " + name;
}
}
对应的XML配置文件需要使用<constructor-arg/>传入构造方法所需的内容,如下
<?xml ...?>
<beans xmlns=.......>
<bean id = "hello" class="learning.spring.helloworld.Hello">
<constructor-arg value="Spring"/>
</bean>
</beans>
其中<constructor-arg>中有不少属性可以配置,如
value-参数的值、ref-Bean ID、type-参数类型、name-参数名称等。
2.1.1.2 基于Setter方法
public class Hello {
private String name;
public String hello() {
return "Hello World! by " + name;
}
public void setName(String name) {
this.name = name;
}
}
<?xml ...?>
<beans xmlns=.......>
<bean id = "hello" class="learning.spring.helloworld.Hello">
<property name="name" value="Spring"/>
</bean>
</beans>
<property/>中的属性value是直接注入的值,用ref属性则可以注入其他Bean。
2.1.1.3 基于自动织入
手动配置依赖在Bean少时还能接受,当Bean的数量变多后,这种配置就变得非常繁琐。在合适的场合,可以让Spring容器替我们自动进行依赖注入,这种机制称为自动织入。自动织入默认是基于setter方法的,有几种模式:
no | 不进行自动织入 |
byName | 根据属性名查找对应的Bean进行自动织入 |
byType | 根据属性类型查找对应的Bean进行自动织入 |
constructor | 同byType,但用于构造方法注入 |
在<bean/>中可以通过autowire属性来设置使用何种自动织入方式,也可以在<beans/>中设置default-autowire属性指定默认的自动织入方式。在使用自动织入时,需要注意以下事项:
- 开启自动织入后,仍可以手动设置依赖,手动设置的依赖优先级高于自动织入;
- 自动织入无法注入基本类型和字符串;
- 对于集合类型的属性,自动织入会把上下文里找到的Bean都放进去,但如果属性不是集合类型,有多个候选Bean就会有问题。
最后,一般情况下Spring容器会根据依赖情况自动调整Bean的初始化顺序。不过有时Bean之间的依赖并不明显,如果我们想要按照某种顺序初始化,可以使用<bean/>的depends-on属性指定当前Bean还要依赖哪些Bean(如果是基于Java类的配置方式,可以用@DependsOn注解)。
2.1.2 配置Bean的方式
2.1.2.1 基于XML文件的配置
Spring Framework提供了<beans/>这个Schema来配置Bean,上面已经简单举例,下面再补充几个重要的属性:scope表明当前Bean是单例还是原型,lazy-init是指当前Bean是否是懒加载的,depends-on明确指定当前Bean的初始化顺序。
<bean id="..." scope="singleton" lazy-init="true" depends-on="xxx"/>
2.1.2.2 基于注解的配置
Spring Framework 2.0引入了@Required注解,但不建议使用;Spring Framework 2.5 又引入了@Autowired、@Component、@Service、@Repository等重要的注解,使用这些注解能简化Bean的配置。我们需要先像如下这样开启对这些注解的支持
<?xml ... ?>
<beans ......>
<context:component-scan base-package="learning.spring"/>
</beans>
<context:component-scan/> 会隐式地配置<context:annotation-config/>,后者替我们注册了一堆BeanPostProcessor。
上述配置会扫描learning.spring包内的类,在类上添加如下四个注解都能让Spring容器把它们配置为Bean:
@Component | 将类标识为普通的组件,即一个Bean |
@Service | 将类标识为服务层的服务 |
@Repository | 将类标识为数据层的数据仓库,一般是DAO |
@Controller | 将类标识为Web层的Web控制器(后来针对REST服务又增加了一个@RestController注解) |
如果不指定Bean的名称,Spring容器会自动生成一个名称,当然也可以明确指定:@Component("name")
如果要注入依赖,可以使用如下的注解:
@Autowired | 根据类型注入依赖,可用于构造方法、Setter方法和成员变量 |
@Resource | JSR-250的注解,根据名称注入依赖 |
@Inject | JSR-350的注解,同@Autowired |
@Autowired比较常用,还可以结合@Qualifier("")注解指定目标依赖的BeanID。除此之外,还可以使用@Value注解注入环境变量、Properties或YAML中配置的属性和SpEL表达式的计算结果。
2.1.2.3 基于Java类的配置
从Spring Framework3.0 开始,我们可以使用Java类代替XML文件,使用@Configuration、@Bean和@ComponentScan等一系列注解,基本可以满足日常所需。
通过AnnotationConfigApplicationContext可以构建一个支持基于注解和Java类的Spring上下文,这个在下面讲初始化过程也会提到:
ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
其中的Config类就是一个加了@Configuration注解的Java类,比如:
@Configuration
@ComponentScan("learning.spring")
public class Config {
@Bean
@Lazy
@Scope("prototype")
public Hello helloBean() {
return new Hello();
}
}
在Java配置类中指定Bean之间的依赖关系有两种方式,通过方法的参数注入依赖,或者直接调用类中带有@Bean注解的方法,如下示例,下面两个方法注入了同一个Bean
@Configuration
public class Config {
@Bean
public Foo foo() {
return new Foo();
}
@Bean
public Bar bar(Foo foo) {
return new Bar(foo);
}
@Bean
public Baz baz() {
return new Baz(foo());
}
}
另外,Spring针对@Configuration类中带有@Bean注解的方法通过CGLIB做了特殊处理,针对返回单例类型Bean的方法,调用多次返回的结果是一样的,并不会真的执行多次。
在配置类中也可以导入其他配置,例如用@Import导入其他配置类,用@ImportResource导入配置文件,如下:
@Configuration
@Import({ConfigA.class, ConfigB.class})
@ImportRespirce("classpath:/spring/*-applicationContext.xml")
public class Config {}
3. 讲述初始化过程
3.1 IOC容器初始化流程
IOC容器初始化的大致步骤如下:
- 从XML文件、Java类或其他地方加载配置元数据。
- 通过 BeanFactoryPostProcessor 对配置元数据进行一轮处理。
- 初始化Bean实例,并根据给定的依赖关系组装对象。
- 通过BeanPostProcessor对Bean进行处理,期间还会触发Bean被构造后的回调方法。
比如有一个基本的类,在没有IOC容器时,我们需要通过new新建一个实例,然后把它传给具体要调用它的对象,以此来管理它的生命周期。
如果是把实例交给Spring托管,则可以选择一种方式装配这个Bean,比如用XML的方式,通过</beans>标签来配置。
然后就需要将配置文件载入容器。BeanFactory默认使用DefaultListableBeanFactory这个实现类,他不关心配置的方式,比如用能读取XML文件的XmlBeanDefinitionReader来读取元数据,通过它来加载CLASSPATH中的beans.xml文件,将其保存到DefaultListableBeanFactory中,代码如下:
public class Application {
private BeanFactory beanFactory;
public static void main(String[] args) {
Application application = new Application();
application.sayHello();
}
public Application() {
beanFactory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader =
new XmlBeanDefinitionReader((DefaultListableBeanFactory) beanFactory );
reader.loadBeanDefinitions("beans.xml");
}
public void sayHello() {
// Hello hello = (Hello) beanFactory.getBean("hello");
Hello hello = beanFactory.getBean("hello", Hello.class);
hello.hello().sout;
}
}
在实际工作中,更多情况下会用到ApplicationContext的各种实现,ApplicationContext接口继承了BeanFactory,在BeanFactory的基础上提供了更丰富的功能,例如事件传播、资源加载、国际化支持等。
常见实现例如:
ClassPathXmlApplicationContext——从CLASSPATH中加载XML文件来配置ApplicationContext;
FileSystemXmlApplicationContext——从文件系统中加载XML文件来配置ApplicationContext;
AnnotationConfigApplicationContext——根据注解和Java类配置ApplicationContext。
使用ApplicationContext也会比BeanFactory更方便一些,因为我们无须自己去注册很多内容,比如AnnotationConfigApplicationContext把常用的一些后置处理器都直接注册好了,所以在绝大多数情况下,建议大家使用ApplicationContext的实现类。
使用ApplicationContext后的代码变为:
public class Application {
private ApplicationContext applicationContext;
public static void main(String[] args) {
Application application = new Application();
application.sayHello();
}
public Application() {
applicationContext = new ClassPathXmlApplicationContext("beans.xml");
}
public void sayHello() {
Hello hello = applicationContext.getBean("hello", Hello.class);
hello.hello().sout;
}
}
3.2 Bean的生命周期
一个Bean先要经过对象的创建(也就是通过new关键字创建一个对象),随后根据容器里的配置注入所需的依赖,最后调用初始化的回调方法,经过这三个步骤才算完成了Bean的初始化。
若不再需要这个Bean,则要进行销毁操作,在正式销毁对象前,会先调用容器的销毁回调方法。
由于这些都是由Spring管理的,所以我们无法任意地在Bean创建后或Bean销毁前增加某些操作。为此,Spring Framework为我们提供了几种途径,可以在这两个时间点调用我们提供给容器的回调方法:
- 实现InitializingBean和DisposableBean接口
- 分别用前者的afterPropertiesSet()方法和后者的destory()方法来操作两个时间点的动作。
- 使用JSR-250的@PostConstruct和@PreDestory注解
- 在配置类的方法上面添加上面两个注解
- 在<bean/>或@Bean里配置初始化和销毁方法
- 在XML文件中配置属性或在@Bean里面使用initMethod、destroyMethod属性,值为要调用的方法名。
另外,如果上述几种方法都使用了,Spring的顺序会按照上述顺序进行调用。
3.2.1 容器的初始化
上面的Bean的生命周期没有包括容器的初始化过程,在这里补充避免遗漏。
- 确认要使用的容器,如果是Spring一般是ClassPathXmlApplicationContext,如果是SpringBoot一般是AnnotationConfigApplicationContext。
- 初始化BeanFactory,扫描Aware接口,并把BeanName、BeanDefinition(定义 Bean 的配置元信息接口,里面存放了Bean的各种信息)放到BeanFactory缓存中。
- 触发BeanFactoryPostProcessor,它会在BeanFactory加载所有Bean定义但尚未对其初始化时介入。它的用法与BeanPostProcessor类似,可以对容器进行一些回调操作。
4. Spring其他用法
4.1 Aware接口
在大部分情况下,我们的Bean感知不到Spring容器的存在,也无须感知。但总有那么一些场景中要用到容器的一些特殊功能,比如如果希望在Bean中获取容器信息,可以通过如下两种方式:
- 实现BeanFactoryAware或ApplicationContextAware接口
- 用@Autowired注解来注入BeanFactory或ApplicationContext
两种方式的本质都是一样的,即让容器注入一个BeanFactory或ApplicationContext对象。
在拿到ApplicationContext(Spring上下文)后,就能操作该对象,比如调用getBean()方法取得想要的Bean。这个接口相比其他的Aware接口使用率更高一些。
如果Bean希望获得自己在容器中定义的Bean名称,可以实现BeanNameAware接口,这个接口的setBeanName()方法是注入一个代表名称的字符串,这算是一个依赖,因此会在Bean的初始化方法前被执行。
如果要用到事件机制,需要用到ApplicationEventPublisher来发送事件,可以通过实现ApplicationEventPublisherAware接口,从容器中获取到ApplicationEventPublisher实例,这个会在下面4.2详细讲到。
4.2 事件机制
ApplicationContext提供了一套事件机制,在容器发生变动时我们可以通过ApplicationEvent的子类通知到ApplicationListener接口的实现类(或增加@EventListener注解),做对应的处理。例如ApplicationContext在启动、停止、关闭和刷新时,分别会发出ContextStartEvent、ContextStoppedEvent、ContextClosedEvent和ContextRefreshEvent事件,这些事件可以让我们有机会感知当前容器的状态。
我们也可以自己监听这些事件,只需实现ApplicationListener接口或在某个Bean方法上增加@EventListener注解即可。通过这个来自定义事件,不过该事件必须继承ApplicationEvent,而且产生事件的类需要实现ApplicationEventPublisherAware,还要从上下文中获取到ApplicationEventPublisher——可以通过上下文applicationContext.publishEvent或通过实现了ApplicationEventPublisherAware接口的类里面直接获得。
Spring的内置事件以及自定义事件其实就是设计模式的观察者模式,关于自定义事件的实现可以参考一下我之前写过的 设计模式-观察者模式
4.3 容器的扩展点
Spring容器时非常灵活的,Spring Framework中有很多机制是通过容器本身的扩展点来实现的,比如Spring AOP等。
BeanPostProcessor接口是用来定制Bean的,这个接口是Bean的后置处理器,接口中有两个方法,postProcessBeforeInitialization()方法在Bean初始化前执行,PostProcessAfterInitialization()方法在Bean初始化之后执行。如果有多个BeanPostProcessor,可以通过@Order注解来指定运行的顺序。
如果说BeanPostProcessor是Bean的后置处理器,那BeanFactoryPostProcessor就是BeanFactory的后置处理器,我们可以通过它来定制Bean的配置元数据,其中postProcessBeanFactory()方法会在BeanFactory加载所有Bean定义但尚未对其进行初始化时介入。
另外,Spring AOP也是通过BeanPostProcessor实现的,因此实现该接口的类,以及其中直接引用的Bean都会被特殊对待,不会被AOP增强。此外,BeanPostProcessor和BeanFactoryPostProcessor都仅对当前容器上下文的Bean有效,不会去处理其他上下文。
5. SpringBean作用域、循环依赖
5.1 Spring Bean的作用域
Spring Framework的作用域基础分为两种:单例-singleton、原型(或者叫多例)-protorype。在2.5之后又针对Web服务增加了request、session、globalSession,如果当前容器是web容器,就可以支持。
- singleton:Spring的默认作用域,容器里拥有唯一的Bean实例。如果是有状态Bean(具有数据存储功能)可能会有线程安全问题。
- protorype:每个getBean请求,容器都会创建一个Bean实例。类似ThreadLocal。
- request:会为每个Http请求创建一个Bean实例。
- session:会为每个session创建一个Bean实例。
- globalSession:会为每个全局Http Session创建一个Bean实例,该作用域仅针对Protlet有效,在Servlet的web容器中效果同session。
5.2 循环依赖
循环依赖就是指两个类互相依赖注入了对方,形成闭环,例如A注入了B,B类又注入了A。在大部分场景下Spring会帮我们解决循环依赖问题,但某些场景却无法解决而导致启动时报错。
无法解决循环依赖的情况:
- 主Bean A以构造函数的方式依赖注入类B。原因是Spring注入的时候在放入三级缓存之前会经过一个步骤,如果是构造函数的方式就先创建,此时因为另一个Bean也没有在三级缓存中,就会出现问题。——也可以用这种方式检测代码中是否有循环依赖。
- Bean的作用域是多例(原型)的。因为多例的Bean每次调用getBean方法都会创建一次,所以是没有缓存的,不会把它放到三级缓存中。
Spring正常解决循环依赖的原理:
实例化Bean可以大致分成两步:通过反射创建对象,对该对象里面的属性赋值。当对象A创建出来后,不会马上往里面赋值,会先放到三级缓存中,接着Spring发现它需要依赖注入对象B,会先去创建对象B,同样放到三级缓存中。然后对象A会依次从一、二、三级缓存中查找B,如果是在三级缓存中查找到,会放入到二级缓存中。
缓存字段名 | 缓存级别 | 对象类型 | 解释 |
singletonObjects | 1 | Map<String,Object> | 存储的是所有创建好了的单例Bean |
earlySingletonObjects | 2 | Map<String,Object> | 保存的是创建好但还没有进行属性注入的Bean |
singletonFactories | 3 | Map<String,ObjectFactory<?>> | 提前暴露的单例工厂,生成Bean并放入到二级缓存 |
6. 总结、直接背
6.1 谈谈你对IOC的理解
IOC控制反转是Spring最重要的两个概念之一,控制反转是一种决定容器如何装配组件的模式,在Spring Framework的官方文档中有一张图,表达的是将业务对象(也就是Bean)和关于组件的配置元数据(比如依赖关系)输入Spring容器中,容器就能为我们组装出一个可用的系统。
IOC的思想是把对象的控制者从使用者变为由Spring来管理,通过DI依赖注入来实现。
依赖注入就是把对应的属性的值注入到具体的对象中,其中很重要的一块是管理依赖——就是管理Bean之间的依赖。有两种基本的注入方式——构造方法注入、setter方法注入。不过除了这两种手动注入,Spring还提供了自动织入的机制,可以在<bean/>(bean标签)中通过autowire属性来设置使用byName、byType或constructor的方式自动织入。此外,在Spring Framework2.5之后又增加了通过注解来实现自动织入。
可讲可不讲:Bean有三种配置方式,分别是基于XML文件的配置、基于注解的配置和基于Java类的配置。XML文件的配置就是通过bean标签中的各种属性来配置Bean;注解的配置是先通过<context:component-scan>标签或@ComponentScan注解来指定要扫描的包名,然后用Spring引入的@Component、@Service、@Repository、@Controller注解来配置类;Java类的配置就是通过使用@Configuration、@Bean、@ComponentScan等一系列注解来创建配置类。
然后我再讲一下IOC容器的初始化流程吧,IOC容器初始化的大致步骤是:
- 从XML文件、Java类或其他地方加载配置元数据。
- 通过 BeanFactoryPostProcessor 对配置元数据进行一轮处理。
- 初始化Bean实例,并根据给定的依赖关系组装对象。
- 通过BeanPostProcessor对Bean进行处理,期间还会触发Bean被构造后的回调方法。
在实际工作中,更多情况下会用到ApplicationContext的各种实现,ApplicationContext接口继承了BeanFactory,比如根据注解和Java类配置ApplicationContext的AnnotationConfigApplicationContext,它把常用的一些后置处理器都直接注册好了。
然后Bean的生命周期可以细化为:创建对象、注入依赖、创建后回调、正常使用、销毁前回调、销毁对象,在经过创建后的回调方法后才算是完成了Bean的初始化。创建后回调和销毁前回调时Spring开放了几种途径来操作,比如实现InitializingBean和DisposableBean接口、在配置类的方法上面使用@PostConstruct和@PreDestory注解、在XML文件中配置<bean/>标签或在@Bean注解里面使用initMethod、destroyMethod属性,值为要调用的方法名。
还要继续说的话,再讲下扩展点:Spring还提供了Aware接口、事件机制和扩展点来满足我们的一些开发需要。Aware接口比如实现BeanFactoryAware或ApplicationContextAware接口可以获取Spring上下文,实现ApplicationEventPublisherAware接口可以从容器中获取到ApplicationEventPublisher实例,用来发送事件;事件机制就是在容器发生变动时我们可以通过ApplicationEvent的子类通知到ApplicationListener接口的实现类(或增加@EventListener注解),做对应的处理。例如ApplicationContext在启动、停止、关闭和刷新时,会发出各种ContextEvent事件,这些事件可以让我们有机会感知当前容器的状态。也可以自定义事件,也就是设计模式的观察者模式,该事件必须继承ApplicationEvent,而且产生事件的类需要实现ApplicationEventPublisherAware。然后Spring中有很多机制是通过容器本身的扩展点来实现的,比如Spring AOP,它是通过BeanPostProcessor接口来实现的,接口中有两个方法,postProcessBeforeInitialization()方法在Bean初始化前执行,PostProcessAfterInitialization()方法在Bean初始化之后执行。如果有多个BeanPostProcessor,可以通过@Order注解来指定运行的顺序。同样BeanFactoryPostProcessor也可以用来定制Bean的配置元数据,其中postProcessBeanFactory()方法会在BeanFactory加载所有Bean定义但尚未对其进行初始化时介入。
最后还不喊停?那再加个Spring的循环依赖:Spring在依赖注入的时候还可能会有循环依赖问题,在大部分情况下Spring会帮我们解决,但如果是通过构造方法的注入,或者是Bean的作用域选择了protorype,就会在容器启动时就报错。
为什么这两种情况无法解决?:构造方法是因为Spring注入的时候在放入三级缓存之前会经过一个步骤,如果是构造函数的方式就先创建,此时因为另一个Bean也没有在三级缓存中,就会出现问题;protorype是因为多例的Bean每次调用getBean方法都会创建一次,所以是没有缓存的,不会把它放到三级缓存中。
一般情况下Spring是是怎么解决的?:实例化Bean可以大致分成两步:通过反射创建对象,对该对象里面的属性赋值。当对象A创建出来后,不会马上往里面赋值,会先放到三级缓存中,接着Spring发现它需要依赖注入对象B,会先去创建对象B,同样放到三级缓存中。然后对象A会依次从一、二、三级缓存中查找B,如果是在三级缓存中查找到,会放入到二级缓存中。