写点什么

Spring 框架 IoC 核心详解

作者:EquatorCoco
  • 2024-12-12
    福建
  • 本文字数:11817 字

    阅读完需:约 39 分钟

介绍


IoC(Inversion of Control:控制反转) 是一种设计思想,而不是一个具体的技术实现。IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理,由 Spring 容器管理 bean 的整个生命周期。通俗来说就是 IoC 是设计思想,DI 是实现方式。


通过反射实现对其他对象的控制,包括初始化、创建、销毁等,解放手动创建对象的过程,同时降低类之间的耦合度。


在 Spring 中, IoC container 是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象,根据 BeanName 或者 Type 获取对象


为何是反转,哪些方面反转了?有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;控制:指的是对象创建(实例化、管理)的权力反转:控制权交给外部环境(Spring 框架、IoC 容器)


IOC 的好处


ioc 的思想最核心的地方在于,资源不由使用资源者管理,而由不使用资源的第三方管理,这可以带来很多好处。


  1. 资源集中管理,实现资源的可配置和易管理。

  2. 降低类之间的耦合度。


比如在实际项目中一个 Service 类可能依赖了很多其他的类,假如我们需要实例化这个 Service,可能要每次都要搞清这个 Service 所有底层类的构造函数,这就变得复杂了。而如果使用 IoC 的话,只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。


IOC 配置的四种方式


在 Spring - 概述 一文中已经给出了三种配置方式,这里再总结下;总体上目前的主流方式是 注解 + Java 配置


xml 配置


顾名思义,就是将 bean 的信息配置.xml 文件里,通过 Spring 加载文件来创建 bean。这种方式出现很多早前的 SSM 项目中,将第三方类库或者一些配置工具类都以这种方式进行配置,主要原因是由于第三方类不支持 Spring 注解。


  • 优点: 可以使用于任何场景,结构清晰,通俗易懂

  • 缺点: 配置繁琐,不易维护,枯燥无味,扩展性差


举例


  1. 配置 xx.xml 文件

  2. 声明命名空间和配置 bean


<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">    <!-- services -->    <bean id="userService" class="com.seven.springframework.service.UserServiceImpl">        <property name="userDao" ref="userDao"/>        <!-- additional collaborators and configuration for this bean go here -->    </bean>    <!-- more bean definitions for services go here --></beans>
复制代码


Java 配置


将类的创建交给我们配置的 JavcConfig 类来完成,Spring 只负责维护和管理,采用纯 Java 创建方式。其本质上就是把在 XML 上的配置声明转移到 Java 配置类中


  • 优点:适用于任何场景,配置方便,因为是纯 Java 代码,扩展性高,十分灵活

  • 缺点:由于是采用 Java 类的方式,声明不明显,如果大量配置,可读性比较差


举例


  1. 创建一个配置类, 添加 @Configuration 注解声明为配置类

  2. 创建方法,方法上加上 @bean,该方法用于创建实例并返回,该实例创建后会交给 spring 管理,方法名建议与实例名相同(首字母小写)。注:实例类不需要加任何注解


@Configurationpublic class BeansConfig {
@Bean("userDao") public UserDaoImpl userDao() { return new UserDaoImpl(); }
@Bean("userService") public UserServiceImpl userService() { UserServiceImpl userService = new UserServiceImpl(); userService.setUserDao(userDao()); return userService; }}
复制代码


注解配置


通过在类上加注解的方式,来声明一个类交给 Spring 管理,Spring 会自动扫描带有 @Component,@Controller,@Service,@Repository 这四个注解的类,然后帮我们创建并管理,前提是需要先配置 Spring 的注解扫描器。


  • 优点:开发便捷,通俗易懂,方便维护。

  • 缺点:具有局限性,对于一些第三方资源,无法添加注解。只能采用 XML 或 JavaConfig 的方式配置


举例


  1. 对类添加 @Component 相关的注解,比如 @Controller,@Service,@Repository

  2. 设置 ComponentScan 的 basePackage, 比如在 xml 文件里设置<context:component-scan base-package='com.seven.springframework'>, 或者在配置类中设置@ComponentScan("com.seven.springframework")注解,或者 直接在 APP 类中 new AnnotationConfigApplicationContext("com.seven.springframework")指定扫描的 basePackage.


