6、依赖查找高级
SpringFramework 中的容器最核心的是 BeanFactory
与 ApplicationContext
。
6.1 BeanFactory/ApplicationContext
ApplicationContext
接口是 BeanFactory
的子接口,它包含后者所有的功能,并拓展了很多特性。
- 面试题:BeanFactory与ApplicationContext的对比
BeanFactory
接口提供了一个抽象的配置和对象的管理机制,ApplicationContext
是 BeanFactory
的子接口,它简化了与 AOP 的整合、消息机制、事件机制,以及对 Web 环境的扩展( WebApplicationContext
等),BeanFactory
是没有这些扩展的。
ApplicationContext
主要扩展了以下功能:
AOP 的支持( `AnnotationAwareAspectJAutoProxyCreator` 作用于 Bean 的初始化之后 )
配置元信息( `BeanDefinition` 、`Environment` 、注解等 )
资源管理( `Resource` 抽象 )
事件驱动机制( `ApplicationEvent` 、`ApplicationListener` )
消息与国际化( `LocaleResolver` )
`Environment` 抽象( SpringFramework 3.1 以后)
6.2 依赖查找高级
ApplicationContext
接口拥有 getBeansOfType()方法,传入一个类型;返回给定类型的所有实现类。
ApplicationContext
中有一个方法叫 getBeansWithAnnotation(),它可以传入一个注解的 class ,返回所有被这个注解标注的 bean 。
ApplicationContext
中另一个方法getBeanDefinitionNames,可以取出 IOC 容器中所有bean的id,并可用String[] 类型接受。
6.3 依赖查找高级——延迟查找
7、注解驱动IOC与组件扫描
7.1 注解驱动IOC
对于 xml 驱动的 IOC 容器中,解析使用的是 ClassPathXmlApplicationContext
注解驱动IOC中,解析使用的是 AnnotationConfigApplicationContext
-
依赖查找
配置类( @Configuration标注 )的编写与Bean的注册( 在配置类内使用
@Bean
注解 )在 xml 中,声明 Bean 是通过
<bean>
标签。<bean id="person" class="com.linkedbear.spring.basic_dl.a_quickstart_byname.bean.Person"/>
在配置类中,要想替换掉
<bean>
标签,自然也能想到,它是使用@Bean
注解。@Bean public Person person() { return new Person(); }
这种使用方式,可以解释为:向 IOC 容器注册一个类型为 Person ,id 为 person 的 Bean 。方法的返回值代表注册的类型,方法名代表 Bean 的 id 。当然,也可以直接在
@Bean
注解上显式的声明 Bean 的 id ,只不过在注解驱动的范畴里,它不叫 id 而是叫 name :@Bean(name = "aaa") // 4.3.3之后可以直接写value public Person person() { return new Person(); }
-
依赖注入
在Bean中直接调用setter方法设置属性
7.2 组件注册与组件扫描
组件产生原因:如果需要注册的组件特别多,手动编写 @Bean
工作量大,Spring中提供 模式注解
来快速注册组件。
-
组件注册:
在类上标注
@Component
注解,即代表该类会被注册到 IOC 容器中作为一个 Bean。可以直接用
@Component("aaa")
声明Bean的 id 属性。 -
组件扫描的方法
-
使用注解驱动的IOC:
- 新建配置类,在配置类上额外标注
@ComponentScan("路径")
,会自动扫描路径下的所有@Component组件 - 不用配置类,使用 AnnotationConfigApplicationContext 中传参为 String 类型(也是路径)的构造方法
- 新建配置类,在配置类上额外标注
-
使用xml驱动的IOC:
<context:component-scan base-package="路径"/>
-
-
使用
@Configuration
注解的类也被视为bean,为什么?@Configuration
注解也标注了@Component
注解,所以它也会被视为 bean ,注册到 IOC 容器。
8、依赖注入-属性注入/SpEL表达式
8.1 setter-属性注入
8.2 构造器式-属性注入
有些 bean 本身没有无参构造器,这个时候就必须使用构造器注入,否则会提示 " No matching constructor found in class 'Person "。分为两种:
- xml式
<bean id="person" class="num8_di__bean_component.bean.Person">
<constructor-arg index="0" value="test-person-byconstructor"/>
<constructor-arg index="1" value="18"/>
</bean>
- 注解式
直接在Bean返回时,调用带参构造器来构造对象。
8.3 注解式-属性注入
之前都是处理使用 @Bean
的方式注册 bean 的情况。那声明式注册好的组件,它们的属性怎么处理呢?
- 在@Component声明的类中,使用 @value 注解在代码中注入属性
@Component
public class Black {
@Value("black-value-ann")
private String name;
... ...
}
-
外部配置文件- @PropertySource
- 配置文件编写
- 在配置类上引入配置文件 : @PropertySource("classpath:basic_di/value/red.properties")
- 字段上方使用 @Value("${red.name}") 进行属性注入
- 解析配置类,启动
注意两种解析方式(包扫描+value注释注入 与 配置类+外部配置文件注入)不能混用!
- SpEL
placeholder
9、依赖注入-自动注入/复杂注入
9.1 自动注入(bean中注入另一个bean)
@Autowired
@Autowired
不仅可以用在 普通 Bean 的属性 上,在 配置类中,注册 @Bean
时也可以标注。
-
@Autowired
的三种方式:- 在 需要注入的属性(bean中bean) 的声明前标注
- 构造器注入
- setter注入
-
若bean中需要的 bean(如Cat类中的Person类)找不到(例如Perosn缺少
@Component
注解),则需要在@Autowired
注解上加一个属性:required = false
-
多个同类型Bean时,保证注入的唯一性?
比如在使用配置类时,已使用
@Autowired
注解自动注入了一个默认的内嵌Bean,但配置类内又定义了一个同类型的Bean,此时需要使用@Qualifier
或@Primary
配合@Autowired
注解使用,否则会报错:expected single matching bean but found 2: administrator,master
-
@Qualifier
指定注入的Bean名称@Qualifier
注解的使用目标是要注入的 Bean:@Bean @Autowired public Cat cat(@Qualifier("administrator") Person person){ ... ... }
-
@Primary
默认Bean@Primary
注解的使用目标是被注入的 Bean:@Bean @Primary public Person master() { Person master = new Person(); master.setName("master"); return master; }
-
面试题:@Autowired注入的原理逻辑
先拿属性对应的类型,去 IOC 容器中找 Bean ,如果找到了一个,直接返回;
如果找到多个类型一样的 Bean , 把属性名拿过去,跟这些 Bean 的 id 逐个对比,如果有一个相同的,直接返回;
如果没有相同的 / 有多个相同的,则会抛出
NoUniqueBeanDefinitionException
异常。
-
多个同类型Bean时,全部注入?
注入一个用单个对象接收,注入一组对象就用集合来接收:
@Component public class Dog { @Autowired private List<Person> persons;
@Resource
@Resource
也是用来属性注入的注解,与 @Autowired
的不同:
@Autowired
是按照类型注入;@Resource
是直接按照属性名 / Bean的名称注入,相当于 @Autowired
+ @Qualifier
了
@Inject
与 @AutoWired
类似:
@Inject // 等同于@Autowired
@Named("admin") // 等同于@Qualifier
面试题:依赖注入的注入方式
注入方式 被注入成员是否可变 是否依赖IOC框架的API 使用场景 构造器注入 不可变 否(xml、编程式注入不依赖) 不可变的固定注入 参数注入 不可变 否(高版本中注解配置类中的 @Bean
方法参数注入可不标注注解)注解配置类中 @Bean
方法注册 bean属性注入 不可变 是(只能通过标注注解来侵入式注入) 通常用于不可变的固定注入 setter注入 可变 否(xml、编程式注入不依赖) 可选属性的注入 面试题:自动注入的注解对比
注解 注入方式 是否支持@Primary 来源 Bean不存在时处理 @Autowired 根据类型注入 是 SpringFramework原生注解 可指定required=false来避免注入失败 @Resource 根据名称注入 是 JSR250规范 容器中不存在指定Bean会抛出异常 @Inject 根据类型注入 是 JSR330规范 ( 需要导jar包 ) 容器中不存在指定Bean会抛出异常
@Qualifier
:如果被标注的成员/方法在根据类型注入时发现有多个相同类型的 Bean ,则会根据该注解声明的 name 寻找特定的 bean
@Primary
:如果有多个相同类型的 Bean 同时注册到 IOC 容器中,使用 “根据类型注入” 的注解时会注入标注@Primary
注解的 bean
9.2 复杂类型注入
-
xml复杂注入
- 数组
- List / Set
- Map
- Properties
-
注解复杂注入
和num8中的SpEL一样,未跟着敲
10、依赖注入-回调注入/延迟注入
10.1 回调注入
回调的根源都是 @Aware
接口,下面两个都是其子接口:
-
ApplicationContextAware
!!示例错误
-
BeanNameAware
如果当前的 bean 需要依赖它本身的 name ,使用
@Autowired
就不好使了,这个时候就得使用BeanNameAware
接口来辅助注入当前 bean 的 name 了。!!未理解
10.2 延迟注入
【面试题】依赖注入的注入方式-扩展
注入方式 | 被注入成员是否可变 | 是否依赖IOC框架的API | 注入时机 | 使用场景 | 支持延迟注入 |
---|---|---|---|---|---|
构造器注入 | 不可变 | 否(xml、编程式注入不依赖) | 对象创建时 | 不可变的固定注入 | 是 |
参数注入 | 不可变 | 是(只能通过标注注解来侵入式注入) | 对象创建后 | 通常用于不可变的固定注入 | 否 |
setter注入 | 可变 | 否(xml、编程式注入不依赖) | 对象创建后 | 可选属性的注入 | 是 |
10.3 依赖注入面试集锦
-
依赖注入的目的和优点?
首先,依赖注入作为 IOC 的实现方式之一,目的就是解耦,我们不再需要直接去 new 那些依赖的类对象(直接依赖会导致对象的创建机制、初始化过程难以统一控制);而且,如果组件存在多级依赖,依赖注入可以将这些依赖的关系简化,开发者只需要定义好谁依赖谁即可。
除此之外,依赖注入的另一个特点是依赖对象的可配置:通过 xml 或者注解声明,可以指定和调整组件注入的对象,借助 Java 的多态特性,可以不需要大批量的修改就完成依赖注入的对象替换(面向接口编程与依赖注入配合近乎完美)。
-
谁把什么注入给谁了?
-
依赖注入具体是如何注入的?
-
使用setter注入还是构造器注入?
11、Bean类型与作用域
11.1 Bean类型
Bean一般有两种设计:普通 Bean 、工厂 Bean 。之前都是普通Bean,下面介绍工厂Bean:
FactoryBean
如果Bean 的创建需要指定一些策略,或者依赖特殊的场景来分别创建,也或者一个对象的创建过程太复杂,使用 xml 或者注解声明也比较复杂。这种情况下,可以借助 FactoryBean
接口 来使用工厂方法创建对象。
FactoryBean
本身是一个接口,是一个创建对象的工厂。如果 Bean 实现了 FactoryBean
接口,则它将不会处理实际的业务逻辑,而是由Bean内创建的对象来处理。
接口实现类需要覆写 getObject、getObjectType 两个方法。接口另有默认方法 default boolean isSingleton()
,接口实现类默认是单实例的Bean。
-
FactoryBean创建Bean的时机
ApplicationContext
初始化 Bean 的时机默认是容器加载时就已经创建;FactoryBean
本身的加载是伴随 IOC 容器的初始化一起的。而FactoryBean
创建 Bean 的机制是延迟生产,通过getBean()方法调用时才创建。
-
取 FactoryBean 的本体
一般情况下,都是类似于之前的演示,取的是 FactoryBean 生产的Bean;某些情况下会需要取 FactoryBean 本体,方法:
//需要在 Bean 的 id 前面加 “&” 符号, 不然取的还是生产的Bean System.out.println(ctx.getBean("&toyFactory"));
-
面试题:BeanFactory与FactoryBean的区别?
BeanFactory
:SpringFramework 中实现 IOC 的最底层容器(此处的回答可以从两种角度出发:从类的继承结构上看,它是最顶级的接口,也就是最顶层的容器实现;从类的组合结构上看,它则是最深层次的容器,ApplicationContext
在最底层组合了BeanFactory
)FactoryBean
:创建对象的工厂 Bean ,可以使用它来直接创建一些初始化流程比较复杂的对象
11.2 Bean的作用域
SpringFramework 中内置了 6 种作用域(5.x 版本):
作用域类型 | 概述 |
---|---|
singleton | 一个 IOC 容器中只有一个【默认值】 |
prototype | 每次获取创建一个 |
request | 一次请求创建一个(仅Web应用可用) |
session | 一个会话创建一个(仅Web应用可用) |
application | 一个 Web 应用创建一个(仅Web应用可用) |
websocket | 一个 WebSocket 会话创建一个(仅Web应用可用) |
singleton(单实例-缺省)
SpringFramework 中默认所有的 Bean 都是单实例的,即:一个 IOC 容器中只有一个。
prototype(原型)
每次对原型 Bean 提出请求时,都会创建一个新的 Bean 实例。
”提出请求“ ,包括任何依赖查找、依赖注入的动作,都算做一次 ”提出请求“ 。
12、Bean的实例化方法
前一章介绍了 普通Bean实例化、FactoryBean实例化,下面介绍 静态工厂实例化、实例工厂实例化:
12.3 静态工厂实例化
有静态工厂如下,内置静态方法 getCar
public class CarStaticFactory {
public static Car getCar() {
return new Car();
}
}
- 静态工厂的使用多借助 xml 方式:
<bean id="car1" class="com.linkedbear.spring.bean.c_instantiate.bean.Car"/>
<bean id="car2" class="com.linkedbear.spring.bean.c_instantiate.bean.CarStaticFactory" factory-method="getCar"/>
可以看出来,上面的注册方式是普通的 Bean 注册方式; 下面的方式会直接引用静态工厂,并声明要创建对象的工厂方法 factory-method
即可。SpringFramework 会依照这个 xml 的方式,解析出规则并调用静态工厂的方法来创建实际的 Bean 对象。
上面两个bean实际都是Car类型的,而CarStaticFactory本身不会被注册到IOC容器中。 例如打印如下内容,会报NoSuchBeanDefinitionException
异常:
System.out.println(ctx.getBean(CarInstanceFactory.class));
- 编程式使用静态工厂
只能使用 注解配置类+编程式 来使用静态工厂
@Bean
public Car car2() {
return CarStaticFactory.getCar();
}
12.4 实例工厂实例化
- xml方式:
在工厂类中将 static 字段去掉,xml如下:
<bean id="carInstanceFatory" class="num12_static_instance_Factory.bean.CarInstanceFactory"/>
<bean id="car3" factory-bean="carInstanceFatory" factory-method="getCar"/>
此时实例工厂通过注册到 IOC 容器中了。
- 编程式方式:
@Bean
public Car car3(CarInstanceFactory carInstanceFactory) {
return carInstanceFactory.getCar();
}
13、Bean生命周期-初始化/销毁
13.1 生命周期的意义
-
一个对象从被创建,到被垃圾回收,可以宏观的划分为 5 个阶段:
- 创建 / 实例化阶段:此时会调用类的构造方法,产生一个新的对象
- 初始化阶段:此时对象已经创建好,但还没有被正式使用,可能这里面需要做一些额外的操作(如预初始化数据库的连接池)
- 运行使用期:此时对象已经完全初始化好,程序正常运行,对象被使用
- 销毁阶段:此时对象准备被销毁,已不再使用,需要预先的把自身占用的资源等处理好(如关闭、释放数据库连接)
- 回收阶段:此时对象已经完全没有被引用了,被垃圾回收器回收
把控好生命周期的步骤,可以在恰当的时机处理一些恰当的逻辑。
-
SpringFramework 如何能让我们干预 Bean 的初始化和销毁呢?
回想一下 Servlet ,
Servlet
里面有两个方法,分别叫init
和destroy
。咱之前在使用 Servlet 开发时,有自己调过这两个方法吗?肯定没有吧。
但是这两个方法有真实的被调用过吗?肯定有。这两个方法都是被 Web 容器( Tomcat 等)调用的,用来初始化和销毁 Servlet 的。这种方法的设计思想其实就是 “回调机制” ,它 不是自己设计的,而是由父类 / 接口定义好的,由第三者(框架、容器等)来调用。回调机制跟前面学的那些
Aware
接口的回调注入,在核心思想上其实是一样的。
先体会一种最容易理解的生命周期阶段:初始化和销毁方法。单实例 Bean 在 SpringFramework 中常见的 3 种生命周期,见下面三章:
13.2 init-method/destroy-method
适用场景:手动声明注册的Bean
-
先创建类,写 init、destroy 方法
-
注意!这些配置的初始化和销毁方法必须具有以下特征:
- 方法访问权限无限制要求( SpringFramework 底层会反射调用的)
- 方法无参数(如果真的设置了参数,SpringFramework 也不知道传什么进去)
- 方法无返回值(返回给 SpringFramework 也没有意义)
- 可以抛出异常(异常不由自己处理,交予 SpringFramework 可以打断 Bean 的初始化 / 销毁步骤)
-
-
再创建配置文件(xml文件) / 配置类
- 若采用配置类,则使用标注: @Bean(initMethod = "init", destroyMethod = "destroy")
- 若采用xml文件,则在<bean class=“” 后增加: init-method="init" destroy-method="destroy"
-
编写启动类
只需注意接收类型不再使用
ApplicationContext
,而是实现类本身。因为ApplicationContext
本身没有close
方法。
有配置类和启动类如下:
public class InitMethodConfiguration {
@Bean(initMethod = "init", destroyMethod = "destroy")
public Dog dog(){
Dog dog = new Dog();
dog.setName("wangwang");
return dog;
}
}
public class InitMethodApplication {
public static void main(String[] args) {
System.out.println("准备初始化IOC容器。。。");
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(InitMethodConfiguration.class);
System.out.println("IOC容器初始化完成。。。");
System.out.println("准备销毁IOC容器。。。");
ctx.close();
System.out.println("IOC容器销毁完成。。。");
}
}
控制台输出:
准备初始化IOC容器。。。 Cat 构造方法执行了。。。 setName 方法执行了。。。 wangwang被初始化了。。。 IOC容器初始化完成。。。
准备销毁IOC容器。。。 wangwang被销毁了。。。 IOC容器销毁完成。。。
可以看出:Bean 的生命周期中,是先对属性赋值,后执行 initMethod
标记的方法。
13.3 JSR250规范
适用场景1:使用模式注解的Bean,即使用@Component的类
JSR250 规范中除了有 @Resource
这样的自动注入注解,还有负责生命周期的注解,包括 @PostConstruct
、 @PreDestroy
两个注解,分别对应 init-method
和 destroy-method
。
这两个注解,直接标注在 Bean 内相应的方法上即可。
适用场景2:不使用 @Component 注解时,JSR250规范可与init-method共存
如果不使用 @Component
注解来注册 Bean 而转用 <bean>
/ @Bean
的方式,那 @PostConstruct
与 @PreDestroy
注解是可以与 init-method
/ destroy-method
共存的。
并且 JSR250 规范的执行优先级高于 init / destroy。
13.4 InitializingBean与DisposableBean
标题中两项为接口,他们都是在 Bean 的初始化和销毁阶段要回调的。
使用方法是使类实现 InitializingBean, DisposableBean
,并在类内覆写 afterPropertiesSet
与 destroy
方法。
同样的,他们既适用于使用 @Component注解 的场景;又可以在不使用 @Component注解 时,与其他两种生命周期共存。
当共存时,执行顺序为: @PostConstruct
→ InitializingBean接口
→ init-method
;
@PreDestroy
→ DisposableBean接口
→ destroy-method
;
13.5 原型Bean的生命周期
对于原型 Bean 的生命周期,使用的方式跟上面是完全一致的,只是它的触发时机就不像单实例 Bean 那样了。
-
单实例 Bean 的生命周期 与 IOC 容器相同。容器初始化,单实例 Bean 也跟着初始化(当然不绝对,后面会介绍延迟 Bean );容器销毁,单实例 Bean 也跟着销毁。
-
原型 Bean 由于每次都是取的时候才产生一个,所以它的生命周期与 IOC 容器无关。
-
原型 Bean 的创建不随 IOC 的初始化而创建;
-
原型 Bean 的初始化动作与单实例Bean完全一致;
-
原型 Bean 在销毁时不处理
destroy-method
标注的方法。- 调用 ctx.getBeanFactroy().destroyBean(pen); 发现控制台只打印了
@PreDestroy
注解和DisposableBean
接口的执行内容,没有触发destroy-mothod
的执行。
- 调用 ctx.getBeanFactroy().destroyBean(pen); 发现控制台只打印了
-
13.6 三种控制Bean生命周期方式-总结
init-method & destroy-method | @PostConstruct & @PreDestroy | InitializingBean & DisposableBean | |
---|---|---|---|
执行顺序 | 最后 | 最先 | 中间 |
组件耦合度 | 无侵入(只在 <bean> 和 @Bean 中使用) | 与 JSR 规范耦合 | 与 SpringFramework 耦合 |
容器支持 | xml 、注解原生支持 | 注解原生支持,xml需开启注解驱动 | xml 、注解原生支持 |
单实例Bean | √ | √ | √ |
原型Bean | 只支持 init-method | √ | √ |