开发者

Spring IOC功能详解

目录
  • 概述
    • 为什么叫控制反转
    • Spring 中的 bean 的作用域
      • 基础概念
      • 对比
    • 有状态和无状态的bean
      • 基本概念
    • 对比
      • Ioc 配置的三种方式
        • XML 配置
          • Java 配置
          • 注解配置
        • 依赖注入的三种方式
          • setter方式
          • 构造方法注入
        • 基于注解(字段)的注入
          • IOC设计体系
            • 流程图
            • IOC初始化流程
            • Bean实例化(生命周期,循环依赖等)
            • Spring中Bean的生命周期

        概述

        为什么叫控制反转

        Spring Bean是什么:Spring里面的bean就类似是定义的一个组件,而这个组件的作用就是实现某个功能的,这里所定义的bean就相当于给了你一个更为简便的方法来调用这个组件去实现你要完成的功能

        Spring框架管理这些Bean的创建工作,即由用户管理Bean转变为框架管理Bean,这个就叫控制反转 - Inversion of Control (IoC)

        • 这些托管创建的Bean放在IoC Container
        • 为了更好让用户配置Bean,引入不同方式来配置Bean:xml配置,Java配置,注解配置等支持
        • Spring也管理整个Bean的生命周期
        • 应用程序代码从Ioc Container中获取依赖的Bean,注入到应用程序中,这个过程叫 依赖注入(Dependency Injection,DI)

        控制反转是通过依赖注入实现的,IoC是设计思想,DI是实现方式

        IOC不是什么技术,是一种设计思想

        在Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制

        • 正转:传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象
        • 反转:由容器来帮忙创建及注入依赖对象
        • 由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找

        Spring 中的 bean 的作用域

        基础概念

        sinjavascriptgleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。

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

        Spring并不管理作用域为prototype的bean的整个生命周期,使用者必须自己销毁 这个bean并释放资源。

        下述作用域只有在web环境下才有用

        • request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。
        • session : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效
        • global-session: 全局 session 作用域,仅仅在基于 Portlet 的 web 应用中才有意义,Spring5 已经没有了
        • Portlet 是能够生成语义代码(例如:html)片段的小型 Java Web 插件。它们基于 portlet 容器,可以像 servlet 一样处理 HTTP 请求。但是,与 servlet 不同,每个 portlet 都有不同的会话

        对比

        作用域说明典型应用场景
        singleton默认作用域,容器内唯一实例无状态Service、工具类
        prototype每次注入创建新实例有状态Bean、含可变状态的组件
        request每个HTTP请求创建新实例Web请求上下文相关对象
        session每个用户会话创建新实例用户购物车、登录信息
        application整个Web应用生命周期单例全局配置

        有状态和无状态的bean

        基本概念

        在 Spring 框架中,有状态 Bean 和 无状态 Bean 是两种重要的对象设计模式,它们的核心区别在于 是否保存可变状态,直接影响线程安全性和作用域管理

        有状态 Bean 注意事项

        • 作用域控制:必须显式声明作用域(如 @Scope(“prototype”))。
        • 避免单例陷阱:禁止在单例 Bean 中注入有状态 Bean(除非使用 ThreadLocal)。
        • 防御性编程:返回数据时返回副本而非引用

        无状态 Bean 最佳实践

        • 方法参数化:所有数据通过方法参数传递。
        • 依赖注入:通过构造函数注入其他 Bean。
        • 工具类设计:将状态抽取到参数中

        建议

        • 优先无状态设计:90%的业务场景应使用无状态Bean
        • 有状态Bean最小化:必须使用时有明确的作用域控制
        • 避免混合状态:不要在同一Bean中混用有状态和无状态代码

        对比

        特征有状态 Bean无状态 Bean
        实例变量/状态存储包含可变的成员变量(会随操作改变)无成员变量,或只有不可变常量
        线程安全需自行保证线程安全(如加锁、隔离)天然线程安全(无共享状态)
        作用域通常用 prototype 作用域(每次注入新实例)通常用 singleton 作用域(全局共享)
        复用性不同请求需不同实例(需频繁创建实例)、资源消耗较高全局共享单个实例、高性能(单例复用)
        典型场景用户会话、购物车、事务上下文工具类、Service层业务逻辑

        Ioc 配置的三种方式

        总体上目前的主流方式是: 注解 + Java 配置

        xml 配置

        将bean的信息配置.xml文件里,通过Spring加载文件为我们创建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.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类的方式,声明不明显,如果大量配置,可读性比较差
        @Configuration
        public class BeansConfig {
            /**
             * @return user dao
             */
            @Bean("userDao")
            public UserDaoImpl userDao() {
                return new UserDaoImpl();
            }
            /**
             * @return user service
             */
            @Bean("userService")
            public UserServiceImpl userService() {
                UserServiceImpl userService = new UserServiceImpl();
                userService.setUserDao(userDao());
                return userService;
            }
        }

        注解配置

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

        • 优点:开发便捷,通俗易懂,方便维护。
        • 缺点:具有局限性,对于一些第三方资源,无法添加注解。只能采用XML或JavaConfig的方式配置

        依赖注入的三种方式

        常用的注入方式主要有三种:构造方法注入(Construct注入),setter注入,基于注解的注入(接口注入)

        setter方式

        通过对应变量的setXXX()方法以及在方法上面使用注解,来完成依赖注入。

        Setter方法注入是通过在类中提供setter方法来注入依赖项的。Spring容器会调用这些setter方法来设置依赖项的值。这种方式更加灵活,因为可以在对象创建后的任何时候注入依赖项。

        比如:在 Spring 4.3 及以后的版本中,setter 上面的 @Autowired 注解是可以不写的。

        private Helper helper;
        @Autowired
        public void setHelper(Helper helper) {
            this.helper = helper;
        }

        优点:

        • 可以在对象创建后的任何时候注入依赖项。
        • 可以重新配置已存在的对象。

        缺点:

        • 可能导致对象状态的不确定性,因为依赖项可以在任何时候被更改。
        • 不支持不可变对象。

        构造方法注入

        将各个必需的依赖全部放在带有注解构造方法的参数中,并在构造方法中完成对应变量的初始化,这种方式,就js是基于构造方法的注入。

        比如:在 Spring 4.3 及以后的版本中,如果这个类只有一个构造方法,那么这个构造方法上面也可以不写 @Autowired 注解。

        private final Svc svc;
        @Autowired
        public HelpService(@Qualifier("svcB") Svc svc) {
            this.svc = svc;
        }

        优点:

        • 确保依赖项在对象创建时就被初始化。
        • 可以设置依赖项为final,确保它们不会被修改。
        • 支持不可变对象。

        缺点:

        • 如果类有很多依赖项,构造函数可能会变得很长且难以阅读

        基于注解(字段)的注入

        示例

        public class MyService {
            @Autowired
            private MyRepository repository;
        }

        优点:

        • 代码简单明了,易于实现。

        缺点:根据Spring官方的建议,推荐使用构造器注入和Setter注入,而尽量避免使用字段注入。字段注入被认为是一种不太理想的方式,因为它具有以下缺点:

        • 不利于不变性:与构造器注入不同,字段注入无法将不变的依赖关系传递给组件,因为字段可以在任何时候更改。
        • 可见性问题:外部无法明确看到组件所依赖的内容,因为依赖关系被隐藏在组件内部。这可能导致代码可读性下降。
        • 紧密绑定:字段注入会导致组件与IoC容器紧密绑定,使组件在没有容器的情况下难以使用,也使单元测试更加复杂。
        • 不明显的依赖关系:当依赖关系变得复杂时,使用字段注入可能导致依赖关系不够明显,难以理解组件的需求。

        推荐@Resource而不是@Autowired注解

        • @Resource具有更广泛的兼容性
          • @Autowired是Spring特定的注解,与Spring框架紧密相关。如果以后决定更换不同的IoC容器,那么@Autowired可能不再适用,因为其他容器可能不支持它。
          • @Resource是Java标准(jsR-250)提供的注解,几乎所有的IoC容器都会支持它。因此,即使你将来更换了容器,使用@Resource的代码仍然可以正常工作,不需要类似的警告。
        • 提高了代码的可移植性:认为@Resource更符合Java的标准化。

        IOC设计体系

        流程图

        Spring IOC功能详解

        IOC初始化流程

        • 初始化的入口在容器实现中的 refresh()调用来完成
        • 对 bean 定义载入 IOC 容器使用的方法是 loadBeanDefinition,其中的大致过程如下:
          • 通过 ResourceLoader 来完成资源文件位置的定位,bean 定义文件时通过抽象成 Resource 来被 IOC 容器处理的
          • 通过 BeanDefinitionReader来完成定义信息的解析和 Bean 信息的js注册
          • 容器解析得到 BeanDefinition 以后,需要把它在 IOC 容器中注册,这由 IOC 实现 BeanDefinitionRegistry 接口来实现。
          • 注册过程就是在 IOC 容器内部维护的一个HashMap 来保存得到的 BeanDefinition 的过程
          • 这个 HashMap 是 IoC 容器持有 bean 信息的场所,以后对 bean 的操作都是围绕这个HashMap 来实现的.
          • 流程图如下:

        Spring IOC功能详解

        • 然后我们就可以通过 BeanFactory 和 ApplicationContext 来享受到 Spring IOC 的服务了

        Bean实例化(生命周期,循环依赖等)

        最终的将Bean的定义即BeanDefinition放到beanDefinitionMap中,本质上是一个ConcurrentHashMap<String, Object>;

        并且BeanDefinition接口中包含了这个类的Class信息以及是否是单例等;

        解决循环依赖问题

        Spring如何解决循环依赖问题

        Spring只是解决了单例模式下属性依赖的循环问题:Spring为了解决单例的循环依赖问题,使用了三级缓存。

        三级缓存详细:见这篇

        Spring为什么不能解决多例的循环依赖:多实例Bean是每次调用一次getBean都会执行一次构造方法并且给属性赋值,根本没有三级缓存,因此不能解决循环依赖

        • 这类循环依赖问题可以通过把bean改成单例的解决

        Spring为什么不能解决prototype作用域循环依赖:spring不会缓存prototype作用域的bean,而spring中循环依赖的解决正是通过缓存来实现的

        • 这类循环依赖问题可以通过使用@Lazy注解解决。 生成代理对象产生的循环依赖

        生成代理对象产生的循环依赖,解决方法很多,主要有:

        • 使用@Lazy注解,延迟加载
        • 使用@DependsOn注解,指定加载先后关系
        • 修改文件名称,改变循环依赖类的加载顺序

        使用@DependsOn产生的循环依赖:这类循环依赖问题要找到@DependsOn注解循环依赖的地方,迫使它www.devze.com不循环依赖就可以解决问题。

        Spring中Bean的生命周期

        Spring 只帮我们管理单例模式 Bean 的完整生命周期,对于 prototype 的 bean ,Spring 在创建好交给使用者之后则不会再管理后续的生命周期

        • prototype 是一种 bean 的作用域。它的主要特点是每次请求都会创建一个新的 bean 实例
        • 与 singleton 作用域不同,singleton只会创建一个共享的实例

        Bean的完整生命周期经历了各种方法调用,这些方法可以划分为以下几类:(结合上图,需要有如下顶层思维)

        • Bean自身的方法: 这个包括了Bean本身调用的方法和通过配置文件中的iniwww.devze.comt-method和destroy-method指定的方法
        • Bean级生命周期接口方法: 这个包括了BeanNameAware、BeanFactoryAware、ApplicationContextAware;当然也包括InitializingBean和DiposableBean这些接口的方法(可以被@PostConstruct和@PreDestroy注解替代)
        • 容器级生命周期接口方法: 这个包括了InstantiationAwareBeanPostProcessor 和 BeanPostProcessor 这两个接口实现,一般称它们的实现类为“后处理器”。
        • 工厂后处理器接口方法: 这个包括了ASPectJWeavingEnabler, ConfigurationClassPostProcessor, CustomAutowireConfigurer等等非常有用的工厂后处理器接口的方法。工厂后处理器也是容器级的。在应用上下文装配配置文件之后立即调用

        Spring IOC功能详解

        到此这篇关于Spring IOC详解的文章就介绍到这了,更多相关Spring IOC详解内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

        0

        上一篇:

        下一篇:

        精彩评论

        暂无评论...
        验证码 换一张
        取 消

        最新开发

        开发排行榜