@Servicepublic class UserServiceImpl {
@Autowired private UserDaoImpl userDao;
public List<User> findUserList() { return userDao.findUserList(); }
}
复制代码


依赖注入 DI 的方式


其原理是将对象的依赖关系由外部容器来管理和注入。这样,对象只需要关注自身的核心功能,而不需要关心如何获取依赖对象。它的目的是解耦组件之间的依赖关系,提高代码的灵活性、可维护性和可测试性。

在 Spring 创建对象的过程中,把对象依赖的属性注入到对象中。


依赖注入主要有三种方式:构造器注入,Setter 方式注入(属性注入)、基于字段的依赖注入

其中基于字段的依赖注入被广泛使用,但是 idea 或者其他静态代码分析工具会给出提示信息,不推荐使用。


Setter 方式注入


在基于 setter 的依赖注入中,setter 方法被标注为 @Autowired。一旦使用无参数构造函数或无参数静态工厂方法实例化 Bean,为了注入 Bean 的依赖项,Spring 容器将调用这些 setter 方法。


  • 在 XML 配置方式中,property 都是 setter 方式注入,比如下面的 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"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">    <!-- services -->    <bean id="userService" class="com.seven.springframework.service.UserServiceImpl">        <property name="userDao" ref="userDao"/>        <!-- additional collaborators and configuration for this bean go here -->    </bean>    <!-- more bean definitions for services go here --></beans>
复制代码


本质上包含两步:


  1. 第一步,需要 new UserServiceImpl()创建对象, 所以需要默认构造函数

  2. 第二步,调用 setUserDao()函数注入 userDao 的值, 所以需要 setUserDao()函数

  • 在注解和 Java 配置下


@Componentpublic class UserServiceImpl {
private UserDao userDao;
@Autowired //这个 @Autowired可以省略 public void setUserDao(UserDao userDao) { this.userDao = userDao; }}
复制代码


将 @Autowired 写在被注入的成员变量上,setter 或者构造器上,就不用再 xml 文件中配置了。


基于属性的依赖注入


在基于属性的依赖注入中,以 @Autowired(自动注入)注解注入为例,修饰符有三个属性:Constructor,byType,byName。默认按照 byType 注入。一旦类被实例化,Spring 容器将设置这些字段。


  • constructor:通过构造方法进行自动注入,spring 会匹配与构造方法参数类型一致的 bean 进行注入,如果有一个多参数的构造方法,一个只有一个参数的构造方法,在容器中查找到多个匹配多参数构造方法的 bean,那么 spring 会优先将 bean 注入到多参数的构造方法中。

  • byName:被注入 bean 的 id 名必须与 set 方法后半截匹配,并且 id 名称的第一个单词首字母必须小写,这一点与手动 set 注入有点不同。

