Skip to content

Spring

1.1 框架概念

1.1.1 生活中框架

  • 框架( Framework )是一个集成了基本结构、规范、设计模式、编程语言和程序库等基础组件的软件系统,它可以用来构建更高级别的应用程序。框架的设计和实现旨在解决特定领域中的常见问题,帮助开发人员更高效、更稳定地实现软件开发目标。

  • 小结框架思维:为解决特定问题而提供的一整套解决方案

image_W0xKmnXYD8

1.1.2 程序中框架

目前市面主流框架有很多,本阶段主要学习:SSM

  • Spring:为所有bean(组件)提供管理解决方案
  • SpringMVC:为解决表述层(控制层)常见问题,而提供的一整套解决方案,如:处理请求数据,响应数据,RESTFul等问题
  • Mybatis:为解决数据访问层(Dao层),而提供一整套解决方案,如:简化传统JDBC代码,优化入参出参等问题

框架的优点包括以下几点:

  1. 提高开发效率:框架提供了许多预先设计好了的组件和工具,能够帮助开发人员快速进行开发。相较于传统手写代码,在框架提供的规范化环境中,开发者可以更快地实现项目的各种要求。
  2. 降低开发成本:框架的提供标准化的编程语言、数据操作等代码片段,避免了重复开发的问题,降低了开发成本,提供深度优化的系统,降低了维护成本,增强了系统的可靠性。
  3. 提高应用程序的稳定性:框架通常经过了很长时间的开发和测试,其中的许多组件、代码片段和设计模式都得到了验证。重复利用这些组件有助于减少bug的出现,从而提高了应用程序的稳定性。
  4. 提供标准化的解决方案:框架通常是针对某个特定领域的,通过提供标准化的解决方案,可以为开发人员提供一种共同的语言和思想基础,有助于更好地沟通和协作。

1.2 组件与容器概念

组件:具有一定功能的对象,常见组件如下:

  • Controller层组件
  • Service层组件
  • Dao层组件

容器:管理组件的对象,包括组件的创建,获取及销毁等

  • 生活中容器
    • 简单容器:水杯,电脑包等
    • 复杂容器:汽车,房屋等
  • 程序中容器
    • 简单容器:数组,集合等
    • 复杂容器:Spring框架

image-20241203091908623

第2章 Spring Framework简介

2.1 Spring广义与狭义

spring官网:https://spring.io/

广义的 Spring:Spring 技术栈(全家桶)

  • 广义上的 Spring 泛指以 Spring Framework 为基础的 Spring 技术栈。

  • 经过十多年的发展,Spring 已经不再是一个单纯的应用框架,而是逐渐发展成为一个由多个不同子项目(模块)组成的成熟技术,例如 Spring Framework、Spring MVC、SpringBoot、Spring Cloud、Spring Data、Spring Security 等,其中 Spring Framework 是其他子项目的基础。

  • 这些子项目涵盖了从企业级应用开发到云计算等各方面的内容,能够帮助开发人员解决软件发展过程中不断产生的各种实际问题,给开发人员带来了更好的开发体验。

狭义的 Spring:Spring Framework(基础框架)

  • 狭义的 Spring 特指 Spring Framework,通常我们将它称为 Spring 框架。

  • Spring Framework(Spring框架)是一个开源的应用程序框架,由SpringSource公司开发,最初是为了解决企业级开发中各种常见问题而创建的。它提供了很多功能,例如:依赖注入(Dependency Injection)、面向切面编程(AOP)、声明式事务管理(TX)等。其主要目标是使企业级应用程序的开发变得更加简单和快速,并且Spring框架被广泛应用于Java企业开发领域。

  • Spring全家桶的其他框架都是以SpringFramework框架为基础!

2.2 Spring Framework概述

Spring是基于IOC和AOP的容器框架

  • IoC容器 | 核心容器

    Spring IoC 容器,负责实例化、配置和组装 bean(组件)。容器通过读取配置元数据来获取有关要实例化、配置和组装组件的指令。

  • IoC(Inversion of Control)控制反转

    IoC 主要是针对对象的创建和调用控制而言的,也就是说,当应用程序需要使用一个对象时,不再是应用程序直接创建该对象,而是由 IoC 容器来创建和管理,即控制权由应用程序转移到 IoC 容器中,也就是“反转”了控制权。这种方式基本上是通过依赖查找的方式来实现的,即 IoC 容器维护着构成应用程序的对象,并负责创建这些对象。

  • DI (Dependency Injection) 依赖注入

    DI 是指在组件之间传递依赖关系的过程中,将依赖关系在容器内部进行处理,这样就不必在应用程序代码中硬编码对象之间的依赖关系,实现了对象之间的解耦合。在 Spring 中,DI 是通过 XML 配置文件或注解的方式实现的。它提供了三种形式的依赖注入:构造函数注入、Setter 方法注入和接口注入。

  • AOP:Aspect Oriented Programming面向切面编程思想,它可以在不修改源代码的情况下,给程序动态统一添加额外功能,AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。(后面章节详细讲解)

  • 主要功能模块如下

    功能模块功能介绍
    Core Container核心容器,控制反转和依赖注入
    AOP&Aspects面向切面编程
    TX声明式事务管理
    Testing快速整合测试环境
    Data Access/Integration提供了对数据访问/集成的功能。
    Spring MVC提供了面向Web应用程序的集成功能。

2.3 Spring Framework特点

  1. 丰富的生态系统:Spring 生态系统非常丰富,支持许多模块和库,如 Spring Boot、Spring Security、Spring Cloud 等等,可以帮助开发人员快速构建高可靠性的企业应用程序。
  2. 模块化的设计:框架组件之间的松散耦合和模块化设计使得 Spring Framework 具有良好的可重用性、可扩展性和可维护性。开发人员可以轻松地选择自己需要的模块,根据自己的需求进行开发。
  3. 简化 Java 开发:Spring Framework 简化了 Java 开发,提供了各种工具和 API,可以降低开发复杂度和学习成本。同时,Spring Framework 支持各种应用场景,包括 Web 应用程序、RESTful API、消息传递、批处理等等。
  4. 不断创新和发展:Spring Framework 开发团队一直在不断创新和发展,保持与最新技术的接轨,为开发人员提供更加先进和优秀的工具和框架。

因此,这些优点使得 Spring Framework 成为了一个稳定、可靠、且创新的框架,为企业级 Java 开发提供了一站式的解决方案。

Spring 使创建 Java 企业应用程序变得容易。它提供了在企业环境中采用 Java 语言所需的一切,支持 Groovy 和 Kotlin 作为 JVM 上的替代语言,并且可以根据应用程序的需求灵活地创建多种架构。从Spring Framework 6.0开始,Spring 需要 Java 17+。

2.4 Spring Framework底层实现

Spring框架底层使用BeanFactory接口实现的,具体相关接口及实现类如下:

BeanFactory 接口提供了一种高级配置机制,能够管理任何类型的对象,它是SpringIoC容器标准化超接口!

ApplicationContextBeanFactory 的子接口。它补充说:

  • 更容易与 Spring 的 AOP 功能集成
  • 消息资源处理(用于国际化)
  • 特定于应用程序给予此接口实现,例如Web 应用程序的 WebApplicationContext 简而言之, BeanFactory 提供了配置框架和基本功能,而 ApplicationContext 添加了更多特定于企业的功能。 ApplicationContextBeanFactory 的完整超集!

第3章 SpringIOC/DI基本实现

实现SpringIOC核心思想

  • 将组件装配到SpringIOC容器对象中
  • 创建SpringIOC容器对象
  • 通过SpringIOC容器对象获取组件

实现Spring框架搭建方式

  • 方式一:纯XML方式
  • 方式二:XML+注解方式
  • 方式三:配置类+注解方式

实现Spring框架环境准备

3.1 基于XML方式装配组件

3.1.1 环境准备

image-20241203101659675

image-20241203103204591

image-20241203102036646

image-20241203103346537

xml
<dependencies>
    <!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>6.0.6</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
     <dependency>
         <groupId>org.junit.jupiter</groupId>
         <artifactId>junit-jupiter-api</artifactId>
         <version>5.10.2</version>
         <scope>test</scope>
    </dependency>


    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
    </dependency>

</dependencies>

3.1.2 实现关键步骤

  • 准备POJO类:Student

    java
    package com.at.bean;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    /**
     * @Author at
     * @CreateTime: 2024/11/15
     */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Student {
    
        private Integer stuId;
        private String stuName;
        private Integer stuAge;
    
    }
  • 创建配置文件:spring.xml

    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">
        <!--    将Student装配到IOC容器-->
        <bean id="zs" class="com.at.bean.Student">
            <property name="stuId" value="1001"></property>
            <property name="stuName" value="zs"></property>
            <property name="stuAge" value="18"></property>
        </bean>
    </beans>
  • 测试类

    java
    @Test
    public void testStudent(){
        //创建容器对象
        ApplicationContext ioc = new ClassPathXmlApplicationContext("spring.xml");
        //从容器获取Student
        Student bean = ioc.getBean(Student.class);
        System.out.println("bean = " + bean);
    }

3.2 基于配置类装配组件

3.2.1 环境准备(同上)

3.2.2 实现关键步骤

  • 准备POJO类:Student

    • 代码同上
  • 创建配置类:SpringConfig

    java
    package com.at;
    
    import com.at.bean.Student;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * @Author at
     * @CreateTime: 2024/11/15
     */
    @Configuration              //标识当前类是一个[配置类:代替xml配置文件]
    public class SpringConfig {
    
        /**
          将student对象装配到IOC容器中,id名称=方法名称
        */
        @Bean
        public Student student(){
            return new Student(1001, "zhangsan", 18);
        }
    
    }
  • 测试类

    java
    @Test
    public void testSpring(){
        //创建容器对象(基于配置类)
        ApplicationContext ioc = new AnnotationConfigApplicationContext(SpringConfig.class);
        Student bean = ioc.getBean(Student.class);
            System.out.println("bean = " + bean);
    }

