• Spring原理-IOC和AOP


    1 概述

    在此记录spring的学习内容。spring官网:https://spring.io/
    在这里插入图片描述

    1.1 一个引申故事

    从前,在Java的大森林中,有一片神奇的土地,名叫"Spring"。这片土地上生长着各种美丽而强大的植物,它们分别象征着Spring框架中的各种功能和特性。

    在这片土地上,有一位智慧而善良的园丁,名叫"Rod Johnson"。Rod是这片土地上的守护者,他有着非凡的见识和智慧。他注意到这片土地原本虽然生机盎然,但由于管理混乱、依赖杂乱等问题而日渐失去活力。于是,他开始策划着一场变革之旅。

    Rod明白,为了让这片土地重焕生机,他需要一种全新的方式来管理这里那些繁杂的植物。于是,他提出了"控制反转"的理念,使得每一株植物可以有自己的生长空间,而不再依赖于别的植物。

    随后,他又强调"依赖注入"这一概念,让每株植物可以从土地中获取所需的养分,而不用亲自去寻找。这样设计使得这些植物的生长变得更为高效。

    Rod还发明了一种神奇的"面向切面编程"技术,一种能够让植物们自由组合、互相辅助的方式,令整片土地都焕发出一种特殊的生机。

    渐渐地,这片土地上充满了生机与活力。每一株植物都在互相配合下茁壮成长,形成了一片绚丽而蓬勃的景象。园丁Rod Johnson因其智慧和勇气,被人们誉为这片土地上的英雄。

    从那以后,人们便将这片土地上的新秩序称为"Spring",这个充满魔力的名字也因此广为传颂。而这位园丁Rod Johnson,则被尊称为Spring框架的缔造者和守护者。

    就这样,Spring框架成为了Java世界中最重要的框架之一,为开发者们带来了许多便利,也为Java企业级应用开发带来了一场新的春天。

    原文链接:https://blog.csdn.net/qq_55482652/article/details/137440957

    2 控制反转IOC

    2.1 控制反转

    控制反转(Inversion of Control,IoC)是一种思想,是Spring框架的核心概念之一,它的主要作用是通过将对象的创建和依赖关系的管理交给Spring容器来实现更松耦合的设计,而不是由程序员显式地进行操作。
    一句话理解IOC、IOC容器、依赖注入:IOC是一种思想。通过IOC容器管理对象。编码时,通过依赖注入的方式从IOC容器中获取依赖的对象。
    IOC容器->创建对象
    DI->给对象的属性赋值

    2.1.1 对比-传统开发

    传统的开发模式中,应用程序本身负责创建和管理对象,并且通过直接调用其他对象的方法来完成业务逻辑。这种方式下,各个对象之间紧耦合,修改其中一个对象的实现会影响到其他所有相关的组件,导致代码的可维护性、可扩展性和可重用性都很差。

    public class ServiceA {
        private ServiceB serviceB = new ServiceB();
    }
    
    

    这种方式中,ServiceA直接依赖于ServiceB的具体实现。

    2.1.2 对比-控制反转IOC

    Spring采用控制反转的方式解决了这个问题。它将对象的创建和管理职责从应用程序代码转移到了IOC容器。这样,对象不再自行创建依赖对象,而是从IOC容器中获取。在IoC容器中,对象的创建、依赖注入和生命周期的管理都由容器自动完成,使得组件之间的关系变得非常简单和清晰。具体来说,控制反转有两种主要方式:

    • 依赖注入(Dependency Injection, DI):通过构造器、setter方法或者字段注入的方式,将依赖对象注入到需要的对象中。
    • 依赖查找(Dependency Lookup):对象在运行时从容器中主动获取它所需要的依赖对象。

    通过控制反转,Spring可以轻松实现面向对象设计中的依赖注入,即将对象所需的依赖关系从对象内部转移到外部(有点类似组合),从而消除了对象之间的硬编码关系,提高了代码的复用性和灵活性。同时,使得项目的结构更加清晰,易于维护。

    2.1.3 代码比较

    假设我们有一个名为UserService的类,它需要依赖于UserDao类来进行数据存储。那么在传统的编程方式中,我们可能会这样写代码:

    public class UserService {
        private UserDao userDao;
    
        public UserService() {
            this.userDao = new UserDao();
        }
    
        public void addUser(User user) {
            // 进行业务逻辑处理
            userDao.save(user);
        }
    
        // 其他方法省略...
    }
    
    

    在上述代码中,我们通过创建UserDao对象来满足UserService对依赖的需求。然而,这种方式会导致UserService和UserDao紧密耦合在一起,不利于代码的维护和扩展,并且使得UserService难以进行单元测试。

    而如果使用依赖注入的话,我们可以将UserDao对象的创建和管理交给外部容器(如Spring容器)来完成,从而解耦和UserService与UserDao之间的关系,代码如下所示:

    public class UserService {
        private UserDao userDao;
    
        public UserService(UserDao userDao) {
            this.userDao = userDao;
        }
    
        public void addUser(User user) {
            // 进行业务逻辑处理
            userDao.save(user);
        }
    
        // 其他方法省略...
    }
    
    

    在上述代码中,我们通过构造函数注入的方式将UserDao对象注入到了UserService类中,而UserDao对象则是由外部容器负责创建和管理的。这样一来,UserService与UserDao之间的耦合就被消除了,同时也提高了代码的可测试性和可维护性。

    总的来说,依赖注入能够大大简化代码的编写和维护工作,使代码更加灵活、易于扩展和测试。

    问题:那这个UserDao是什么时候注入到UserService类中的呢?

    UserDao对象是在创建UserService实例时通过依赖注入的方式注入到UserService类中的。具体来说,在使用Spring框架时,可以通过在配置文件中配置UserDao的实现类和UserService的构造函数参数来实现自动注入;或者使用@Autowired注解标记UserService类中的UserDao字段,让Spring容器自动完成注入。这样一来,当需要使用UserService对象时,Spring容器会自动创建一个UserDao对象并将其注入到UserService对象中。这样就实现了UserServiceUserDao之间的解耦合。

    问题:那这个创建的UserDao是单例吗?

    在Spring容器中,默认情况下,所有的Bean都是单例的。也就是说,当使用依赖注入的方式创建UserDao对象时,默认情况下会创建一个单例对象,并且在整个应用程序的生命周期内都可以使用这个对象。如果需要创建多个实例,则需要在配置文件中进行相应的设置。可以使用@Scope注解来指定Bean的作用域,常见的作用域有单例(Singleton)、原型(Prototype)、请求(Request)、会话(Session)等。例如,可以使用@Scope("prototype")注解来将UserDao配置为原型作用域,这样每次从Spring容器中获取UserDao对象时,都会创建一个新的实例。

    默认情况下会创建一个单例对象,并且在整个应用程序的生命周期内都可以使用这个对象。如果容器中已经创建了UserDao对象,则后续获取UserDao对象时不会再次创建新的对象,而是直接返回之前已经创建好的对象。

    需要注意的是,在某些情况下,例如多线程环境下,使用单例模式可能会存在线程安全问题,此时建议将Bean的作用域配置为原型,这样可以每次都创建一个新的对象实例来避免线程安全问题。

    2.1.3 对比总结

    在传统的开发模式中,我们的程序主要由业务逻辑和控制逻辑组成。而在这种模式中,构建应用程序的过程通常是这样的:当需要一个对象时,我们会直接使用 new 运算符等方式来手动创建这个对象,并将其注入到其他对象中使用。这种过程中,创建和维护对象的职责完全由程序员负责。

    而在IOC容器的设计思想下,控制权被完全反转了。即对象的创建和维护都交由外部容器管理,而程序员只需关注于定义依赖关系以及编写具体的业务逻辑(程序员只要确定需要什么依赖,并且关注业务逻辑即可,不用关心依赖对象的创建和维护)。实现这种控制反转的核心技术就是依赖注入、依赖查找。在Spring框架中,DI 通常通过三种方式进行:构造函数注入、 Setter 方法注入和字段注入。

    总的来说,控制反转的目的是减少程序员对对象的手动创建和维护工作,实现代码重用、灵活性和可维护性。同时,依赖注入也使得程序的结构更加清晰,易于扩展和测试。因此,IOC容器和依赖注入已经成为了现代编程语言和框架中不可或缺的一部分。

    2.2 IOC容器

    IOC控制反转是一种思想,而在Spring框架中,负责创建、管理和注入依赖对象的容器就叫IOC容器(也可以叫做Bean容器)。

    具体来说,当使用IOC容器时,我们将需要依赖的对象交给容器管理,而不是自行手动创建和维护这些对象。IOC容器会自动扫描项目中所有Bean定义,根据配置文件或者注解等方式实例化Bean对象,并将其注册到容器中。当我们需要使用某个依赖项时,只需要从容器中获取即可,而无需关心对象如何创建和维护。这种方式下,我们只需要关注业务逻辑的实现,从而使代码更加简洁、灵活和易于维护。

    Spring框架中常用的IOC容器有两种:BeanFactory 和ApplicationContext。其中,BeanFactory 是Spring框架最基本的IOC容器,提供了最基本的IOC能力。ApplicationContext 是在BeanFactory 的基础上进行了扩展,提供了更多的特性和功能,例如国际化支持、AOP、事件机制等,因此是Spring框架中常用的IOC容器。

    问题:IOC容器是如何管理依赖对象的?

    2.3 依赖注入

    依赖注入(Dependency Injection,DI)是一种应用程序设计模式,它将类之间的依赖关系从代码内部提取出来,转而通过外部容器进行管理。通过使用依赖注入,对象不再创建和维护它所依赖的其他对象,而是由 IOC 容器负责创建这些对象,并将其注入到需要使用它们的地方。依赖注入的核心思想是松耦合(loose coupling),也就是减少模块之间的依赖关系,使得系统更加灵活、易于扩展和维护。同时,依赖注入也有助于提高代码的可测试性,因为我们可以使用模拟对象来替代真实的依赖对象进行单元测试。
    (说人话就是,依赖注入是给对象的属性赋值,也是从IOC容器中找东西)

    实现依赖注入DI有3种方法:

    1. 构造器注入
    2. Setter方法注入
    3. 字段注入

    2.3.1 构造器注入

    构造器注入是通过构造函数将依赖注入到目标对象中的。这种方式确保了依赖对象在目标对象创建时即被提供,适用于强制性依赖。
    示例:

    @Component
    public class ServiceA {
        private final ServiceB serviceB;
    
        @Autowired
        public ServiceA(ServiceB serviceB) {
            this.serviceB = serviceB;
        }
    }
    
    

    优点:
    确保依赖关系在对象创建时即被注入,不会出现未初始化的问题。
    对于必须的依赖来说,构造器注入更加明确和安全。

    缺点:
    当依赖项较多时,构造函数的参数列表会变得很长,不太优雅。

    2.3.2 Setter方法注入

    Setter注入是通过setter方法将依赖注入到目标对象中的。适用于可选的依赖,或在对象创建之后进行依赖注入。
    示例:

    @Component
    public class ServiceA {
        private ServiceB serviceB;
    
        @Autowired
        public void setServiceB(ServiceB serviceB) {
            this.serviceB = serviceB;
        }
    }
    
    

    优点:
    灵活性高,可以在对象创建之后再进行依赖注入。
    适用于可选依赖和配置复杂对象。

    缺点:
    可能导致对象处于不完全初始化状态,增加了调试和维护的复杂性。

    2.3.3 字段注入

    字段注入是通过直接在字段上使用注解将依赖注入到目标对象中的。这种方式简单直观,但不推荐使用,因为违反了依赖倒置原则。

    示例:

    @Component
    public class ServiceA {
        @Autowired
        private ServiceB serviceB;
    }
    
    

    优点:
    实现简单,代码简洁。

    缺点:
    使单元测试变得困难,因为依赖关系在对象创建之后无法更改。
    违反了依赖倒置原则(Dependence Inversion Principle),使得类的设计不够优雅。

    2.3.4 依赖注入方式总结

    选择依赖注入方式的考虑

    • 构造器注入适用于强制性依赖,确保对象在创建时即被完全初始化。
    • Setter注入适用于可选依赖和需要在对象创建之后进行配置的情况。
    • 字段注入由于违反设计原则,通常不推荐使用,除非是在非常简单的场景中。

    总结来说,选择合适的依赖注入方式应根据具体的需求和设计原则来决定。构造器注入和Setter注入是更推荐的方式,而字段注入应尽量避免。

    2.4 IOC实现原理

    spring提供两种方式创建Bean,一种是xml,一种是注解。

    2.4.1 基于XML的原理

    读取解析xml配置文件->反射实例化对象->存储在Map集合中

    1 读xml

    读取配置文件(spring.xml)->xml解析->获取bean的id和类的全路径名

    
    <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">
    
        <bean id="empDao" class="com.zou.learnspring.dao.impl.EmpDaoImpl2"/>
    
    beans>
    

    补充
    bean标签的属性: 点击跳转

    2 实例化对象

    通过反射实例化对象->全路径名获取字节码对象->通过字节码实例化->放到容器中(当作一个Map集合)
    Class clazz = Class.forName(“com.zou.learnspring.dao.impl.EmpDaoImpl2”)
    Object obj = clazz.newInstance();
    map.put(“empDao”, obj);

    3 getBean获取对象

    工厂模式返回Bean对象
    BeanFactory接口定义的getBean方法,该方法底层就是map.get(“name”);

    接口的继承ApplicationContext -> … -> BeanFactory
    ApplicationContext的实现类:ClassPathXmlApplicationContext。其中间也是间接实现了ApplicationContext接口。
    在idea中,在ApplicationContext接口中,通过Ctrl+H可以查看接口的实现类
    在这里插入图片描述

    2.5 xml配置实现DI

    点击跳转:spring通过xml配置文件实现依赖注入的方法

    2.5.1 xml读取配置文件

    在xml中读取指定配置文件的信息
    添加命名空间
    xmlns:context=“http://www.springframework.org/schema/context”
    自定义了一个test1.properties文件,里面就两个数据对
    在这里插入图片描述
    接下来通过xml配置文件的方式读取这两个值
    xml配置文件:

    
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
        <context:property-placeholder location="classpath:test1.properties"/>
        <bean id="user1" class="com.zou.learnspring.bean.User">
            <property name="username" value="${test_key1}"/>
            <property name="password" value="${test_key2}"/>
        bean>
    beans>
    

    测试方法:

        @Test
        public void testGetUser1(){
            ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
            User user = applicationContext.getBean("user1", User.class);
            System.out.println(user);
        }
    

    测试结果:
    在这里插入图片描述

    2.6 Bean的生命周期

    在 Spring 框架中,bean 的生命周期涉及从创建、初始化、使用到销毁的整个过程。了解 bean 的生命周期有助于我们更好地控制和管理 Spring 应用中的 bean。

    2.6.1 Bean生命周期

    可以概括为5个主要阶段

    1. 实例化阶段
    2. 属性注入阶段
    3. 初始化阶段
    4. 使用阶段
    5. 销毁阶段

    2.6.2 生命周期的详细分类和描述

    1. 实例化阶段
    • 步骤1:Bean实例化
      • Spring 创建 bean 实例,通常通过构造函数或静态工厂方法。
      • 作用: 创建 bean 的初始对象。
    2. 属性注入阶段
    • 步骤2:属性注入(依赖注入)
      • Spring 注入 bean 的依赖属性,通过构造函数参数或 setter 方法。
      • 作用: 配置 bean 的依赖,使其具备完整的功能。
    3. 初始化阶段
    • 步骤3:setBeanName()

      • 如果实现了 BeanNameAware 接口,调用 setBeanName 方法,传递 bean 的名称。
      • 作用: 让 bean 知道它在 Spring 配置中的名称。
    • 步骤4:setBeanFactory()

      • 如果实现了 BeanFactoryAware 接口,调用 setBeanFactory 方法,传递 BeanFactory 实例。
      • 作用: 使 bean 可以与 Spring 容器进行交互。
    • 步骤5:setApplicationContext()

      • 如果实现了 ApplicationContextAware 接口,调用 setApplicationContext 方法,传递 ApplicationContext 实例。
      • 作用: 使 bean 可以与 Spring 上下文进行交互。
    • 步骤6:postProcessBeforeInitialization()

      • 如果配置了 BeanPostProcessor,在初始化方法之前调用 postProcessBeforeInitialization 方法。
      • 作用: 在 bean 初始化前进行一些处理或修改。
    • 步骤7:afterPropertiesSet()

      • 如果实现了 InitializingBean 接口,调用 afterPropertiesSet 方法。
      • 作用: 在所有属性设置完毕后,进行一些初始化工作。
    • 步骤8:init-method

      • 如果在配置文件中指定了 init-method,调用该方法。
      • 作用: 执行自定义的初始化逻辑。
    • 步骤9:postProcessAfterInitialization()

      • 如果配置了 BeanPostProcessor,在初始化方法之后调用 postProcessAfterInitialization 方法。
      • 作用: 在 bean 初始化后进行一些处理或修改。
    4. 使用阶段
    • 步骤10:Bean 准备就绪,可以使用
      • bean 已经完全初始化,可以在应用程序中使用。
      • 作用: bean 具备所有配置和初始化,准备提供服务。
    5. 销毁阶段
    • 步骤11:容器关闭

      • Spring 容器关闭时,开始销毁 bean。
      • 作用: 准备释放资源和执行清理工作。
    • 步骤12:destroy()

      • 如果实现了 DisposableBean 接口,调用 destroy 方法。
      • 作用: 在 bean 被销毁前执行清理逻辑。
    • 步骤13:destroy-method

      • 如果在配置文件中指定了 destroy-method,调用该方法。
      • 作用: 执行自定义的销毁逻辑。
    • 步骤14:Bean 被销毁

      • bean 被销毁,相关资源被释放。
      • 作用: 确保 bean 资源被正确清理和释放。

    2.6.3 简化后的生命周期流程图

    1. 实例化阶段
        - Bean实例化
    
    2. 属性注入阶段
        - 属性注入(依赖注入)
    
    3. 初始化阶段
        - setBeanName() - 如果实现 BeanNameAware
        - setBeanFactory() - 如果实现 BeanFactoryAware
        - setApplicationContext() - 如果实现 ApplicationContextAware
        - postProcessBeforeInitialization() - BeanPostProcessor
        - afterPropertiesSet() - 如果实现 InitializingBean
        - init-method - 配置的初始化方法
        - postProcessAfterInitialization() - BeanPostProcessor
    
    4. 使用阶段
        - Bean 准备就绪,可以使用
    
    5. 销毁阶段
        - 容器关闭
        - destroy() - 如果实现 DisposableBean
        - destroy-method - 配置的销毁方法
        - Bean 被销毁
    

    2.6.4 示例代码(包括所有生命周期接口和方法)

    Java 代码:

    package com.zou.learnspring.bean;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import org.springframework.beans.factory.*;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    
    @Data
    @AllArgsConstructor
    public class ExampleBean implements BeanNameAware, BeanFactoryAware, ApplicationContextAware,
            InitializingBean, DisposableBean {
    
        private String property;
    
        public ExampleBean() {
            System.out.println("[1.1] 实例化阶段: 构造器调用,创建 bean 实例");
        }
    
        public void setProperty(String property) {
            this.property = property;
            System.out.println("[2.1] 属性注入阶段: setProperty method called with value: " + property);
        }
    
        @Override
        public void setBeanName(String name) {
            System.out.println("[3.1] 初始化阶段: BeanNameAware: setBeanName method called with name: " + name);
        }
    
        @Override
        public void setBeanFactory(BeanFactory beanFactory) {
            System.out.println("[3.2] 初始化阶段: BeanFactoryAware: setBeanFactory method called");
        }
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) {
            System.out.println("[3.3] 初始化阶段: ApplicationContextAware: setApplicationContext method called");
        }
    
        @Override
        public void afterPropertiesSet() {
            System.out.println("[3.4] 初始化阶段: InitializingBean: afterPropertiesSet method called");
        }
    
        public void customInit() {
            System.out.println("[3.5] 初始化阶段: Custom init-method called");
        }
    
        @Override
        public void destroy() {
            System.out.println("[5.1] 销毁阶段: DisposableBean: destroy method called");
        }
    
        public void customDestroy() {
            System.out.println("[5.2] 销毁阶段: Custom destroy-method called");
        }
    }
    
    

    XML 配置文件:

    <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">
        
        <bean id="exampleBean" class="com.example.ExampleBean"
              init-method="customInit" destroy-method="customDestroy">
            <property name="property" value="someValue"/>
        bean>
    beans>
    

    测试代码:
    写了个测试类

    import com.zou.learnspring.bean.ExampleBean;
    import com.zou.learnspring.bean.User;
    import com.zou.learnspring.dao.EmpDao;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import org.springframework.context.support.FileSystemXmlApplicationContext;
    
    public class ProjectTest {
    
        @Test
        public void testGetExampleBean(){
            ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
            ExampleBean exampleBean = applicationContext.getBean("exampleBean", ExampleBean.class);
            System.out.println("[4.1] Bean 准备就绪,可以使用bean:" + exampleBean);
            applicationContext.close();
        }
        
    }
    
    

    执行结果:
    在这里插入图片描述

    Bean创建的源码

    面向切面编程

    作用:事务控制、日志记录、性能检测、权限控制

  • 相关阅读:
    JDK中「SPI」原理分析
    C复习-语句
    Python MQTT客户端 paho-mqtt
    基于ssm技术的校自助阅览室的设计与实现毕业设计源码242326
    前端关于对象中套用对象传参的小问题
    Linux安装Redis
    【数据结构】B树(B-树)和B+树
    SAP BW 常用知识点
    diffusion model
    Python 使用openpyxl处理Excel文件
  • 原文地址:https://blog.csdn.net/qq_37398465/article/details/137465288