  • byType:查找所有的 set 方法,将符合符合参数类型的 bean 注入。


@Componentpublic class FieldBasedInjection {    @Autowired    private InjectedBean injectedBean;}
复制代码


可能存在的缺点


正如所看到的,这是依赖注入最干净的方法,因为它避免了添加样板代码,并且不需要声明类的构造函数。代码看起来很干净简洁,但是正如代码检查器已经向我们暗示的那样,这种方法有一些缺点:


不允许声明不可变域

基于字段的依赖注入在声明为 final/immutable 的字段上不起作用,因为这些字段必须在类实例化时实例化。声明不可变依赖项的唯一方法是使用基于构造器的依赖注入。


容易违反单一职责设计原则

使用基于字段的依赖注入,高频使用的类随着时间的推移,会在类中逐渐添加越来越多的依赖项,用着很爽,但很容易忽略类中的依赖已经太多了。但是如果使用基于构造函数的依赖注入,随着越来越多的依赖项被添加到类中,构造函数会变得越来越大,一眼就可以察觉到哪里不对劲。有一个有超过 10 个参数的构造函数是一个明显的信号,表明类已经转变一个大而全的功能合集,需要将类分割成更小、更容易维护的块。因此,尽管属性注入并不是破坏单一责任原则的直接原因,但它隐藏了信号,使我们很容易忽略这些信号。


与依赖注入容器紧密耦合


使用基于字段的依赖注入的主要原因是为了避免 getter 和 setter 的样板代码或为类创建构造函数。最后,这意味着设置这些字段的唯一方法是通过 Spring 容器实例化类并使用反射注入它们,否则字段将保持 null。


依赖注入计算模式将类依赖项的创建与类本身分离开来,并将此责任转移到类注入容器,从而允许程序设计解耦,并遵循单一职责和依赖项倒置原则(同样可靠)。因此,通过自动装配(autowiring)字段来实现的类的解耦,最终会因为再次与类注入容器(在本例中是 Spring)耦合而丢失,从而使类在 Spring 容器之外变得无用。


这意味着,如果想在应用程序容器之外使用您的类,例如用于单元测试,将被迫使用 Spring 容器来实例化您的类,因为没有其他可能的方法(除了反射)来设置自动装配字段。


隐藏依赖关系

在使用依赖注入时,受影响的类应该使用公共接口清楚地公开这些依赖项,方法是在构造函数中公开所需的依赖项,或者使用方法(setter)公开可选的依赖项。当使用基于字段的依赖注入时,实质上是将这些依赖对外隐藏了。


@Autowired 和 @Resource 以及 @Inject 等注解注的区别


@Autowired


在 Spring 2.5 引入了 @Autowired 注解


@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Autowired {  boolean required() default true;//默认是true}
复制代码


从 Autowired 注解源码上看,可以使用在下面这些地方:


@Target(ElementType.CONSTRUCTOR) #构造函数@Target(ElementType.METHOD) #方法@Target(ElementType.PARAMETER) #方法参数@Target(ElementType.FIELD) #字段、枚举的常量@Target(ElementType.ANNOTATION_TYPE) #注解
复制代码


  • 简单总结


  1. @Autowired 是 Spring 自带的注解,通过 AutowiredAnnotationBeanPostProcessor 类实现的依赖注入

  2. @Autowired 可以作用在 CONSTRUCTOR、METHOD、PARAMETER、FIELD、ANNOTATION_TYPE

  3. @Autowired 默认是根据类型(byType )进行自动装配的

  4. 如果有多个类型一样的 Bean 候选者,需要指定按照名称(byName )进行装配,则需要配合 @Qualifier。


    指定名称后,如果 Spring IOC 容器中没有对应的组件 bean 抛出 NoSuchBeanDefinitionException。也可以将 @Autowired 中 required 配置为 false,如果配置为 false 之后,当没有找到相应 bean 的时候,系统不会抛异常


在 SpringBoot 中也可以使用 @Bean + @Autowired 进行组件注入,将 @Autowired 加到参数上,其实也可以省略。


@Beanpublic Person getPerson(@Autowired Car car){ return new Person();}// @Autowired 其实也可以省略
复制代码


@Resource


Resource 注解源码:


@Target({TYPE, FIELD, METHOD})@Retention(RUNTIME)public @interface Resource {    String name() default "";//指定注入指定名称的组件,name 的作用类似 @Qualifier    // 其他省略}
复制代码


从 Resource 注解源码上看,可以使用在下面这些地方:


@Target(ElementType.TYPE) #接口、类、枚举、注解@Target(ElementType.FIELD) #字段、枚举的常量@Target(ElementType.METHOD) #方法
复制代码


  • 简单总结


  1. @Resource 是 JSR250 规范的实现,在 javax.annotation 包下

  2. @Resource 可以作用 TYPE、FIELD、METHOD 上

  3. @Resource 是默认根据属性名称进行自动装配的,如果有多个类型一样的 Bean 候选者,则可以通过 name 进行指定进行注入


@Inject


  • Inject 注解源码

@Target({ METHOD, CONSTRUCTOR, FIELD })@Retention(RUNTIME)@Documentedpublic @interface Inject {}
复制代码


从 Inject 注解源码上看,可以使用在下面这些地方:


@Target(ElementType.CONSTRUCTOR) #构造函数@Target(ElementType.METHOD) #方法@Target(ElementType.FIELD) #字段、枚举的常量
复制代码


  • 简单总结

  1. @Inject 是 JSR330 (Dependency Injection for Java)中的规范,需要导入 javax.inject.Inject jar 包 ,才能实现注入

  2. @Inject 可以作用 CONSTRUCTOR、METHOD、FIELD 上

  3. @Inject 是根据类型进行自动装配的,如果需要按名称进行装配,则需要配合 @Named;@Named 的作用类似 @Qualifier!


构造器注入


  • 在 XML 配置方式中<constructor-arg>是通过构造函数参数注入,比如下面的 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"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">    <!-- services -->    <bean id="userService" class="com.seven.springframework.service.UserServiceImpl">        <constructor-arg name="userDao" ref="userDao"/>        <!-- additional collaborators and configuration for this bean go here -->    </bean>    <!-- more bean definitions for services go here --></beans>
复制代码


  • 在注解和 Java 配置方式下

在基于构造函数的依赖注入中,类构造函数被标注为 @Autowired,并包含了许多与要注入的对象相关的参数。


@Componentpublic class ConstructorBasedInjection {        private final InjectedBean injectedBean;        @Autowired //当然,这个@Autowired可以省略    public ConstructorBasedInjection(InjectedBean injectedBean) {                this.injectedBean = injectedBean;        }}
复制代码


将 @Autowired 写在被注入的成员变量上,setter 或者构造器上,就不用再 xml 文件中配置了。注意:不能提供无参构造方法,否则 Springboot 默认会加载无参的构造方法,Bean 实例对象会为 null。并且构造器的权限需要为 public


为什么建议使用构造器注入


一般推荐构造器注入,为什么?Spring 文档里的解释如下:


The Spring team generally advocates constructor injection as it enables one to implement application components as immutable objects and to ensure that required dependencies are not null. Furthermore constructor-injected components are always returned to client (calling) code in a fully initialized state.


翻译一下就是:这个构造器注入的方式能够保证注入的组件不可变,并且确保需要的依赖不为空。此外,构造器注入的依赖总是能够在返回客户端(组件)代码的时候保证完全初始化的状态。


  • 依赖不可变:其实说的就是 final 关键字。

  • 依赖不为空:(省去了我们对其检查):当要实例化 UserServiceImpl 的时候,由于自己实现了有参数的构造函数,所以不会调用默认构造函数,那么就需要 Spring 容器传入所需要的参数,所以就两种情况:1、有该类型的参数->传入,OK 。2:无该类型的参数->报错。

  • 完全初始化的状态:这个可以跟上面的依赖不为空结合起来,向构造器传参之前,要确保注入的内容不为空,那么肯定要调用依赖组件的构造方法完成实例化。而在 Java 类加载实例化的过程中,构造方法是最后一步(之前如果有父类先初始化父类,然后自己的成员变量,最后才是构造方法),所以返回来的都是初始化之后的状态。


如果使用 setter 注入,缺点显而易见,对于 IOC 容器以外的环境,除了使用反射来提供它需要的依赖之外,无法复用该实现类。而且将一直是个潜在的隐患,因为你不调用将一直无法发现 NPE 的存在


// 这里只是模拟一下,正常来说我们只会暴露接口给客户端,不会暴露实现。 UserServiceImpl userService = newUserServiceImpl(); userService.findUserList();// -> NullPointerException, 潜在的隐患
复制代码


总结:对于必需的依赖,建议使用基于构造函数的注入,设置它们为不可变的,并防止它们为 null。对于可选的依赖项,建议使用基于 setter 的注入。


借助 lombok 简化


然而,手动编写构造函数可能会使代码显得冗长不美观。Lombok 是一个非常好的工具,它能够通过注解自动生成构造函数,从而使代码更加简洁和优雅。


Lombok 提供的@AllArgsConstructor注解可以帮助我们自动生成包含所有字段的构造函数。此外,@RequiredArgsConstructor注解可以生成包含所有final字段和带有@NonNull注解字段的构造函数,这通常是我们在依赖注入中需要的。


@Component@RequiredArgsConstructor // 自动生成包含final字段的构造函数public class ConstructorBasedInjection {        private final InjectedBean injectedBean;    }
复制代码


Bean 注入的七种方式


  1. 使用 xml 方式来声明 Bean 的定义,Spring 容器在启动会加载并解析这个 xml,把 bean 装载到 IOC 容器中

  2. 使用 @CompontScan 注解来扫描声明了 @Controller、@Service、@Repository、@Component 注解的类

  3. 使用 @Configuration 注解声明配置类,并使用 @Bean 注解实现 Bean 的定义,这种方式其实是 xml 配置方式的一种演变,是 Spring 迈入到无 xml 时代的里程碑

  4. 使用 @Import 注解,导入配置类或者普通的 Bean

  5. 使用 FactoryBean 工厂 bean, 动态构建一个 Bean 实例,Spring Cloud OpenFeign 里面的动态代理实例就是使用 FactoryBean 来实现的

  6. 实现 ImportBeanDefinitionRegistrar 接口,可以动态注入 Bean 实例。这个在 Spring Boot 里面的启动注解有用到

  7. 实现 ImportSelector 接口,动态批量注入配置类或者 Bean 对象,这个在 Spring Boot 里面的自动装配机制里面有用到


Bean 的作用域


  1. singleton:单例,Spring 中的 bean 默认都是单例的。

  2. prototype:每次请求都会创建一个新的 bean 实例。

  3. request:每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。

  4. session:每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。

  5. global-session:全局 session 作用域。

  6. websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。


如何配置 bean 的作用域呢?


xml 方式:


<bean id="..." class="..." scope="singleton"></bean>
复制代码


注解方式:


@Bean@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)public Person personPrototype() {    return new Person();}
复制代码


Bean 是线程安全的吗


Spring 框架中的 Bean 是否线程安全,取决于其作用域和状态。


这里以最常用的两种作用域 prototype 和 singleton 为例介绍。几乎所有场景的 Bean 作用域都是使用默认的 singleton ,重点关注 singleton 作用域即可。


prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。


不过,大部分 Bean 实际都是无状态(没有定义可变的成员变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。


对于有状态单例 Bean 的线程安全问题,常见的有两种解决办法:


  1. 在 Bean 中尽量避免定义可变的成员变量。

  2. 在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。


IOC 的结构设计



Spring Bean 的创建是典型的工厂模式,这一系列的 Bean 工厂,也即 IOC 容器为开发者管理对象间的依赖关系提供了很多便利和基础服务,在 Spring 中有许多的 IOC 容器的实现供用户选择和使用,这是 IOC 容器的基础;在顶层的结构设计主要围绕着 BeanFactory 和 xxxRegistry 进行:



BeanFactory


  • BeanFactory: 工厂模式定义了 IOC 容器的基本功能规范


BeanFactory 作为最顶层的一个接口类,它定义了 IOC 容器的基本功能规范,BeanFactory 有三个子类:ListableBeanFactory、HierarchicalBeanFactory 和 AutowireCapableBeanFactory。先看下 BeanFactory 接口:


public interface BeanFactory {              //用于取消引用实例并将其与FactoryBean创建的bean区分开来。例如,如果命名的bean是FactoryBean,则获取将返回Factory,而不是Factory返回的实例。    String FACTORY_BEAN_PREFIX = "&";             //根据bean的名字和Class类型等来得到bean实例        Object getBean(String name) throws BeansException;        Object getBean(String name, Class requiredType) throws BeansException;        Object getBean(String name, Object... args) throws BeansException;    <T> T getBean(Class<T> requiredType) throws BeansException;    <T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
//返回指定bean的Provider <T> ObjectProvider<T> getBeanProvider(Class<T> requiredType); <T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);
//检查工厂中是否包含给定name的bean,或者外部注册的bean boolean containsBean(String name);
//检查所给定name的bean是否为单例/原型 boolean isSingleton(String name) throws NoSuchBeanDefinitionException; boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
//判断所给name的类型与type是否匹配 boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException; boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
//获取给定name的bean的类型 @Nullable Class<?> getType(String name) throws NoSuchBeanDefinitionException;
//返回给定name的bean的别名 String[] getAliases(String name); }
复制代码


BeanFactory 的其他接口主要是为了区分在 Spring 内部在操作过程中对象的传递和转化过程中,对对象的数据访问所做的限制。


  • ListableBeanFactory:该接口定义了访问容器中 Bean 基本信息的若干方法,如查看 Bean 的个数、获取某一类型 Bean 的配置名、查看容器中是否包括某一 Bean 等方法;

  • HierarchicalBeanFactory:父子级联 IoC 容器的接口,子容器可以通过接口方法访问父容器; 通过 HierarchicalBeanFactory 接口, Spring 的 IoC 容器可以建立父子层级关联的容器体系,子容器可以访问父容器中的 Bean,但父容器不能访问子容器的 Bean。Spring 使用父子容器实现了很多功能,比如在 Spring MVC 中,展现层 Bean 位于一个子容器中,而业务层和持久层的 Bean 位于父容器中。这样,展现层 Bean 就可以引用业务层和持久层的 Bean,而业务层和持久层的 Bean 则看不到展现层的 Bean。

  • ConfigurableBeanFactory:是一个重要的接口,增强了 IoC 容器的可定制性,它定义了设置类装载器、属性编辑器、容器初始化后置处理器等方法;

  • ConfigurableListableBeanFactory: ListableBeanFactory 和 ConfigurableBeanFactory 的融合;AutowireCapableBeanFactory:定义了将容器中的 Bean 按某种规则(如按名字匹配、按类型匹配等)进行自动装配的方法;


BeanRegistry


  • BeanRegistry: 向 IOC 容器手工注册 BeanDefinition 对象的方法


Spring 配置文件中每一个节点元素在 Spring 容器里都通过一个 BeanDefinition 对象表示,它描述了 Bean 的配置信息。而 BeanDefinitionRegistry 接口提供了向容器手工注册 BeanDefinition 对象的方法。


BeanDefinition


各种 Bean 对象及其相互的关系


  • BeanDefinition 定义了各种 Bean 对象及其相互的关系

  • BeanDefinitionReader 这是 BeanDefinition 的解析器

  • BeanDefinitionHolder 这是 BeanDefination 的包装类,用来存储 BeanDefinition,name 以及 aliases 等。


BeanDefinition


SpringIOC 容器管理了定义的各种 Bean 对象及其相互的关系,Bean 对象在 Spring 实现中是以 BeanDefinition 来描述的,其继承体系如下



BeanDefinitionReader


Bean 的解析过程非常复杂,功能被分的很细,因为这里需要被扩展的地方很多,必须保证有足够的灵活性,以应对可能的变化。Bean 的解析主要就是对 Spring 配置文件的解析。这个解析过程主要通过下图中的类完成:



BeanDefinitionHolder


BeanDefinitionHolder 是 BeanDefination 的包装类,用来存储 BeanDefinition,name 以及 aliases 等



ApplicationContext


IoC 容器的接口类是 ApplicationContext,很显然它必然继承 BeanFactory 对 Bean 规范(最基本的 ioc 容器的实现)进行定义。而 ApplicationContext 表示的是应用的上下文,除了对 Bean 的管理外,还至少应该包含了


  • 访问资源: 对不同方式的 Bean 配置(即资源)进行加载。(实现 ResourcePatternResolver 接口)

  • 国际化: 支持信息源,可以实现国际化。(实现 MessageSource 接口)

  • 应用事件: 支持应用事件。(实现 ApplicationEventPublisher 接口)


接口设计


ApplicationContext 整体结构:



  • HierarchicalBeanFactory 和 ListableBeanFactory: ApplicationContext 继承了 HierarchicalBeanFactory 和 ListableBeanFactory 接口,在此基础上,还通过多个其他的接口扩展了 BeanFactory 的功能

  • ApplicationEventPublisher:让容器拥有发布应用上下文事件的功能,包括容器启动事件、关闭事件等。实现了 ApplicationListener 事件监听接口的 Bean 可以接收到容器事件 , 并对事件进行响应处理 。 在 ApplicationContext 抽象实现类 AbstractApplicationContext 中,我们可以发现存在一个 ApplicationEventMulticaster,它负责保存所有监听器,以便在容器产生上下文事件时通知这些事件监听者。

  • MessageSource:为应用提供 i18n 国际化消息访问的功能;

  • ResourcePatternResolver : 所 有 ApplicationContext 实现类都实现了类似于 PathMatchingResourcePatternResolver 的功能,可以通过带前缀的 Ant 风格的资源文件路径装载 Spring 的配置文件。

  • LifeCycle:该接口是 Spring 2.0 加入的,该接口提供了 start()和 stop()两个方法,主要用于控制异步处理过程。在具体使用时,该接口同时被 ApplicationContext 实现及具体 Bean 实现, ApplicationContext 会将 start/stop 的信息传递给容器中所有实现了该接口的 Bean,以达到管理和控制 JMX、任务调度等目的


接口的实现


ApplicationContext 接口的实现,关键的点在于,不同 Bean 的配置方式(比如 xml,groovy,annotation 等)有着不同的资源加载方式,这便衍生除了众多 ApplicationContext 的实现类。



第一,从类结构设计上看, 围绕着是否需要 Refresh 容器衍生出两个抽象类:


GenericApplicationContext: 是初始化的时候就创建容器,往后的每次 refresh 都不会更改

AbstractRefreshableApplicationContext: AbstractRefreshableApplicationContext 及子类的每次 refresh 都是先清除已有(如果不存在就创建)的容器,然后再重新创建;AbstractRefreshableApplicationContext 及子类无法做到 GenericApplicationContext 混合搭配从不同源头获取 bean 的定义信息


第二, 从加载的源来看(比如 xml,groovy,annotation 等), 衍生出众多类型的 ApplicationContext, 典型比如:


FileSystemXmlApplicationContext: 从文件系统下的一个或多个 xml 配置文件中加载上下文定义,也就是说系统盘符中加载 xml 配置文件。


ClassPathXmlApplicationContext: 从类路径下的一个或多个 xml 配置文件中加载上下文定义,适用于 xml 配置的方式。


AnnotationConfigApplicationContext: 从一个或多个基于 java 的配置类中加载上下文定义,适用于 java 注解的方式。


ConfigurableApplicationContext: 扩展于 ApplicationContext,它新增加了两个主要的方法: refresh()和 close(),让 ApplicationContext 具有启动、刷新和关闭应用上下文的能力。在应用上下文关闭的情况下调用 refresh()即可启动应用上下文,在已经启动的状态下,调用 refresh()则清除缓存并重新装载配置信息,而调用 close()则可关闭应用上下文。这些接口方法为容器的控制管理带来了便利,但作为开发者,我们并不需要过多关心这些方法。


第三, 更进一步理解:


设计者在设计时 AnnotationConfigApplicationContext 为什么是继承 GenericApplicationContext? 因为基于注解的配置,是不太会被运行时修改的,这意味着不需要进行动态 Bean 配置和刷新容器,所以只需要 GenericApplicationContext。


而基于 XML 这种配置文件,这种文件是容易修改的,需要动态性刷新 Bean 的支持,所以 XML 相关的配置必然继承 AbstractRefreshableApplicationContext; 且存在多种 xml 的加载方式(位置不同的设计),所以必然会设计出 AbstractXmlApplicationContext, 其中包含对 XML 配置解析成 BeanDefination 的过程。


文章转载自:Seven

原文链接:https://www.cnblogs.com/seven97-top/p/18600989

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
Spring框架IoC核心详解_Java_EquatorCoco_InfoQ写作社区