3.3 @Bean注解详解

在Spring框架中,@Bean注解是用于方法级别的注解,它告诉Spring容器这是一个bean的定义,并且该方法会返回一个对象,这个对象应该被注册为Spring应用上下文中的一个bean。@Bean注解通常与@Configuration类一起使用,以声明性的方式配置Spring IoC容器。

3.3.1 @Bean源码

java
package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
    
    @AliasFor("name")
    String[] value() default {};

    @AliasFor("value")
    String[] name() default {};

    boolean autowireCandidate() default true;

    String initMethod() default "";

    String destroyMethod() default "(inferred)";
}

@Bean注解中属性说明

  • name:指定bean的名称。如果不提供,默认会采用方法名作为bean的名称。
  • value和name属性是别名关系,即它们代表相同的配置项。
  • initMethod:指定初始化方法,在bean注入完成之后执行。
  • destroyMethod:指定销毁方法,在包含bean的应用上下文关闭时调用。
  • autowireCandidate: 定义了是否将此bean视为自动装配的候选者。默认值为true,意味着除非明确设置为false,否则该bean是可以作为其他bean依赖注入的候选对象。

3.3.2 使用@Bean装配DruidDataSource(第三方Bean)

  • 导入DruidDataSource依赖

    xml
    <!--DuirdDataSource坐标 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.15</version>
    </dependency>
    <!--DruidDataSource启动器坐标(与上面坐标二选一即可)-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.8</version>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
  • 编写配置文件:application.properties

    properties
    #配置DruidDataSource
    jdbc.driverClassName=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/0923_demo
    jdbc.username=root
    jdbc.password=root
  • 编写配置类:

    • 未用属性类
    java
    package com.at.config;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import com.at.properties.JdbcProperties;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    
    /**
     * @Author at
     * @CreateTime: 2024/11/26
     */
    @Configuration
    public class DruidConfig {
    
        @Value("${jdbc.driverClassName}")
        private String driverClassName;
        @Value("${jdbc.url}")
        private String url;
        @Value("${jdbc.username}")
        private String username;
        @Value("${jdbc.password}")
        private String password;
        
        @Bean
        public DataSource dataSource(){
            DruidDataSource ds = new DruidDataSource();
            ds.setDriverClassName(driverClassName);
            ds.setUrl(url);
            ds.setUsername(username);
            ds.setPassword(password);
            return ds;
        }
    
    }
    • 使用属性类:
    java
    package com.at.properties;
    
    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    /**
     * @Author at
     * @CreateTime: 2024/11/26
     */
    @Component("jdbcProperties")
    @ConfigurationProperties(prefix = "jdbc")
    @Data
    public class JdbcProperties {
    
        private String driverClassName;
        private String url;
        private String username;
        private String password;
    
    }
    java
    package com.at.config;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import com.at.properties.JdbcProperties;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    
    /**
     * @Author at
     * @CreateTime: 2024/11/26
     */
    @Configuration
    public class DruidConfig {
    
        @Autowired
        @Qualifier("jdbcProperties")
        private JdbcProperties jdbcProperties;
    
        @Bean
        public DataSource dataSource(/*JdbcProperties jdbcProperties*/){
            DruidDataSource ds = new DruidDataSource();
            ds.setDriverClassName(jdbcProperties.getDriverClassName());
            ds.setUrl(jdbcProperties.getUrl());
            ds.setUsername(jdbcProperties.getUsername());
            ds.setPassword(jdbcProperties.getPassword());
            return ds;
        }
    
    }
    java
    package com.at.day13_springboot.property;
    
    import lombok.Data;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    /**
     * @Author at
     * @CreateTime: 2025/01/14
     */
    //@Data
    @Component("jdbcProperties")
    //定义属性类,可以指定属性前缀(prefix)
    @ConfigurationProperties(prefix = "jdbc")
    public class JdbcProperties {
    
    //    @Value("${jdbc.driverClassName}")
        private String driverClassName;
        private String url;
        private String username;
        private String password;
    
        @Override
        public String toString() {
            return "JdbcProperties{" +
                    "driverClassName='" + driverClassName + '\'' +
                    ", url='" + url + '\'' +
                    ", username='" + username + '\'' +
                    ", password='" + password + '\'' +
                    '}';
        }
    
        public String getDriverClassName() {
            return driverClassName;
        }
    
        public void setDriverClassName(String driverClassName) {
            this.driverClassName = driverClassName;
        }
    
        public String getUrl() {
            return url;
        }
    
        public void setUrl(String url) {
            this.url = url;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    }
    java
    package com.at.day13_springboot.config;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import com.at.day13_springboot.property.JdbcProperties;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    
    /**
     * @Author at
     * @CreateTime: 2025/01/14
     */
    @Configuration
    public class DruidConfig {
    
    //    @Value("${jdbc.driverClassName}")
    //    private String driverClassName;
    //    @Value("${jdbc.url}")
    //    private String url;
    //    @Value("${jdbc.username}")
    //    private String username;
    //    @Value("${jdbc.password}")
    //    private String password;
    
        @Autowired
        @Qualifier("jdbcProperties")
        private JdbcProperties jdbcProperties;
    
    
        @Bean("druidDataSource")
        public DruidDataSource getDruidDataSource() {
    
            DruidDataSource druidDataSource = new DruidDataSource();
    
            druidDataSource.setDriverClassName(jdbcProperties.getDriverClassName());
            druidDataSource.setUrl(jdbcProperties.getUrl());
            druidDataSource.setUsername(jdbcProperties.getUsername());
            druidDataSource.setPassword(jdbcProperties.getPassword());
    
            return druidDataSource;
        }
    
    }
    java
    package com.at.day13_springboot;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import com.alibaba.druid.pool.DruidPooledConnection;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ConfigurableApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    
    import java.sql.SQLException;
    
    @SpringBootApplication
    public class Day13SpringbootApplication {
    
        public static void main(String[] args) throws SQLException {
    
            //springboot启动器,默认返回容器对象
            ConfigurableApplicationContext ioc = SpringApplication.run(Day13SpringbootApplication.class, args);
    
            //获取容器对象
    //        ApplicationContext context = new AnnotationConfigApplicationContext();
    
            DruidDataSource dataSource = ioc.getBean(DruidDataSource.class);
            DruidPooledConnection connection = dataSource.getConnection();
            System.out.println("connection = " + connection);
    
        }
    
    }

3.3.3 Bean生命周期

  • 生命周期的六个阶段

    1. 阶段一:加载Bean定义

      • Spring容器读取XML文件或其他配置文件,解析配置信息。
      • 将解析后的配置信息转换为Spring内部数据结构(BeanDefinition对象)。
      • 存储BeanDefinition对象,待进行组件实例化。
    2. 阶段二:实例化Bean组件

      • 根据BeanDefinition中的信息,实例化Bean对象。
      • 如果有依赖其他Bean的情况,先实例化被依赖的Bean。
      • 此步骤单纯实例化Bean和依赖的Bean组件,不会进行属性赋值。
    3. 阶段三:设置Bean属性: - Spring容器将根据BeanDefinition中的配置,通过setter方法或字段直接注入属性值。 - Spring容器属性和实例化过程是分离的,所有在配置的时候,组件声明和引用不分先后顺序。

    4. 阶段四:调用Bean的初始化方法

      • 如果Bean实现了InitializingBean接口,Spring将调用其afterPropertiesSet()方法。
      • 如果在XML配置中定义了init-method,则执行该方法。
      • 如果Bean使用了@PostConstruct注解,则执行被注解的方法。
      • 此阶段调用自定义初始化方法,可以进行相关的初始化工作,类似: Servletinit方法。
    5. 阶段五:Bean可以使用

      • 此时Bean已经初始化完成,可以被其他Bean引用或者容器直接使用。
    6. 阶段六:调用Bean的销毁方法阶段(仅适用于单例Bean):

      • 如果Bean实现了DisposableBean接口,Spring将调用其destroy()方法。
      • 如果在XML配置中定义了destroy-method,则执行该方法。
      • 如果Bean使用了@PreDestroy注解,则在销毁之前执行被注解的方法。
      • 此阶段调用自定义销毁方法,可以进行相关的初始化工作,类似: Servletdestroy方法。
  • 案例代码

    • POJO类:Student

      java
      package com.at.bean;
      
      import lombok.AllArgsConstructor;
      import lombok.Data;
      import lombok.NoArgsConstructor;
      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.stereotype.Component;
      
      /**
       * @Author at
       * @CreateTime: 2024/11/15
       */
      public class Student {
      
          private Integer stuId;
          private String stuName;
          private Integer stuAge;
      
          public Student() {
              System.out.println("Student==>构造器!!!");
          }
      
          public Student(Integer stuId, String stuName, Integer stuAge) {
              this.stuId = stuId;
              this.stuName = stuName;
              this.stuAge = stuAge;
          }
      
          public Integer getStuId() {
              return stuId;
          }
      
          public void setStuId(Integer stuId) {
              System.out.println("==>Student->setId()!!!");
              this.stuId = stuId;
          }
      
          public String getStuName() {
              return stuName;
          }
      
          public void setStuName(String stuName) {
              this.stuName = stuName;
          }
      
          public Integer getStuAge() {
              return stuAge;
          }
      
          public void setStuAge(Integer stuAge) {
              this.stuAge = stuAge;
          }
      
          public void initStudent() {
              System.out.println("==>Student->init-method()!!!");
          }
      
          public void destroyStudent() {
              System.out.println("==>Student->destroy-method()!!!");
          }
      
      }
    • 配置类:SpringConfig

      java
      package com.at;
      
      import com.at.bean.Student;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.ComponentScan;
      import org.springframework.context.annotation.Configuration;
      
      /**
       * @Author at
       * @CreateTime: 2024/11/15
       */
      @Configuration              //标识当前类是一个[配置类:代替xml配置文件]
      public class SpringConfig {
      
          @Bean(value = "student", initMethod = "initStudent", destroyMethod = "destroyStudent")
          public Student student(){
              Student student = new Student();
              student.setStuId(101);
              return student;
          }
      
      
      }
    • 测试类

      java
      /**
       * @Author at
       * @CreateTime: 2024/11/15
       */
      public class TestSpring {
      
          @Test
          public void testSpring(){
              //创建容器对象(基于配置类)
              ConfigurableApplicationContext ioc = new AnnotationConfigApplicationContext(SpringConfig.class);
          //从容器中获取Student对象
              Student bean = ioc.getBean(Student.class);
              System.out.println("bean = " + bean);
          //关闭IOC容器对象
              ioc.close();
              
          }

3.3.4 Bean作用域

  • Bean作用域概念

    在Spring框架中,bean的作用域(@Scope)决定了该bean的生命周期和可见性。Spring提供了多种作用域来满足不同的需求。

  • 常用作用域

    取值含义创建对象的时机默认值
    singleton在 IOC 容器中,这个 bean 的对象始终为单实例IOC 容器初始化时
    prototype这个 bean 在 IOC 容器中有多个实例获取 bean 时
    • 如果是在WebApplicationContext环境下还会有另外两个作用域(但不常用):
    取值含义创建对象的时机默认值
    request请求范围内有效的实例每次请求
    session会话范围内有效的实例每次会话
  • 案例代码

    • POJO类:Student

      java
      package com.at;
      
      import com.at.bean.Student;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.ComponentScan;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.context.annotation.Scope;
      
      /**
       * @Author at
       * @CreateTime: 2024/11/15
       */
      @Configuration              //标识当前类是一个[配置类:代替xml配置文件]
      public class SpringConfig {
      
          @Bean(value = "student", initMethod = "initStudent", destroyMethod = "destroyStudent")
      //    @Scope(value = "prototype")
          @Scope(value = "singleton")
          public Student student(){
              Student student = new Student();
              student.setStuId(101);
              return student;
          }
      
      
      }
    • 测试类

      java
      @Test
      public void testSpring(){
      
          //创建容器对象(基于配置类)
          ApplicationContext ioc = new AnnotationConfigApplicationContext(SpringConfig.class);
      
          Student bean = ioc.getBean(Student.class);
          Student bean2 = ioc.getBean(Student.class);
          System.out.println("是否为同一对象:" + (bean==bean2));
          
      }

第4章 SpringIOC/DI 深入学习

4.1 开启组件扫描实现分层研发

java
/**
 * @Author at
 * @CreateTime: 2024/11/15
 */
@Configuration              					//标识当前类是一个[配置类:代替xml配置文件]
@ComponentScan(basePackages = "com.at")	//开启组件扫描:扫描com.atguigu包及其子包中的有效注解
public class SpringConfig {}

4.1.1 分层管理组件

分层管理组件,本质上是:将不同层组件装配到IOC容器,使用@Bean注解可以实现不同层组件的装配,但相对比较复杂,在实际研发中,一般使用如下注解,将分层中的组件装配到IOC容器.

注解说明
@Component该注解用于描述 Spring 中的 Bean,它是一个泛化的概念,仅仅表示容器中的一个组件(Bean),并且可以作用在应用的任何层次,例如 Service 层、Dao 层等。 使用时只需将该注解标注在相应类上即可。
@Repository该注解用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
@Service该注解通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
@Controller该注解通常作用在控制层(如SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
  • 通过查看源码我们得知,@Controller、@Service、@Repository这三个注解只是在@Component注解的基础上起了三个新的名字。
  • 对于Spring使用IOC容器管理这些组件来说没有区别,也就是语法层面没有区别。所以@Controller、@Service、@Repository这三个注解只是给开发人员看的,让我们能够便于分辨组件的作用。

注意:虽然它们本质上一样,但是为了代码的可读性、程序结构严谨!我们肯定不能随便胡乱标记。

  • 案例代码

    java
    @Component(value = "zs")          //将Student组件装配到IOC容器
    public class Student {}
    
    @Controller(value = "studentController")
    public class StudentController {}
    
    @Repository("studentDao")
    public class StudentDaoImpl implements StudentDao {}
    
    @Service("stuService")
    public class StudentServiceImpl implements StudentService {}

4.1.2 分层管理对象中属性

分层管理对象中属性,本质上是实现属性的自动注入,常用注解如下:

@Value

  • 作用:为字面量实现自动装配
  • 字面量:基本类型及其包装类+String

@Autowired

  • 作用:为非字面量类型实现自动装配(自动将IOC容器中组件,装配到指定的位置)

  • 自动装配规则

    • 先按类型去IOC容器匹配对象
      • 匹配1个对象,自动装配成功
      • 匹配多个对象,再按id筛选对象
  • 源码

    • 位置:@Autowired注解可以书写在方法上(setxxx())或书写在成员变量上
    • 属性:required
      • true:表示当前被@Autowired注解标识的组件,必须自动装配,否则报错
      • false:表示当前被@Autowired注解标识的组件,不必须自动装配,否则报错

    image-20241115142034454

@Qualifier

  • 作用:配合@Autowired一起使用,指定需要被依赖注入的beanId
  • 在Spring框架中,@Qualifier注解用于解决当有多个相同类型的bean时,自动装配(autowiring)哪一个bean的问题。当你的Spring应用上下文中存在多个相同类型的bean,并且你希望明确指定使用哪一个bean进行依赖注入时,@Qualifier就显得非常有用
  • 注意:@Qualifier一般不能单独使用,需要配合@Autowired一起使用

@Resource

  • @Resource 注解是Java EE(现在称为Jakarta EE)的一部分,主要导入相关依赖,它也可以在Spring框架中使用。

  • @Resource装配规则

    • @Resource 默认情况下会按照名称进行匹配。它首先尝试根据字段名或setter方法参数名查找对应的bean
      • 如果找到了一个匹配的bean,则注入该bean;如果找不到,则抛出异常。
    • 如果没有找到与名称相匹配的bean,@Resource 会退而求其次,尝试根据类型进行匹配。
      • 如果有且仅有一个匹配类型的bean,则注入该bean;如果有多个匹配类型的bean但名称不匹配,则抛出异常。
  • 注意:@Resource注解属于JDK,如JDK高于JDK11或低于JDK8,需要导入如下依赖

    xml
    <dependency>
        <groupId>jakarta.annotation</groupId>
        <artifactId>jakarta.annotation-api</artifactId>
        <version>2.1.1</version>
    </dependency>

4.2 @Import注解

4.2.1 @Import注解简介

@Import是Spring框架提供的一个功能强大的工具,用于将一个或多个配置类导入到当前的配置中。它允许你在一个地方集中管理所有的配置类,从而简化了配置管理和维护

4.2.2 @Import注解作用

  1. 导入其他配置类:通过@Import注解,你可以将一个或多个配置类导入到当前的配置中,使得这些配置类中的bean定义和配置能够被当前应用上下文识别。

    • 导入单个配置类

      java
      @Configuration
      @Import(DatabaseConfig.class)
      public class AppConfig {
          // ...
      }
    • 导入多个配置类

      java
      @Configuration
      @Import({ DatabaseConfig.class, WebConfig.class })
      public class AppConfig {
          // ...
      }
  2. 引入非@Configuration类作为bean:可以使用@Import来注册普通类(即不是用@Configuration标注的类)为bean。

    如果你有一个普通的Java类,并希望它在Spring应用上下文中作为一个bean存在,可以直接使用@Import将其导入:

    java
    @Import(MyService.class)
    public class AppConfig {
        // MyService 将自动注册为一个bean
    }
  3. 引入实现ImportSelectorImportBeanDefinitionRegistrar接口的类:这允许你在导入时动态决定哪些bean应该被注册到Spring容器中。

    使用ImportSelector动态选择要导入的配置类

    ImportSelector是一个接口,它允许你根据一定的逻辑动态决定要导入哪些配置类。你需要实现selectImports方法,在该方法中返回一个字符串数组,表示要导入的配置类的全限定名。

    java
    public class MyImportSelector implements ImportSelector {
    
        @Override
        public String[] selectImports(AnnotationMetadata importingClassMetadata) {
            // 根据条件返回需要导入的配置类名称
            return new String[] { "com.example.DatabaseConfig" };
        }
    }
    @Configuration
    @Import(MyImportSelector.class)
    public class AppConfig {
        // 根据MyImportSelector的选择结果导入配置类
    }

    使用ImportBeanDefinitionRegistrar注册自定义bean定义

    ImportBeanDefinitionRegistrar接口允许你在运行时向Spring容器注册额外的bean定义。这通常用于更复杂的场景,比如根据环境变量或其他条件动态创建bean。

    java
    public class MyBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    
        @Override
        public void registerBeanDefinitions(
                AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            // 动态注册bean定义
            RootBeanDefinition beanDefinition = new RootBeanDefinition(MyCustomBean.class);
            registry.registerBeanDefinition("myCustomBean", beanDefinition);
        }
    }
    
    @Configuration
    @Import(MyBeanDefinitionRegistrar.class)
    public class AppConfig {
        // 动态注册bean
    }

4.2.3 @Import注解注意事项

  • @Import不会触发组件扫描(@ComponentScan),所以如果你想要让某些类成为Spring管理的bean,要么它们必须被显式地导入,要么它们所在的包需要被组件扫描。
  • 如果你正在使用Spring Boot并且你的主应用程序类位于根包下,那么通常不需要显式使用@Import,因为Spring Boot会自动处理很多常见的配置类。
  • @Import@Configuration一起使用效果最佳,因为它确保了所有相关的bean都在同一个上下文中被正确地初始化和管理。

4.3 @Conditional注解详解

4.3.1 @Conditional简介

@Conditional 注解是Spring框架中用于条件化配置的核心注解之一。它允许根据特定的条件来决定是否应该创建一个bean或应用某个配置类。这是实现灵活和动态配置的关键机制,特别适用于微服务架构、多环境部署和支持多种运行时行为的应用程序。

4.3.2 @Conditional作用

  1. 条件化装配Bean:只有当某些条件满足时,才会创建该Bean。

    @Conditional 需要与实现了 Condition 接口的类一起使用。Condition 接口只有一个方法 matches(),该方法返回一个布尔值,指示条件是否匹配。如果返回 true,则创建Bean或应用配置;如果返回 false,则跳过。

    java
    public class MyCondition implements Condition {
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            // 实现条件逻辑
            return true;
            //return false;
        }
    }
    java
    @Configuration
    public class AppConfig {
    
        @Bean
        @Conditional(MyCondition.class)
        public MyService myService() {
            return new MyServiceImpl();
        }
    }
  2. 支持复杂的条件逻辑:通过自定义条件类,可以实现非常复杂的决策逻辑。

    为了简化常见的条件判断,Spring提供了一些@Conditional的衍生条件注解:

    • @ConditionalOnProperty:基于配置文件中的属性值。
    • @ConditionalOnClass@ConditionalOnMissingClass:基于类路径中是否存在某个类。
    • @ConditionalOnBean@ConditionalOnMissingBean:基于容器中是否存在某个类型的bean。
    • @ConditionalOnExpression:基于SpEL表达式的条件。
    • @ConditionalOnWebApplication@ConditionalOnNotWebApplication:基于应用程序是否为Web应用。
    • @ConditionalOnJava:基于Java版本。
    • @ConditionalOnResource:基于资源是否存在。

    例如,@ConditionalOnProperty 的使用:

    java
    @Configuration
    public class DatabaseConfig {
    
        @Bean
        @ConditionalOnProperty(name = "app.use-druid", havingValue = "true")
        public DataSource dataSource() {
            return new DruidDataSource();
        }
    }

    这段代码表示只有当配置文件中有 app.use-druid=true 这个属性时,dataSource Bean 才会被创建并加载到IOC容器。

4.4 其他注解(了解)

4.5 FactoryBean与BeanFactory区别

4.5.1 BeanFactory

BeanFactory 是Spring IoC容器的核心接口之一,提供了管理和检索bean的基本功能。它是Spring容器的基础接口,负责管理bean的生命周期、依赖注入等操作。与 ApplicationContext 不同的是,BeanFactory 提供了更底层的操作,并且具有更高的性能(因为它不预加载所有的单例bean),但是缺少一些高级特性(如事件发布、国际化支持等)。

BeanFactory 主要用于那些对性能敏感的应用程序,或者是那些只需要基本IoC功能而不需要额外特性的应用程序。对于大多数Web应用和其他需要丰富特性的应用场景,推荐使用 ApplicationContext,它是 BeanFactory 的子接口,提供了更多的企业级特性。

BeanFactory接口主要方法

  • Object getBean(String name):根据名称从容器中获取一个bean实例。
  • <T> T getBean(Class<T> requiredType):根据类型从容器中获取一个bean实例。
  • Object getBean(String name, Object... args):根据名称和构造参数获取一个bean实例。
  • void preInstantiateSingletons():提前实例化所有非懒加载的单例bean。

4.5.2 FactoryBean

FactoryBean 是一个接口,它允许你自定义bean的创建逻辑。通过实现这个接口,你可以控制如何创建某个特定类型的bean。FactoryBean 通常用于需要复杂初始化逻辑或需要返回代理对象(如AOP代理)的场景。

FactoryBean接口主要方法如下:

  • Object getObject():返回由 FactoryBean 创建的对象实例。
  • Class<?> getObjectType():返回 getObject() 方法返回对象的类型。如果类型是动态确定的,可以返回 null
  • boolean isSingleton():指示 FactoryBean 创建的对象是否为单例(singleton)。默认情况下,Spring容器会假定所有bean都是单例,除非另有说明。

使用场景

  • 当你想要自定义bean的创建过程时,比如你需要在创建bean之前执行某些初始化代码、根据条件选择不同的实现类,或者需要返回一个代理对象而不是直接返回目标类的实例,这时就可以使用 FactoryBean
  • 案例代码

    java
    public class MyFactoryBean implements FactoryBean<MyInterface> {
    
        @Override
        public MyInterface getObject() throws Exception {
            // 自定义创建逻辑
            return new MyInterfaceImpl();
        }
    
        @Override
        public Class<?> getObjectType() {
            return MyInterface.class;
        }
    
        @Override
        public boolean isSingleton() {
            return true; // 或者 false,取决于你的需求
        }
    }

4.5.3 FactoryBean与BeanFactory区别

特性FactoryBeanBeanFactory
角色定义如何创建一个特定类型的bean管理bean的生命周期和依赖关系
主要用途控制bean创建逻辑,例如返回代理对象提供bean的获取、管理和生命周期管理
接口实现实现者提供具体的bean创建逻辑Spring容器的基础接口,用于管理bean生命周期
使用频率较少使用,主要用于特殊需求底层API,广泛应用于Spring内部
性能可能涉及额外的创建逻辑,影响不大更高效,因为它是轻量级的
高级特性不提供额外的企业级特性不提供额外的企业级特性

第5章 SpringAOP详解

5.1 AOP前奏

5.1.1 设定场景复现问题

  • 案例描述

    1. 定义Calc接口及计算相应方法
       - add():加法
       - sub():减法
       - mul():乘法
       - div():除法
    2. 为Calc定义实现类CalcImpl,分别实现加减乘除相应方法
       - 添加日志功能
         - 在计算之前,显示计算**方法名,及参数
         - 在计算之后,显示计算**方法名,及结算结果
       - 添加验证功能等等
  • 案例实现

    • Calc接口

      java
      package com.at.aopbefore;
      
      /**
       * @Author at
       * @CreateTime: 2024/08/16
       */
      public interface Calc1 {
      
          /**
          * @Author: at
          * @Date: 2024/8/16 9:09
           * 加法
          */
          public int add(int i,int j);
      
          /**
           * @Author: at
           * @Date: 2024/8/16 9:09
           * 减法
           */
          public int sub(int i,int j);
      
          /**
           * @Author: at
           * @Date: 2024/8/16 9:09
           * 乘法
           */
          public int mul(int i,int j);
      
          /**
           * @Author: at
           * @Date: 2024/8/16 9:09
           * 除法
           */
          public int div(int i,int j);
      
      }
    • CalcImpl实现类

      java
      package com.at.aopbefore;
      
      /**
       * @Author at
       * @CreateTime: 2024/08/16
       * **添加日志功能**
       * **添加验证功能等等**
       *
       */
      public class Calc1Impl implements Calc1{
      
          @Override
          public int add(int i, int j) {
              //验证.... 非核心业务代码
              //日志... 非核心业务代码
      //        System.out.println("==>add()正在执行,参数:i:"+i+",j:"+j);
      //        MyLogging.methodBefore("add",i,j);
      
              int rs = i + j;     //核心代码
      
      //        MyLogging.methodAfter("add",rs);
      //        System.out.println("==>add()执行完毕,结果为rs:"+rs);
              return rs;
          }
      
          @Override
          public int sub(int i, int j) {
               //验证.... 非核心业务代码
              //日志... 非核心业务代码
      //        System.out.println("==>sub()正在执行,参数:i:"+i+",j:"+j);
              int rs = i - j;
      //        System.out.println("==>sub()执行完毕,结果为rs:"+rs);
              return rs;
          }
      
          @Override
          public int mul(int i, int j) {
      
              int rs = i * j;
      
              return rs;
          }
      
          @Override
          public int div(int i, int j) {
              int rs = i / j;
              return rs;
          }
      }

5.1.2 发现问题及解决方案

  • 发现问题

    • 核心业务代码(计算操作)中,直接书写非核心业务代码(日志,验证等)
    • 导致代码分散代码混乱问题
    • 代码分散:每个计算方法都书写相同代码
    • 代码混乱:核心代码与非核心代码耦合(书写在一处)
      • 高内聚,低耦合
  • 解决方案

    • 解决代码分散:提取MyLogging工具类,提高代码可重用性

      java
      package com.at.aopbefore;
      
      import java.util.Arrays;
      
      /**
       * @Author at
       * @CreateTime: 2024/11/16
       */
      public class MyLogging {        //切面类
      
      
          /**
          * @Author: at
          * @Date: 2024/11/16 10:45
           * 日志(方法前)通知
          */
          public static void methodBefore(String methodName,Object... args){
               System.out.println("==>"+methodName+"()正在执行,参数:"+ Arrays.toString(args));
          }
      
      
          /**
          * @Author: at
          * @Date: 2024/11/16 10:45
           * 日志(方法后)通知
          */
          public static void methodAfter(String methodName,Object rs){
               System.out.println("==>"+methodName+"()执行完毕!,结果rs:"+rs);
          }
      
      }
    • 解决代理混乱:将非核心业务代码(日志)先横向提取工具类中,再动态织入到核心代码中(CalcImpl)

      • 需要使用代理模式解决该问题

5.2 代理模式

代理模式(Proxy Pattern)是软件设计模式中的一种结构型模式,它为其他对象提供一个代理以控制对这个对象的访问。代理模式的核心思想是在客户端和目标对象之间增加一层间接性,这层间接性由代理对象来实现。

  • 生活中代理
    • 海外代理商
    • 经纪人
    • 房屋中介等
  • 程序中代理
    • 静态代理:代理对象是固定
    • 动态代理:代理对象不固定

5.2.1 静态代理

静态代理是指代理类和委托类(即真实主题)的关系是在编译时就确定下来的。也就是说,你需要为每个委托类手写一个对应的代理类。静态代理的代理类实现了与委托类相同的接口,并且持有一个委托类的实例,在调用方法时通过这个实例来间接调用委托类的方法。

java
package com.at.aopbefore;

/**
 * @Author at
 * @CreateTime: 2024/08/16
 * Calc1Impl的静态代理
 */
public class Calc1ImplStaticProxy implements Calc1 {

    //目标对象
    private Calc1Impl calc1Impl;

    //有参构造器:使用静态代理,必须指定目标对象
    public Calc1ImplStaticProxy(Calc1Impl calc1Impl){
        this.calc1Impl = calc1Impl;
    }

    @Override
    public int add(int i, int j) {
        MyLogging.methodBefore("add",i,j);
        int rs = calc1Impl.add(1, 2);
        MyLogging.methodAfter("add",rs);
        return rs;
    }

    @Override
    public int sub(int i, int j) {
        MyLogging.methodBefore("sub",i,j);
        int rs = calc1Impl.sub(1, 2);
        MyLogging.methodAfter("sub",rs);
        return rs;
    }

    @Override
    public int mul(int i, int j) {
        MyLogging.methodBefore("mul",i,j);
        int rs = calc1Impl.mul(1, 2);
        MyLogging.methodAfter("mul",rs);
        return rs;
    }

    @Override
    public int div(int i, int j) {
        MyLogging.methodBefore("div",i,j);
        int rs = calc1Impl.div(1, 2);
        MyLogging.methodAfter("div",rs);
        return rs;
    }


}
java
 /**
* @Author: at
* @Date: 2024/11/16 11:31
 * 测试静态代理模式
*/
@Test
void testStaticProxy() {
    Calc1ImplStaticProxy sp = new Calc1ImplStaticProxy(new Calc1Impl());
    sp.add(1,2);
}

5.2.2 动态代理

动态代理是指代理类和委托类之间的关系是在运行时由Java反射机制动态生成的。与静态代理不同的是,你不需要为每个委托类都编写一个代理类。相反,你可以定义一个通用的代理处理器(InvocationHandler),它可以在运行时处理对任意接口或类的代理。

java
package com.at.aopbefore;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @Author at
 * @CreateTime: 2024/08/16
 * 动态代理类
 *      1. 目标对象
 *      2. 有参构造器
 *      3. 获取动态代理对象
 */
public class Calc1ImplDynamicProxy {

    private Object target;  //目标对象(不固定)

    //使用动态代理时,必须指定目标对象
    public Calc1ImplDynamicProxy(Object target){
        this.target = target;
    }

    /**
    * @Author: at
    * @Date: 2024/8/16 10:20
     * 获取动态代理对象
     *      1. 获取代理对象(Proxy.newProxyInstance())
     *      2. 动态执行目标对象相应方法(知道目标对象实现接口)
    */
    public Object getProxy(){
        //定义代理对象
        Object proxyObj = null;
        //获取代理对象
        //1. 目标对象类加载器
        ClassLoader classLoader = target.getClass().getClassLoader();
        //2. 目标对象实现接口(代理类实现接口集合)
        Class<?>[] interfaces = target.getClass().getInterfaces();
        //3. InvocationHandler:动态执行目标对象相应方法
        proxyObj = Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
            /**
            * @Author: at
            * @Date: 2024/8/16 10:29
             * Student{ 学生属性:静态特征,属性方法:动态特征}
             * Cat{ 猫属性,猫方法}
             * 将class共同特征,提取Class
             *  Method:方法
             *  Filed:字段
            */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //获取目标对象方法名称
                String methodName = method.getName();
                //日志(方法前):
                MyLogging.methodBefore(methodName,args);
                //动态执行目标对象方法
                Object rs = method.invoke(target, args);
                //日志(方法后):
                MyLogging.methodAfter(methodName,rs);
                return rs;
            }
        });

        return proxyObj;
    }

    //内部类
