Spring IOC
(Inversion of Control,控制反转)容器是 Spring
框架的核心组件之一,用于管理和组织应用程序中的对象(bean
),它负责创建、配置和管理这些对象,以实现对象之间的解耦和依赖注入。
一、基本介绍
1. 主要功能
Spring IOC
容器的主要功能包括下述几点:
- 实例化对象:
IOC
容器负责实例化应用程序中定义的bean
对象。它根据配置文件或注解信息创建对象的实例。 - 管理生命周期:
IOC
容器负责管理bean
对象的生命周期,包括对象的创建、初始化和销毁。容器可以根据配置指定对象的初始化方法和销毁方法,确保对象在正确的时机被创建和销毁。 - 依赖注入:
IOC
容器实现了依赖注入(DI
)机制,它通过自动将对象的依赖注入到相应的位置,消除了手动编写代码进行依赖关系的管理。 - 解耦和松耦合:
IOC
容器通过控制对象的创建和依赖注入,实现了对象之间的解耦。它使得对象的配置和使用可以独立于彼此进行修改,提高了代码的灵活性和可维护性。
2. 生命周期
Spring
中的 Bean
生命周期包括以下五个阶段:
实例化(Instantiation): 这是
Bean
对象被创建的阶段。在这个阶段Spring
使用Bean
的构造方法来实例化对象,可以通过构造函数、静态工厂方法或者工厂Bean
来创建实例。属性赋值(Population): 在这个阶段
Spring
将配置文件中或者注解中定义的属性值注入到Bean
中。这包括基本类型、引用类型、集合等等,这一过程可以通过XML
配置、Java
注解、JavaConfig
等方式进行。初始化(Initialization): 在这个阶段
Spring
会调用Bean
的初始化方法。这个初始化方法可以是通过配置文件中的init-method
属性指定,也可以是实现了InitializingBean
接口的afterPropertiesSet
方法。使用(In Use): 此时
Bean
对象已经被完全初始化,可以被应用程序使用了。销毁(Destruction): 这是
Bean
生命周期的最后阶段。在这个阶段Spring
会调用Bean
的销毁方法,该方法可以是通过配置文件中的destroy-method
属性指定,也可以是实现了DisposableBean
接口的destroy
方法。
二、装配获取
Spring
提供了多个 IOC
容器实现,最常用的是基于 XML
配置的 ApplicationContext
,此外还提供了 Java
配置类(如 @Configuration、@Bean
等)方式进行配置。
1. 装配方式
(Ⅰ) XML方式装配
首先先回顾一下传统 xml
配置文件装配 bean
对象,在工程 resources
下新建 spring-context.xml
文件,其对应的文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
<bean id="testBean" class="xyz.ibudai.bean.User">
<property name="id" value="123" />
<property name="name" value="Alex" />
</bean>
</beans>
完成后即可通过加载配置文件获取应用上下文 ApplicationContext
从而读取 Bean
实例。
// 测试类
public class Test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-context.xml");
// 获取 Bean
User user = (User) context.getBean("testBean");
// User: {id=123, name=Alex}
System.out.println(user.toString());
}
}
(Ⅱ) 注解方式装配
在 Spring Boot
可配合 @Configuration
与 @Bean
注解可达到同样效果。
(1) @Configuration
- 作用于类上, 用于配置
Spring
容器应用上下文。 - 作用效果等价于
xml
配置文件中的beans
标签。
(2) @Bean
- 作用于方法上,等价于
xml
配置文件中bean
标签。 - 通过
@Bean(name)
指定bean
名称,未指定时为方法名(首字母自动小写)。
(3) @Scope
- 通过
@Scope
定义bean
作用域。 - 作用于方法上,与
@Bean
搭配使用。
作用域 | 描述 |
---|---|
singleton | 默认值,在整个 IoC 容器中只存在一个共享的 bean 实例。 |
prototype | 每次通过容器的 getBean() 方法获取 bean 时都会创建一个新的实例。 |
request | 在一次 HTTP 请求中,该 bean 实例将保持活动状态。 |
session | 在一个 HTTP Session 中,该 bean 实例将保持活动状态。 |
application | 在 ServletContext 范围内,该 bean 实例将保持活动状态。 |
下述为通过注解配置类的方式装配 bean
对象,最终实现效果等价于上述 xml
方式。
// 开启配置
@Configuration
public class TestConfig {
@Bean
@Scope("prototype")
public User testBean() {
User user = new User();
user.setId("123");
user.setName("Alex");
return user;
}
}
// 测试类
public class Test {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
// 获取 Bean
User user = (User) context.getBean("testBean");
// User: {id=123, name=Alex}
System.out.println(user.toString());
}
}
2. 装配类型
Bean
的装配一共存在两种类型,分别为 ByName
与 ByType
。
(Ⅰ) ByName(按名称注入)
- 根据依赖的名称来查找与之匹配的
bean
对象进行注入。 ByName
要求依赖的名称在容器中是唯一的或者能够通过自动装配策略(@Qualifier)
解决。- 通过名字查找与属性完全一致的
bean
,并将其与属性自动装配。 - 若容器中找不到与依赖名称匹配的
bean
对象则会抛出异常;如果存在多个与依赖名称匹配的bean
对象,会根据自动装配策略进行选择。
(Ⅱ) ByType(按类型注入)
- 根据依赖的类型
(class)
来查找与之匹配的bean
对象进行注入。 ByType
要求依赖的类型在容器中是唯一的或者明确指定了所需的bean
对象。- 通过类型查找与属性完全一致的
bean
,并将其与属性自动装配。 - 如果存在多个该类型
bean
则会抛出异常,并指出不能使用byType
方式进行自动装配;如果没有匹配的bean
对象,会将依赖设置为null
。
3. 注入方式
Spring
在装配 bean
的时提供了两种方式,但需要注意无论通过哪种方式其对应的对象需要在 IOC
容器中存在,否则需要加上属性 required=false
表示忽略当前要注入的 bean
,否则程序将无法正常运行。
(1) @Resource
@Resource
是由 J2EE
本身提供的,注解默认通过 byName
方式注入。
当存在多个类型不同但名称相同的 bean
对象时此方式注入将会抛出异常。
(2) @Autowired
@Autowired
是由 Spring
提供的,注解默认通过 byType
方式注入。
@Autowired
默认读取与声明实例变量同名的 bean
对象,当工程中同时包含多个实例时必须指明实例名否则将会异常报错,可以通过 @Qualifier
指定实例名。
// 创建两个同类型实例
@Service("userService1")
public class UserServiceImpl1 implements UserService { }
@Service("userService2")
public class UserServiceImpl2 implements UserService { }
// 测试类
public class UserController {
@Autowired
private UserService userService1;
@Autowired
@Qualifier(value = "userService2")
private UserService userService;
// 声明非法,多个实例时必须指明 bean 名称
@Autowired
private UserService userService;
}
4. Bean获取
在上面介绍了 bean
的注入和装配, Spring
中同时也提供通过 bean
名称直接获取 bean
对象,通常搭配反射等特性使用,下面介绍两种的不同的 bean
对象获取方式。
(Ⅰ) 接口方式获取 Bean 实例
通过实现 ApplicationContextAware
接口从而获取应用上下文对象 ApplicationContext
,即可利用其实现 bean
对象获取。
下面是一个 bean
对象查询示例,注意类需要标注 @Component
注解。
@Component
public class BeanService implements ApplicationContextAware {
private ApplicationContext applicationContext;
/**
* 获取上下文对象
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 通过名称获取 bean 对象
*/
public Object getBean(String name) throws BeansException {
return applicationContext.getBean(name);
}
}
(Ⅱ) 注解方式获取 Bean 实例
Spring
中同样提供了注解的方式获取应用上下文对象 ApplicationContext
,无需上述那么复杂直接使用 @Autowire
注入即可,更推荐使用此类方式。
public class BeanService {
/**
* 通过装配获取上下文对象
*/
@Autowired
private ApplicationContext applicationContext;
/**
* 通过名称获取 bean 对象
*/
public User getBean(String bean) {
return (User) applicationContext.getBean(bean);
}
}
三、Bean导入
1. 默认导入
在 Spring
中除了 @Bean
注解还提供 @Import
注入 bean
对象,其使用方式与前者类似,默认注入的 bean
对象名为类的完整限定民。
如下示例中即注入了一个 User
对象,若 User
类的完整包路径为:xyz.ibudai
,则对应的 bean
名称为:xyz.ibudai.User
。
@Configuration
@Import(User.class)
public class ImportConfig {
}
完成上述操作后当启动项目将注册对应 Bean
对象,我们即可利用 ApplicationContext
查询获取该 Bean
实例。
@Service
public class UserService {
@Autowired
private ApplicationContext applicationContext;
public Object getBean() {
return applicationContext.getBean("xyz.ibudai.User");
}
}
2. 手动导入
默认 @Import
注入 bean
对象属性值都为空且对象名为类的完整限定名,若想要配置更复杂信息则需要配合 ImportBeanDefinitionRegistrar
使用。
在 Spring Boot
工程启动时则会执行 ImportBeanDefinitionRegistrar
的 registerBeanDefinitions()
方法,顾名思义即执行 bean
对象的注入。其中方法的第一个参数 clsMetaData
为 @Import
注解所作用的类的元信息,如上述示例中及为类 ImportConfig
的元数据对象,而第二参数为注册器。
下述示例中通过实现 ImportBeanDefinitionRegistrar
接口手动向 IOC
容器中注入了一个 User
的 bean
对象。
public class UserRegisterFactory implements ImportBeanDefinitionRegistrar {
/**
* @param clsMetaData annotation metadata of the importing class
* @param registry current bean definition registry
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata clsMetaData, BeanDefinitionRegistry registry) {
Class<User> aClass = User.class;
if (!registry.containsBeanDefinition(aClass.getSimpleName())) {
// If not contain then register it
BeanDefinition beanDefinition = new RootBeanDefinition(aClass);
registry.registerBeanDefinition(aClass.getSimpleName(), beanDefinition);
}
}
}
四、容器缓存
1. 二级缓存
在 Spring
中为了解决循环依赖的问题引入了二级缓存从而解决该问题。
首先我们先看一下循环依赖带来的问题,假设存在两个 Bean
实例,其中 BeanA
中依赖了 BeanB
,而 BeanB
又依赖了 BeanA
,代码描述如下:
public class BeanA {
BeanB beanB;
}
public class BeanB {
BeanA beanA;
}
二者对象的依赖关系图示如下:
在使用 IOC
容器注入 BeanA
实例时,当为其注入属性时由于其又依赖于 BeanB
,因此需要先创建出 BeanB
实例,而 BeanB
又依赖于 BeanA
二者则陷入了死循环,这也是循环依赖所带来的问题。
那二级缓存又是如何解决如何这个问题的呢?让我们一步一步来拆解。
循环依赖问题在于属性注入阶段对象属性与自身互为成员变量,那么只需每次在在创建 bean
实例后将其存入一份至缓存中(Spring
中通过 Map
对象缓存),后续在执行属性注入时若需要依赖了直接读取缓存即可,避免了相互依赖导致的死循环。
2. 三级缓存
既然二级缓存已经解决了循环依赖的问题那为什么还需要引入三级缓存?三级缓存的引入主要是为了解决切面等动态代理生成的 bean
对象。
先看下图中两个流程,第一个为普通的 bean
实例创建流程,第二为包含动态代理等操作时的流程。
在二级缓存中讲过了缓存的存入时间在实例创建之后放入,而当包含动态代理时存入缓存的实例与最终存入 IOC
容器的对象显然不是同一个,这就造成了一个缓存不一致的原因,因此在此需要引入三级缓存从而解决缓存对象不一致。
五、Bean定义
在 IOC
容器中 Bean
对象存在两个十分重要的定义,即 BeanDefinitionHolder
和 BeanDefinition
,二者都是关于 Bean
元信息的类,它们的主要作用是描述和持有 Bean
的定义信息。
1. 对象定义
在 Spring
中每个注册到 IOC
容器中的 Bean
都有一个关联的 BeanDefinition
对象,该对象描述了如何创建和配置该 Bean
。其定义了 Bean
的属性、依赖关系、作用域(scope
)、初始化方法、销毁方法等配置信息。
BeanDefinition
中包含的主要方法即描述参考下标:
方法 | 作用 |
---|---|
getBeanClassName() | 获取 Bean 的类名。 |
getScope() | 获取 Bean 的作用域。 |
isSingleton() | 判断是否是单例。 |
isPrototype() | 判断是否是原型。 |
getConstructorArgumentValues() | 获取构造函数参数值。 |
getPropertyValues() | 获取属性值。 |
getInitMethodName() | 获取初始化方法名。 |
getDestroyMethodName() | 获取销毁方法名。 |
2. 定义包装
BeanDefinitionHolder
是对 BeanDefinition
的包装,同时持有一个 String
类型的 beanName
,通常在 Spring
容器中扫描、注册或管理 Bean
时使用。
在 Spring
中,通常在注册 Bean
定义时会使用 BeanDefinitionHolder
,将 Bean
的定义信息和名称一并封装,然后一起注册到容器中,从而更方便管理和操作 Bean
。
六、动态导入
基于 @Import
的功能特性,即可实现动态的 Bean
注册,扫描指定包路径下的类并注册到 Spring
容器。
1. 注解定义
定义 @BeanScan
注解用于标注目标类包路径,以及 @BeanItem
注解标注需要被扫描的类。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import(BeanRegisterFactory.class)
public @interface BeanScan {
String[] basePackages() default {};
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface BeanItem {
String value() default "";
}
2. 工程配置
在启动类上通过 @BeanScan
注解指定需要被扫描的包路径。
@SpringBootApplication
@BeanScan(basePackages = "xyz.ibudai.ioc")
public class BeanRegisterApplication {
public static void main(String[] args) {
SpringApplication.run(BeanRegisterApplication.class, args);
}
}
3. 注册实现
创建 BeanRegisterFactory
用于执行具体的扫描注册逻辑,启动项目时获取 @BeanScan
配置的包路径。
获取目标包后通过 BeanScannerFactory
实现 bean
的扫描生成 BeanDefinitionHolder
对象,并通过 registry
实现自定义对象的注册。
public class BeanRegisterFactory implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata clsMetaData, BeanDefinitionRegistry registry) {
// Get annotation attributes
Map<String, Object> attrMaps = clsMetaData.getAnnotationAttributes(BeanScan.class.getName());
// Convert attribute type to "AnnotationAttributes"
AnnotationAttributes attributes = AnnotationAttributes.fromMap(attrMaps);
if (attributes == null) {
System.err.println(">>>>>>>>>>>>>>>>>>>>>>> Annotation attribute is null");
return;
}
// Register annotation and Scan the package to find
// the class that use the registered annotation.
String[] basePackages = attributes.getStringArray("basePackages");
BeanScannerFactory scannerFactory = new BeanScannerFactory(registry);
scannerFactory.registerTypeFilter();
scannerFactory.doScan(basePackages);
}
}
4. Bean扫描
在 Spring
中通过 ClassPathBeanDefinitionScanner
实现 bean
的扫描注册,即通过扫描目标包将 bean
注册为上述的 BeanDefinitionHolder
对象从而实现管理。
ClassPathBeanDefinitionScanner
中涉及了两个相对重要的方法,其描述如下:
方法 | 作用 |
---|---|
registerTypeFilter() | 添加扫描过滤器,如只扫描某一部分特定类。 |
doScan() | 根据过滤器扫描指定包下的类。 |
如下示例中即定义了只扫描类上包含 BeanItem
注解的类,其中 proxy()
方法为通过 FactoryBean
自定义自定义构建 BeanDefinitionHolder
对象,并通过 registerTypeFilter()
方法筛选被 @BeanItem
注解标识的类。
public class BeanScannerFactory extends ClassPathBeanDefinitionScanner {
public BeanScannerFactory(BeanDefinitionRegistry registry) {
super(registry, false);
}
/**
* Register the annotation that want scan
*/
public void registerTypeFilter() {
// Only scan the class the Annotation of "BeanItem"
super.addIncludeFilter(new AnnotationTypeFilter(BeanItem.class));
}
/**
* Scan the specify package path to find the bean.
*/
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
beanDefinitionHolders = proxy(beanDefinitionHolders);
return beanDefinitionHolders;
}
/**
* Convert bean set to proxy bean set.
*/
protected Set<BeanDefinitionHolder> proxy(Set<BeanDefinitionHolder> beanDefinitionHolders) {
Set<BeanDefinitionHolder> holderSet = new HashSet<>(beanDefinitionHolders.size());
for (BeanDefinitionHolder holder : beanDefinitionHolders) {
GenericBeanDefinition beanDefinition = (GenericBeanDefinition) holder.getBeanDefinition();
String beanName = holder.getBeanName();
String beanClassName = beanDefinition.getBeanClassName();
if (!StringUtils.hasLength(beanClassName)) {
continue;
}
Class<?> aClass;
try {
aClass = Class.forName(beanClassName);
} catch (ClassNotFoundException e) {
continue;
}
// Transfer the parameter.
// getConstructorArgumentValues(): 获取 beanDefinition 中构造函数参数值的方法
// addGenericArgumentValue: 用于向 Bean 的构造函数参数中添加通用(泛型)参数值的方法。
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(aClass);
// Set "BeanFactory" to use "dynamic proxy" instance the bean object
beanDefinition.setBeanClass(MyFactoryBean.class);
holderSet.add(new BeanDefinitionHolder(beanDefinition, beanName));
}
return holderSet;
}
}
5. Bean工厂
通过扫描注册到 IOC
容器的仅为 BeanDefinitionHolder
,即 bean
对象的相关元信息,只有在使用到 bean
对象的时候才会进行实例化。
而通过工厂类 FactoryBean
即可实现动态的对象生成等操作,如实现 AOP
切面与 RPC
远程服务调用等等。
如下示例中的 MyFactoryBean
即通过动态代理 BeanInvokeHandler
方式生成代理对象从而实现方法的调用信息打印实现切面的效果。
public class MyFactoryBean<T> implements FactoryBean<T> {
private final Class<?> aClass;
public MyFactoryBean(Class<?> aClass) {
this.aClass = aClass;
}
@Override
@SuppressWarnings("unchecked")
public T getObject() throws Exception {
Class<?> aInterface = aClass.getInterfaces()[0];
Constructor<?> constructor = aClass.getDeclaredConstructor();
Object instance = constructor.newInstance();
BeanInvokeHandler handler = new BeanInvokeHandler(instance);
return (T) Proxy.newProxyInstance(aClass.getClassLoader(), new Class[]{aInterface}, handler);
}
@Override
public Class<?> getObjectType() {
return aClass;
}
}
/**
* 动态代理处理器
*/
public class BeanInvokeHandler implements InvocationHandler {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final Object object;
public BeanInvokeHandler(Object object) {
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
logger.error("Invoke method of [{}].", method.getName());
Object result = method.invoke(this.object, args);
logger.error("Invoke handle finish, [{}].", result);
return result;
}
}
参考链接