//    class MyInvocationHandler implements InvocationHandler {
//        @Override
//        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//            return null;
//        }
//    }

}
java
 /**
    * @Author: at
    * @Date: 2024/11/16 11:31
     * 测试动态代理模式
    */
    @Test
    void testDynamicProxy() {
        //创建动态代理工具类
        Calc1ImplDynamicProxy dp = new Calc1ImplDynamicProxy(new Calc1Impl());
        //代理对象
//        Calc1Impl proxy = (Calc1Impl) dp.getProxy();   //错误(**代理对象不能转换为目标对象**)
        Calc1 proxy = (Calc1) dp.getProxy();
        System.out.println("proxy.getClass().getName() = " + proxy.getClass().getName());
        int add = proxy.add(1, 3);

    }

5.2.3 静态代理与动态代理小结

特性静态代理动态代理
创建时机编译时确定运行时通过反射机制动态创建
灵活性固定,每次新增委托类需新建代理类高度灵活,支持任意接口或类的代理
代码量较多,每个委托类对应一个代理类较少,通用的代理处理器可复用
性能较好,无反射开销稍微逊色,但现代JVM优化后影响不大
适用场景小规模项目或需求明确的情况下大规模项目、框架开发、AOP等

目标对象与代理对象是平级关系(兄弟关系)

  • 兄弟关系不能相互转换

  • 代理对象不能转换为目标对象

    image-20241116113920017

  • 识别代理对象,以$开头对象,都是代理对象

    • jdk.proxy2.$Proxy98

5.2.4 Spring中支持的动态代理

在Spring框架中,动态代理是实现AOP(面向切面编程)和其他高级功能的关键技术之一。Spring支持两种主要的动态代理方式:JDK动态代理和CGLIB动态代理。这两种方式各有优缺点,适用于不同的场景。以下是它们的区别、实现机制以及适用情况的详细解析。

  • JDK动态代理
    • JDK 动态代理基于Java反射机制,通过 java.lang.reflect.Proxy 类和 InvocationHandler 接口来创建代理对象。它只能为实现了接口的对象创建代理实例。当调用代理对象的方法时,实际调用的是 InvocationHandlerinvoke() 方法,在这里可以添加额外的逻辑(如事务管理、日志记录等),然后再转发给目标对象的真实方法。
    • 配置方式:proxy-target-class="false"
  • CGLIB动态代理
    • CGLIB(Code Generation Library)是一个强大的高性能字节码生成库。与JDK动态代理不同,CGLIB可以在运行时动态地生成一个给定类的子类,并覆盖其中的方法以插入自定义逻辑。由于它是通过继承的方式工作,因此不需要目标对象实现任何接口。
    • 配置方式:proxy-target-class="true"
特性JDK 动态代理CGLIB 动态代理
创建方式基于Java反射机制基于字节码生成
适用对象必须实现接口的对象可以是任意类,但不能是final类
性能较好稍微逊色,但现代JVM优化后影响不大
复杂度简单,基于标准Java API相对复杂,涉及字节码操作
与目标对象的关系代理对象与目标对象是兄弟关系代理对象与目标对象是父子关系
默认选择当目标对象实现了接口时,Spring首选JDK代理当目标对象没有实现接口时,Spring使用CGLIB代理

5.3 基于注解实现AOP

5.3.1 AOP概念

AOP:Aspect Oriented Programming面向切面编程思想,它可以在不修改源代码的情况下,给程序动态统一添加额外功能

AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

使用AOP,可以在不修改原来代码的基础上添加新功能。

5.3.2 AOP相关术语

  • 横切关注点:非核心业务代码(提取类之前)

    • 如:日志代码
  • 通知:非核心业务代码提取到类中后,称之为通知(提取类之后)

  • 连接点:非核心业务代码织入到核心业务代码的位置(通知之前)

    • 日志功能在加减乘除方法中书写位置
  • **切入点:**非核心业务代码织入到核心业务代码的位置(通知之后)

  • 切面类(Aspect):将非核心业务代码提取到类中,这个类称之为切面类

    • 将日志功能提取MyLoggin,MyLogging就是切面类
  • 目标对象:被代理的对象称之为目标对象

    • 如:CalcImpl是目标对象
  • 代理对象:通过代理类中getProxy()方法获取的对象,称之为代理对象

5.3.3 使用AspectJ基于注解实现AOP

  • 导入启动器坐标

    xml
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-aop</artifactId>
     </dependency>
  • 定义切面类及定义通知

    java
    package com.at.aop;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.stereotype.Component;
    
    import java.util.Arrays;
    
    /**
     * @Author at
     * @CreateTime: 2024/11/16
     */
    @Component                  //将组件装配到IOC容器
    @Aspect                     //标识当前类是一个切面类
    public class MyLogging {
    
        /**
        * @Author: at
        * @Date: 2024/11/16 10:45
         * 前置通知:日志(方法前)通知
        */
        @Before("execution(public int com.at.aop.CalcImpl.add(int,int))")
        public void methodBefore(JoinPoint joinPoint){
            String methodName = joinPoint.getSignature().getName();
            Object[] args = joinPoint.getArgs();
            System.out.println("==>"+methodName+"()正在执行,参数:"+ Arrays.toString(args));
        }
    }
  • 开启AspectJ注解支持(SpringBoot环境下可省略)

    java
    package com.at.config;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    /**
     * @Author at
     * @CreateTime: 2024/11/16
     */
    @Configuration
    @ComponentScan(basePackages = "com.at.aop")
    //开启AspectJ动态代理
    @EnableAspectJAutoProxy				
    public class SpringConfigAop {
    }
  • 测试

    java
    package com.at;
    
    import com.at.aop.Calc;
    import com.at.aop.CalcImpl;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    /**
     * @Author at
     * @CreateTime: 2024/11/16
     */
    //@SpringBootTest注解,自动整合Junit
    @SpringBootTest
    public class TestAOP {
    
        @Autowired
        private CalcImpl calc;
    //    private Calc calc;
    
        @Test
        void testAOP(){
            System.out.println("calc.getClass().getName() = " + calc.getClass().getName());
            calc.add(1,2);
        }
    
    }
  • 小结

    • @SpringBootTest注解作用:自动在Spring中整合Junit
      • 在测试类中可以获取IOC容器中的组件
    • SpringBoot中,自动开启AspectJ注解支持
    • SpringBoot环境中默认代理方式:CGLIB动态代理
      • com.at.aop.CalcImpl**$$SpringCGLIB$$0**
    • 设置JDK动态代理方式如下:spring.aop.proxy-target-class=false

5.3.4 切入点表达式详解

语法:

java
@Before("execution(public int com.at.aop.CalcImpl.add(int,int))")
@Before("execution(* com.at..*.*(..))")
@Before("execution(* com.at.aop.*.*(..))")

重用切入点表达式

  • 提取重用表达式

    java
    @Pointcut("execution(* com.at.aop.*.*(..))")
    public void myJoinPoint(){}
  • 引用表达式

    java
    @Before("myJoinPoint()")
    public void methodBefore(JoinPoint joinPoint){}
  • [*]

    • [*]可以代表任意权限修饰符及返回值类型
    • [*]可以代表任意包名,任意类名及任意方法名
  • [..]

    • 任意层包名
    • 任意形参数量及类型

5.3.5 JoinPoint接口

java
// 获取方法签名=方法名+参数列表
Signature signature = joinPoint.getSignature();
// 获取方法名称
String methodName = signature.getName();
// 获取参数
Object[] args = joinPoint.getArgs();

5.3.6 AOP的五大通知(通知)

  • 前置通知:@Before

    • 执行时机:在目标方法执行之前执行

    • 注意:如目标方法有异常,前置通知会执行

      java
      @Before("myJoinPoint()")
      public void methodBefore(JoinPoint joinPoint){}
  • 后置通知:@After

    • 执行时机:在目标方法执行之后执行(最后)

    • 注意:如目标方法有异常,后置通知执行

      java
      @After("myJoinPoint()")
      public void methodAfter(JoinPoint joinPoint){}
  • 返回通知:@AfterReturning

    • 执行时机:在目标方法返回结果执行

    • 注意:与目标方法有无异常没有关系(一般有异常时不返回结果,所以返回通知不执行)

      java
      @AfterReturning(value = "myJoinPoint()",returning = "rs")
      public void methodAfterRetuning(JoinPoint joinPoint,Object rs){
          String methodName = joinPoint.getSignature().getName();
          System.out.println("返回通知==>"+methodName+"()方法返回结果rs:"+rs);
      }
  • 异常通知:@AfterThrowing

    • 执行时机:在目标方法抛出异常时执行

    • 注意:有异常执行,无异常不执行,且throwing = "ex"与形参列表中的ex一致

      java
      @AfterThrowing(value = "myJoinPoint()",throwing = "ex")
      public void methodAfterThrowing(JoinPoint joinPoint,Exception ex){
          String methodName = joinPoint.getSignature().getName();
          System.out.println("异常通知==>"+methodName+"()方法的异常ex:"+ex);
      }
  • 环绕通知:

    • 环绕通知:整合以上四个通知(前置&后置&返回&异常)

    • 注意

      • 环绕通知参数必须使用ProceedingJoinPoint,
        • 使用ProceedingJoinPoint的joinPoint.proceed();可以手动触发目标对象的相应方法
      • 环绕通知必须将目标方法的返回值返回,如不设置返回值,会报如下错:

      org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for: public abstract int com.at.aop.Calc.add(int,int)

    • 示例代码

      java
      /**
      * @Author: at
      * @Date: 2024/11/16 15:55
       * 环绕通知(四合一)
      */
      @Around("myJoinPoint()")
      public Object methodAround(ProceedingJoinPoint pjp){
          String methodName = pjp.getSignature().getName();
          Object[] args = pjp.getArgs();
          Object rs = null;
          try {
              //前置通知
              System.out.println("前置通知==>"+methodName+"()正在执行,参数:"+ Arrays.toString(args));
              // 执行目标方法,如:add() sub() mul() div()
              rs = pjp.proceed();
              //返回通知
              System.out.println("返回通知==>"+methodName+"()方法返回结果rs:"+rs);
          } catch (Throwable e) {
              //异常通知
              System.out.println("异常通知==>"+methodName+"()方法的异常ex:"+e);
              throw new RuntimeException(e);
          } finally {
              //后置通知
              System.out.println("后置通知==>"+methodName+"()执行完毕!");
          }
          return rs;
      }
  • 小结

    • 通知执行顺序
      • 有异常:前置通知 -> 异常通知 -> 后置通知
      • 无异常:前置通知 -> 返回通知 -> 后置通知

5.3.7 定义切面优先级

语法:@Order(index)

  • index数值越小优先级越高,但一般推荐正整数

源码

java
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface Order {
    int value() default Integer.MAX_VALUE;
}

5.3.8 AOP实现方式(动态代理实现方式)

  • JDK支持动态代理,底层目标对象实现接口方式**(代理对象目标对象关系:兄弟)**

    • springboot设置jdk动态代理方式:spring.aop.proxy-target-class=false
    • jdk.proxy2.$Proxy98
  • CGLIB(cglib)默认方式,底层目标对象不实现接口方式(认干爹:代理对象与目标对象关系:父子)

    • com.at.aop.CalcImpl$$SpringCGLIB$$0

    image-20241203164237298

第6章 Spring之事务管理

6.1 JdbcTemplate基本用法

6.1.1 JdbcTemplate概念

  • Spring为简化特定领域代码,封装了很多 『Template』形式的模板类。例如:JdbcTemplate、RedisTemplate、RestTemplate 等等。

  • JdbcTemplate 是 Spring JDBC 的核心类之一,它简化了与关系型数据库的交互。通过使用 JdbcTemplate,开发者不需要编写样板代码来管理资源(如打开或关闭连接),处理 SQL 语句,或者处理异常转换。以下是 JdbcTemplate 的一些关键特性和使用方式:

    • 减少样板代码:自动处理数据库资源的获取和释放。
    • SQL 执行:提供了多种方法来执行查询、更新等操作。
    • 结果集映射:支持将结果集映射为 Java 对象,简化数据处理。
    • 事务管理:可以轻松地在 Spring 的事务管理框架下工作。
    • 异常处理:将 JDBC 异常转换为 Spring 的 DataAccessExceptions,便于错误处理。

6.1.2 JdbcTemplate基本使用

  • 导入jar包(启动器)

    image-20241204090301027

  • 编写配置文件:application.properties

    properties
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/spring_tx
    spring.datasource.username=root
    spring.datasource.password=root
  • 将JdbcTemplate对象装配到IOC容器中

    • SpringBoot自动配置原理,默认将JdbcTemplate装配到IOC容器
    • SpringBoot自动配置相关概念后续重点讲解

6.1.3 JdbcTemplate常用API

jdbcTemplate.update(String sql ,Object... args):通用增删改功能

jdbcTemplate.queryForObject(String sql,RowMapper):查询单个对象

jdbcTemplate.queryForList(String sql):查询多个数据Map<String,Object>

jdbcTemplate.query(String sql,RowMapper):查询多个对象

6.1.4 相关案例代码

java
package com.at;

import com.at.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import java.util.List;
import java.util.Map;

@SpringBootTest
class Day15SpringTxApplicationTests {

    @Autowired
    private JdbcTemplate jdbcTemplate;


    @Test
    void contextLoads() {

        System.out.println("jdbcTemplate = " + jdbcTemplate);

    }

/**
       添加user信息
    */
    @Test
    public void testAddStudent(){
        String sql = "INSERT INTO t_user(ACCOUNT,PASSWORD,nickname)VALUES(?,?,?)";
        jdbcTemplate.update(sql,"zhangsan","123456","普通员工");
    }

    /**
       查询单个对象
    */
    @Test
    public void testQueryStudent(){
        String sql = "SELECT id,`account`,`password`,nickname FROM t_user WHERE id = ?";
        // 创建一个映射器
        RowMapper<User> rowMapper = new BeanPropertyRowMapper<>(User.class);

        User user = jdbcTemplate.queryForObject(sql, rowMapper,20010);
        System.out.println("user = " + user);
    }

    /**
       查询所有对象
    */
    @Test
    public void testQueryForList(){
        String sql = "SELECT id,`account`,`password`,nickname FROM t_user";
        // 创建一个映射器
        RowMapper<User> rowMapper = new BeanPropertyRowMapper<>(User.class);

        List<User> userList = jdbcTemplate.query(sql, rowMapper);
        for (User user : userList) {
            System.out.println("user = " + user);
        }

        System.out.println(" ====================== ");
        List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);

        for (Map<String, Object> map : maps) {
            System.out.println("map = " + map);
        }
    }
}

6.2 事务概念回顾

6.2.1 事务概念

在 MySQL 中,事务(Transaction)是一组 SQL 操作的集合,这组操作要么全部执行成功,要么全部不执行,以此来确保数据的一致性和完整性。事务是数据库管理系统(DBMS)执行过程中的一个逻辑工作单元,它具有四个关键属性,通常被简称为 ACID 属性

  1. 原子性(Atomicity)
    • 事务是一个不可分割的工作单位,事务中包含的所有操作要么全部提交成功,要么全部回滚失败。任何一部分操作的失败都会导致整个事务的撤销。
  2. 一致性(Consistency)
    • 事务必须使数据库从一个一致状态转变到另一个一致状态。这意味着即使在系统崩溃或断电的情况下,事务也必须保证数据的完整性和约束条件不会被破坏。
  3. 隔离性(Isolation)
    • 多个并发事务之间的操作应该是隔离的,即一个事务的结果不应该影响其他正在运行的事务,除非它们已经提交。MySQL 提供了不同的隔离级别来实现不同程度的隔离效果。
  4. 持久性(Durability)
    • 一旦事务提交,它对数据库所做的更改就会永久保存下来,即使系统发生故障也不会丢失。

6.2.2 事务控制语句

在 MySQL 中,你可以使用以下 SQL 语句来控制事务:

  • START TRANSACTIONBEGIN
    • 开始一个新的事务。
  • COMMIT
    • 提交当前事务,使得所有更改成为永久性的。提交后不能回滚。
  • ROLLBACK
    • 回滚当前事务,撤销所有未提交的操作。这会将数据库恢复到事务开始之前的状态。

6.2.3 事务隔离级别

MySQL 支持四种标准的事务隔离级别,可以通过 SET TRANSACTION ISOLATION LEVEL 命令来设置:

  • 读未提交(READ UNCOMMITTED)
    • 最低的隔离级别,允许脏读、不可重复读和幻读。
  • 读已提交(READ COMMITTED)
    • 允许不可重复读和幻读,但不允许脏读。
  • 可重复读(REPEATABLE READ)
    • MySQL 的默认隔离级别,仅允许幻读,防止脏读和不可重复读。
  • 串行化(SERIALIZABLE)
    • 最高的隔离级别,完全串行化的读写操作,避免了脏读、不可重复读和幻读。

6.3 Spring声明式事务基本使用

6.3.1 事务管理概念

spring中支持两种事务管理,分别是编程式事务管理声明式事务管理两种.

  • 编程式事务管理概念

    • 编程式事务是指手动编写程序来管理事务,即通过编写代码的方式直接控制事务的提交和回滚。在 Java 中,通常使用事务管理器(如 Spring 中的 PlatformTransactionManager)来实现编程式事务。

    • 编程式事务的主要优点是灵活性高,可以按照自己的需求来控制事务的粒度、模式等等。但是,编写大量的事务控制代码容易出现问题,对代码的可读性和可维护性有一定影响。

    java
    Connection conn = ...;
      
    try {
        // 开启事务:关闭事务的自动提交
        conn.setAutoCommit(false);
        // 核心操作
        // 业务代码
        // 提交事务
        conn.commit();
      
    }catch(Exception e){
      
        // 回滚事务
        conn.rollBack();
      
    }finally{
      
        // 释放数据库连接
        conn.close();
      
    }
    • 编程式的实现方式存在缺陷:
      • 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
      • 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
      • 事务管理代码与核心业务代码相耦合
  • 声明式事务管理概念

    • 在 Spring 框架中,声明式事务管理是一种通过配置而非编程方式来管理事务的技术。它允许开发者以非侵入的方式定义方法的事务行为,而无需修改业务逻辑代码。Spring 的声明式事务管理主要依赖于 AOP(面向切面编程)来实现,并且可以通过 XML 配置或者基于注解的方式来进行配置。
    • 声明式事务管理的优势
      • 减少样板代码:减少了围绕事务控制所需的大量重复代码。
      • 提高代码可读性:将事务逻辑与业务逻辑分离,使代码更易于维护和理解。
      • 集中化管理:可以在一处集中管理事务规则,便于维护和调整。
  • 小结

    • 编程式事务需要手动编写代码来管理事务(不推荐使用)
    • 声明式事务可以通过配置文件或注解来控制事务。(推荐使用)

6.3.2 声明式事务管理基本实现

SpringBoot环境中默认装配事务管理器,在service层使用@Transactional注解管理事务即可

当然也可以手动装配事务管理器,具体代码如下:

  • 编写配置文件:jdbc.properties

    properties
    #配置DruidDataSource
    jdbc.driverClassName=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/0923_demo
    jdbc.username=root
    jdbc.password=root
  • 编写属性类:JdbcProperties

    java
    package com.at.properties;
    
    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    /**
     * @Author at
     * @CreateTime: 2024/11/26
     */
    @Component("jdbcProperties")
    @ConfigurationProperties(prefix = "jdbc")
    @Data
    public class JdbcProperties {
        private String driverClassName;
        private String url;
        private String username;
        private String password;
    }
  • 编写配置类:DruidConfig

    java
    package com.at.config;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import com.at.properties.JdbcProperties;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    
    /**
     * @Author at
     * @CreateTime: 2024/11/26
     */
    @Configuration
    public class DruidConfig {
    
        @Autowired
        @Qualifier("jdbcProperties")
        private JdbcProperties jdbcProperties;
    
        @Bean
        public DataSource dataSource(/*JdbcProperties jdbcProperties*/){
            DruidDataSource ds = new DruidDataSource();
            ds.setDriverClassName(jdbcProperties.getDriverClassName());
            ds.setUrl(jdbcProperties.getUrl());
            ds.setUsername(jdbcProperties.getUsername());
            ds.setPassword(jdbcProperties.getPassword());
            return ds;
        }
        
        /**
         * 实例化JdbcTemplate对象,需要使用ioc中的DataSource
         * @param dataSource
         * @return
         */
        @Bean
        public JdbcTemplate jdbcTemplate(DataSource dataSource){
            JdbcTemplate jdbcTemplate = new JdbcTemplate();
            jdbcTemplate.setDataSource(dataSource);
            return jdbcTemplate;
        }
    
    }
  • 编写事务管理器配置类:TxConfig

    java
    /**
     * @Author at
     * @CreateTime: 2024/11/26
     */
    @Configuration
    @EnableTransactionManagement	//开启声明式事务管理
    public class TxConfig {
    
        /**
         * 装配事务管理实现对象
         * @param dataSource
         * @return
         */
        @Bean
        public TransactionManager transactionManager(DataSource dataSource){
            return new DataSourceTransactionManager(dataSource);
        }
    
    }
  • 整合DruidConfig与TxConfig

    java
    @Import(value = {DruidConfig.class, TxConfig.class})
    @ComponentScan(basePackages = "com.at")
    @Configuration
    public class SpringConfig {
    
    }
  • 使用@Transactional注解实现:声明式事务管理

    • service层代码

      java
      package com.at.service;
      
      import com.at.pojo.User;
      
      /**
       * @Author at
       * @CreateTime: 2024/11/18
       */
      public interface UserService {
      
          /**
          * @Author: at
          * @Date: 2024/11/18 10:43
           * 修改User信息
          */
          public void updateUser(User user);
      
          /**
          * @Author: at
          * @Date: 2024/11/18 14:07
           * 通过用户名修改密码
          */
          public void updatePwdByUsername(String username, String password);
      
          /**
          * @Author: at
          * @Date: 2024/11/18 14:07
           * 通过用户名修改昵称
          */
          public void updateNicknameByUsername(String nickname, String username);
      
      }
      java
      package com.at.service.impl;
      
      import com.at.dao.UserDao;
      import com.at.pojo.User;
      import com.at.service.UserService;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Service;
      import org.springframework.transaction.annotation.Isolation;
      import org.springframework.transaction.annotation.Propagation;
      import org.springframework.transaction.annotation.Transactional;
      
      /**
       * @Author at
       * @CreateTime: 2024/11/18
       */
      @Service
      //@Transactional
      public class UserServiceImpl implements UserService {
      
          @Autowired
          private UserDao userDao;
      
          /**
          * @Author: at
          * @Date: 2024/11/18 10:44
           * 1. 通过姓名修改密码
           * 2. 通过姓名修改昵称
          */
          @Transactional(
                  readOnly = false,
                  timeout = 3,
                  noRollbackFor = ArithmeticException.class,
                  isolation = Isolation.REPEATABLE_READ
          )
          @Override
          public void updateUser(User user) {
              //1. 通过姓名修改密码
              userDao.updatePwdByUsername(user.getAccount(),user.getPassword());
      
              //事务超时
      //        try {
      //            Thread.sleep(4000);
      //        } catch (InterruptedException e) {
      //            throw new RuntimeException(e);
      //        }
      
      //        bug
              int i = 1/0;
              //2. 通过姓名修改昵称
              userDao.updateNicknameByUsername(user.getNickname(),user.getAccount());
          }
      
          @Transactional(propagation=Propagation.REQUIRES_NEW)
          @Override
          public void updatePwdByUsername(String username, String password) {
              userDao.updatePwdByUsername(username, password);
          }
      
          @Transactional(propagation=Propagation.REQUIRES_NEW)
          @Override
          public void updateNicknameByUsername(String nickname, String username) {
              userDao.updateNicknameByUsername(nickname, username);
          }
      
      }
      java
      package com.at.service.impl;
      
      import com.at.service.UserService;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Service;
      import org.springframework.transaction.annotation.Propagation;
      import org.springframework.transaction.annotation.Transactional;
      
      /**
       * @Author at
       * @CreateTime: 2024/11/18
       */
      @Service("userServiceAll")
      public class UserServiceAll {
      
          @Autowired
          private UserService userService;
      
          /**
          * @Author: at
          * @Date: 2024/11/18 14:19
           * 测试事务传播行为(Propagation默认值:REQUIRED)
          */
          @Transactional
          public void updateUserAll(){
              //1. 通过用户名修改密码
              userService.updatePwdByUsername("zhangsan", "666666");
      //        int i = 1/0;
              //2. 通过用户名修改昵称(普通员工)
              userService.updateNicknameByUsername("普通员工", "zhangsan");
          }
      
          /**
          * @Author: at
          * @Date: 2024/11/18 14:19
           * 测试事务传播行为(Propagation默认值:REQUIRES_NEW)
          */
          @Transactional(propagation = Propagation.REQUIRED)
          public void updateUserAllNew(){
              //1. 通过用户名修改密码
              userService.updatePwdByUsername("zhangsan", "666666");
      //        int i = 1/0;
              //2. 通过用户名修改昵称(普通员工)
              userService.updateNicknameByUsername("普通员工", "zhangsan");
          }
      
      }
    • 测试类代码

      java
      package com.at;
      
      import com.at.pojo.User;
      import com.at.service.UserService;
      import com.at.service.impl.UserServiceAll;
      import org.junit.jupiter.api.Test;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.beans.factory.annotation.Qualifier;
      import org.springframework.boot.test.context.SpringBootTest;
      
      /**
       * @Author at
       * @CreateTime: 2024/11/18
       */
      
      @SpringBootTest
      public class TestUserService {
      
          @Autowired
          private UserService userService;
      
          @Autowired
          @Qualifier("userServiceAll")
          private UserServiceAll userServiceAll;
      
          /**
          * @Author: at
          * @Date: 2024/11/18 14:16
           * 测试事务四个属性
          */
          @Test
          public void testUpdateUser()
          {
              User user = new User();
              user.setAccount("zhangsan");
              user.setPassword("666666");
              user.setNickname("普通员工666");
              userService.updateUser(user);
      
          }
      
          /**
          * @Author: at
          * @Date: 2024/11/18 14:16
           * 测试事务propagation属性(传播行为)
          */
          @Test
          public void testPropagation() {
              //测试 REQUIRED
      //        userServiceAll.updateUserAll();
              System.out.println("userService.getClass().getName() = " + userService.getClass().getName());
              System.out.println("userServiceAll.getClass().getName() = " + userServiceAll.getClass().getName());
      
              //测试 REQUIRES_NEW
              userServiceAll.updateUserAllNew();
      
          }
      
      }

6.4 @Transactional注解(事务属性)

6.4.1 事务只读

  1. 只读介绍

    对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。

  2. 设置方式

    java
    // readOnly = true把当前事务设置为只读 默认是false!
    @Transactional(readOnly = true)
  3. 针对DML动作设置只读模式

    会抛出下面异常:

    Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed

  4. @Transactional注解放在类上

    1. 生效原则

      如果一个类中每一个方法上都使用了 @Transactional 注解,那么就可以将 @Transactional 注解提取到类上。反过来说:@Transactional 注解在类级别标记,会影响到类中的每一个方法。同时,类级别标记的 @Transactional 注解中设置的事务属性也会延续影响到方法执行时的事务属性。除非在方法上又设置了 @Transactional 注解。

      对一个方法来说,离它最近的 @Transactional 注解中的事务属性设置生效。

    2. 用法举例

      在类级别@Transactional注解中设置只读,这样类中所有的查询方法都不需要设置@Transactional注解了。因为对查询操作来说,其他属性通常不需要设置,所以使用公共设置即可。

      然后在这个基础上,对增删改方法设置@Transactional注解 readOnly 属性为 false。

    java
    @Service
    @Transactional(readOnly = true)
    public class EmpService {
        
        // 为了便于核对数据库操作结果,不要修改同一条记录
        @Transactional(readOnly = false)
        public void updateTwice(……) {
        ……
        }
        
        // readOnly = true把当前事务设置为只读
        // @Transactional(readOnly = true)
        public String getEmpName(Integer empId) {
        ……
        }
        
    }

6.4.2 事务超时

  1. 需求

    事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。

    此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。

    概括来说就是一句话:超时回滚,释放资源。

  2. 设置超时时间

    java
    @Service
    public class StudentService {
    
        @Autowired
        private StudentDao studentDao;
    
        /**
         * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
         */
        @Transactional(readOnly = false,timeout = 3)
        public void changeInfo(){
            studentDao.updateAgeById(100,1);
            //休眠4秒,等待方法超时!
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            studentDao.updateNameById("test1",1);
        }
    }
  3. 测试超时效果

    org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Wed May 24 09:10:43 IRKT 2023

6.4.3 事务回滚|不回滚异常

  1. 默认情况

    默认只针对运行时异常回滚,编译时异常不回滚。情景模拟代码如下:

  2. 设置回滚异常

    java
    @Service
    public class StudentService {
    
        @Autowired
        private StudentDao studentDao;
    
        /**
         * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
         * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
         * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
         */
        @Transactional(readOnly = false,timeout = 3)
        public void changeInfo() throws FileNotFoundException {
            studentDao.updateAgeById(100,1);
            //主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内! 
            new FileInputStream("xxxx");
            studentDao.updateNameById("test1",1);
        }
    }
  3. 设置不回滚的异常

    在默认设置和已有设置的基础上,再指定一个异常类型,碰到它不回滚。

    noRollbackFor属性:指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!

    java
    @Service
    public class StudentService {
    
        @Autowired
        private StudentDao studentDao;
    
        /**
         * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
         * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
         * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
         */
        @Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class,noRollbackFor = FileNotFoundException.class)
        public void changeInfo() throws FileNotFoundException {
            studentDao.updateAgeById(100,1);
            //主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内!
            new FileInputStream("xxxx");
            studentDao.updateNameById("test1",1);
        }
    }

6.4.4 事务隔离级别

  1. 事务隔离级别回滚

    数据库事务的隔离级别是指在多个事务并发执行时,数据库系统为了保证数据一致性所遵循的规定。常见的隔离级别包括:

    • 读未提交(Read Uncommitted):事务可以读取未被提交的数据,容易产生脏读、不可重复读和幻读等问题。实现简单但不太安全,一般不用。
    • 读已提交(Read Committed):事务只能读取已经提交的数据,可以避免脏读问题,但可能引发不可重复读和幻读。
    • 可重复读(Repeatable Read):在一个事务中,相同的查询将返回相同的结果集,不管其他事务对数据做了什么修改。可以避免脏读和不可重复读,但仍有幻读的问题。
    • 串行化(Serializable):最高的隔离级别,完全禁止了并发,只允许一个事务执行完毕之后才能执行另一个事务。可以避免以上所有问题,但效率较低,不适用于高并发场景。 不同的隔离级别适用于不同的场景,需要根据实际业务需求进行选择和调整。
  2. 事务隔离级别设置

    java
    package com.at.service;
    
    import com.at.dao.StudentDao;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Isolation;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    
    /**
     * projectName: com.at.service
     */
    @Service
    public class StudentService {
    
        @Autowired
        private StudentDao studentDao;
    
        /**
         * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
         * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
         * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
         * isolation = 设置事务的隔离级别,mysql默认是repeatable read!
         */
        @Transactional(readOnly = false,
                       timeout = 3,
                       rollbackFor = Exception.class,
                       noRollbackFor = FileNotFoundException.class,
                       isolation = Isolation.REPEATABLE_READ)
        public void changeInfo() throws FileNotFoundException {
            studentDao.updateAgeById(100,1);
            //主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内!
            new FileInputStream("xxxx");
            studentDao.updateNameById("test1",1);
        }
    }

6.4.5 事务传播行为

  • 事务传播行为概念

    事务传播行为(Propagation Behavior)定义了当一个方法被调用时,如何与已有的事务进行交互。在 Spring 框架中,@Transactional 注解的 propagation 属性用于指定方法的事务传播行为。不同的传播行为决定了当前方法是否应该运行在一个新事务中、加入现有事务、或者以非事务方式运行等。

    举例说明:

    • 假设MethodA()方法的事务是tx1,MethodB()方法的事务tx2,MethodA()中调用MethodB()
    • 事务传播行为决定了MethodB()按照tx1或tx2事务执行
      • REQUIRED(默认值):tx1
      • REQUIRES_NEW:tx2
  • 案例代码

    java
    @Transactional
    public void MethodA(){
        // ...
        MethodB();
        // ...
    }
    
    //在被调用的子方法中设置传播行为,代表如何处理调用的事务! 是加入,还是新事务等!
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void MethodB(){
        // ...
    }
  • propagation属性

    image-20241204101443720

    • propagation属性值

      spring中支持七种事务传播行为,常用两种:REQUIRED和REQUIRES_NEW

      1. REQUIRED(默认值)
        • 如果当前存在事务,则加入该事务;如果不存在,则创建一个新的事务。
      2. SUPPORTS
        • 如果当前存在事务,则加入该事务;如果没有事务,则以非事务方式执行。这意味着此方法对事务的存在与否不敏感。
      3. MANDATORY
        • 必须在一个已有事务中运行,否则抛出异常(IllegalTransactionStateException)。这通常用于必须确保在事务上下文中执行的方法。
      4. REQUIRES_NEW
        • 创建一个新的事务,如果当前存在事务,则挂起当前事务。这意味着即使有现有的事务,也会为这个方法启动一个全新的事务。
      5. NOT_SUPPORTED
        • 不支持事务,总是以非事务方式执行。如果有活动的事务,它将被暂停。
      6. NEVER
        • 方法不应该在事务中运行。如果尝试在事务中调用该方法,将会抛出异常。
      7. NESTED
        • 如果当前存在事务,则在嵌套事务内执行;如果不存在,则创建新的事务。嵌套事务可以独立于外部事务回滚,但它的提交依赖于外部事务的提交。
    • 测试事务传播行为

      java
      package com.at.service.impl;
      
      import com.at.service.UserService;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Service;
      import org.springframework.transaction.annotation.Propagation;
      import org.springframework.transaction.annotation.Transactional;
      
      /**
       * @Author at
       * @CreateTime: 2024/11/18
       */
      @Service("userServiceAll")
      public class UserServiceAll {
      
          @Autowired
          private UserService userService;
      
          /**
          * @Author: at
          * @Date: 2024/11/18 14:19
           * 测试事务传播行为(Propagation默认值:REQUIRED)
          */
          @Transactional
          public void updateUserAll(){
              //1. 通过用户名修改密码
              userService.updatePwdByUsername("zhangsan", "666666");
      //        int i = 1/0;
              //2. 通过用户名修改昵称(普通员工)
              userService.updateNicknameByUsername("普通员工", "zhangsan");
          }
      
          /**
          * @Author: at
          * @Date: 2024/11/18 14:19
           * 测试事务传播行为(Propagation默认值:REQUIRES_NEW)
          */
          @Transactional(propagation = Propagation.REQUIRED)
          public void updateUserAllNew(){
              //1. 通过用户名修改密码
              userService.updatePwdByUsername("zhangsan", "666666");
      //        int i = 1/0;
              //2. 通过用户名修改昵称(普通员工)
              userService.updateNicknameByUsername("普通员工", "zhangsan");
          }
      
      }