Srping

4/10/2022 Spring

Spring大纲

# 简介

Spring框架图标

Spring 是 Java EE 编程领域的一款轻量级的开源框架,由被称为“Spring 之父”的 Rod Johnson 于 2002 年提出并创立,它的目标就是要简化 Java 企业级应用程序的开发难度和周期。

Spring 自诞生以来备受青睐,一直被广大开发人员作为 Java 企业级应用程序开发的首选。时至今日,Spring 俨然成为了 Java EE 代名词,成为了构建 Java EE 应用的事实标准。

# Spring的诞生与发展

早期的 J2EE(Java EE 平台)推崇以 EJB 为核心的开发方式,但这种开发方式在实际的开发过程中存在种种弊端,例如使用复杂、代码臃肿、代码侵入性强、开发周期长、移植难度大等。

Rod Johnson 在其 2002 年编著的畅销书《Expert One-on-One J2EE Design and Development》中,针对 EJB 各种臃肿的结构进行了逐一的分析和否定,并分别以更加简洁的方式进行了替换。

在这本书中,Rod Johnson 通过一个包含 3 万行代码的附件,展示了如何在不使用 EJB 的情况下构建一个高质量、可扩展的 Java 应用程序。在这个附件中,Rod Johnson 编写了上万行基础结构代码,其中包含了许多可重用的 Java 接口和类,例如 ApplicationContext、BeanFactory 等。这些类的根包被命名为 com.interface21,含义为:这是提供给 21 世纪的一个参考。

这本书影响甚远,后来 Rod Johnson 将 com.interface21 的代码开源,并把这个新框架并命名为“Spring”,含义为:Spring 像一缕春风一样,扫平传统 J2EE 的寒冬。

2003 年 2 月,Spring 0.9 版本发布,它采用了 Apache 2.0 开源协议;2004 年 4 月,Spring 1.0 版本正式发布。到目前为止,Spring 已经步入到了第 5 个大版本,也就是我们常说的 Spring 5。

# 狭义Spring和广义Spring

  • 广义Spring:Spring技术栈

    广义上的 Spring 泛指以 Spring Framework 为核心的 Spring 技术栈。

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

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

    项目名称 描述
    Spring Data Spring 提供的数据访问模块,对 JDBC 和 ORM 提供了很好的支持。通过它,开发人员可以使用一种相对统一的方式,来访问位于不同类型数据库中的数据。
    Spring Batch 一款专门针对企业级系统中的日常批处理任务的轻量级框架,能够帮助开发人员方便的开发出健壮、高效的批处理应用程序。
    Spring Security 前身为 Acegi,是 Spring 中较成熟的子模块之一。它是一款可以定制化的身份验证和访问控制框架。
    Spring Mobile 是对 Spring MVC 的扩展,用来简化移动端 Web 应用的开发。
    Spring Boot 是 Spring 团队提供的全新框架,它为 Spring 以及第三方库一些开箱即用的配置,可以简化 Spring 应用的搭建及开发过程。
    Spring Cloud 一款基于 Spring Boot 实现的微服务框架。它并不是某一门技术,而是一系列微服务解决方案或框架的有序集合。它将市面上成熟的、经过验证的微服务框架整合起来,并通过 Spring Boot 的思想进行再封装,屏蔽调其中复杂的配置和实现原理,最终为开发人员提供了一套简单易懂、易部署和易维护的分布式系统开发工具包。
  • 狭义Spring:Spring Framework

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

    Spring 框架是一个分层的、面向切面的 Java 应用程序的一站式轻量级解决方案,它是 Spring 技术栈的核心和基础,是为了解决企业级应用开发的复杂性而创建的。

    Spring 有两个核心部分: IOC 和 AOP。

    核心 描述
    IOC Inverse of Control 的简写,译为“控制反转”,指把创建对象过程交给 Spring 进行管理。
    AOP Aspect Oriented Programming 的简写,译为“面向切面编程”。 AOP 用来封装多个类的公共行为,将那些与业务无关,却为业务模块所共同调用的逻辑封装起来,减少系统的重复代码,降低模块间的耦合度。另外,AOP 还解决一些系统层面上的问题,比如日志、事务、权限等。

Spring 是一种基于 Bean 的编程技术,它深刻地改变着 Java 开发世界。Spring 使用简单、基本的 Java Bean 来完成以前只有 EJB 才能完成的工作,使得很多复杂的代码变得优雅和简洁,避免了 EJB 臃肿、低效的开发模式,极大的方便项目的后期维护、升级和扩展。

在实际开发中,服务器端应用程序通常采用三层体系架构,分别为表现层(web)、业务逻辑层(service)、持久层(dao)。

Spring 致力于 Java EE 应用各层的解决方案,对每一层都提供了技术支持。

  • 在表现层提供了对 Spring MVC、Struts2 等框架的整合;
  • 在业务逻辑层提供了管理事务和记录日志的功能;
  • 在持久层还可以整合 MyBatis、Hibernate 和 JdbcTemplate 等技术,对数据库进行访问。

这充分地体现了 Spring 是一个全面的解决方案,对于那些已经有较好解决方案的领域,Spring 绝不做重复的事情。

从设计上看,Spring 框架给予了 Java 程序员更高的自由度,对业界的常见问题也提供了良好的解决方案,因此在开源社区受到了广泛的欢迎,并且被大部分公司作为 Java 项目开发的首选框架。

# Spring Framework的特点

Spring 框架具有以下几个特点。

  • 方便解耦,简化开发

    Spring 就是一个大工厂,可以将所有对象的创建和依赖关系的维护交给 Spring 管理。

  • 方便集成各种优秀框架

    Spring 不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如 Struts2、Hibernate、MyBatis 等)的直接支持。

  • 降低 Java EE API 的使用难度

    Spring 对 Java EE 开发中非常难用的一些 API(JDBC、JavaMail、远程调用等)都提供了封装,使这些 API 应用的难度大大降低。

  • 方便程序的测试

    Spring 支持 JUnit4,可以通过注解方便地测试 Spring 程序。

  • AOP 编程的支持

    Spring 提供面向切面编程,可以方便地实现对程序进行权限拦截和运行监控等功能。

  • 声明式事务的支持

    只需要通过配置就可以完成对事务的管理,而无须手动编程。

# Spring体系结构

Spring 框架基本涵盖了企业级应用开发的各个方面,它包含了 20 多个不同的模块。

Spring体系结构图

上图中包含了 Spring 框架的所有模块,这些模块可以满足一切企业级应用开发的需求,在开发过程中可以根据需求有选择性地使用所需要的模块。下面分别对这些模块的作用进行简单介绍。

  1. Data Access/Intergration(数据访问/集成)

    数据访问/集成层包括 JDBC、ORM、OXM、JMS 和 Transactions 模块,具体介绍如下。

    • JDBC 模块:提供了一个 JBDC 的样例模板,使用这些模板能消除传统冗长的 JDBC 编码还有必须的事务控制,而且能享受到 Spring 管理事务的好处。
    • ORM 模块:提供与流行的“对象-关系”映射框架无缝集成的 API,包括 JPA、JDO、Hibernate 和 MyBatis 等。而且还可以使用 Spring 事务管理,无需额外控制事务。
    • OXM 模块:提供了一个支持 Object /XML 映射的抽象层实现,如 JAXB、Castor、XMLBeans、JiBX 和 XStream。将 Java 对象映射成 XML 数据,或者将XML 数据映射成 Java 对象。
    • JMS 模块:指 Java 消息服务,提供一套 “消息生产者、消息消费者”模板用于更加简单的使用 JMS,JMS 用于用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。
    • Transactions 事务模块:支持编程和声明式事务管理。
  2. Web模块

    Spring 的 Web 层包括 Web、Servlet、WebSocket 和 Portlet 组件,具体介绍如下。

    • Web 模块:提供了基本的 Web 开发集成特性,例如多文件上传功能、使用的 Servlet 监听器的 IOC 容器初始化以及 Web 应用上下文。
    • Servlet 模块:提供了一个 Spring MVC Web 框架实现。Spring MVC 框架提供了基于注解的请求资源注入、更简单的数据绑定、数据验证等及一套非常易用的 JSP 标签,完全无缝与 Spring 其他技术协作。
    • WebSocket 模块:提供了简单的接口,用户只要实现响应的接口就可以快速的搭建 WebSocket Server,从而实现双向通讯。
    • Portlet 模块:提供了在 Portlet 环境中使用 MVC 实现,类似 Web-Servlet 模块的功能。
  3. Core Container(Spring的核心容器)

    Spring 的核心容器是其他模块建立的基础,由 Beans 模块、Core 核心模块、Context 上下文模块和 SpEL 表达式语言模块组成,没有这些核心容器,也不可能有 AOP、Web 等上层的功能。具体介绍如下。

    • Beans 模块:提供了框架的基础部分,包括控制反转和依赖注入。
    • Core 核心模块:封装了 Spring 框架的底层部分,包括资源访问、类型转换及一些常用工具类。
    • Context 上下文模块:建立在 Core 和 Beans 模块的基础之上,集成 Beans 模块功能并添加资源绑定、数据验证、国际化、Java EE 支持、容器生命周期、事件传播等。ApplicationContext 接口是上下文模块的焦点。
    • SpEL 模块:提供了强大的表达式语言支持,支持访问和修改属性值,方法调用,支持访问及修改数组、容器和索引器,命名变量,支持算数和逻辑运算,支持从 Spring 容器获取 Bean,它也支持列表投影、选择和一般的列表聚合等。
  4. AOP、Aspects、Instrumentation和Messaging

    在 Core Container 之上是 AOP、Aspects 等模块,具体介绍如下:

    • AOP 模块:提供了面向切面编程实现,提供比如日志记录、权限控制、性能统计等通用功能和业务逻辑分离的技术,并且能动态的把这些功能添加到需要的代码中,这样各司其职,降低业务逻辑和通用功能的耦合。
    • Aspects 模块:提供与 AspectJ 的集成,是一个功能强大且成熟的面向切面编程(AOP)框架。
    • Instrumentation 模块:提供了类工具的支持和类加载器的实现,可以在特定的应用服务器中使用。
    • messaging 模块:Spring 4.0 以后新增了消息(Spring-messaging)模块,该模块提供了对消息传递体系结构和协议的支持。
  5. Test模块

    Test 模块:Spring 支持 Junit 和 TestNG 测试框架,而且还额外提供了一些基于 Spring 的测试功能,比如在测试 Web 框架时,模拟 Http 请求的功能。

# Hello Spring

# 创建maven项目

参考传送门 (opens new window)

# 导入Spring依赖

<dependencies>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-web -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在Spring体系结构可知,Spring包含很多模块,其中Core Container(Spring的核心容器)是核心。在此处利用导入spring-web模块,由于Maven会管理其子依赖,会将核心依赖一并导入,就不再需要分批导入核心依赖(core、bean)。

image-20220227160116217

# 创建Java类

在top.snake8859.spring包创建HelloSpring.java类。

package top.snake8859.spring;

public class HelloSpring {
    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 创建配置文件

在src的resources目录下,创建Spring配置文件Beans.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-3.0.xsd">

    <bean id="helloSpring" class="top.snake8859.spring.HelloSpring">
        <property name="message" value="Hello Spring"></property>
    </bean>

</beans>
1
2
3
4
5
6
7
8
9
10
11

Beans.xml 用于给不同的 Bean 分配唯一的 ID,并给相应的 Bean 属性赋值。例如,在以上代码中,我们可以在不影响其它类的情况下,给 message 变量赋值。

# 测试

在src的java目录下,创建Spring测试类MainApp.java,内容如下。

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.snake8859.spring.HelloSpring;

public class MainApp {
    public static void main(String[] args) {
        // 加载Spring配置文件,创建上下文对象
        ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
        // 获取Bean
        HelloSpring helloSpring = context.getBean("helloSpring", HelloSpring.class);
        System.out.println(helloSpring.getMessage());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

关于以上代码,需要注意以下两点:

  • 创建 ApplicationContext 对象时使用了 ClassPathXmlApplicationContext 类,这个类用于加载 Spring 配置文件、创建和初始化所有对象(Bean)。
  • ApplicationContext.getBean() 方法用来获取 Bean,该方法返回值类型为 Object,通过强制类型转换为 HelloWorld 的实例对象,调用其中的 getMessage() 方法。

# Spring IOC理解

IoC是Inversion of Control的简写,翻译为“控制反转”,它不是一门技术,而是一种设计思想,是一个重要的面向对象编程法则,能够帮助我们设计出松耦合的程序。

Spring通过IoC容器来管理所有Java对象的实例化和初始化,控制对象与对象之间的依赖关系。我们将由IoC容器管理的Java对象称为Spring Bean,它与使用关键字new创建的Java对象没有任何区别。

IoC容器是Spring框架中最重要的核心组件之一,它贯穿了Spring从诞生到成长的整个过程。

# 控制反转(IoC)

在传统的Java应用中,一个类想要调用另一个类中的属性或者方法,通过会先在其代码里通过new Object()的方式将后者的对象创建出来,然后才能实现属性或者方法的调用。我们将前者(一个类)称为“调用者”,将后者(另一个类)称为“被调用者”。也就是说,调用者掌握着被调用者对象创建的控制权。

例如我们定义与用户相关的Dao和Service类,代码如下。

  1. UserDao接口

    public interface UserDao {
        void getUser();
    }
    
    1
    2
    3
  2. UserDaoImpl实现类

    public class UserDaoImpl implements UserDao {
        public void getUser() {
            System.out.println("默认获取用户数据");
        }
    }
    
    1
    2
    3
    4
    5
  3. UserService接口

    public interface UserService {
        void getUser();
    }
    
    1
    2
    3
  4. UserServiceImpl实现类

    public class UserServiceImpl implements UserService {
        private UserDao userDao = new UserDaoImpl();
        public void getUser() {
            userDao.getUser();
        }
    }
    
    1
    2
    3
    4
    5
    6
  5. 测试和调用

    public class MyTest {
        public static void main(String[] args) {
            //用户实际调用的是业务层,dao层他们不需要接触!
            UserService userService = new UserServiceImpl();
            userService.getUser();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7

可以看到对于MyTest(调用者)掌握UserService(被调用者)的控制权,但是在实际生产环境下,需求的变更可能导致UserService实现变化,从而我们需要主动去修改MyTest的源码。在程序代码量较大的情况下,这样的行为成本代价高,这也侧面反映这种方式破坏面向对象第一原则(开闭原则)。

但是在Spring应用中,Java对象创建的控制权是掌握在IoC容器里,获取对象的大致步骤如下。

  1. 我们通过XML配置文件、注解或者Java配置类等方式,对Java对象进行定义,例如在XML配置文件中使用<bean>标签,在Java类上使用@Component注解等。
  2. Spring启动时,IoC容器会自动根据对象定义,将这些对象创建并管理起来。这些被IoC容器创建并管理的对象被称为Spring Bean。
  3. 当我们想要使用某个Bean时,可以直接从IoC容器中获取(例如通过 ApplicationContext 的 getBean() 方法),而不在需要手动通过代码(例如new Object()的方式)创建。

IoC带来的最大改变不是代码层面,而是从思想层面上发生了“主从换位”的改变。原本调用者是主动的一方,它想要使用什么自由就会主动出击,自己创建。但在Spring应用中,IoC容器掌握着主动权,调用者变成了被动的一方,被动的等待IoC容器创建它所需要的对象(Bean)。

这个过程在职责层面发生了控制权的反转,把原本调用者通过代码实现的对象创建,反转给IoC容器来帮忙实现,因此这个过程也称为Spring的“控制反转”。

# 依赖注入(DI)

依赖注入(Denpendency Injection,简写为 DI)是 Martin Fowler 在 2004 年在对“控制反转”进行解释时提出的。Martin Fowler 认为“控制反转”一词很晦涩,无法让人很直接的理解“到底是哪里反转了”,因此他建议使用“依赖注入”来代替“控制反转”。

在面向对象中,对象和对象之间是存在一种叫做“依赖”的关系。简单来说,依赖关系就是在一个对象中需要调用到另一个对象,即对象存在一个属性,该属性是另一个类的对象。

例如,有一个名为 B 的 Java 类,它的代码如下。

public class B {
    String bid;
    A a;
}
1
2
3
4

从代码可以看出,B 中存在一个 A 类型的对象属性 a,此时我们就可以说 B 的对象依赖于对象 a。而依赖注入就是就是基于这种“依赖关系”而产生的。

我们知道,控制反转核心思想就是由 Spring 负责对象的创建。在对象创建过程中,Spring 会自动根据依赖关系,将它依赖的对象注入到当前对象中,这就是所谓的“依赖注入”。

依赖注入本质上是 Spring Bean 属性注入 (opens new window)的一种,只不过这个属性是一个对象属性而已。

# IoC的工作原理

在Java软件开发过程中,系统中的各个对象之间,各个模块之间,软件系统和硬件系统之间,或多或少都存在一定的耦合关系。

若一个系统的耦合度过高,那么就会造成难以维护的问题,但完全没有耦合的代码几乎无法完成任何工作,这是由于几乎所有的功能都需要代码之间相互协作,相互依赖才能完成。因此我们在程序设计时,所秉承的思想一般都是在不影响系统功能的前提下,最大限度的降低耦合度。

IoC底层通过工厂模式、Java的反射机制、XML解析等技术,将代码的耦合度降低到最低限度,其主要步骤如下。

  1. 在配置文件(例如Beans.xml)中,对各个对象以及它们之间的依赖关系进行配置;
  2. 可以把IoC容器当做一个工厂、这个工厂的生产的产品就是Spring Bean;
  3. 容器启动时会加载并解析这些配置文件、得到对象的基本信息以及它们之间的依赖关系;
  4. IoC利用Java的反射机制,根据类名生成相应的对象(即Spring Bean),并根据依赖关系将这个对象注入到依赖它的对象中。

由于对象的基本信息、对象之间的依赖关系都是在配置文件中定义的,并没有在代码中紧密耦合,因此即使对象发生改变,我们也只需要在配置文件中进行修改即可,而无须对Java代码进行修改,这就是Spring IoC实现解耦的原理。

# IoC容器的两种实现

IoC 思想基于 IoC 容器实现的,IoC 容器底层其实就是一个 Bean 工厂。Spring 框架为我们提供了两种不同类型 IoC 容器,它们分别是 BeanFactory 和 ApplicationContext。

  • BeanFactory

    BeanFactory 是 IoC 容器的基本实现,也是 Spring 提供的最简单的 IoC 容器,它提供了 IoC 容器最基本的功能,由 org.springframework.beans.factory.BeanFactory 接口定义。

    BeanFactory 采用懒加载(lazy-load)机制,容器在加载配置文件时并不会立刻创建 Java 对象,只有程序中获取(使用)这个对对象时才会创建。

    示例:下面我们通过一个实例演示,来演示下 BeanFactory 的使用。

    将 MainApp 的代码修改为使用 BeanFactory 获取 HelloSpring的对象,具体代码如下。

    import org.springframework.beans.factory.BeanFactory;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import top.snake8859.spring.HelloSpring;
    
    public class MainApp {
        public static void main(String[] args) {
            // 加载Spring配置文件,创建上下文对象
            BeanFactory context = new ClassPathXmlApplicationContext("Beans.xml");
            // 获取Bean
            HelloSpring helloSpring = context.getBean("helloSpring", HelloSpring.class);
            System.out.println(helloSpring.getMessage());
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    注意:BeanFactory 是 Spring 内部使用接口,通常情况下不提供给开发人员使用。

  • ApplicationContext

    ApplicationContext 是 BeanFactory 接口的子接口,是对 BeanFactory 的扩展。ApplicationContext 在 BeanFactory 的基础上增加了许多企业级的功能,例如 AOP(面向切面编程)、国际化、事务支持等。

    ApplicationContext 接口有两个常用的实现类,具体如下表。

    实现类 描述 示例代码
    ClassPathXmlApplicationContext 加载类路径 ClassPath 下指定的 XML 配置文件,并完成 ApplicationContext 的实例化工作 ApplicationContext applicationContext = new ClassPathXmlApplicationContext(String configLocation);
    FileSystemXmlApplicationContext 加载指定的文件系统路径中指定的 XML 配置文件,并完成 ApplicationContext 的实例化工作 ApplicationContext applicationContext = new FileSystemXmlApplicationContext(String configLocation);

    在上表的示例代码中,参数 configLocation 用于指定 Spring 配置文件的名称和位置,如 Beans.xml。

# Spring Bean

由Spring IoC容器管理的对象称为Bean,Bean根据Spring配置文件中的信息创建。

我们可以把 Spring IoC 容器看作是一个大工厂,Bean 相当于工厂的产品。如果希望这个大工厂生产和管理 Bean,就需要告诉容器需要哪些 Bean,以哪种方式装配。

Spring 配置文件支持两种格式,即 XML 文件格式和 Properties 文件格式。

  • Properties 配置文件主要以 key-value 键值对的形式存在,只能赋值,不能进行其他操作,适用于简单的属性配置。
  • XML 配置文件采用树形结构,结构清晰,相较于 Properties 文件更加灵活。但是 XML 配置比较繁琐,适用于大型的复杂的项目。

通常情况下,Spring 的配置文件都是使用 XML 格式的。XML 配置文件的根元素是<beans>,该元素包含了多个子元素 <bean>。每一个<bean> 元素都定义了一个 Bean,并描述了该 Bean 是如何被装配到 Spring 容器中的。

在 XML 配置的<beans>元素中可以包含多个属性或子元素,常用的属性或子元素如下表所示。

属性名称 描述
id Bean 的唯一标识符,Spring IoC 容器对 Bean 的配置和管理都通过该属性完成。id 的值必须以字母开始,可以使用字母、数字、下划线等符号。
name 该属性表示 Bean 的名称,我们可以通过 name 属性为同一个 Bean 同时指定多个名称,每个名称之间用逗号或分号隔开。Spring 容器可以通过 name 属性配置和管理容器中的 Bean。
class 该属性指定了 Bean 的具体实现类,它必须是一个完整的类名,即类的全限定名。
scope 表示 Bean 的作用域,属性值可以为 singleton(单例)、prototype(原型)、request、session 和 global Session。默认值是 singleton。
constructor-arg <bean> 元素的子元素,我们可以通过该元素,将构造参数传入,以实现 Bean 的实例化。该元素的 index 属性指定构造参数的序号(从 0 开始),type 属性指定构造参数的类型。
property <bean>元素的子元素,用于调用 Bean 实例中的 setter 方法对属性进行赋值,从而完成属性的注入。该元素的 name 属性用于指定 Bean 实例中相应的属性名。
ref <property><constructor-arg>等元素的子元索,用于指定对某个 Bean 实例的引用,即 <bean>元素中的 id 或 name 属性。
value <property><constractor-arg>等元素的子元素,用于直接指定一个常量值。
list 用于封装 List 或数组类型的属性注入。
set 用于封装 Set 类型的属性注入。
map 用于封装 Map 类型的属性注入。
entry <map> 元素的子元素,用于设置一个键值对。其 key 属性指定字符串类型的键值,ref 或 value 子元素指定其值。
init-method 容器加载 Bean 时调用该方法,类似于 Servlet 中的 init() 方法
destroy-method 容器删除 Bean 时调用该方法,类似于 Servlet 中的 destroy() 方法。该方法只在 scope=singleton 时有效
lazy-init 懒加载,值为 true,容器在首次请求时才会创建 Bean 实例;值为 false,容器在启动时创建 Bean 实例。该方法只在 scope=singleton 时有效

# Bean 属性注入(普通注入)

所谓 Bean 属性注入,简单点说就是将属性注入到 Bean 中的过程,而这属性既可以普通属性,也可以是一个对象(Bean)。

Spring 主要通过以下 2 种方式实现属性注入:

  • 构造函数注入

    我们可以通过 Bean 的带参构造函数,以实现 Bean 的属性注入。

    使用构造函数实现属性注入大致步骤如下:

    1. 在 Bean 中添加一个有参构造函数,构造函数内的每一个参数代表一个需要注入的属性;
    2. 在 Spring 的 XML 配置文件中,通过 <beans>及其子元素 <bean>对 Bean 进行定义;
    3. <bean>元素内使用 <constructor-arg>元素,对构造函数内的属性进行赋值,Bean 的构造函数内有多少参数,就需要使用多少个 <constructor-arg> 元素。

    创建一个Grade的类,代码如下。

    package top.snake8859.pojo;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    public class Grade {
        private static final Log LOGGER = LogFactory.getLog(Grade.class);
        private Integer gradeId;
        private String gradeName;
        public Grade(Integer gradeId, String gradeName) {
            LOGGER.info("正在执行 Course 的有参构造方法,参数分别为:gradeId=" + gradeId + ",gradeName=" + gradeName);
            this.gradeId = gradeId;
            this.gradeName = gradeName;
        }
        @Override
        public String toString() {
            return "Grade{" +
                    "gradeId=" + gradeId +
                    ", gradeName='" + gradeName + '\'' +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    再创建一个Student类,代码如下。

    package top.snake8859.pojo;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    public class Student {
        private static final Log LOGGER = LogFactory.getLog(Student.class);
        private int id;
        private String name;
        private Grade grade;
    
        public Student(int id, String name, Grade grade) {
            LOGGER.info("正在执行 Course 的有参构造方法,参数分别为:id=" + id + ",name=" + name + ",grade=" + grade);
            this.id = id;
            this.name = name;
            this.grade = grade;
        }
        @Override
        public String toString() {
            return "Student{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", grade=" + grade +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    在 Spring 配置文件 Beans.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-3.0.xsd">
    
        <bean id="student" class="top.snake8859.pojo.Student">
            <constructor-arg name="id" value="2"/>
            <constructor-arg name="name" value="李四"/>
            <constructor-arg name="grade" ref="grade"/>
        </bean>
    
        <bean id="grade" class="top.snake8859.pojo.Grade">
            <constructor-arg name="gradeId" value="4"/>
            <constructor-arg name="gradeName" value="四年级"/>
        </bean>
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    在MainApp类内测试。

    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import top.snake8859.pojo.Student;
    import top.snake8859.spring.HelloSpring;
    
    public class MainApp {
    
        private static final Log LOGGER = LogFactory.getLog(MainApp.class);
    
        public static void main(String[] args) {
            //获取 ApplicationContext 容器
            ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
            //获取名为 student 的 Bean
            Student student = context.getBean("student", Student.class);
            //通过日志打印学生信息
            LOGGER.info(student.toString());
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    输出结果如下。

    2月 28, 2022 9:04:37 下午 top.snake8859.pojo.Grade <init>
    信息: 正在执行 Course 的有参构造方法,参数分别为:gradeId=4,gradeName=四年级
    2月 28, 2022 9:04:37 下午 top.snake8859.pojo.Student <init>
    信息: 正在执行 Course 的有参构造方法,参数分别为:id=2,name=李四,grade=Grade{gradeId=4, gradeName='四年级'}
    2月 28, 2022 9:04:37 下午 MainApp main
    信息: Student{id=2, name='李四', grade=Grade{gradeId=4, gradeName='四年级'}}
    
    1
    2
    3
    4
    5
    6
  • setter 注入(又称设值注入)

    我们可以通过 Bean 的 setter 方法,将属性值注入到 Bean 的属性中。

    在 Spring 实例化 Bean 的过程中,IoC 容器首先会调用默认的构造方法(无参构造方法)实例化 Bean(Java 对象),然后通过 Java 的反射机制调用这个 Bean 的 setXxx() 方法,将属性值注入到 Bean 中。

    使用 setter 注入的方式进行属性注入,大致步骤如下:

    1. 在 Bean 中提供一个默认的无参构造函数(在没有其他带参构造函数的情况下,可省略),并为所有需要注入的属性提供一个 setXxx() 方法;
    2. 在 Spring 的 XML 配置文件中,使用<beans>及其子元素 <bean>对 Bean 进行定义;
    3. <bean> 元素内使用 <property> 元素对各个属性进行赋值。

    修改 Student 类的代码。

    package top.snake8859.pojo;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    public class Student {
        private static final Log LOGGER = LogFactory.getLog(Student.class);
        private int id;
        private String name;
        private Grade grade;
    
        //无参构造方法,在没有其他带参构造方法的情况下,可以省略
        public Student() {
        }
        //id 属性的 setter 方法
        public void setId(int id) {
            LOGGER.info("正在执行 Student 类的 setId() 方法…… ");
            this.id = id;
        }
        //name 属性的 setter 方法
        public void setName(String name) {
            LOGGER.info("正在执行 Student 类的 setName() 方法…… ");
            this.name = name;
        }
        public void setGrade(Grade grade) {
            LOGGER.info("正在执行 Student 类的 setGrade() 方法…… ");
            this.grade = grade;
        }
    
        @Override
        public String toString() {
            return "Student{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", grade=" + grade +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38

    修改 Grade 类的代码。

    package top.snake8859.pojo;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    public class Grade {
        private static final Log LOGGER = LogFactory.getLog(Grade.class);
        private Integer gradeId;
        private String gradeName;
        /**
         * 无参构造函数
         * 若该类中不存在其他的带参构造函数,则这个默认的无参构造函数可以省略
         */
        public Grade() {
        }
        public void setGradeId(Integer gradeId) {
            LOGGER.info("正在执行 Grade 类的 setGradeId() 方法…… ");
            this.gradeId = gradeId;
        }
        public void setGradeName(String gradeName) {
            LOGGER.info("正在执行 Grade 类的 setGradeName() 方法…… ");
            this.gradeName = gradeName;
        }
    
        @Override
        public String toString() {
            return "Grade{" +
                    "gradeId=" + gradeId +
                    ", gradeName='" + gradeName + '\'' +
                    '}';
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33

    修改配置文件 Beans.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-3.0.xsd">
    
        <bean id="helloSpring" class="top.snake8859.spring.HelloSpring">
            <property name="message" value="Hello Spring"></property>
        </bean>
    
        <bean id="student" class="top.snake8859.pojo.Student">
            <!--使用 property 元素完成属性注入
                name: 类中的属性名称,例如 id,name
                value: 向属性注入的值 例如 学生的 id 为 1,name 为张三
            -->
            <property name="id" value="1"></property>
            <property name="name" value="张三"></property>
            <property name="grade" ref="grade"></property>
        </bean>
        <bean id="grade" class="top.snake8859.pojo.Grade">
            <property name="gradeId" value="3"></property>
            <property name="gradeName" value="三年级"></property>
        </bean>
    
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    在不修改MainApp的情况下,运行代码,得到结果如下。

    2月 28, 2022 9:14:08 下午 top.snake8859.pojo.Grade setGradeId
    信息: 正在执行 Grade 类的 setGradeId() 方法…… 
    2月 28, 2022 9:14:08 下午 top.snake8859.pojo.Grade setGradeName
    信息: 正在执行 Grade 类的 setGradeName() 方法…… 
    2月 28, 2022 9:14:08 下午 top.snake8859.pojo.Student setId
    信息: 正在执行 Student 类的 setId() 方法…… 
    2月 28, 2022 9:14:08 下午 top.snake8859.pojo.Student setName
    信息: 正在执行 Student 类的 setName() 方法…… 
    2月 28, 2022 9:14:08 下午 top.snake8859.pojo.Student setGrade
    信息: 正在执行 Student 类的 setGrade() 方法…… 
    2月 28, 2022 9:14:08 下午 MainApp main
    信息: Student{id=1, name='张三', grade=Grade{gradeId=3, gradeName='三年级'}}
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

# Bean 属性注入(C和P命名空间)

我们在通过构造函数或 setter 方法进行属性注入时,通常是在 <bean>元素中嵌套<property><constructor-arg>元素来实现的。这种方式虽然结构清晰,但书写较繁琐。

Spring 框架提供了 2 种短命名空间,可以简化 Spring 的 XML 配置,如下表。

短命名空间 简化的 XML 配置 说明
p 命名空间 <bean>元素中嵌套的 <property> 元素 是 setter 方式属性注入的一种快捷实现方式
c 命名空间 <bean>元素中嵌套的<constructor> 元素 是构造函数属性注入的一种快捷实现方式
  • p命名空间

    p 命名空间是 setter 方式属性注入的一种快捷实现方式。通过它,我们能够以 bean 属性的形式实现 setter 方式的属性注入,而不再使用嵌套的<property> 元素,以实现简化 Spring 的 XML 配置的目的。

    首先我们需要在配置文件的<beans> 元素中导入以下 XML 约束。

    xmlns:p="http://www.springframework.org/schema/p"
    
    1

    在导入 XML 约束后,我们就能通过以下形式实现属性注入。

    <bean id="Bean 唯一标志符" class="包名+类名" p:普通属性="普通属性值" p:对象属性-ref="对象的引用">
    
    1

    使用 p 命名空间注入依赖时,必须注意以下 3 点:

    • Java 类中必须有 setter 方法;
    • Java 类中必须有无参构造器(类中不包含任何带参构造函数的情况,无参构造函数默认存在);
    • 在使用 p 命名空间实现属性注入前,XML 配置的<beans>元素内必须先导入 p 命名空间的 XML 约束。
  • c命名空间

    c 命名空间是构造函数注入的一种快捷实现方式。通过它,我们能够以 <bean>属性的形式实现构造函数方式的属性注入,而不再使用嵌套的 <constructor-arg>元素,以实现简化 Spring 的 XML 配置的目的。

    首先我们需要在配置文件的<beans> 元素中导入以下 XML 约束。

    xmlns:c="http://www.springframework.org/schema/c"
    
    1

    在导入 XML 约束后,我们就能通过以下形式实现属性注入。

    <bean id="Bean 唯一标志符" class="包名+类名" c:普通属性="普通属性值" c:对象属性-ref="对象的引用">
    
    1

    使用 p 命名空间注入依赖时,必须注意以下 2 点:

    • Java 类中必须包含对应的带参构造器;
    • 在使用 c 命名空间实现属性注入前,XML 配置的 <beans>元素内必须先导入 c 命名空间的 XML 约束。

# 内部Bean

我们将定义在<bean> 元素的<property><constructor-arg>元素内部的 Bean,称为“内部 Bean”。

  • setter方式注入内部Bean

    我们可以通过 setter 方式注入内部 Bean。此时,我们只需要在<bean> 标签下的 <property> 元素中,再次使用 <bean>元素对内部 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-3.0.xsd">
        <bean id="outerBean" class="……">
            <property name="……" >
                <!-- 定义内部 Bean -->
                <bean class="……">
                    <property name="……" value="……" ></property>
                    ……
                </bean>
            </property>
        </bean>
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    注意:内部 Bean 都是匿名的,不需要指定 id 和 name 的。即使制定了,IoC 容器也不会将它作为区分 Bean 的标识符,反而会无视 Bean 的 Scope 标签。因此内部 Bean 几乎总是匿名的,且总会随着外部的 Bean 创建。内部 Bean 是无法被注入到它所在的 Bean 以外的任何其他 Bean 的。

    示例:使用 setter 方法注入内部 Bean。

    创建一个名为 Dept 的类,代码如下。

    package top.snake8859.pojo;
    
    public class Dept {
        //部门编号
        private String deptNo;
        //部门名称
        private String deptName;
    
        public void setDeptNo(String deptNo) {
            this.deptNo = deptNo;
        }
    
        public void setDeptName(String deptName) {
            this.deptName = deptName;
        }
    
        @Override
        public String toString() {
            return "Dept{" +
                    "deptNo='" + deptNo + '\'' +
                    ", deptName='" + deptName + '\'' +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    创建一个名为 Employee 的类,代码如下。

    package top.snake8859.pojo;
    
    public class Employee {
        //员工编号
        private String empNo;
        //员工姓名
        private String empName;
        //部门信息
        private Dept dept;
    
        public void setEmpNo(String empNo) {
            this.empNo = empNo;
        }
    
        public void setEmpName(String empName) {
            this.empName = empName;
        }
    
        public void setDept(Dept dept) {
            this.dept = dept;
        }
    
        @Override
        public String toString() {
            return "Employee{" +
                    "empNo='" + empNo + '\'' +
                    ", empName='" + empName + '\'' +
                    ", dept=" + dept +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31

    Spring 配置文件 Beans.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-3.0.xsd">
        <bean id="employee" class="top.snake8859.pojo.Employee">
            <property name="empNo" value="001"/>
            <property name="empName" value="小王"/>
            <property name="dept">
                <!--内部Bean-->
                <bean class="top.snake8859.pojo.Dept">
                    <property name="deptNo" value="004"/>
                    <property name="deptName" value="技术部"/>
                </bean>
            </property>
        </bean>
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    在MainApp 的类,创建Bean,代码如下。

    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.beans.factory.BeanFactory;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import top.snake8859.pojo.Employee;
    import top.snake8859.pojo.Student;
    import top.snake8859.spring.HelloSpring;
    
    public class MainApp {
    
        private static final Log LOGGER = LogFactory.getLog(MainApp.class);
    
        public static void main(String[] args) {
            //获取 ApplicationContext 容器
            ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
            //获取名为 employee 的 Bean
            Employee employee = context.getBean("employee", Employee.class);
            //通过日志打印员工信息
            LOGGER.info(employee.toString());
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    输出结果如下。

    2月 28, 2022 9:28:13 下午 top.snake8859.pojo.Grade setGradeId
    信息: 正在执行 Grade 类的 setGradeId() 方法…… 
    2月 28, 2022 9:28:13 下午 top.snake8859.pojo.Grade setGradeName
    信息: 正在执行 Grade 类的 setGradeName() 方法…… 
    2月 28, 2022 9:28:13 下午 top.snake8859.pojo.Student setId
    信息: 正在执行 Student 类的 setId() 方法…… 
    2月 28, 2022 9:28:13 下午 top.snake8859.pojo.Student setName
    信息: 正在执行 Student 类的 setName() 方法…… 
    2月 28, 2022 9:28:13 下午 top.snake8859.pojo.Student setGrade
    信息: 正在执行 Student 类的 setGrade() 方法…… 
    2月 28, 2022 9:28:13 下午 MainApp main
    信息: Employee{empNo='001', empName='小王', dept=Dept{deptNo='004', deptName='技术部'}}
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  • 构造函数方式注入内部Bean

    我们可以通过构造方法注入内部 Bean。此时,我们只需要在<bean>标签下的<constructor-arg>元素中,再次使用 <bean>元素对内部 Bean 进行定义,格式如下。

    <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-3.0.xsd">
        <bean id="……" class="……">
            <constructor-arg name="……">
                <!--内部 Bean-->
                <bean class="……">
                    <constructor-arg name="……" value="……"></constructor-arg>
                    ……
                </bean>
            </constructor-arg>
        </bean>
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

# Bean 集合注入

我们还可以在 Bean 标签下的 <property>元素中,使用以下元素配置 Java 集合类型的属性和参数,例如 List、Set、Map 以及 Properties 等。

标签 说明
<list> 用于注入 list 类型的值,允许重复
<set> 用于注入 set 类型的值,不允许重复
<map> 用于注入 key-value 的集合,其中 key 和 value 都可以是任意类型
<props> 用于注入 key-value 的集合,其中 key 和 value 都是字符串类型
  • 示例1:在集合设置普通类型的值

    创建一个名为 JavaCollection 的类,代码如下。

    package top.snake8859.pojo;
    
    import java.util.Arrays;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    
    public class JavaCollection {
        //1 数组类型属性
        private String[] courses;
        //2 list 集合类型属性
        private List<String> list;
        //3 map 集合类型属性
        private Map<String, String> maps;
        //4 set 集合类型属性
        private Set<String> set;
    
        public void setCourses(String[] courses) {
            this.courses = courses;
        }
    
        public void setList(List<String> list) {
            this.list = list;
        }
    
        public void setMaps(Map<String, String> maps) {
            this.maps = maps;
        }
    
        public void setSet(Set<String> set) {
            this.set = set;
        }
    
        @Override
        public String toString() {
            return "JavaCollection{" +
                    "courses=" + Arrays.toString(courses) +
                    ", list=" + list +
                    ", maps=" + maps +
                    ", set=" + set +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43

    在Spring 配置文件 Beans.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-3.0.xsd">
    
        <bean id="javaCollection" class="top.snake8859.pojo.JavaCollection">
            <!--数组类型-->
            <property name="courses">
                <array>
                    <value>Java</value>
                    <value>PHP</value>
                    <value>C 语言</value>
                </array>
            </property>
            <!--List类型-->
            <property name="list">
                <list>
                    <value>张三</value>
                    <value>李四</value>
                    <value>王五</value>
                </list>
            </property>
            <!--Map类型-->
            <property name="maps">
                <map>
                    <entry key="JAVA" value="java"/>
                    <entry key="PHP" value="php"/>
                </map>
            </property>
            <!--Set类型-->
            <property name="set">
                <set>
                    <value>MySQL</value>
                    <value>Redis</value>
                </set>
            </property>
        </bean>
    
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40

    在MainApp中测试,代码如下。

    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.beans.factory.BeanFactory;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import top.snake8859.pojo.JavaCollection;
    
    public class MainApp {
    
        private static final Log LOGGER = LogFactory.getLog(MainApp.class);
    
        public static void main(String[] args) {
            //获取 ApplicationContext 容器
            ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
            //获取名为 javaCollection 的 Bean
            JavaCollection javaCollection = context.getBean("javaCollection", JavaCollection.class);
            //通过日志打印员工信息
            LOGGER.info(javaCollection.toString());
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    运行结果如下。

    2月 28, 2022 9:59:15 下午 top.snake8859.pojo.Grade setGradeId
    信息: 正在执行 Grade 类的 setGradeId() 方法…… 
    2月 28, 2022 9:59:15 下午 top.snake8859.pojo.Grade setGradeName
    信息: 正在执行 Grade 类的 setGradeName() 方法…… 
    2月 28, 2022 9:59:15 下午 top.snake8859.pojo.Student setId
    信息: 正在执行 Student 类的 setId() 方法…… 
    2月 28, 2022 9:59:15 下午 top.snake8859.pojo.Student setName
    信息: 正在执行 Student 类的 setName() 方法…… 
    2月 28, 2022 9:59:15 下午 top.snake8859.pojo.Student setGrade
    信息: 正在执行 Student 类的 setGrade() 方法…… 
    2月 28, 2022 9:59:15 下午 MainApp main
    信息: JavaCollection{courses=[Java, PHP, C 语言], list=[张三, 李四, 王五], maps={JAVA=java, PHP=php}, set=[MySQL, Redis]}
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • 示例2:在集合中设置对象类型的值

    在上面的示例中,都是在集合中通过 value 属性设置的普通类型的值,我们还可以通过 ref 属性在注入到 Bean 的集合中设置对象类型的值。

    创建一个名为 Course 的类,代码如下。

    package top.snake8859.pojo;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    public class Course {
        private static final Log LOGGER = LogFactory.getLog(Course.class);
        //课程编号
        private Integer courseId;
        //课程名称
        private String courseName;
        public void setCourseId(Integer courseId) {
            this.courseId = courseId;
        }
        public void setCourseName(String courseName) {
            this.courseName = courseName;
        }
        @Override
        public String toString() {
            return "Course{" +
                    "courseId=" + courseId +
                    ", courseName='" + courseName + '\'' +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    将 JavaCollection 中的代码修改成以下形式。

    package top.snake8859.pojo;
    
    import java.util.Arrays;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    
    public class JavaCollection {
        //1 数组类型属性
        private Course[] courses;
        //2 list 集合类型属性
        private List<String> list;
        //3 map 集合类型属性
        private Map<String, String> maps;
        //4 set 集合类型属性
        private Set<String> set;
    
        public void setCourses(Course[] courses) {
            this.courses = courses;
        }
    
        public void setList(List<String> list) {
            this.list = list;
        }
    
        public void setMaps(Map<String, String> maps) {
            this.maps = maps;
        }
    
        public void setSet(Set<String> set) {
            this.set = set;
        }
    
        @Override
        public String toString() {
            return "JavaCollection{" +
                    "courses=" + Arrays.toString(courses) +
                    ", list=" + list +
                    ", maps=" + maps +
                    ", set=" + set +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43

    将 Beans.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-3.0.xsd">
    
        <bean id="course" class="top.snake8859.pojo.Course">
            <property name="courseId" value="1"></property>
            <property name="courseName" value="Java课程"></property>
        </bean>
        <bean id="course2" class="top.snake8859.pojo.Course">
            <property name="courseId" value="2"></property>
            <property name="courseName" value="PHP课程"></property>
        </bean>
        <bean id="course3" class="top.snake8859.pojo.Course">
            <property name="courseId" value="3"></property>
            <property name="courseName" value="C语言课程"></property>
        </bean>
    
        <bean id="javaCollection" class="top.snake8859.pojo.JavaCollection">
            <!--数组类型-->
            <property name="courses">
                <array>
                    <ref bean="course"></ref>
                    <ref bean="course2"></ref>
                    <ref bean="course3"></ref>
                </array>
            </property>
            <!--List类型-->
            <property name="list">
                <list>
                    <value>张三</value>
                    <value>李四</value>
                    <value>王五</value>
                </list>
            </property>
            <!--Map类型-->
            <property name="maps">
                <map>
                    <entry key="JAVA" value="java"/>
                    <entry key="PHP" value="php"/>
                </map>
            </property>
            <!--Set类型-->
            <property name="set">
                <set>
                    <value>MySQL</value>
                    <value>Redis</value>
                </set>
            </property>
        </bean>
    
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53

    运行 MainApp 类中的 main() 方法,输出结果如下。

    2月 28, 2022 10:06:24 下午 MainApp main
    信息: JavaCollection{courses=[Course{courseId=1, courseName='Java课程'}, Course{courseId=2, courseName='PHP课程'}, Course{courseId=3, courseName='C语言课程'}], list=[张三, 李四, 王五], maps={JAVA=java, PHP=php}, set=[MySQL, Redis]}
    
    1
    2

# Bean 其他类型注入

  • 注入Null值

    可以在 XML 配置文件中,通过<null/> 元素将 Null 值注入到 Bean 中。

    创建一个名为 ExampleBean 的类,代码如下。

    package top.snake8859.pojo;
    
    public class ExampleBean {
        private String propertyNull;
        public void setPropertyNull(String propertyNull) {
            this.propertyNull = propertyNull;
        }
        @Override
        public String toString() {
            return "ExampleBean{" +
                    "propertyNull='" + propertyNull + '\'' +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    在 Spring 配置文件 Beans.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-3.0.xsd">
    
        <bean id="exampleBean" class="top.snake8859.pojo.ExampleBean">
            <!--使用null 标签注入 Null 值-->
            <property name="propertyNull">
                <null/>
            </property>
        </bean>
    
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    在MainApp内测试,代码如下。

    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.beans.factory.BeanFactory;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import top.snake8859.pojo.ExampleBean;
    
    public class MainApp {
    
        private static final Log LOGGER = LogFactory.getLog(MainApp.class);
    
        public static void main(String[] args) {
            //获取 ApplicationContext 容器
            ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
            //获取名为 exampleBean 的 Bean
            ExampleBean exampleBean = context.getBean("exampleBean", ExampleBean.class);
            //通过日志打印信息
            LOGGER.info(exampleBean.toString());
    
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    运行结果如下。

    2月 28, 2022 10:39:48 下午 MainApp main
    信息: ExampleBean{propertyNull='null'}
    
    1
    2
  • 注入空字符串

    Spring 会将属性中空参数直接当作空字符串来处理。

    在 ExampleBean 类中添加一个名为 propertyEmpty 的属性,代码如下。

    package top.snake8859.pojo;
    
    public class ExampleBean {
        private String propertyNull;
        private String propertyEmpty;
        public void setPropertyNull(String propertyNull) {
            this.propertyNull = propertyNull;
        }
        public void setPropertyEmpty(String propertyEmpty) {
            this.propertyEmpty = propertyEmpty;
        }
    
        @Override
        public String toString() {
            return "ExampleBean{" +
                    "propertyNull='" + propertyNull + '\'' +
                    ", propertyEmpty='" + propertyEmpty + '\'' +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    在 Spring 配置文件 Beans.xml 中,将 propertyEmpty 属性配置为空字符串,配置如下。

    <?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-3.0.xsd">
    
        <bean id="exampleBean" class="top.snake8859.pojo.ExampleBean">
            <!--使用null 标签注入 Null 值-->
            <property name="propertyNull">
                <null/>
            </property>
    		<!--使用空参数注入空字符串-->
            <property name="propertyEmpty" value=""></property>
        </bean>
    
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    运行结果如下。

    3月 01, 2022 10:10:00 下午 MainApp main
    信息: ExampleBean{propertyNull='null', propertyEmpty=''}
    
    1
    2
  • 注入字面量

    我们知道,在 XML 配置中“<”、“>”、“&”等特殊字符是不能直接保存的,否则 XML 语法检查时就会报错。此时,我们可以通过以下两种方式将包含特殊符号的属性注入 Bean 中。

    1. 转义

      在 XML 中,特殊符号必须进行转义才能保存进 XML 配置中,例如“<”、“>”、“&”等。

      在 XML 中,需要转义的字符如下表所示。

      特殊字符 转义字符
      & &
      < <
      > >
      "
      '

      在转义过程中,需要注意以下几点:

      • 转义序列字符之间不能有空格;
      • 转义序列必须以“;”结束;
      • 单独出现的“&”不会被认为是转义的开始;
      • 区分大小写。

      在 ExampleBean 类中添加一个名为 propertyLiteral 的属性,代码如下。

      package top.snake8859.pojo;
      
      public class ExampleBean {
          //Null值
          private String propertyNull;
          //空字符串
          private String propertyEmpty;
          //包含特殊符号的字面量
          private String propertyLiteral;
          public void setPropertyNull(String propertyNull) {
              this.propertyNull = propertyNull;
          }
          public void setPropertyEmpty(String propertyEmpty) {
              this.propertyEmpty = propertyEmpty;
          }
          public void setPropertyLiteral(String propertyLiteral) {
              this.propertyLiteral = propertyLiteral;
          }
      
          @Override
          public String toString() {
              return "ExampleBean{" +
                      "propertyNull='" + propertyNull + '\'' +
                      ", propertyEmpty='" + propertyEmpty + '\'' +
                      ", propertyLiteral='" + propertyLiteral + '\'' +
                      '}';
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28

      在 Spring 配置文件 Beans.xml 中,对 propertyLiteral 属性进行配置,配置如下。

      <?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-3.0.xsd">
      
          <bean id="exampleBean" class="top.snake8859.pojo.ExampleBean">
              <!--使用null 标签注入 Null 值-->
              <property name="propertyNull">
                  <null/>
              </property>
      		<!--使用空参数注入空字符串-->
              <property name="propertyEmpty" value=""></property>
      	<!--通过转义注入包含特殊符号的字面量-->
              <property name="propertyLiteral" value="&lt;snake8859&gt;"></property>
          </bean>
      
      </beans>
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18

      运行结果如下。

      3月 01, 2022 10:18:39 下午 MainApp main
      信息: ExampleBean{propertyNull='null', propertyEmpty='', propertyLiteral='<snake8859.top>'}
      
      1
      2
    2. 使用短字符串<![CDATA[]]>

      通过短字符串<![CDATA[]]>将包含特殊符号的属性值包裹起来,可以让 XML 解析器忽略对其中内容的解析,以属性原本的样子注入到 Bean 中。

      使用短字符串<![CDATA[]]>需要注意以下几点:

      • 此部分不能再包含”]]>”;
      • 不允许嵌套使用;
      • “]]>”中不能包含空格或者换行。

      在 Spring 配置文件 Beans.xml 中,在<property> 下的<value>元素中使用<![CDATA[]]>,对 propertyLiteral 属性进行配置,配置内容如下。

      <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-3.0.xsd">
          <bean id="exampleBean" class="net.biancheng.c.ExampleBean">
              <!--使用null 标签注入 Null 值-->
              <property name="propertyNull">
                  <null/>
              </property>
              <!--使用空参数注入空字符串-->
              <property name="propertyEmpty" value=""></property>
              <!--使用 <![CDATA[]]> 将包含特殊符号的字面量注入-->
              <property name="propertyLiteral">
                  <value><![CDATA[<snake8859.top>]]></value>
              </property>
          </bean>
      </beans>
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
  • 级联属性赋值

    我们可以在 <bean><property>子元素中,为它所依赖的 Bean 的属性进行赋值,这就是所谓的“级联属性赋值”。

    使用级联属性赋值时,需要注意以下 3点:

    • Java 类中必须有 setter 方法;
    • Java 类中必须有无参构造器(默认存在);
    • 依赖其他 Bean 的类中,必须提供一个它依赖的 Bean 的 getXxx() 方法。

    创建一个名为 DependBean 类,代码如下。

    package top.snake8859.pojo;
    
    public class DependBean {
        private String dependProperty;
    
        public void setDependProperty(String dependProperty) {
            this.dependProperty = dependProperty;
        }
    
        @Override
        public String toString() {
            return "DependBean{" +
                    "dependProperty='" + dependProperty + '\'' +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    在 ExampleBean 中添加一个类型为 DependBean 的对象属性,代码如下。

    package top.snake8859.pojo;
    
    public class ExampleBean {
        //Null值
        private String propertyNull;
        //空字符串
        private String propertyEmpty;
        //包含特殊符号的字面量
        private String propertyLiteral;
        //依赖的 Bean(对象属性)
        private DependBean dependBean;
    
        public void setPropertyNull(String propertyNull) {
            this.propertyNull = propertyNull;
        }
        public void setPropertyEmpty(String propertyEmpty) {
            this.propertyEmpty = propertyEmpty;
        }
        public void setPropertyLiteral(String propertyLiteral) {
            this.propertyLiteral = propertyLiteral;
        }
        public void setDependBean(DependBean dependBean) {
            this.dependBean = dependBean;
        }
        //使用级联属性赋值时,需提供一个依赖对象的 getXxx() 方法
        public DependBean getDependBean() {
            return dependBean;
        }
    
        @Override
        public String toString() {
            return "ExampleBean{" +
                    "propertyNull='" + propertyNull + '\'' +
                    ", propertyEmpty='" + propertyEmpty + '\'' +
                    ", propertyLiteral='" + propertyLiteral + '\'' +
                    ", dependBean=" + dependBean +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39

    修改 Beans.xml 中的配置,在 ExampleBean 的 <bean> 元素中通过 <property> 对 DependBean 中的属性进行级联赋值,配置如下。

    <?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-3.0.xsd">
    
        <bean id="exampleBean" class="top.snake8859.pojo.ExampleBean">
            <!--使用null 标签注入 Null 值-->
            <property name="propertyNull">
                <null/>
            </property>
            <!--使用空参数注入空字符串-->
            <property name="propertyEmpty" value=""></property>
            <!--通过转义注入包含特殊符号的字面量-->
            <property name="propertyLiteral" value="&lt;snake8859.top&gt;"/>
            <!--注入依赖的Bean-->
            <property name="dependBean" ref="dependBean"/>
            <!--级联属性赋值-->
            <property name="dependBean.dependProperty" value="级联属性赋值"/>
        </bean>
    
        <!--对ExampleBean依赖的Bean进行定义-->
        <bean id="dependBean" class="top.snake8859.pojo.DependBean">
            <!--对属性进行赋值-->
            <property name="dependProperty" value="依赖Bean内部赋值"/>
        </bean>
    
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

    运行结果如下。

    3月 01, 2022 10:28:42 下午 MainApp main
    信息: ExampleBean{propertyNull='null', propertyEmpty='', propertyLiteral='<snake8859.top>', dependBean=DependBean{dependProperty='级联属性赋值'}}
    
    1
    2

# Bean继承

在 Spring 中,Bean 和 Bean 之间也存在继承关系。我们将被继承的 Bean 称为父 Bean,将继承父 Bean 配置信息的 Bean 称为子 Bean。

Spring Bean 的定义中可以包含很多配置信息,例如构造方法参数、属性值。子 Bean 既可以继承父 Bean 的配置数据,也可以根据需要重写或添加属于自己的配置信息。

在 Spring XML 配置中,我们通过子 Bean 的 parent 属性来指定需要继承的父 Bean,配置格式如下。

<!--父Bean-->
<bean id="parentBean" class="xxx.xxxx.xxx.ParentBean" >
    <property name="xxx" value="xxx"></property>
    <property name="xxx" value="xxx"></property>
</bean> 
<!--子Bean--> 
<bean id="childBean" class="xxx.xxx.xxx.ChildBean" parent="parentBean"></bean>
1
2
3
4
5
6
7

示例:演示Spring Bean的继承。

创建一个名为 Animal 的类,代码如下。

package top.snake8859.pojo;

public class Animal {
    private String name;
    private Integer age;

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Animal{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

创建一个名为 Dog 的类,代码如下。

package top.snake8859.pojo;

public class Dog {
    private String name;
    private Integer age;
    private String call;

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public void setCall(String call) {
        this.call = call;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", call='" + call + '\'' +
                '}';
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

在 Spring 配置文件 Beans.xml,配置内容如下。

<bean id="animal" class="top.snake8859.pojo.Animal">
    <property name="name" value="动物"></property>
    <property name="age" value="10"></property>
</bean>
<bean id="dog" class="top.snake8859.pojo.Dog" parent="animal">
    <property name="name" value="小狗"></property>
    <property name="call" value="汪汪汪……"></property>
</bean>
1
2
3
4
5
6
7
8

测试代码如下。

//获取 ApplicationContext 容器
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
//获取名为 exampleBean 的 Bean
Dog dog = context.getBean("dog", Dog.class);
//通过日志打印信息
LOGGER.info(dog.toString());
1
2
3
4
5
6

运行结果。

3月 01, 2022 10:38:37 下午 MainApp main
信息: Dog{name='小狗', age=10, call='汪汪汪……'}
1
2

在父 Bean 的定义中,有一个十分重要的属性,那就是 abstract 属性。如果一个父 Bean 的 abstract 属性值为 true,则表明这个 Bean 是抽象的。

抽象的父 Bean 只能作为模板被子 Bean 继承,它不能实例化,也不能被其他 Bean 引用,更不能在代码中根据 id 调用 getBean() 方法获取,否则就会返回错误。

在父 Bean 的定义中,既可以指定 class 属性,也可以不指定 class 属性。如果父 Bean 定义没有明确地指定 class 属性,那么这个父 Bean 的 abstract 属性就必须为 true。

<bean id="animal" class="top.snake8859.pojo.Animal" abstract="true">
    <property name="name" value="动物"></property>
    <property name="age" value="10"></property>
</bean>
<bean id="dog" class="top.snake8859.pojo.Dog" parent="animal">
    <property name="name" value="小狗"></property>
    <property name="call" value="汪汪汪……"></property>
</bean>
1
2
3
4
5
6
7
8

# Bean作用域

默认情况下,所有的 Spring Bean 都是单例的,也就是说在整个 Spring 应用中, Bean 的实例只有一个。

我们可以在 <bean> 元素中添加 scope 属性来配置 Spring Bean 的作用范围。例如,如果每次获取 Bean 时,都需要一个新的 Bean 实例,那么应该将 Bean 的 scope 属性定义为 prototype,如果 Spring 需要每次都返回一个相同的 Bean 实例,则应将 Bean 的 scope 属性定义为 singleton。

Spring 5 共提供了 6 种 scope 作用域,如下表。

作用范围 描述
singleton 默认值,单例模式,表示在 Spring 容器中只有一个 Bean 实例
prototype 原型模式,表示每次通过 Spring 容器获取 Bean 时,容器都会创建一个新的 Bean 实例。
request 每次 HTTP 请求,容器都会创建一个 Bean 实例。该作用域只在当前 HTTP Request 内有效。
session 同一个 HTTP Session 共享一个 Bean 实例,不同的 Session 使用不同的 Bean 实例。该作用域仅在当前 HTTP Session 内有效。
application 同一个 Web 应用共享一个 Bean 实例,该作用域在当前 ServletContext 内有效。 与 singleton 类似,但 singleton 表示每个 IoC 容器中仅有一个 Bean 实例,而一个 Web 应用中可能会存在多个 IoC 容器,但一个 Web 应用只会有一个 ServletContext,也可以说 application 才是 Web 应用中货真价实的单例模式。
websocket websocket 的作用域是 WebSocket ,即在整个 WebSocket 中有效。

注意:在以上 6 种 Bean 作用域中,除了 singleton 和 prototype 可以直接在常规的 Spring IoC 容器(例如 ClassPathXmlApplicationContext)中使用外,剩下的都只能在基于 Web 的 ApplicationContext 实现(例如 XmlWebApplicationContext)中才能使用,否则就会抛出一个 IllegalStateException 的异常。

  • singleton

    singleton 是 Spring 容器默认的作用域。当 Bean 的作用域为 singleton 时,Spring IoC 容器中只会存在一个共享的 Bean 实例。这个 Bean 实例将存储在高速缓存中,所有对于这个 Bean 的请求和引用,只要 id 与这个 Bean 定义相匹配,都会返回这个缓存中的对象实例。

    如果一个 Bean 定义的作用域为 singleton ,那么这个 Bean 就被称为 singleton bean。在 Spring IoC 容器中,singleton bean 是 Bean 的默认创建方式,可以更好地重用对象,节省重复创建对象的开销。

    在 Spring 配置文件中,可以使用<bean>元素的 scope 属性,将 Bean 的作用域定义成 singleton,其配置方式如下所示:

    <bean id="..." class="..." scope="singleton"/>
    
    1
  • prototype

    如果一个 Bean 定义的作用域为 prototype,那么这个 Bean 就被称为 prototype bean。对于 prototype bean 来说,Spring 容器会在每次请求该 Bean 时,都创建一个新的 Bean 实例。

    从某种意义上说,Spring IoC 容器对于 prototype bean 的作用就相当于 Java 的 new 操作符。

    在 Spring 配置文件中,可以使用 <bean>元素的 scope 属性将 Bean 的作用域定义成 prototype,其配置方式如下所示:

    <bean id="..." class="..." scope="prototype"/>
    
    1

# Bean生命周期

在传统的 Java 应用中,Bean 的生命周期很简单,使用 Java 关键字 new 进行 Bean 的实例化后,这个 Bean 就可以使用了。一旦这个 Bean 长期不被使用,Java 自动进行垃圾回收。

相比之下,Spring 中 Bean 的生命周期较复杂,大致可以分为以下 5 个阶段:

  1. Bean 的实例化
  2. Bean 属性赋值
  3. Bean 的初始化
  4. Bean 的使用
  5. Bean 的销毁

Spring 根据 Bean 的作用域来选择 Bean 的管理方式,

  • 对于 singleton 作用域的 Bean 来说,Spring IoC 容器能够精确地控制 Bean 何时被创建、何时初始化完成以及何时被销毁;
  • 对于 prototype 作用域的 Bean 来说,Spring IoC 容器只负责创建,然后就将 Bean 的实例交给客户端代码管理,Spring IoC 容器将不再跟踪其生命周期。

# Spring生命周期流程

Spring Bean 的完整生命周期从创建 Spring IoC 容器开始,直到最终 Spring IoC 容器销毁 Bean 为止,其具体流程如下图所示。

Spring 生命周期流程

Beam生命周期的整个执行过程描述如下。

  1. Spring启动,查找并加载需要被Spring管理的Bean,对Bean进行实例化。
  2. 对Bean进行属性注入。
  3. 如果Bean实现了BeanNameAware接口,则Springg调用Bean的setBeanName()方法传入当前Bean的id值。
  4. 如果Bean四线了BeanFactoryAware接口,则Spring调用setBenaFactory()方法传入当前工厂实例的引用。
  5. 如果 Bean 实现了 ApplicationContextAware 接口,则 Spring 调用 setApplicationContext() 方法传入当前 ApplicationContext 实例的引用。
  6. 如果 Bean 实现了 BeanPostProcessor 接口,则 Spring 调用该接口的预初始化方法 postProcessBeforeInitialzation() 对 Bean 进行加工操作,此处非常重要,Spring 的 AOP 就是利用它实现的
  7. 如果 Bean 实现了 InitializingBean 接口,则 Spring 将调用 afterPropertiesSet() 方法。
  8. 如果在配置文件中通过 init-method 属性指定了初始化方法,则调用该初始化方法。
  9. 如果 BeanPostProcessor 和 Bean 关联,则 Spring 将调用该接口的初始化方法 postProcessAfterInitialization()。此时,Bean 已经可以被应用系统使用了。
  10. 如果在 <bean> 中指定了该 Bean 的作用域为 singleton,则将该 Bean 放入 Spring IoC 的缓存池中,触发 Spring 对该 Bean 的生命周期管理;如果在 <bean>中指定了该 Bean 的作用域为 prototype,则将该 Bean 交给调用者,调用者管理该 Bean 的生命周期,Spring 不再管理该 Bean。
  11. 如果 Bean 实现了 DisposableBean 接口,则 Spring 会调用 destory() 方法销毁 Bean;如果在配置文件中通过 destory-method 属性指定了 Bean 的销毁方法,则 Spring 将调用该方法对 Bean 进行销毁。

# 自定义Bean的生命周期

我们可以在 Spring Bean 生命周期的某个特定时刻,指定一些生命周期回调方法完成一些自定义的操作,对 Bean 的生命周期进行管理。

Bean 的生命周期回调方法主要有两种:

  • 初始化回调方法:在 Spring Bean 被初始化后调用,执行一些自定义的回调操作。
  • 销毁回调方法:在 Spring Bean 被销毁前调用,执行一些自定义的回调操作。

可以通过以下 3 种方式自定义 Bean 的生命周期回调方法:

  • 通过接口实现

    我们可以在 Spring Bean 的 Java 类中,通过实现 InitializingBean 和 DisposableBean 接口,指定 Bean 的生命周期回调方法。

    回调方式 接口 方法 说明
    初始化回调 InitializingBean afterPropertiesSet() 指定初始化回调方法,这个方法会在 Spring Bean 被初始化后被调用,执行一些自定义的回调操作。
    销毁回调 DisposableBean destroy() 指定销毁回调方法,这个方法会在 Spring Bean 被销毁前被调用,执行一些自定义的回调操作。

    注意:通常情况下,我们不建议通过这种方式指定生命周期回调方法,这是由于这种方式会导致代码的耦合性过高。

  • 通过 XML 配置实现

    我们还可以在 Spring 的 XML 配置中,通过 <bean>元素中的 init-method 和 destory-method 属性,指定 Bean 的生命周期回调方法。

    XML 配置属性 描述
    init-method 指定初始化回调方法,这个方法会在 Spring Bean 被初始化后被调用,执行一些自定义的回调操作。
    destory-method 指定销毁回调方法,这个方法会在 Spring Bean 被销毁前被调用,执行一些自定义的回调操作。

    创建一个名为 XMLLifeCycleBean 的 Java 类,代码如下。

    package top.snake8859.pojo;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    public class XMLLifeCycleBean {
        private static final Log LOGGER = LogFactory.getLog(XMLLifeCycleBean.class);
        //网站名称
        private String webName;
        //网站地址
        private String url;
        public void setWebName(String webName) {
            this.webName = webName;
        }
        public void setUrl(String url) {
            this.url = url;
        }
        @Override
        public String toString() {
            return "XMLLifeCycleBean{" +
                    "webName='" + webName + '\'' +
                    ", url='" + url + '\'' +
                    '}';
        }
        /**
         * 初始化回调方法
         */
        public void init() {
            LOGGER.info("在 XML 配置中通过 init-method 属性指定初始化方法:init() 方法");
        }
        /**
         * 销毁回调方法
         */
        public void destroy() {
            LOGGER.info("在 XML 配置中通过 destroy-method 属性指定回调方法:destroy() 方法");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37

    在 Spring 配置文件 Beans.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-3.0.xsd">
        <!--通过 XML 配置指定生命周期回调方法-->
        <bean id="xmlLifeCycleBean" class="top.snake8859.pojo.XMLLifeCycleBean" init-method="init" destroy-method="destroy">
            <property name="url" value="snkae8859.top"/>
            <property name="webName" value="snake8859"/>
        </bean>
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    在MainApp测试,代码如下。

    //获取 ApplicationContext 容器
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
    //获取名为 exampleBean 的 Bean
    XMLLifeCycleBean xmlLifeCycleBean = context.getBean("xmlLifeCycleBean", XMLLifeCycleBean.class);
    //通过日志打印信息
    LOGGER.info(xmlLifeCycleBean.toString());
    //手动销毁
    context.close();
    
    1
    2
    3
    4
    5
    6
    7
    8

    运行结果如下。

    3月 03, 2022 8:28:55 下午 top.snake8859.pojo.XMLLifeCycleBean init
    信息: 在 XML 配置中通过 init-method 属性指定初始化方法:init() 方法
    3月 03, 2022 8:28:55 下午 MainApp main
    信息: XMLLifeCycleBean{webName='snake8859', url='snkae8859.top'}
    3月 03, 2022 8:28:55 下午 top.snake8859.pojo.XMLLifeCycleBean destroy
    信息: 在 XML 配置中通过 destroy-method 属性指定回调方法:destroy() 方法
    
    1
    2
    3
    4
    5
    6
  • 使用注解实现

    可以通过 @PostConstruct 和 @PreDestroy 注解,指定 Bean 的生命周期回调方法。

    注解 描述
    @PostConstruct 指定初始化回调方法,这个方法会在 Spring Bean 被初始化后被调用,执行一些自定义的回调操作。
    @PreDestroy 指定销毁回调方法,这个方法会在 Spring Bean 被销毁前被调用,执行一些自定义的回调操作。
    package top.snake8859.pojo;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import javax.annotation.PostConstruct;
    import javax.annotation.PreDestroy;
    
    public class AnnotationLifeCycleBean {
        private static final Log LOGGER = LogFactory.getLog(AnnotationLifeCycleBean.class);
        //网站名称
        private String webName;
        //网站地址
        private String url;
        public AnnotationLifeCycleBean(String webName, String url) {
            this.webName = webName;
            this.url = url;
        }
        @Override
        public String toString() {
            return "AnnotationLifeCycleBean{" +
                    "webName='" + webName + '\'' +
                    ", url='" + url + '\'' +
                    '}';
        }
    
        /**
         * 初始化回调方法
         */
        @PostConstruct
        public void init(){
            LOGGER.info("通过 @PostConstruct 注解,指定初始化方法:init() 方法");
        }
    
        /**
         * 销毁回调方法
         */
        @PreDestroy
        public void destroy() {
            LOGGER.info("通过 @PreDestroy 注解,指定初始化方法:destroy() 方法");
        }
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    <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-3.0.xsd
        http://www.springframework.org/schema/context
                http://www.springframework.org/schema/context/spring-context.xsd">
        <!--注解扫描-->
        <context:component-scan base-package="top.snake8859.pojo"></context:component-scan>
        <bean id="annotationLifeCycleBean" class="top.snake8859.pojo.AnnotationLifeCycleBean">
            <constructor-arg name="webName" value="C语言中文网3"></constructor-arg>
            <constructor-arg name="url" value="c.biancheng.net"></constructor-arg>
        </bean>
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    注意:@PostConstruct和 @PreDestroy注解位于 java.xml.ws.annotation包是Java EE的模块的一部分。J2EE已经在Java 9中被弃用,并且计划在Java 11中删除它。

    解决办法:添加相关依赖。

    <dependency>
        <groupId>javax.annotation</groupId>
        <artifactId>javax.annotation-api</artifactId>
        <version>1.3.2</version>
    </dependency>
    
    1
    2
    3
    4
    5

如果一个 Bean 中有多种生命周期回调方法时,优先级顺序为:注解 > 接口 > XML 配置。

# Spring后置处理器

在生命周期内提及BeanPostProcessor接口,这个接口也被称为后置处理器,通过该接口可以自定义调用初始化前后执行的操作方法。

BeanPostProcessor 接口源码如下:

public interface BeanPostProcessor {
    Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
    Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
1
2
3
4

该接口中包含了两个方法:

  • postProcessBeforeInitialization() 方法:在 Bean 实例化、属性注入后,初始化前调用。
  • postProcessAfterInitialization() 方法:在 Bean 实例化、属性注入、初始化都完成后调用。

当需要添加多个后置处理器实现类时,默认情况下 Spring 容器会根据后置处理器的定义顺序来依次调用。也可以通过实现 Ordered 接口的 getOrder 方法指定后置处理器的执行顺序。该方法返回值为整数,默认值为 0,取值越大优先级越低。

示例:BeanPostProcessor 接口的用法。

创建一个名为 HelloWorld 的类,代码如下。

package top.snake8859.pojo;

public class HelloWorld {
    private String message;
    public void setMessage(String message) {
        this.message = message;
    }
    public void getMessage() {
        System.out.println("Message : " + message);
    }
    public void init() {
        System.out.println("Bean 正在初始化");
    }
    public void destroy() {
        System.out.println("Bean 将要被销毁");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

创建一个名为 InitHelloWorld1 的类,代码如下。

package top.snake8859.pojo;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;

public class InitHelloWorld1 implements BeanPostProcessor, Ordered {
    @Override
    public int getOrder() {
        return 1;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("A Before : " + beanName);
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("A After : " + beanName);
        return bean;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

创建一个名为 InitHelloWorld2 的类,代码如下。

package top.snake8859.pojo;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;

public class InitHelloWorld2 implements BeanPostProcessor, Ordered {
    @Override
    public int getOrder() {
        return 0;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("B Before : " + beanName);
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("B After : " + beanName);
        return bean;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

需要注意的是,postProcessBeforeInitialization 和 postProcessAfterInitialization 方法返回值不能为 null,否则会报空指针异常或者通过 getBean() 方法获取不到 Bean 实例对象。

在Spring 的 XML 配置文件 Beans.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-3.0.xsd
    http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="helloWorld" class="top.snake8859.pojo.HelloWorld" init-method="init" destroy-method="destroy">
        <property name="message" value="Hello World!"/>
    </bean>

    <!-- 注册处理器 -->
    <bean class="top.snake8859.pojo.InitHelloWorld1" />
    <bean class="top.snake8859.pojo.InitHelloWorld2" />
</beans>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在MainApp 的类,测试代码如下。

AbstractApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
HelloWorld obj = (HelloWorld) context.getBean("helloWorld");
obj.getMessage();
context.registerShutdownHook();
1
2
3
4

运行结果如下。

B Before : helloWorld
A Before : helloWorld
Bean 正在初始化
B After : helloWorld
A After : helloWorld
Message : Hello World!
Bean 将要被销毁
1
2
3
4
5
6
7

由运行结果可以看出,postProcessBeforeInitialization 方法是在 Bean 实例化和属性注入后,自定义初始化方法前执行的。而 postProcessAfterInitialization 方法是在自定义初始化方法后执行的。由于 getOrder 方法返回值越大,优先级越低,因此 InitHelloWorld2 先执行。

# Spring自动装配

Spring 在 Bean 与 Bean 之间建立依赖关系的行为称为“装配”。

Spring 的 IOC 容器虽然功能强大,但它本身不过只是一个空壳而已,它自己并不能独自完成装配工作。需要我们主动将 Bean 放进去,并告诉它 Bean 和 Bean 之间的依赖关系,它才能按照我们的要求完成装配工作。

在之前都是在 XML 配置中通过 <constructor-arg><property> 中的 ref 属性,手动维护 Bean 与 Bean 之间的依赖关系的。

对于只包含少量 Bean 的应用来说,这种方式已经足够满足我们的需求了。但随着应用的不断发展,容器中包含的 Bean 会越来越多,Bean 和 Bean 之间的依赖关系也越来越复杂,这就使得我们所编写的 XML 配置也越来越复杂,越来越繁琐。

我们知道,过于复杂的 XML 配置不但可读性差,而且编写起来极易出错,严重的降低了开发人员的开发效率。为了解决这一问题,Spring 框架还为我们提供了“自动装配”功能。

# 基于XML

基于XML的Spring自动装配功能可以让 Spring 容器依据某种规则(自动装配的规则,有五种),为指定的 Bean 从应用的上下文(AppplicationContext 容器)中查找它所依赖的 Bean,并自动建立 Bean 之间的依赖关系。而这一过程是在完全不使用任何<constructor-arg><property> 元素 ref 属性的情况下进行的。

Spring 的自动装配功能能够有效地简化 Spring 应用的 XML 配置,因此在配置数量相当多时采用自动装配降低工作量。

Spring 框架式默认不支持自动装配的,要想使用自动装配,则需要对 Spring XML 配置文件中 <bean>元素的 autowire 属性进行设置。

Spring 共提供了 5 中自动装配规则,它们分别与 autowire 属性的 5 个取值对应,具体说明如下表。

属性值 说明
byName 按名称自动装配。 Spring 会根据的 Java 类中对象属性的名称,在整个应用的上下文 ApplicationContext(IoC 容器)中查找。若某个 Bean 的 id 或 name 属性值与这个对象属性的名称相同,则获取这个 Bean,并与当前的 Java 类 Bean 建立关联关系。
byType 按类型自动装配。 Spring 会根据 Java 类中的对象属性的类型,在整个应用的上下文 ApplicationContext(IoC 容器)中查找。若某个 Bean 的 class 属性值与这个对象属性的类型相匹配,则获取这个 Bean,并与当前的 Java 类的 Bean 建立关联关系。
constructor 与 byType 模式相似,不同之处在与它应用于构造器参数(依赖项),如果在容器中没有找到与构造器参数类型一致的 Bean,那么将抛出异常。 其实就是根据构造器参数的数据类型,进行 byType 模式的自动装配。
default 表示默认采用上一级元素 <beans>设置的自动装配规则(default-autowire)进行装配。
no 默认值,表示不使用自动装配,Bean 的依赖关系必须通过<constructor-arg><property>元素的 ref 属性来定义。

示例:对 Spring 的自动装配功能进行演示。

创建一个名为 Dept 的类,代码如下。

package top.snake8859.pojo;

public class Dept {
    //部门编号
    private String deptNo;
    //部门名称
    private String deptName;
    public Dept() {
        System.out.println("正在执行 Dept 的无参构造方法>>>>");
    }
    public Dept(String deptNo, String deptName) {
        System.out.println("正在执行 Dept 的有参构造方法>>>>");
        this.deptNo = deptNo;
        this.deptName = deptName;
    }
    public void setDeptNo(String deptNo) {
        System.out.println("正在执行 Dept 的 setDeptNo 方法>>>>");
        this.deptNo = deptNo;
    }
    public void setDeptName(String deptName) {
        System.out.println("正在执行 Dept 的 setDeptName 方法>>>>");
        this.deptName = deptName;
    }
    public String getDeptNo() {
        return deptNo;
    }
    public String getDeptName() {
        return deptName;
    }
    @Override
    public String toString() {
        return "Dept{" +
                "deptNo='" + deptNo + '\'' +
                ", deptName='" + deptName + '\'' +
                '}';
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

创建一个名为 Employee 的类,代码如下。

package top.snake8859.pojo;

public class Employee {
    //员工编号
    private String empNo;
    //员工姓名
    private String empName;
    //部门信息
    private Dept dept;
    public Employee() {
        System.out.println("正在执行 Employee 的无参构造方法>>>>");
    }
    public Employee(String empNo, String empName, Dept dept) {
        System.out.println("正在执行 Employee 的有参构造方法>>>>");
        this.empNo = empNo;
        this.empName = empName;
        this.dept = dept;
    }
    public void setEmpNo(String empNo) {
        System.out.println("正在执行 Employee 的 setEmpNo 方法>>>>");
        this.empNo = empNo;
    }
    public void setEmpName(String empName) {
        System.out.println("正在执行 Employee 的 setEmpName 方法>>>>");
        this.empName = empName;
    }
    public void setDept(Dept dept) {
        System.out.println("正在执行 Employee 的 setDept 方法>>>>");
        this.dept = dept;
    }
    public Dept getDept() {
        return dept;
    }
    public String getEmpNo() {
        return empNo;
    }
    public String getEmpName() {
        return empName;
    }
    @Override
    public String toString() {
        return "Employee{" +
                "empNo='" + empNo + '\'' +
                ", empName='" + empName + '\'' +
                ", dept=" + dept +
                '}';
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

在MainApp类内,测试。

package top.snake8859;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.snake8859.pojo.Employee;

public class MainApp {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
        Employee employee = context.getBean("employee", Employee.class);
        System.out.println(employee);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. 不适用自动装配(autowire="no")

    autowire="no" 表示不使用自动装配,此时我们必须通过<bean> 元素的<constructor-arg><property>元素的 ref 属性维护 Bean 的依赖关系。

    <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-3.0.xsd" default-autowire="constructor">
        <bean id="dept" class="top.snake8859.pojo.Dept">
            <property name="deptNo" value="1"/>
            <property name="deptName" value="技术部"/>
        </bean>
     	<!-- 不适用自动装配 -->
        <bean id="employee" class="top.snake8859.pojo.Employee" autowire="no">
            <property name="empNo" value="002"></property>
            <property name="empName" value="小郭"></property>
            <property name="dept" ref="dept"></property>
        </bean>
    
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  2. 按名称自动配置(autowire="byName")

    autowire="byName" 表示按属性名称自动装配,XML 文件中 Bean 的 id 或 name 必须与类中的属性名称相同

    <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-3.0.xsd" default-autowire="constructor">
        <bean id="dept" class="top.snake8859.pojo.Dept">
            <property name="deptNo" value="1"/>
            <property name="deptName" value="技术部"/>
        </bean>
            
        <!-- 按名称自动装配-->
        <bean id="employee" class="top.snake8859.pojo.Employee" autowire="byName">
            <property name="empNo" value="002"></property>
            <property name="empName" value="小郭"></property>
        </bean>
    
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  3. 按类型自动装配(autowire="byType")

    autowire="byType" 表示按类中对象属性数据类型进行自动装配。即使 XML 文件中 Bean 的 id 或 name 与类中的属性名不同,只要 Bean 的 class 属性值与类中的对象属性的类型相同,就可以完成自动装配。

    <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-3.0.xsd" >
        <bean id="dept2" class="top.snake8859.pojo.Dept">
            <property name="deptNo" value="1"/>
            <property name="deptName" value="技术部"/>
        </bean>
            <!-- 按类型自动装配 -->
        <bean id="employee" class="top.snake8859.pojo.Employee" autowire="byType">
            <property name="empNo" value="002"></property>
            <property name="empName" value="小郭"></property>
        </bean>
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    如果同时存在多个相同类型的 Bean,则注入失败,并且引发异常。

  4. 构造函数自动装配(autowire="constructor")

    autowire="constructor" 表示按照 Java 类中构造函数进行自动装配。

    <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-3.0.xsd" >
        <bean id="dept2" class="top.snake8859.pojo.Dept">
            <property name="deptNo" value="1"/>
            <property name="deptName" value="技术部"/>
        </bean>
        <!-- 构造函数自动装配 -->
    	<bean id="employee" class="top.snake8859.pojo.Employee" autowire="constructor">
            <property name="empNo" value="002"></property>
            <property name="empName" value="小郭"></property>
        </bean>
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  5. 默认的自动状态(autowire="default")

    默认采用上一级标签<bean>设置的自动装配规则(default-autowire)进行装配。

    <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-3.0.xsd" >
        <bean id="dept2" class="top.snake8859.pojo.Dept">
            <property name="deptNo" value="1"/>
            <property name="deptName" value="技术部"/>
        </bean>
        <!-- 默认的自动装配模式 -->
    	<bean id="employee" class="top.snake8859.pojo.Employee" autowire="default">
            <property name="empNo" value="002"></property>
            <property name="empName" value="小郭"></property>
        </bean>
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

# 基于注解

从 Java 5 开始,Java 增加了对注解(Annotation)的支持,它是代码中的一种特殊标记,可以在编译、类加载和运行时被读取,执行相应的处理。开发人员可以通过注解在不改变原有代码和逻辑的情况下,在源代码中嵌入补充信息。

Spring 从 2.5 版本开始提供了对注解技术的全面支持,我们可以使用注解来实现自动装配,简化 Spring 的 XML 配置。

Spring 通过注解实现自动装配的步骤如下:

  1. 引入依赖
  2. 开启组件扫描
  3. 使用注解定义 Bean
  4. 依赖注入

# 1. 引入依赖

使用注解的第一步,就是要在项目中引入以下 依赖。

 <dependencies>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-expression -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-expression</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>
        
         <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>
     
           <!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>

    </dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

注意,除了 spring 的四个基础包和 commons-logging-xxx外,想要使用注解实现 Spring 自动装配,还需要引入Spring 提供的 spring-aop 的依赖。

# 2. 开启组件扫描

Spring 默认不使用注解装配 Bean,因此我们需要在 Spring 的 XML 配置中,通过 <context:component-scan>元素开启 Spring Beans的自动扫描功能。开启此功能后,Spring 会自动从扫描指定的包(base-package 属性设置)及其子包下的所有类,如果类上使用了 @Component 注解,就将该类装配到容器中。

<?xml version="1.0" encoding="UTF-8"?>
<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-3.0.xsd
    http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd">
    <!--开启组件扫描功能-->
    <context:component-scan base-package="top.snake8859.annotation"/>
</beans>
1
2
3
4
5
6
7
8
9
10
11

注意:在使用 <context:component-scan>元素开启自动扫描功能前,首先需要在 XML 配置的一级标签 <beans>中添加 context 相关的约束。

# 3. 使用注解定义Bean

Spring 提供了以下多个注解,这些注解可以直接标注在 Java 类上,将它们定义成 Spring Bean。

注解 说明
@Component 该注解用于描述 Spring 中的 Bean,它是一个泛化的概念,仅仅表示容器中的一个组件(Bean),并且可以作用在应用的任何层次,例如 Service 层、Dao 层等。 使用时只需将该注解标注在相应类上即可。
@Repository 该注解用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
@Service 该注解通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
@Controller 该注解通常作用在控制层(如 Struts2 的 Action、SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。

# 4. 基于注解方式实现依赖注入

  • @Autowired

    可以应用到 Bean 的属性变量、setter 方法、非 setter 方法及构造函数等,默认按照 Bean 的类型进行装配。

    @Autowired 注解默认按照 Bean 的类型进行装配,默认情况下它要求依赖对象必须存在,如果允许 null 值,可以设置它的 required 属性为 false。如果我们想使用按照名称(byName)来装配,可以结合 @Qualifier 注解一起使用

  • @Resource

    作用与 Autowired 相同,区别在于 @Autowired 默认按照 Bean 类型装配,而 @Resource 默认按照 Bean 的名称进行装配

    @Resource 中有两个重要属性:name 和 type。

    • Spring 将 name 属性解析为 Bean 的实例名称,type 属性解析为 Bean 的实例类型。
    • 如果指定 name 属性,则按实例名称进行装配;
    • 如果指定 type 属性,则按 Bean 类型进行装配;

    如果都不指定,则先按 Bean 实例名称装配,如果不能匹配,则再按照 Bean 类型进行装配;如果都无法匹配,则抛出 NoSuchBeanDefinitionException 异常。

    @Resource是javax的包,对于JDK11以上的版本,需要导入依赖。

    <dependency>
        <groupId>javax.annotation</groupId>
        <artifactId>javax.annotation-api</artifactId>
        <version>1.3.2</version>
    </dependency>
    
    1
    2
    3
    4
    5
  • @Qualifier

    与 @Autowired 注解配合使用,会将默认的按 Bean 类型装配修改为按 Bean 的实例名称装配,Bean 的实例名称由 @Qualifier 注解的参数指定。

# 示例

创建一个名为 UserDao 的接口,代码如下。

package top.snake8859.annotation.dao;

public interface UserDao {
    public void selectAllUser();
}

1
2
3
4
5
6

创建 UserDao 的实现类 UserDaoImpl,代码如下。

package top.snake8859.annotation.dao.impl;

import org.springframework.stereotype.Repository;
import top.snake8859.annotation.dao.UserDao;

@Repository("userDao")
public class UserDaoImpl implements UserDao {
    @Override
    public void selectAllUser() {
        System.out.println("userDao : select * from user");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

创建一个名为 UserService 的接口,代码如下。

package top.snake8859.annotation.service;

public interface UserService {
    public void getAllUser();
}

1
2
3
4
5
6

创建 UserService 的实现类 UserServiceImpl,代码如下。

package top.snake8859.annotation.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import top.snake8859.annotation.dao.UserDao;
import top.snake8859.annotation.service.UserService;
@Service("userService")
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    @Override
    public void getAllUser() {
        userDao.selectAllUser();
        System.out.println("userService : getAllUser");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

创建一个名为 UserController 的类,代码如下。

package top.snake8859.annotation.controller;

import org.springframework.stereotype.Controller;
import top.snake8859.annotation.service.UserService;

import javax.annotation.Resource;

@Controller("userController")
public class UserController {
    @Resource
    private UserService userService;

    public void returnAllUser(){
        userService.getAllUser();
        System.out.println("userController : returnAllUser");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在Spring 配置文件 Beans.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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd">
    <!--开启组件扫描功能-->
    <context:component-scan base-package="top.snake8859.annotation"/>
</beans>
1
2
3
4
5
6
7
8
9
10
11

在MainApp 的类内测试,代码如下。

package top.snake8859;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.snake8859.annotation.controller.UserController;
import top.snake8859.pojo.Employee;

public class MainApp {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
        UserController userController = context.getBean("userController", UserController.class);
        userController.returnAllUser();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

输出如下。

userDao : select * from user
userService : getAllUser
userController : returnAllUser
1
2
3

# Spring JavaConfig

早期的Spring采用XML的方式来定义Bean以及Bean之间的依赖关系,但是随着项目工程规模的增加,导致XML越来越复杂,开发人员难以维护。为了避免这个问题,Spring团队推出JavaConfig的方式来解决。JavaConfig原本是Spring的一个子项目,它采用Java代码和Annotation注解的形式来描述Bean之间的依赖关系。在Spring4之后,JavaConfig成为了一个核心。

# @Configuration

@Configuration注解用于标注在类上,它的作用是将该类指定为配置类,相当于在XML配置中的<beans>标签。

package top.snake8859.config;

import org.springframework.context.annotation.Configuration;

@Configuration
public class WebSiteConfiguration {
    
}
1
2
3
4
5
6
7
8

在使用@Configuration时,有几点需要注意的地方。

  • @Configuration 标注的类不可以是匿名类
  • @Configuration标注的类不可以是final类型

# @Bean

@Bean注解用于标注在方法上,它的作用是声明一个Bean,相当于在XML配置中的<bean>标签。

@Bean标注的方法表示该方法返回的对象会加载到Spring容器内,因此该方法定义有一定限制,主要有:

  • 方法需要带返回值,返回类型为加载Spring容器内的对象。
  • Bean的name默认为方法的名,如果需要自定义name,可以使用@Bean注解的name属性。

演示:

  1. 创建一个User类,代码如下。

    package top.snake8859.pojo;
    
    public class User {
        private String username;
        private String password;
        private String message;
    
        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;
        }
    
        public String getMessage() {
            return message;
        }
    
        public void setMessage(String message) {
            this.message = message;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "username='" + username + '\'' +
                    ", password='" + password + '\'' +
                    ", message='" + message + '\'' +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
  2. 创建UserConfig类,代码如下。

    package top.snake8859.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import top.snake8859.pojo.User;
    
    @Configuration
    public class UserConfig {
        @Bean
        public User getUser(){
            return new User();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  3. 在MainApp类测试,代码如下。

    package top.snake8859;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import top.snake8859.config.UserConfig;
    import top.snake8859.pojo.User;
    
    
    public class MainApp {
        public static void main(String[] args) {
            ApplicationContext context = new AnnotationConfigApplicationContext(UserConfig.class);
            User user = context.getBean("getUser", User.class);
            System.out.println(user);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  4. 输出结果如下。

    User{username='snake8859', password='snake8859', message='Hello User'}
    
    1

# @ComponentScan

@ComponentScan注解用于标注在类上,相当于XML配置文件中的<context:component-scan/> 表示启动组件扫描。开启后Spring会自动扫码所有通过注解配置的Bean,然后注册到IOC容器中。

@ComponentScan的basePackages属性用于指定自动扫描的范围。如果不指定,则默认从声明@ComponentScan所在类的package进行扫描。

演示:

  1. 创建WebSite类,代码如下。

    package top.snake8859.pojo;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    @Component
    public class WebSite {
        @Value("192.168.0.1")
        private String ip;
        @Value("snake8859.top")
        private String domain;
    
        public String getIp() {
            return ip;
        }
    
        public void setIp(String ip) {
            this.ip = ip;
        }
    
        public String getDomain() {
            return domain;
        }
    
        public void setDomain(String domain) {
            this.domain = domain;
        }
    
        @Override
        public String toString() {
            return "WebSite{" +
                    "ip='" + ip + '\'' +
                    ", domain='" + domain + '\'' +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
  2. 创建WebSiteConfig类,代码如下。

    package top.snake8859.config;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @ComponentScan(basePackages = "top.snake8859.pojo")
    public class WebSiteConfiguration {
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  3. 在MainApp内测试,代码如下。

    package top.snake8859;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import top.snake8859.config.WebSiteConfiguration;
    import top.snake8859.pojo.WebSite;
    
    
    public class MainApp {
        public static void main(String[] args) {
            ApplicationContext context = new AnnotationConfigApplicationContext(WebSiteConfiguration.class);
            WebSite webSite = context.getBean("webSite", WebSite.class);
            System.out.println(webSite);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  4. 输出内容如下。

    WebSite{ip='192.168.0.1', domain='snake8859.top'}
    
    1

# @Import

@Import注解提供了@Bean注解的功能,同时还有XML配置文件里面标签组织多个分散的XML文件的功能,当然在这里是组织多个分散的@Configuration,因为一个配置类就约等于一个XML配置文件。

注意:@Import注解只允许放到类上面,不允许放到方法上。

@Import注解的三种用法主要包括:

  1. 直接填写class数组的方式

    在配置类上使用@Import注解,并将导入的类,即可快速将类注册到容器内。

    • 创建Red和Blue类,代码如下

      package top.snake8859.pojo;
      
      public class Blue {
          private String name = "Blue";
      }
      
      1
      2
      3
      4
      5
      package top.snake8859.pojo;
      
      public class Red {
          private String name = "Red";
      }
      
      1
      2
      3
      4
      5
    • 创建ColorConfig,使用@Import注解,将Blue和Red注册到容器内。

      package top.snake8859.config;
      
      import org.springframework.context.annotation.Configuration;
      import org.springframework.context.annotation.Import;
      import top.snake8859.pojo.Blue;
      import top.snake8859.pojo.Red;
      
      @Configuration
      @Import({Blue.class, Red.class}) // @Import快速地导入组件,id默认是组件的全类名
      public class ColorConfig {
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
    • 在MainApp.java内,打印容器内的Bean名字,输出如下。

      package top.snake8859;
      
      import org.springframework.context.ApplicationContext;
      import org.springframework.context.annotation.AnnotationConfigApplicationContext;
      import top.snake8859.config.ColorConfig;
      
      public class MainApp {
          public static void main(String[] args) {
              ApplicationContext context = new AnnotationConfigApplicationContext(ColorConfig.class);
      
              String[] beanDefinitionNames = context.getBeanDefinitionNames();
              for (String beanName : beanDefinitionNames) {
                  System.out.println(beanName);
              }
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      org.springframework.context.annotation.internalConfigurationAnnotationProcessor
      org.springframework.context.annotation.internalAutowiredAnnotationProcessor
      org.springframework.context.event.internalEventListenerProcessor
      org.springframework.context.event.internalEventListenerFactory
      colorConfig
      top.snake8859.pojo.Blue
      top.snake8859.pojo.Red
      
      1
      2
      3
      4
      5
      6
      7

      可以看到Red和Blue类都配置到容器内,且id默认是组件的全类名。

  2. ImportSelector接口的方式,即批量导入

    ImportSelector接口可以实现批量Bean导入,其源码如下:

    /*
     * Copyright 2002-2020 the original author or authors.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      https://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    package org.springframework.context.annotation;
    
    import java.util.function.Predicate;
    
    import org.springframework.core.type.AnnotationMetadata;
    import org.springframework.lang.Nullable;
    
    /**
     *
     * @author Chris Beams
     * @author Juergen Hoeller
     * @since 3.1
     * @see DeferredImportSelector
     * @see Import
     * @see ImportBeanDefinitionRegistrar
     * @see Configuration
     */
    public interface ImportSelector {
    
    	/**
    	 * Select and return the names of which class(es) should be imported based on
    	 * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
    	 * @return the class names, or an empty array if none
    	 */
    	String[] selectImports(AnnotationMetadata importingClassMetadata);
    
    	/**
    	 * Return a predicate for excluding classes from the import candidates, to be
    	 * transitively applied to all classes found through this selector's imports.
    	 * <p>If this predicate returns {@code true} for a given fully-qualified
    	 * class name, said class will not be considered as an imported configuration
    	 * class, bypassing class file loading as well as metadata introspection.
    	 * @return the filter predicate for fully-qualified candidate class names
    	 * of transitively imported configuration classes, or {@code null} if none
    	 * @since 5.2.4
    	 */
    	@Nullable
    	default Predicate<String> getExclusionFilter() {
    		return null;
    	}
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58

    在接口方法描述内详细说该接口的主要用法。其中selectImports方法的返回值就是向Spring容器导入的类的全类名字String[]。其参数AnnotationMetadata是代表注解元信息,即@Import注解信息和被@Import注解标注的类的信息。

    • 创建Green和NI类。

      package top.snake8859.pojo;
      
      public class Green {
          private String name = "Green";
      }
      
      1
      2
      3
      4
      5
      package top.snake8859.pojo;
      
      public class NI {
          private String name = "NI";
      }
      
      1
      2
      3
      4
      5
    • 创建MyImportSelector类,实现ImportSelector接口。

      package top.snake8859.config;
      
      import org.springframework.context.annotation.ImportSelector;
      import org.springframework.core.type.AnnotationMetadata;
      
      public class MyImportSelector implements ImportSelector {
          /**
           * 返回值:导入容器中的组件的全类名
           * @param importingClassMetadata 注解元信息
           * @return
           */
          @Override
          public String[] selectImports(AnnotationMetadata importingClassMetadata) {
              return new String[]{"top.snake8859.pojo.Green", "top.snake8859.pojo.NI"};
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
    • 在MainApp.java内,打印容器内的Bean名字,输出如下。

      package top.snake8859;
      
      import org.springframework.context.ApplicationContext;
      import org.springframework.context.annotation.AnnotationConfigApplicationContext;
      import top.snake8859.config.ColorConfig;
      
      public class MainApp {
          public static void main(String[] args) {
              ApplicationContext context = new AnnotationConfigApplicationContext(ColorConfig.class);
      
              String[] beanDefinitionNames = context.getBeanDefinitionNames();
              for (String beanName : beanDefinitionNames) {
                  System.out.println(beanName);
              }
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      org.springframework.context.annotation.internalConfigurationAnnotationProcessor
      org.springframework.context.annotation.internalAutowiredAnnotationProcessor
      org.springframework.context.event.internalEventListenerProcessor
      org.springframework.context.event.internalEventListenerFactory
      colorConfig
      top.snake8859.pojo.Blue
      top.snake8859.pojo.Red
      top.snake8859.pojo.Green
      top.snake8859.pojo.NI
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
  3. ImportBeanDefinitionRegistrar接口方式,即手工注册bean到容器中。

    ImportBeanDefinitionRegistrar也是一个接口,其源码如下。

    package org.springframework.context.annotation;
    
    import org.springframework.beans.factory.support.BeanDefinitionRegistry;
    import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
    import org.springframework.beans.factory.support.BeanNameGenerator;
    import org.springframework.core.type.AnnotationMetadata;
    
    /**
     * Interface to be implemented by types that register additional bean definitions when
     * processing @{@link Configuration} classes. Useful when operating at the bean definition
     * level (as opposed to {@code @Bean} method/instance level) is desired or necessary.
     *
     * @author Chris Beams
     * @author Juergen Hoeller
     * @since 3.1
     * @see Import
     * @see ImportSelector
     * @see Configuration
     */
    public interface ImportBeanDefinitionRegistrar {
    
    
    	/**
    	 * Register bean definitions as necessary based on the given annotation metadata of
    	 * the importing {@code @Configuration} class.
    	 * <p>Note that {@link BeanDefinitionRegistryPostProcessor} types may <em>not</em> be
    	 * registered here, due to lifecycle constraints related to {@code @Configuration}
    	 * class processing.
    	 * <p>The default implementation is empty.
    	 * @param importingClassMetadata annotation metadata of the importing class
    	 * @param registry current bean definition registry
    	 */
    	default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    	}
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36

    接口中有一个registerBeanDefinitions方法用于向Spring容器注册Bean。其中BeanDefinitionRegistry registry参数,可以获取当前容器内Bean信息,用于作一些条件判断。

# @Conditional

@Conditional注解用于标注在类或者方法上,它表示满足某种条件之后才开始初始化一个Bean或者启动某些配置。

如果一个@Configuration类标记了 @Conditional,则该类中所有标识了@Bean的方法和@Import注解导入的相关类将遵从这些条件。

如果我们需要自定义自己的条件类,所要做的就是实现Condtition接口,并覆盖它的matches()方法。

演示:

  1. 实现Condition接口,覆盖matches()

    public class JdbcTemplateCondition implements Condition {
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            try {
                context.getClassLoader().loadClass("org.springframework.jdbc.core.JdbcTemplate");
                return true;
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            return false;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  2. 当用注解来声明一个Bean时,可以使用这个自定义条件类

    @Conditional(JdbcTemplateCondition.class)
    @Service
    public class JdbcDemoService {
    
    }
    
    1
    2
    3
    4
    5

    它表明只有当JdbcTemplateCondition类的条件成立时才会创建JdbcDemoService这个Bean,也就是说JdbcDemoService这个Bean的创建条件是classpath里面包括JdbcTemplate,否则这个bean的声明就会被忽略掉。

# @ConfigurationProperties与@EnableConfigurationProperties

@ConfigurationProperties和@EnableConfigurationProperties注解用于标注在类上,主要用于导入外部配置。

  • @ConfigureProperties

    传送门 (opens new window)

  • @EnableConfigurationProperties

    如果一个配置类只配置@ConfigurationProperties注解,而没有使用@Component,那么在IOC容器中是获取不到properties 配置文件转化的Bean。

    @EnableConfigurationProperties的作用相当于把使用@ConfigurationProperties的类进行一次IOC容器注入。

    @EnableConfigurationProperties=@Component+@ConfigurationProperties

    1. 使用 @EnableConfigurationProperties 进行注册

      @ConfigurationProperties(prefix = "service.properties")
      public class HelloServiceProperties {
          private static final String SERVICE_NAME = "test-service";
      
          private String msg = SERVICE_NAME;
             set/get
      }
      
      
      @Configuration
      @EnableConfigurationProperties(HelloServiceProperties.class)
      @ConditionalOnClass(HelloService.class)
      @ConditionalOnProperty(prefix = "hello", value = "enable", matchIfMissing = true)
      public class HelloServiceAutoConfiguration {
      
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16

      若HelloServiceAutoConfiguration 头部不使用 @EnableConfigurationProperties,则访问不到HelloServiceProperties内的属性。

    2. 不使用 @EnableConfigurationProperties 进行注册,使用 @Component 注册

      @ConfigurationProperties(prefix = "service.properties")
      @Component
      public class HelloServiceProperties {
          private static final String SERVICE_NAME = "test-service";
      
          private String msg = SERVICE_NAME;
      
          public String getMsg() {
              return msg;
          }
      
          public void setMsg(String msg) {
              this.msg = msg;
          }
      
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16

      如果注释掉 @Component ,则无法在容器内获取HelloServiceProperties对象,也就获取不到其属性。

# AOP

除了控制反转(IoC)和依赖注入(DI)外,Spring 框架还提供了对面向切面编程(AOP)的支持。

AOP 的全称是“Aspect Oriented Programming”,译为“面向切面编程”,和 OOP(面向对象编程)类似,它也是一种编程思想。

应用程序的组成

通常情况下,我们会根据业务使用 OOP(面向对象)思想,将应用划分为多个不同的业务模块,每个模块的核心功能都只为特定的业务领域提供服务,例如电商系统中的订单模块、商品模块、库存模块就分别是为维护电商系统的订单信息、商品信息以及库存信息而服务的。

但除此之外,应用中往往还存在一些非业务的通用功能,例如日志管理、权限管理、事务管理、异常管理等。这些通用功能虽然与应用的业务无关,但几乎所有的业务模块都会使用到它们,因此这些通用功能代码就只能横向散布式地嵌入到多个不同的业务模块之中。这无疑会产生大量重复性代码,不利于各个模块的复用。

您可能会想,可以将这些重复性代码封装成为公共函数,然后在业务模块中显式的调用,不也能减少重复性代码吗?是的,这样做的确能一定程度上减少重复性代码,但这样也增加了业务代码与公共函数的耦合性,任何对于公共函数的修改都会对所有与之相关的业务代码造成影响。

# 面向切面编程(AOP)

与 OOP 中纵向的父子继承关系不同,AOP 是通过横向的抽取机制实现的。它将应用中的一些非业务的通用功能抽取出来单独维护,并通过声明的方式(例如配置文件、注解等)定义这些功能要以何种方式作用在那个应用中,而不是在业务模块的代码中直接调用。

这虽然设计公共函数有几分类似,但传统的公共函数除了在代码直接硬调用之外并没有其他手段。AOP 则为这一问题提供了一套灵活多样的实现方法(例如 Proxy 代理、拦截器、字节码翻译技术等),可以在无须修改任何业务代码的基础上完成对这些通用功能的调用和修改

AOP编程和OOP编程的目标是一致,都是为了减少程序中的重复性代码,让开发人员有更多的精力专注于业务逻辑的开发,只不过两者的实现方式不相同。

OOP就像一根"绣花针",是一种婉约派的选择,它使用继承和组合方式,仔细地为所有涉及通用功能的模块编制成一套类和对象的体系,以达到减少重复性代码的目标。而AOP则更像一把"砍柴刀",是一种豪放派的选择,大刀阔斧的规定,凡是某包某类下的某方法都一并进行处理。

AOP不是用来替换OOP的,而是OOP的一种延伸,用来解决OOP编程中遇到的问题。

# AOP联盟

为了更好的应用 AOP 技术,技术专家们成立了 AOP 联盟(AOP Alliance)。

AOP 联盟定义了一套用于规范 AOP 实现的底层 API,通过这些统一的底层 API,使得各个AOP 框架及工具产品之间可以相互移植。这些 API 主要以标准接口的形式提供,是 AOP 编程思想所要解决各种问题的最高抽象。所有的 AOP 框架都应该是对这些 AOP 接口规范的具体实现,因此通常我们也将 AOP 框架称作 AOP 实现。

目前最流行的 AOP 实现(框架)主要有两个,分别为 Spring AOP (opens new window)AspectJ (opens new window)

AOP 框架 说明
Spring AOP 是一款基于 AOP 编程的框架,它能够有效的减少系统间的重复代码,达到松耦合的目的。 Spring AOP 使用纯 Java 实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码。Spring AOP 支持 2 种代理方式,分别是基于接口的 JDK 动态代理和基于继承的 CGLIB 动态代理。
AspectJ 是一个基于 Java 语言的 AOP 框架,从 Spring 2.0 开始,Spring AOP 引入了对 AspectJ 的支持。 AspectJ 扩展了 Java 语言,提供了一个专门的编译器,在编译时提供横向代码的植入。

# AOP术语

与大多数的技术一样,AOP 已经形成一套属于自己的概念和术语。

名称 说明
Joinpoint(连接点) AOP 的核心概念,指的是程序执行期间明确定义的一个点,例如方法的调用、类初始化、对象实例化等。 在 Spring 中,连接点则指可以被动态代理拦截目标类的方法。
Pointcut(切入点) 又称切点,指要对哪些 Joinpoint 进行拦截,即被拦截的连接点。
Advice(通知) 指拦截到 Joinpoint 之后要执行的代码,即对切入点增强的内容。
Target(目标) 指代理的目标对象,通常也被称为被通知(advised)对象。
Weaving(织入) 指把增强代码应用到目标对象上,生成代理对象的过程。
Proxy(代理) 指生成的代理对象。
Aspect(切面) 切面是切入点(Pointcut)和通知(Advice)的结合。

Advice 直译为通知,也有人将其翻译为“增强处理”,共有 5 种类型,如下表所示。

通知 说明
before(前置通知) 通知方法在目标方法调用之前执行
after(后置通知) 通知方法在目标方法返回或异常后调用
after-returning(返回后通知) 通知方法会在目标方法返回后调用
after-throwing(抛出异常通知) 通知方法会在目标方法抛出异常后调用
around(环绕通知) 通知方法会将目标方法封装起来

# AOP类型

AOP 可以被分为以下 2 个不同的类型。

  1. 动态AOP

    动态 AOP 的织入过程是在运行时动态执行的。其中最具代表性的动态 AOP 实现就是 Spring AOP,它会为所有被通知的对象创建代理对象,并通过代理对象对被原对象进行增强。

    相较于静态 AOP 而言,动态 AOP 的性能通常较差,但随着技术的不断发展,它的性能也在不断的稳步提升。

    动态 AOP 的优点是它可以轻松地对应用程序的所有切面进行修改,而无须对主程序代码进行重新编译。

  2. 静态AOP

    静态 AOP 是通过修改应用程序的实际 Java 字节码,根据需要修改和扩展程序代码来实现织入过程的。最具代表性的静态 AOP 实现是 AspectJ。

    相较于动态 AOP 来说,性能较好。但它也有一个明显的缺点,那就是对切面的任何修改都需要重新编译整个应用程序。

  • AOP优势

    AOP 是 Spring 的核心之一,在 Spring 中经常会使用 AOP 来简化编程。

    在 Spring 框架中使用 AOP 主要有以下优势。

    • 提供声明式企业服务,特别是作为 EJB 声明式服务的替代品,最重要的是,这种服务是声明式事务管理。
    • 允许用户实现自定义切面。在某些不适合用 OOP 编程的场景中,采用 AOP 来补充。
    • 可以对业务逻辑的各个部分进行隔离,从而使业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时也提高了开发效率。

# 代理模式——AOP底层

参考传送门 (opens new window)

# Spring AOP

Spring AOP 是 Spring 框架的核心模块之一,它使用纯 Java 实现,因此不需要专门的编译过程和类加载器,可以在程序运行期通过代理方式向目标类织入增强代码。

# Spring AOP的代理机制

Spring 在运行期会为目标对象生成一个动态代理对象,并在代理对象中实现对目标对象的增强。

Spring AOP 的底层是通过以下 2 种动态代理机制,为目标对象(Target Bean)执行横向织入的。

代理技术 描述
JDK 动态代理 Spring AOP 默认的动态代理方式,若目标对象实现了若干接口,Spring 使用 JDK 的 java.lang.reflect.Proxy 类进行代理。
CGLIB 动态代理 若目标对象没有实现任何接口,Spring 则使用 CGLIB 库生成目标对象的子类,以实现对目标对象的代理。

注意:由于被标记为 final 的方法是无法进行覆盖的,因此这类方法不管是通过 JDK 动态代理机制还是 CGLIB 动态代理机制都是无法完成代理的。

# Spring AOP的连接点

Spring AOP 并没有像其他 AOP 框架(例如 AspectJ)一样提供了完成的 AOP 功能,它是 Spring 提供的一种简化版的 AOP 组件。其中最明显的简化就是,Spring AOP 只支持一种连接点类型:方法调用。您可能会认为这是一个严重的限制,但实际上 Spring AOP 这样设计的原因是为了让 Spring 更易于访问。

方法调用连接点是迄今为止最有用的连接点,通过它可以实现日常编程中绝大多数与 AOP 相关的有用的功能。如果需要使用其他类型的连接点(例如成员变量连接点),我们可以将 Spring AOP 与其他的 AOP 实现一起使用,最常见的组合就是 Spring AOP + ApectJ。

# Spring AOP通知类型

AOP 联盟为通知(Advice)定义了一个 org.aopalliance.aop.Interface.Advice 接口。

Spring AOP 按照通知(Advice)织入到目标类方法的连接点位置,为 Advice 接口提供了 6 个子接口,如下表。

通知类型 接口 描述
前置通知 org.springframework.aop.MethodBeforeAdvice 在目标方法执行前实施增强。
后置通知 org.springframework.aop.AfterReturningAdvice 在目标方法执行后实施增强。
后置返回通知 org.springframework.aop.AfterReturningAdvice 在目标方法执行完成,并返回一个返回值后实施增强。
环绕通知 org.aopalliance.intercept.MethodInterceptor 在目标方法执行前后实施增强。
异常通知 org.springframework.aop.ThrowsAdvice 在方法抛出异常后实施增强。
引入通知 org.springframework.aop.IntroductionInterceptor 在目标类中添加一些新的方法和属性。

# Spring AOP切面类型

Spring 使用 org.springframework.aop.Advisor 接口表示切面的概念,实现对通知(Adivce)和连接点(Joinpoint)的管理。

在 Spring AOP 中,切面可以分为三类:一般切面、切点切面和引介切面。

  • 一般切面(org.springframework.aop.Advisor)

    Spring AOP 默认的切面类型。由于 Advisor 接口仅包含一个 Advice(通知)类型的属性,而没有定义 PointCut(切入点),因此它表示一个不带切点的简单切面。

    这样的切面会对目标对象(Target)中的所有方法进行拦截并织入增强代码。由于这个切面太过宽泛,因此我们一般不会直接使用。

  • 切点切面(org.springframework.aop.PointcutAdvisor)

    Advisor 的子接口,用来表示带切点的切面,该接口在 Advisor 的基础上还维护了一个 PointCut(切点)类型的属性。

    使用它,我们可以通过包名、类名、方法名等信息更加灵活的定义切面中的切入点,提供更具有适用性的切面。

  • 引介切点(org.springframework.aop.IntroductionAdvisor)

    Advisor 的子接口,用来代表引介切面,引介切面是对应引介增强的特殊的切面,它应用于类层面上,所以引介切面适用 ClassFilter 进行定义。

# 一般切面

当我们在使用 Spring AOP 开发时,若没有对切面进行具体定义,Spring AOP 会通过 Advisor 为我们定义一个一般切面(不带切点的切面),然后对目标对象(Target)中的所有方法连接点进行拦截,并织入增强代码。

下面演示一般切面的AOP开发流程。

  1. 导入依赖。

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-expression -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-expression</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>
    
    </dependencies>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
  2. 创建UserDao接口,代码如下。

    package top.snake8859.dao;
    
    public interface UserDao {
        public void add();
        public void delete();
        public void modify();
        public void get();
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
  3. 创建 UserDao 的实现类 UserDaoImpl,代码如下。

    package top.snake8859.dao.impl;
    
    import top.snake8859.dao.UserDao;
    
    public class UserDaoImpl implements UserDao {
        @Override
        public void add() {
            System.out.println("正在执行 UserDao 的 add() 方法……");
        }
        @Override
        public void delete() {
            System.out.println("正在执行 UserDao 的 delete() 方法……");
        }
        @Override
        public void modify() {
            System.out.println("正在执行 UserDao 的 modify() 方法……");
        }
        @Override
        public void get() {
            System.out.println("正在执行 UserDao 的 get() 方法……");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
  4. 创建一个名为 UserDaoBeforeAdvice 的前置增强类,代码如下。

    package top.snake8859.advice;
    
    import org.springframework.aop.MethodBeforeAdvice;
    
    import java.lang.reflect.Method;
    
    public class UserDaoBeforeAdvice implements MethodBeforeAdvice {
        @Override
        public void before(Method method, Object[] objects, Object o) throws Throwable {
            System.out.println("正在执行前置增强操作…………");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  5. 在 Spring 的配置文件 Beans.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"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
                http://www.springframework.org/schema/context/spring-context.xsd">
    
        <!--Advisor: 代表一般切面, Advice本身就是一个切面, 对目标类所有方法进行拦截(* 不带有切点的切面.针对所有方法进行拦截)-->
        <!--定义目标(target)对象-->
        <bean id="userDao" class="top.snake8859.dao.impl.UserDaoImpl"></bean>
    
        <!--定义增强-->
        <bean id="beforeAdvice" class="top.snake8859.advice.UserDaoBeforeAdvice"></bean>
    
        <!--通过配置生成代理UserDao的代理对象-->
        <bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
            <!--设置目标对象-->
            <property name="target" ref="userDao"/>
            <!--设置实现的接口, value中写接扣的全路径-->
            <property name="proxyInterfaces" value="top.snake8859.dao.UserDao"/>
            <!--需要使用value: 增强Bean的名称-->
            <property name="interceptorNames" value="beforeAdvice"/>
        </bean>
    
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

    Spring 能够基于 org.springframework.aop.framework.ProxyFactoryBean 类,根据目标对象的类型(是否实现了接口)自动选择使用 JDK 动态代理或 CGLIB 动态代理机制,为目标对象(Target Bean)生成对应的代理对象(Proxy Bean)。

    ProxyFactoryBean 的常用属性如下表所示。

    属性 描述
    target 需要代理的目标对象(Bean)
    proxyInterfaces 代理需要实现的接口,如果需要实现多个接口,可以通过 <list>元素进行赋值。
    proxyTargetClass 针对类的代理,该属性默认取值为 false(可省略), 表示使用 JDK 动态代理;取值为 true,表示使用 CGlib 动态代理
    interceptorNames 拦截器的名字,该属性的取值既可以是拦截器、也可以是 Advice(通知)类型的 Bean,还可以是切面(Advisor)的 Bean。
    singleton 返回的代理对象是否为单例模式,默认值为 true。
    optimize 是否对创建的代理进行优化(只适用于CGLIB)
  6. 在MainApp 的类测试,代码如下。

    package top.snake8859;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import top.snake8859.dao.UserDao;
    
    public class MainApp {
        public static void main(String[] args) {
            // 获取ApplicationContext容器
            ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
            // 获取代理对象
            UserDao userDao = context.getBean("userDaoProxy", UserDao.class);
            //调用 UserDao 中的各个方法
            userDao.add();
            userDao.delete();
            userDao.get();
            userDao.modify();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
  7. 输出结果如下。

    正在执行前置增强操作…………
    正在执行 UserDao 的 add() 方法……
    正在执行前置增强操作…………
    正在执行 UserDao 的 delete() 方法……
    正在执行前置增强操作…………
    正在执行 UserDao 的 get() 方法……
    正在执行前置增强操作…………
    正在执行 UserDao 的 modify() 方法……
    
    1
    2
    3
    4
    5
    6
    7
    8

# 切点切面

PointCutAdvisor 是 Adivsor 接口的子接口,用来表示带切点的切面。使用它,我们可以通过包名、类名、方法名等信息更加灵活的定义切面中的切入点,提供更具有适用性的切面。

Spring 提供了多个 PointCutAdvisor 的实现,其中常用实现类如如下。

  • NameMatchMethodPointcutAdvisor:指定 Advice 所要应用到的目标方法名称,例如 hello* 代表所有以 hello 开头的所有方法。
  • RegExpMethodPointcutAdvisor:使用正则表达式来定义切点(PointCut),RegExpMethodPointcutAdvisor 包含一个 pattern 属性,该属性使用正则表达式描述需要拦截的方法。

下面演示切点切面的开发流程。

  1. 导入依赖

    与一般切面一致依赖。

  2. 创建一个名为 OrderDao和OrderDaoImpl,代码如下。

    package top.snake8859.dao;
    
    public interface OrderDao {
        public void add();
        public void adds();
        public void delete();
        public void modify();
        public void get();
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package top.snake8859.dao.impl;
    
    import top.snake8859.dao.OrderDao;
    
    public class OrderDaoImpl implements OrderDao {
        @Override
        public void add() {
            System.out.println("正在执行 UserDao 的 add() 方法……");
        }
    
        @Override
        public void adds() {
            System.out.println("正在执行 UserDao 的 adds() 方法……");
        }
    
        @Override
        public void delete() {
            System.out.println("正在执行 UserDao 的 delete() 方法……");
        }
    
        @Override
        public void modify() {
            System.out.println("正在执行 UserDao 的 modify() 方法……");
        }
    
        @Override
        public void get() {
            System.out.println("正在执行 UserDao 的 get() 方法……");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
  3. 创建一个名为 OrderDaoAroundAdvice 的环绕增强类,代码如下。

    package top.snake8859.advice;
    
    import org.aopalliance.intercept.MethodInterceptor;
    import org.aopalliance.intercept.MethodInvocation;
    
    public class OrderDaoAroundAdvice implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation methodInvocation) throws Throwable {
            System.out.println("环绕增强前********");
            //执行被代理对象中的逻辑
            Object result = methodInvocation.proceed();
            System.out.println("环绕增强后********");
            return result;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  4. 在 Beans1.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"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
                http://www.springframework.org/schema/context/spring-context.xsd">
    
        <!--带切点的切面-->
        <!--定义目标(target)对象-->
        <bean id="orderDao" class="top.snake8859.dao.impl.OrderDaoImpl"></bean>
    
        <!--定义增强-->
        <bean id="aroundAdvice" class="top.snake8859.advice.OrderDaoAroundAdvice"></bean>
    
        <!--定义切面-->
        <bean id="myPointCutAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
            <!--定义表达式,规定哪些方法进行拦截 .* 表示所有方法-->
            <!--<property name="pattern" value=".*"></property>-->
            <property name="patterns" value="top.snake8859.dao.OrderDao.add.*,top.snake8859.dao.OrderDao.delete.*"></property>
            <property name="advice" ref="aroundAdvice"></property>
        </bean>
    
        <!--Spring通过配置生成代理-->
        <bean id="orderDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
            <!--配置目标-->
            <property name="target" ref="orderDao"/>
            <!-- 针对类的代理,该属性默认取值为 false(可省略), 表示使用 JDK 动态代理;取值为 true,表示使用 CGlib 动态代理-->
            <!--<property name="proxyTargetClass" value="true"></property>-->
            <!-- 在目标上应用增强 -->
            <property name="interceptorNames" value="myPointCutAdvisor"></property>
        </bean>
    
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
  5. 在MainApp 的类测试,代码如下。

    package top.snake8859;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import top.snake8859.dao.OrderDao;
    import top.snake8859.dao.UserDao;
    
    public class MainApp {
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("Beans1.xml");
            //获取代理对象
            OrderDao orderDao = context.getBean("orderDaoProxy", OrderDao.class);
            //调用 OrderDao 中的各个方法
            orderDao.add();
            orderDao.adds();
            orderDao.delete();
            orderDao.get();
            orderDao.modify();
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
  6. 输出如下。

    环绕增强前********
    正在执行 UserDao 的 add() 方法……
    环绕增强后********
    环绕增强前********
    正在执行 UserDao 的 adds() 方法……
    环绕增强后********
    环绕增强前********
    正在执行 UserDao 的 delete() 方法……
    环绕增强后********
    正在执行 UserDao 的 get() 方法……
    正在执行 UserDao 的 modify() 方法……
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

# 自动代理(常用)

在前面的两种Spring AOP开发案例中,所有目标对象(Target Bean)的代理对象(Proxy Bean)都是在 XML 配置中通过 ProxyFactoryBean 创建的。但在实际开发中,一个项目中往往包含非常多的 Bean, 如果每个 Bean 都通过 ProxyFactoryBean 创建,那么开发和维护成本会十分巨大。为了解决这个问题,Spring 为我们提供了自动代理机制。

Spring 提供的自动代理方案,都是基于后处理 Bean 实现的,即在 Bean 创建的过程中完成增强,并将目标对象替换为自动生成的代理对象。通过 Spring 的自动代理,我们在程序中直接拿到的 Bean 就已经是 Spring 自动生成的代理对象了。

Spring 为我们提供了 3 种自动代理方案:

  • BeanNameAutoProxyCreator:根据 Bean 名称创建代理对象。
  • DefaultAdvisorAutoProxyCreator:根据 Advisor 本身包含信息创建代理对象。
  • AnnotationAwareAspectJAutoProxyCreator:基于 Bean 中的 AspectJ 注解进行自动代理对象。

对 BeanNameAutoProxyCreator 和 DefaultAdvisorAutoProxyCreator 进行演示。

# 根据Bean名称创建代理对象

  1. 创建一个名为 Beans2.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"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
                http://www.springframework.org/schema/context/spring-context.xsd">
    
        <!-- 定义目标(target)对象 -->
        <bean id="userDao" class="top.snake8859.dao.impl.UserDaoImpl"></bean>
        <bean id="orderDao" class="top.snake8859.dao.impl.OrderDaoImpl"></bean>
    
        <!-- 定义增强 -->
        <bean id="beforeAdvice" class="top.snake8859.advice.UserDaoBeforeAdvice"></bean>
        <bean id="aroundAdvice" class="top.snake8859.advice.OrderDaoAroundAdvice"></bean>
    
        <!--Spring自动代理: 根据Bean名称创建代理独享-->
        <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
            <property name="beanNames" value="*Dao"/>
            <property name="interceptorNames" value="beforeAdvice,aroundAdvice"/>
        </bean>
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
  2. 修改 MainApp 中 main 方法,代码如下。

    package top.snake8859;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import top.snake8859.dao.OrderDao;
    import top.snake8859.dao.UserDao;
    
    public class MainApp {
        public static void main(String[] args) {
            //获取 ApplicationContext 容器
            ApplicationContext context = new ClassPathXmlApplicationContext("Beans2.xml");
            //获取代理对象
            UserDao userDao = context.getBean("userDao", UserDao.class);
            //获取代理对象
            OrderDao orderDao = context.getBean("orderDao", OrderDao.class);
            //调用 UserDao 中的各个方法
            userDao.add();
            userDao.delete();
            userDao.modify();
            userDao.get();
            //调用 OrderDao 中的各个方法
            orderDao.add();
            orderDao.adds();
            orderDao.delete();
            orderDao.get();
            orderDao.modify();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
  3. 输出如下。

    正在执行前置增强操作…………
    环绕增强前********
    正在执行 UserDao 的 add() 方法……
    环绕增强后********
    正在执行前置增强操作…………
    环绕增强前********
    正在执行 UserDao 的 delete() 方法……
    环绕增强后********
    正在执行前置增强操作…………
    环绕增强前********
    正在执行 UserDao 的 modify() 方法……
    环绕增强后********
    正在执行前置增强操作…………
    环绕增强前********
    正在执行 UserDao 的 get() 方法……
    环绕增强后********
    正在执行前置增强操作…………
    环绕增强前********
    正在执行 UserDao 的 add() 方法……
    环绕增强后********
    正在执行前置增强操作…………
    环绕增强前********
    正在执行 UserDao 的 adds() 方法……
    环绕增强后********
    正在执行前置增强操作…………
    环绕增强前********
    正在执行 UserDao 的 delete() 方法……
    环绕增强后********
    正在执行前置增强操作…………
    环绕增强前********
    正在执行 UserDao 的 get() 方法……
    环绕增强后********
    正在执行前置增强操作…………
    环绕增强前********
    正在执行 UserDao 的 modify() 方法……
    环绕增强后********
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36

# 根据切面中信息创建代理对象

  1. 创建一个名为 Beans3.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"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
                http://www.springframework.org/schema/context/spring-context.xsd">
        <!-- 定义目标(target)对象 -->
        <bean id="userDao" class="top.snake8859.dao.impl.UserDaoImpl"></bean>
        <bean id="orderDao" class="top.snake8859.dao.impl.OrderDaoImpl"></bean>
    
        <!-- 定义增强 -->
        <bean id="beforeAdvice" class="top.snake8859.advice.UserDaoBeforeAdvice"></bean>
        <bean id="aroundAdvice" class="top.snake8859.advice.OrderDaoAroundAdvice"></bean>
    
        <!--定义切面-->
        <bean id="myPointCutAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
            <!--定义表达式,规定哪些方法进行拦截 .* 表示所有方法-->
            <!--<property name="pattern" value=".*"></property>-->
            <property name="patterns" value="top.snake8859.dao.OrderDao.add.*,top.snake8859.dao.OrderDao.delete.*"></property>
            <property name="advice" ref="aroundAdvice"></property>
        </bean>
    
    
        <!--Spring自动代理: 根据切面myPointCutAdvisor中信息创建代理对象-->
        <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
    
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
  2. 输出如下。

    正在执行 UserDao 的 add() 方法……
    正在执行 UserDao 的 delete() 方法……
    正在执行 UserDao 的 modify() 方法……
    正在执行 UserDao 的 get() 方法……
    环绕增强前********
    正在执行 UserDao 的 add() 方法……
    环绕增强后********
    环绕增强前********
    正在执行 UserDao 的 adds() 方法……
    环绕增强后********
    环绕增强前********
    正在执行 UserDao 的 delete() 方法……
    环绕增强后********
    正在执行 UserDao 的 get() 方法……
    正在执行 UserDao 的 modify() 方法……
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

# Spring AspectJ

Spring AOP 是一个简化版的 AOP 实现,并没有提供完整版的 AOP 功能。通常情况下,Spring AOP 是能够满足我们日常开发过程中的大多数场景的,但在某些情况下,我们可能需要使用 Spring AOP 范围外的某些 AOP 功能。

例如 Spring AOP 仅支持执行公共(public)非静态方法的调用作为连接点,如果我们需要向受保护的(protected)或私有的(private)的方法进行增强,此时就需要使用功能更加全面的 AOP 框架来实现,其中使用最多的就是 AspectJ。

AspectJ 是一个基于 Java 语言的全功能的 AOP 框架,它并不是 Spring 组成部分,是一款独立的 AOP 框架。

但由于 AspectJ 支持通过 Spring 配置 AspectJ 切面,因此它是 Spring AOP 的完美补充,通常情况下,我们都是将 AspectJ 和 Spirng 框架一起使用,简化 AOP 操作。

Spring AspectJ的依赖如下。

<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.6</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.2.9.RELEASE</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.2.9.RELEASE</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 基于XML的AspectJ AOP开发

我们可以在 Spring 项目中通过 XML 配置,对切面(Aspect 或 Advisor)、切点(PointCut)以及通知(Advice)进行定义和管理,以实现基于 AspectJ 的 AOP 开发。

Spring 提供了基于 XML 的 AOP 支持,并提供了一个名为“aop”的命名空间,该命名空间提供了一个 <aop:config> 元素。

  • 在 Spring 配置中,所有的切面信息(切面、切点、通知)都必须定义在<aop:config>元素中;
  • 在 Spring 配置中,可以使用多个 <aop:config>
  • 每一个<aop:config>元素内可以包含 3 个子元素: pointcut、advisor 和 aspect ,这些子元素必须按照这个顺序进行声明。

# 引入aop命名空间

首先,我们需要在 XML 配置文件中导入 Spring aop 命名空间的约束,如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd ">
    ...
</beans>
1
2
3
4
5
6
7
8
9
10

# 定义切面<aop:aspect>

在 Spring 配置文件中,使用 <aop:aspect> 元素定义切面。该元素可以将定义好的 Bean 转换为切面 Bean,所以使用 <aop:aspect>之前需要先定义一个普通的 Spring Bean。

<aop:config>
    <aop:aspect id="myAspect" ref="aBean">
        ...
    </aop:aspect>
</aop:config>
1
2
3
4
5

其中,id 用来定义该切面的唯一标识名称,ref 用于引用普通的 Spring Bean。

# 定义切入点<aop:pointcut>

<aop:pointcut>用来定义一个切入点,用来表示对哪个类中的那个方法进行增强。它既可以在<aop:pointcut>元素中使用,也可以在 <aop:aspect> 元素下使用。

  • <aop:pointcut>元素作为 <aop:config>元素的子元素定义时,表示该切入点是全局切入点,它可被多个切面所共享
  • <aop:pointcut>元素作为<aop:aspect>元素的子元素时,表示该切入点只对当前切面有效
<aop:config>
    <aop:pointcut id="myPointCut"
        expression="execution(* top.snake8859.service.*.*(..))"/>
</aop:config>
1
2
3
4

其中,id 用于指定切入点的唯一标识名称,execution 用于指定切入点关联的切入点表达式

execution 的语法格式格式为:

execution([权限修饰符] [返回值类型] [类的完全限定名] [方法名称]([参数列表]) 
1

其中:返回值类型、方法名、参数列表是必须配置的选项,而其它参数则为可选配置项。

  • 返回值类型:*表示可以为任何返回值。如果返回值为对象,则需指定全路径的类名。
  • 类的完全限定名:指定包名 + 类名。
  • 方法名:*代表所有方法,set* 代表以 set 开头的所有方法。
  • 参数列表:(..)代表所有参数;(*)代表只有一个参数,参数类型为任意类型;(*,String)代表有两个参数,第一个参数可以为任何值,第二个为 String 类型的值。

举例 1:对 top.snake8859.dao 包下 UserDao 类中的 add() 方法进行增强,配置如下。

execution(* top.snake8859.dao.UserDao.add(..))
1

举例 2:对 top.snake8859.dao 包下 UserDao 类中的所有方法进行增强,配置如下。

execution(* top.snake8859.dao.UserDao.*(..))
1

举例 3:对 top.snake8859.dao 包下所有类中的所有方法进行增强,配置如下。

execution(* top.snake8859.dao.*.*(..))
1

# 定义通知

AspectJ 支持 5 种类型的 advice,如下。

<aop:aspect id="myAspect" ref="aBean">
    <!-- 前置通知 -->
    <aop:before pointcut-ref="myPointCut" method="..."/>
   
    <!-- 后置通知 -->
    <aop:after-returning pointcut-ref="myPointCut" method="..."/>
    <!-- 环绕通知 -->
    <aop:around pointcut-ref="myPointCut" method="..."/>
    <!-- 异常通知 -->
    <aop:after-throwing pointcut-ref="myPointCut" method="..."/>
    <!-- 最终通知 -->
    <aop:after pointcut-ref="myPointCut" method="..."/>
    .... 
</aop:aspect>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 示例

演示下 Spring 集成 AspectJ 基于 XML 实现 AOP 开发。

  1. 创建一个名为 OrderDao 的接口,代码如下。

    package top.snake8859.dao;
    
    public interface OrderDao {
        public void add();
        public void delete();
        public Integer modify();
        public void get();
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
  2. 创建 OrderDao 的实现类 OrderDaoImpl,代码如下。

    package top.snake8859.dao.impl;
    
    import top.snake8859.dao.OrderDao;
    
    public class OrderDaoImpl implements OrderDao {
        @Override
        public void add() {
            System.out.println("正在执行 OrderDao 中的 add() 方法");
        }
        @Override
        public void delete() {
            System.out.println("正在执行 OrderDao 中的 delete() 方法");
        }
        @Override
        public Integer modify() {
            System.out.println("正在执行 OrderDao 中的 modify() 方法");
            return 1;
        }
        @Override
        public void get() {
            //异常
            int a = 10 / 0;
            System.out.println("正在执行 OrderDao 中的 get() 方法");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
  3. 创建一个名为 MyOrderAspect 的类,代码如下。

    package top.snake8859.aspect;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    
    public class MyOrderAspect {
        public void before() {
            System.out.println("前置增强……");
        }
        public void after() {
            System.out.println("最终增强……");
        }
        public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            System.out.println("环绕增强---前……");
            proceedingJoinPoint.proceed();
            System.out.println("环绕增强---后……");
        }
        public void afterThrow(Throwable exception) {
            System.out.println("异常增强…… 异常信息为:" + exception.getMessage());
        }
        public void afterReturning(Object returnValue) {
            System.out.println("后置返回增强…… 方法返回值为:" + returnValue);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
  4. 在 Spring 配置文件 Beans.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"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-3.0.xsd ">
    
        <!--定义Bean-->
        <bean id="orderDao" class="top.snake8859.dao.impl.OrderDaoImpl"/>
        <!--定义切面-->
        <bean id="myOrderAspect" class="top.snake8859.aspect.MyOrderAspect"/>
    
        <aop:config>
            <!--定义切点-->
            <aop:pointcut id="beforePointCut" expression="execution(* top.snake8859.dao.OrderDao.add(..))"/>
            <aop:pointcut id="throwPointCut" expression="execution(* top.snake8859.dao.OrderDao.get(..))"/>
            <aop:pointcut id="afterReturnPointCut" expression="execution(* top.snake8859.dao.OrderDao.modify(..))"/>
            <aop:pointcut id="afterPointCut" expression="execution(* top.snake8859.dao.OrderDao.*(..))"/>
    
            <!--定义切面-->
            <aop:aspect ref="myOrderAspect">
                <!--前置增强-->
                <aop:before method="before" pointcut-ref="beforePointCut"/>
                <!--后置返回增强-->
                <aop:after-returning method="afterReturning" pointcut-ref="afterReturnPointCut" returning="returnValue"/>
                <!--异常增强-->
                <aop:after-throwing method="afterThrow" pointcut-ref="afterPointCut" throwing="exception"/>
                <!--最终通知-->
                <aop:after method="after" pointcut-ref="afterPointCut"/>
                <!--环绕通知-->
                <aop:around method="around" pointcut-ref="beforePointCut"/>
            </aop:aspect>
        </aop:config>
    
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
  5. 在 MainApp 的类测试,代码如下。

    package top.snake8859;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import top.snake8859.dao.OrderDao;
    
    public class MainApp {
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
            OrderDao orderDao = context.getBean("orderDao", OrderDao.class);
            orderDao.add();
            orderDao.delete();
            orderDao.modify();
            orderDao.get();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  6. 输出结果如下。

    前置增强……
    环绕增强---前……
    正在执行 OrderDao 中的 add() 方法
    环绕增强---后……
    最终增强……
    正在执行 OrderDao 中的 delete() 方法
    最终增强……
    正在执行 OrderDao 中的 modify() 方法
    最终增强……
    后置返回增强…… 方法返回值为:1
    最终增强……
    异常增强…… 异常信息为:/ by zero
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

# 基于注解的AspectJ AOP开发

在 Spring 中,虽然我们可以使用 XML 配置文件可以实现 AOP 开发,但如果所有的配置都集中在 XML 配置文件中,就势必会造成 XML 配置文件过于臃肿,从而给维护和升级带来一定困难。

为此,AspectJ 框架为 AOP 开发提供了一套 @AspectJ 注解。它允许我们直接在 Java 类中通过注解的方式对切面(Aspect)、切入点(Pointcut)和增强(Advice)进行定义,Spring 框架可以根据这些注解生成 AOP 代理。

关于注解的介绍如下表所示。

名称 说明
@Aspect 用于定义一个切面。
@Pointcut 用于定义一个切入点。
@Before 用于定义前置通知,相当于 BeforeAdvice。
@AfterReturning 用于定义后置通知,相当于 AfterReturningAdvice。
@Around 用于定义环绕通知,相当于 MethodInterceptor。
@AfterThrowing 用于定义抛出通知,相当于 ThrowAdvice。
@After 用于定义最终通知,不管是否异常,该通知都会执行。
@DeclareParents 用于定义引介通知,相当于 IntroductionInterceptor(不要求掌握)。

# 启用@AspectJ注解支持

在使用 @AspectJ 注解进行 AOP 开发前,首先我们要先启用 @AspectJ 注解支持。

我们可以通过以下 2 种方式来启用 @AspectJ 注解。

  1. 使用Java配置类启用

    我们可以在 Java 配置类(标注了 @Configuration 注解的类)中,使用 @EnableAspectJAutoProxy 和 @ComponentScan 注解启用 @AspectJ 注解支持。

    @Configuration
    @ComponentScan(basePackages = "top.snake8859") //注解扫描
    @EnableAspectJAutoProxy //开启 AspectJ 的自动代理
    public class AppConfig {
    }
    
    1
    2
    3
    4
    5
  2. 基于XML配置启用

    在 Spring 的 XML 配置文件中,添加以下内容启用 @AspectJ 注解支持。

    <!-- 开启注解扫描 -->
    <context:component-scan base-package="net.biancheng.c"></context:component-scan>
    <!--开启AspectJ 自动代理-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    
    1
    2
    3
    4

# 定义切面Aspect

可以通过 @Aspect 注解将一个 Bean 定义为切面。

在启用了 @AspectJ 注解支持的情况下,Spring 会自动将 IoC 容器(ApplicationContext)中的所有使用了 @Aspect 注解的 Bean 识别为一个切面。

可以在 XML 配置中通过一些配置将这个类定义为一个 Bean,如下。

<bean id = "myAspect" class = "top.snake8859.aspect.MyAspect">
   ...
</bean>
1
2
3

在定义完 Bean 后,我们只需要在Bean 对应的 Java 类中使用一个 @Aspect 注解,将这个 Bean 定义为一个切面,代码如下。

package top.snake8859.aspect;
import org.aspectj.lang.annotation.*;
@Aspect //定义为切面
public class MyAspect {
}
1
2
3
4
5

也可以在 Java 类上使用以下 2 个注解,使用全注解方式定义切面。

package top.snake8859.aspect;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component // 定义成 Bean
@Aspect //定义为切面
public class MyAspect {
}
1
2
3
4
5
6
7

在以上代码中共使用两个注解:

  • @Component 注解:将这个类的对象定义为一个 Bean;
  • @Aspect 注解:则是将这个 Bean 定义为一个切面。

# 定义切片@PointCut

在 AspectJ 中,我们可以使用 @Pointcut 注解用来定义一个切点。需要注意的是,定义为切点的方法,它的返回值类型必须为 void,示例代码如下。

// 要求:方法必须是private,返回值类型为 void,名称自定义,没有参数
@Pointcut("execution(*net.biancheng..*.*(..))")
private void myPointCut() {
}
1
2
3
4

@Pointcut 注解中有一个 value 属性,这个属性的值就是切入点表达式。

值得注意的是,我除了可以通过切入点表达式(execution)直接对切点进行定义外,还可以通过切入点方法的名称来引用其他的切入点。在使用方法名引用其他切入点时,还可以使用“&&”、“||”和“!”等表示“与”、“或”、“非”的含义,示例代码如下。

/**
* 将 top.snake8859.dao包下 UserDao 类中的 get() 方法定义为一个切点
*/
@Pointcut(value ="execution(* top.snake8859.dao.UserDao.get(..))")
public void pointCut1(){
}
/**
* 将 top.snake8859.dao包下 UserDao 类中的 delete() 方法定义为一个切点
*/
@Pointcut(value ="execution(* top.snake8859.dao.UserDao.delete(..))")
public void pointCut2(){
}
/**
* 除了 top.snake8859.dao包下 UserDao 类中 get() 方法和 delete() 方法外,其他方法都定义为切点
*
* ! 表示 非 ,即 "不是" 的含义,求补集
* * && 表示 与,即 ”并且“ ,求交集
* || 表示 或,即 “或者”,求并集
*/
@Pointcut(value ="!pointCut1() && !pointCut2()")
public void pointCut3(){
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 定义通知

AspectJ 为我们提供了以下 6 个注解,来定义 6 种不同类型的通知(Advice),如下表。

注解 说明
@Before 用于定义前置通知,相当于 BeforeAdvice。
@AfterReturning 用于定义后置通知,相当于 AfterReturningAdvice。
@Around 用于定义环绕通知,相当于 MethodInterceptor。
@AfterThrowing 用于定义抛出通知,相当于 ThrowAdvice。
@After 用于定义最终通知,不管是否异常,该通知都会执行。
@DeclareParents 用于定义引介通知,相当于 IntroductionInterceptor(不要求掌握)。

以上这些通知注解中都有一个 value 属性,这个 value 属性的取值就是这些通知(Advice)作用的切点(PointCut),它既可以是切入点表达式,也可以是切入点的引用(切入点对应的方法名称),示例代码如下。

@Pointcut(value ="execution(* top.snake8859.dao.UserDao.get(..))")
public void pointCut1(){
}
@Pointcut(value ="execution(* top.snake8859.dao.UserDao.delete(..))")
public void pointCut2(){
}
@Pointcut(value ="!pointCut1() && !pointCut2()")
public void pointCut3(){
}
//使用切入点引用
@Before("MyAspect.pointCut3()")
public void around() throws Throwable {
    System.out.println("环绕增强……");
}
//使用切入点表达式
@AfterReturning(value = "execution(* top.snake8859.dao.UserDao.get(..))" ,returning = "returnValue")
public void afterReturning(Object returnValue){
    System.out.println("方法返回值为:"+returnValue);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 示例

演示下如何通过注解的方式实现 AspectJ AOP 开发。

  1. 创建一个名为 UserDao 的接口,代码如下。

    package top.snake8859.dao;
    public class UserDao {
        public void add();
        public void delete();
        public int modify();
        public void get();
    }
    
    1
    2
    3
    4
    5
    6
    7
  2. 创建 UserDao 的实现类 UserDaoImpl,代码如下。

    package top.snake8859.dao.impl;
    
    import org.springframework.stereotype.Component;
    import top.snake8859.dao.UserDao;
    
    @Component("userDao")
    public class UserDaoImpl implements UserDao {
        @Override
        public void add() {
            System.out.println("正在执行 UserDao 的 add 方法");
        }
        @Override
        public void delete() {
            System.out.println("正在执行 UserDao 的 delete 方法");
        }
        @Override
        public int modify() {
            System.out.println("正在执行 UserDao 的 modify 方法");
            return 1;
        }
        @Override
        public void get() {
            System.out.println("正在执行 UserDao 的 get 方法");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
  3. 在 Spring 配置文件 Beans1.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"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    
        <!-- 开启注解扫描 -->
        <context:component-scan base-package="top.snake8859"/>
        <!--开启AspectJ 自动代理-->
        <aop:aspectj-autoproxy/>
    
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
  4. 创建一个名为 MyAspect 的切面类,代码如下。

    package top.snake8859.aspect;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    
    @Component // 定义成Bean
    @Aspect //定义为切面
    public class MyUserAspect {
        @Before("execution(* top.snake8859.dao.UserDao.add(..))")
        public void before(JoinPoint joinPoint){
            System.out.println("前置增强……" + joinPoint);
        }
    
        @After("execution(* top.snake8859.dao.UserDao.get(..))")
        public void after(JoinPoint joinPoint) {
            System.out.println("最终增强……" + joinPoint);
        }
    
        /**
         * 将 top.snake8859.dao.包下的 UserDao 类中的 get() 方法 定义为一个切点
         */
        @Pointcut(value = "execution(* top.snake8859.dao.UserDao.get(..))")
        public void pointCut1() {
        }
        /**
         * 将 top.snake8859.dao.包下的 UserDao 类中的 delete() 方法 定义为一个切点
         */
        @Pointcut(value = "execution(* top.snake8859.dao.UserDao.delete(..))")
        public void pointCut2() {
        }
    
        //使用切入点引用
        @Around("MyUserAspect.pointCut2()")
        public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            System.out.println("环绕增强……1");
            proceedingJoinPoint.proceed();
            System.out.println("环绕增强……2");
        }
    
        //使用切入点表达式
        @AfterReturning(value = "execution(* top.snake8859.dao.UserDao.modify(..))", returning = "returnValue")
        public void afterReturning(Object returnValue) {
            System.out.println("后置返回增强……,方法返回值为:" + returnValue);
        }
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
  5. 在MainApp 的类,测试代码如下。

    package top.snake8859;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import top.snake8859.dao.UserDao;
    
    public class MainApp {
        public static void main(String[] args) {
            ApplicationContext context2 = new ClassPathXmlApplicationContext("Beans1.xml");
            UserDao userDao = context2.getBean("userDao", UserDao.class);
            userDao.add();
            userDao.modify();
            userDao.delete();
            userDao.get();
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  6. 输出如下。

    前置增强……execution(void top.snake8859.dao.UserDao.add())
    正在执行 UserDao 的 add 方法
    正在执行 UserDao 的 modify 方法
    后置返回增强……,方法返回值为:1
    环绕增强……1
    正在执行 UserDao 的 delete 方法
    环绕增强……2
    正在执行 UserDao 的 get 方法
    最终增强……execution(void top.snake8859.dao.UserDao.get())
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

# Spring JDBC

JDBC 是 Java 提供的一种用于执行 SQL 语句的 API,可以对多种关系型数据库(例如 MySQL、Oracle 等)进行访问。但在实际的企业级应用开发中,却很少有人直接使用原生的 JDBC API 进行开发,这是因为使用 JDBC API 对数据库进行操作十分繁琐,需要我们对每一步都做到“步步把控,处处关心”,例如我们需要手动控制数据库连接的开启,异常处理、事务处理、最后还要手动关闭连接释放资源等等。

Spring 提供了一个 Spring JDBC 模块,它对 JDBC API 进行了封装,其的主要目的降低 JDBC API 的使用难度,以一种更直接、更简洁的方式使用 JDBC API。

使用 Spring JDBC,开发人员只需要定义必要的参数、指定需要执行的 SQL 语句,即可轻松的进行 JDBC 编程,对数据库进行访问。

至于驱动的加载、数据库连接的开启与关闭、SQL 语句的创建与执行、异常处理以及事务处理等繁杂乏味的工作,则都是由 Spring JDBC 完成的。这样就可以使开发人员从繁琐的 JDBC API 中解脱出来,有更多的精力专注于业务的开发。

Spring JDBC 提供了多个实用的数据库访问工具,以简化 JDBC 的开发,其中使用最多就是 JdbcTemplate。

# JdbcTemplate

JdbcTemplate 是 Spring JDBC 核心包(core)中的核心类,它可以通过配置文件、注解、Java 配置类等形式获取数据库的相关信息,实现了对 JDBC 开发过程中的驱动加载、连接的开启和关闭、SQL 语句的创建与执行、异常处理、事务处理、数据类型转换等操作的封装。我们只要对其传入SQL 语句和必要的参数即可轻松进行 JDBC 编程。

JdbcTemplate 的全限定命名为 org.springframework.jdbc.core.JdbcTemplate,它提供了大量的查询和更新数据库的方法,如下所示。

  • public int update(String sql) | public int update(String sql,Object... args)

    用于执行新增、更新、删除等语句;

    • sql:需要执行的 SQL 语句;
    • args 表示需要传入到 SQL 语句中的参数。
  • public void execute(String sql) | public T execute(String sql, PreparedStatementCallback action)

    可以执行任意 SQL,一般用于执行 DDL 语句;

    • sql:需要执行的 SQL 语句;
    • action 表示执行完 SQL 语句后,要调用的函数。
  • public <T> List<T> query(String sql, RowMapper<T> rowMapper, @Nullable Object... args)

    public <T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args)

    用于执行查询语句;

    • sql:需要执行的 SQL 语句;
    • rowMapper:用于确定返回的集合(List)的类型;
    • args:表示需要传入到 SQL 语句的参数。
  • public int[] batchUpdate(String sql, List<Object[]> batchArgs, final int[] argTypes)

    用于批量执行新增、更新、删除等语句;

    • sql:需要执行的 SQL 语句;
    • argTypes:需要注入的 SQL 参数的 JDBC 类型;
    • batchArgs:表示需要传入到 SQL 语句的参数。

# 示例

  1. 在 MySQL 数据库中创建一个 spring_jdbc 数据库实例,并执行以下 SQL 语句创建一个用户信息(user)表。

    DROP TABLE IF EXISTS `user`;
    CREATE TABLE `user` (
      `user_id` int NOT NULL AUTO_INCREMENT COMMENT '用户 ID',
      `user_name` varchar(255) DEFAULT NULL COMMENT '用户名',
      `status` varchar(255) DEFAULT NULL COMMENT '用户状态',
      PRIMARY KEY (`user_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
    
    1
    2
    3
    4
    5
    6
    7
  2. 导入Maven依赖。

     <dependencies>
            <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>5.2.9.RELEASE</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-core</artifactId>
                <version>5.2.9.RELEASE</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-beans</artifactId>
                <version>5.2.9.RELEASE</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aop</artifactId>
                <version>5.2.9.RELEASE</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.springframework/spring-expression -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-expression</artifactId>
                <version>5.2.9.RELEASE</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.springframework/spring-tx -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-tx</artifactId>
                <version>5.2.9.RELEASE</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aop</artifactId>
                <version>5.2.9.RELEASE</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-jdbc</artifactId>
                <version>5.2.0.RELEASE</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
            <dependency>
                <groupId>commons-logging</groupId>
                <artifactId>commons-logging</artifactId>
                <version>1.2</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.22</version>
            </dependency>
    
        </dependencies>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
  3. 在 resource 目录下创建一个 jdbc.properties,并在该配置文件中对数据库连接信息进行配置。

    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://127.0.0.1:3306/springjdbc?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    jdbc.username=root
    jdbc.password=root
    
    1
    2
    3
    4

    注意jdbc.url的编码和时区设置

  4. 在resource目录下创建一个 XML 配置文件 Beans.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"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
                http://www.springframework.org/schema/context/spring-context.xsd">
        <!--开启组件扫描-->
        <context:component-scan base-package="top.snake8859"/>
        <!--引入db.properties中的配置-->
        <context:property-placeholder location="classpath:db.properties"/>
    
        <!--定义数据源 Bean-->
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <!--数据库连接地址-->
            <property name="url" value="${jdbc.url}"/>
            <!--数据库的用户名-->
            <property name="username" value="${jdbc.username}"/>
            <!--数据库的密码-->
            <property name="password" value="${jdbc.password}"/>
            <!--数据库驱动-->
            <property name="driverClassName" value="${jdbc.driver}"/>
        </bean>
    
        <!--定义JdbcTemplate Bean-->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <!--将数据源的Bean注入到JdbcTemlate内-->
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32

    在以上配置中,我们共定义了两个 Bean,

    • dataSource 为数据库连接池对象的 Bean。
    • jdbcTemplate 则为 JdbcTemplate 的 Bean,它由一个名为 datasSource 的属性。

    Spring 默认使用 DriverManagerDataSource 对数据库连接池进行管理,我们可以在 Spring 的 XML 配置文件中定义 DriverManagerDataSource 的 Bean,并注入到 JdbcTempate 的 Bean 中。

    在 dataSource 中,定义了 4 个连接数据库的属性,如下表所示。

    属性名 说明
    driverClassName 所使用的驱动名称,对应驱动 JAR 包中的 Driver 类
    url 数据源所在地址
    username 访问数据库的用户名
    password 访问数据库的密码
  5. 创建名为 User 的实体类,代码如下。

    package top.snake8859.pojo;
    
    public class User {
        private Integer userId;
        private String userName;
        private String status;
    
        public Integer getUserId() {
            return userId;
        }
    
        public void setUserId(Integer userId) {
            this.userId = userId;
        }
    
        public String getUserName() {
            return userName;
        }
    
        public void setUserName(String userName) {
            this.userName = userName;
        }
    
        public String getStatus() {
            return status;
        }
    
        public void setStatus(String status) {
            this.status = status;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "userId=" + userId +
                    ", userName='" + userName + '\'' +
                    ", status='" + status + '\'' +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
  6. 创建一个名为 UserDao 的 Dao 接口,代码如下。

    package top.snake8859.dao;
    
    import top.snake8859.pojo.User;
    
    import java.util.List;
    
    public interface UserDao {
        /**
         * 新增一条用户
         *
         * @param user
         * @return
         */
        int addUer(User user);
        /**
         * 更新指定的用户信息
         *
         * @param user
         * @return
         */
        int update(User user);
        /**
         * 删除指定的用户信息
         *
         * @param user
         * @return
         */
        int delete(User user);
        /**
         * 统计用户个数
         *
         * @param user
         * @return
         */
        int count(User user);
        /**
         * 查询用户列表
         *
         * @param user
         * @return
         */
        List<User> getList(User user);
        /**
         * 查询单个用户信息
         *
         * @param user
         * @return
         */
        User getUser(User user);
        /**
         * 批量增加用户
         *
         * @param batchArgs
         */
        void batchAddUser(List<Object[]> batchArgs);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
  7. 创建 UserDao 的实现类 UserDaoImpl,代码如下。

    package top.snake8859.dao.impl;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.BeanPropertyRowMapper;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    import top.snake8859.dao.UserDao;
    import top.snake8859.pojo.User;
    
    import java.util.List;
    @Repository
    public class UserDaoImpl implements UserDao {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
        
        @Override
        public int addUer(User user) {
            String sql = "INSERT into `user` (`user`.user_name,`user`.`status`) VALUES(?,?);";
            int i = jdbcTemplate.update(sql, user.getUserName(), user.getStatus());
            return i;
        }
    
        @Override
        public int update(User user) {
            String sql = "UPDATE `user` SET status=? WHERE user_name=?;";
            int update = jdbcTemplate.update(sql, user.getStatus(), user.getUserName());
            return update;
        }
    
        @Override
        public int delete(User user) {
            String sql = "DELETE FROM `user` where user_name=?;";
            return jdbcTemplate.update(sql, user.getUserName());
        }
    
        @Override
        public int count(User user) {
            String sql = "SELECT COUNT(*) FROM `user` where `status`=?;";
            Integer count = jdbcTemplate.queryForObject(sql, Integer.class, user.getStatus());
            return count;
        }
    
        @Override
        public List<User> getList(User user) {
            String sql = "SELECT * FROM `user` where `status`=?;";
            List<User> userList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<User>(User.class), user.getStatus());
            return userList;
        }
    
        @Override
        public User getUser(User user) {
            String sql = "SELECT * FROM `user` where `user_id`=?;";
            User user1 = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<User>(User.class), user.getUserId());
            return user1;
        }
    
        @Override
        public void batchAddUser(List<Object[]> batchArgs) {
            String sql = "INSERT into `user` (`user`.user_name,`user`.`status`) VALUES(?,?);";
            int[] adds = jdbcTemplate.batchUpdate(sql, batchArgs);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
  8. 创建一个名为 UserService 的 Service 接口,代码如下。

    package top.snake8859.service;
    
    import top.snake8859.pojo.User;
    
    import java.util.List;
    
    public interface UserService {
        /**
         * 新增用户数据
         *
         * @param user
         * @return
         */
        public int addUser(User user);
        /**
         * 更新用户数据
         *
         * @param user
         * @return
         */
        public int updateUser(User user);
        /**
         * 删除用户数据
         *
         * @param user
         * @return
         */
        public int deleteUser(User user);
        /**
         * 统计用户数量
         *
         * @param user
         * @return
         */
        public int countUser(User user);
        /**
         * 查询用户数据
         *
         * @param user
         * @return
         */
        public List<User> getUserList(User user);
        /**
         * 查询单个用户信息
         *
         * @param user
         * @return
         */
        public User getUser(User user);
        /**
         * 批量添加用户
         *
         * @param batchArgs
         */
        public void batchAddUser(List<Object[]> batchArgs);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
  9. 创建 UserService 的实现类 UserServiceImpl,代码如下。

    package top.snake8859.service.impl;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import top.snake8859.dao.UserDao;
    import top.snake8859.pojo.User;
    import top.snake8859.service.UserService;
    
    import java.util.List;
    @Service
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private UserDao userDao;
    
        @Override
        public int addUser(User user) {
            return userDao.addUer(user);
        }
    
        @Override
        public int updateUser(User user) {
            return userDao.update(user);
        }
    
        @Override
        public int deleteUser(User user) {
            return userDao.delete(user);
        }
    
        @Override
        public int countUser(User user) {
            return userDao.count(user);
        }
    
        @Override
        public List<User> getUserList(User user) {
            return userDao.getList(user);
        }
    
        @Override
        public User getUser(User user) {
            return userDao.getUser(user);
        }
    
        @Override
        public void batchAddUser(List<Object[]> batchArgs) {
            userDao.batchAddUser(batchArgs);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
  10. 创建一个名为 MainApp 的类,代码如下。

    package top.snake8859;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import top.snake8859.pojo.User;
    import top.snake8859.service.UserService;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class MainApp {
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
            UserService userService = context.getBean("userService", UserService.class);
    
            User user = new User();
            user.setUserName("小张");
            user.setStatus("离线线");
    
            //新增一个用户
            int i = userService.addUser(user);
            System.out.println("新增用户成功!");
    
            //修改一个用户
            User user1 = new User();
            user1.setUserName("小张");
            user1.setStatus("在线");
            int u = userService.updateUser(user1);
            System.out.println("修改用户成功");
    
            // 批量新增用户
            List<Object[]> batchArgs = new ArrayList<>();
            Object[] o1 = {"小明", "在线"};
            Object[] o2 = {"小龙", "离线"};
            Object[] o3 = {"小林", "在线"};
            Object[] o4 = {"小李", "在线"};
            batchArgs.add(o1);
            batchArgs.add(o2);
            batchArgs.add(o3);
            batchArgs.add(o4);
            userService.batchAddUser(batchArgs);
            System.out.println("批量增加完毕");
    
            //查询在线用户
            User user2 = new User();
            user2.setStatus("在线");
            int i1 = userService.countUser(user2);
            System.out.println("在线用户的个数为:" + i1);
    
            List<User> userList = userService.getUserList(user2);
            System.out.println("在线用户列表查询成功!");
            for (User user4 : userList) {
                System.out.println("用户 ID:" + user4.getUserId() + ",用户名:" + user4.getUserName() + ",状态:" + user4.getStatus());
            }
    
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
  11. 输出结果如下。

    新增用户成功!
    修改用户成功
    批量增加完毕
    在线用户的个数为:4
    在线用户列表查询成功!
    用户 ID:6,用户名:小张,状态:在线
    用户 ID:7,用户名:小明,状态:在线
    用户 ID:9,用户名:小林,状态:在线
    用户 ID:10,用户名:小李,状态:在线
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

# Spring 事务

事务(Transaction)是基于关系型数据库(RDBMS)的企业应用的重要组成部分。在软件开发领域,事务扮演者十分重要的角色,用来确保应用程序数据的完整性和一致性。

事务具有 4 个特性:原子性、一致性、隔离性和持久性,简称为 ACID 特性。

  • 原子性(Atomicity):一个事务是一个不可分割的工作单位,事务中包括的动作要么都做要么都不做。
  • 一致性(Consistency):事务必须保证数据库从一个一致性状态变到另一个一致性状态,一致性和原子性是密切相关的。
  • 隔离性(Isolation):一个事务的执行不能被其它事务干扰,即一个事务内部的操作及使用的数据对并发的其它事务是隔离的,并发执行的各个事务之间不能互相打扰。
  • 持久性(Durability):持久性也称为永久性,指一个事务一旦提交,它对数据库中数据的改变就是永久性的,后面的其它操作和故障都不应该对其有任何影响。

事务允许我们将几个或一组操作组合成一个要么全部成功、要么全部失败的工作单元。如果事务中的所有的操作都执行成功,那自然万事大吉。但如果事务中的任何一个操作失败,那么事务中所有的操作都会被回滚,已经执行成功操作也会被完全清除干净,就好像什么事都没有发生一样。

在现实世界中,最常见的与事务相关的例子可能就是银行转账了。假设我们需要将 1000 元从 A 账户中转到 B 账户中,这个转账操作共涉及了以下两个操作。

  • 从 A 账户中扣除 1000 元;
  • 往 B 账户中存入 1000 元。

如果 A 账户成功地扣除了 1000 元,但向 B 账户存入时失败的话,那么我们将凭空损失 1000 元;如果 A 账户扣款时失败,但却成功地向 B 账户存入 1000 元的话,我们的账户就凭空多出了 1000 元,那么银行就会遭受损失。因此我们必须保证事务中的所有操作要么全部成功,要么全部失败,理解了这一点,我们也就抓住了事务的核心。

# 事务管理方式

Spring 支持以下 2 种事务管理方式。

事务管理方式 说明
编程式事务管理 编程式事务管理是通过编写代码实现的事务管理。 这种方式能够在代码中精确地定义事务的边界,我们可以根据需求规定事务从哪里开始,到哪里结束。
声明式事务管理 Spring 声明式事务管理在底层采用了 AOP 技术,其最大的优点在于无须通过编程的方式管理事务,只需要在配置文件中进行相关的规则声明,就可以将事务规则应用到业务逻辑中。

选择编程式事务还是声明式事务,很大程度上就是在控制权细粒度和易用性之间进行权衡。

  • 编程式对事物控制的细粒度更高,我们能够精确的控制事务的边界,事务的开始和结束完全取决于我们的需求,但这种方式存在一个致命的缺点,那就是事务规则与业务代码耦合度高,难以维护,因此我们很少使用这种方式对事务进行管理。
  • 声明式事务易用性更高,对业务代码没有侵入性,耦合度低,易于维护,因此这种方式也是我们最常用的事务管理方式。

Spring 声明式事务管理是通过 AOP 实现的,其本质是对方法前后进行拦截,然后在目标方法开始之前创建(或加入)一个事务,在执行完目标方法后,根据执行情况提交或者回滚事务。

声明式事务最大的优点就是对业务代码的侵入性低,可以将业务代码和事务管理代码很好地进行解耦。

# 事务管理器

Spring 并不会直接管理事务,而是通过事务管理器对事务进行管理的。

在 Spring 中提供了一个 org.springframework.transaction.PlatformTransactionManager 接口,这个接口被称为 Spring 的事务管理器,其源码如下。

public interface PlatformTransactionManager extends TransactionManager {
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
    void commit(TransactionStatus status) throws TransactionException;
    void rollback(TransactionStatus status) throws TransactionException;
}
1
2
3
4
5

该接口中各方法说明如下:

名称 说明
TransactionStatus getTransaction(TransactionDefinition definition) 用于获取事务的状态信息
void commit(TransactionStatus status) 用于提交事务
void rollback(TransactionStatus status) 用于回滚事务

Spring 为不同的持久化框架或平台(例如 JDBC、Hibernate、JPA 以及 JTA 等)提供了不同的 PlatformTransactionManager 接口实现,这些实现类被称为事务管理器实现。

实现类 说明
org.springframework.jdbc.datasource.DataSourceTransactionManager 使用 Spring JDBC 或 iBatis 进行持久化数据时使用。
org.springframework.orm.hibernate3.HibernateTransactionManager 使用 Hibernate 3.0 及以上版本进行持久化数据时使用。
org.springframework.orm.jpa.JpaTransactionManager 使用 JPA 进行持久化时使用。
org.springframework.jdo.JdoTransactionManager 当持久化机制是 Jdo 时使用。
org.springframework.transaction.jta.JtaTransactionManager 使用 JTA 来实现事务管理,在一个事务跨越多个不同的资源(即分布式事务)使用该实现。

这些事务管理器的使用方式十分简单,我们只要根据持久化框架(或平台)选用相应的事务管理器实现,即可实现对事物的管理,而不必关心实际事务实现到底是什么。

# TransactionDefinition 接口

Spring 将 XML 配置中的事务信息封装到对象 TransactionDefinition 中,然后通过事务管理器的 getTransaction() 方法获得事务的状态(TransactionStatus),并对事务进行下一步的操作。

TransactionDefinition 接口提供了获取事务相关信息的方法,接口定义如下。

public interface TransactionDefinition {
    int getPropagationBehavior();
    int getIsolationLevel();
    String getName();
    int getTimeout();
    boolean isReadOnly();
}
1
2
3
4
5
6
7

该接口中方法说明如下。

方法 说明
String getName() 获取事务的名称
int getIsolationLevel() 获取事务的隔离级别
int getPropagationBehavior() 获取事务的传播行为
int getTimeout() 获取事务的超时时间
boolean isReadOnly() 获取事务是否只读
  • 事物隔离级别

    事务的隔离级别定义了一个事务可能受其他并发事务影响的程度。

    在实际应用中,经常会出现多个事务同时对同一数据执行不同操作,来实现各自的任务的情况。此时就有可能导致脏读、幻读以及不可重复读等问题的出现。

    在理想情况下,事务之间是完全隔离的,这自然不会出现上述问题。但完全的事务隔离会导致性能问题,而且并不是所有的应用都需要事务的完全隔离,因此有时应用程序在事务隔离上也有一定的灵活性。

    Spring 中提供了以下隔离级别,我们可以根据自身的需求自行选择合适的隔离级别。

    方法 说明
    ISOLATION_DEFAULT 使用后端数据库默认的隔离级别
    ISOLATION_READ_UNCOMMITTED 允许读取尚未提交的更改,可能导致脏读、幻读和不可重复读
    ISOLATION_READ_COMMITTED Oracle 默认级别,允许读取已提交的并发事务,防止脏读,可能出现幻读和不可重复读
    ISOLATION_REPEATABLE_READ MySQL 默认级别,多次读取相同字段的结果是一致的,防止脏读和不可重复读,可能出现幻读
    ISOLATION_SERIALIZABLE 完全服从 ACID 的隔离级别,防止脏读、不可重复读和幻读
  • 事物的传播行为

    事务传播行为(propagation behavior)指的是,当一个事务方法被另一个事务方法调用时,这个事务方法应该如何运行。例如,事务方法 A 在调用事务方法 B 时,B 方法是继续在调用者 A 方法的事务中运行呢,还是为自己开启一个新事务运行,这就是由事务方法 B 的事务传播行为决定的。

    事务方法指的是能让数据库表数据发生改变的方法,例如新增数据、删除数据、修改数据的方法。

    Spring 提供了以下 7 种不同的事务传播行为。

    名称 说明
    PROPAGATION_MANDATORY 支持当前事务,如果不存在当前事务,则引发异常。
    PROPAGATION_NESTED 如果当前事务存在,则在嵌套事务中执行。
    PROPAGATION_NEVER 不支持当前事务,如果当前事务存在,则引发异常。
    PROPAGATION_NOT_SUPPORTED 不支持当前事务,始终以非事务方式执行。
    PROPAGATION_REQUIRED 默认传播行为,如果存在当前事务,则当前方法就在当前事务中运行,如果不存在,则创建一个新的事务,并在这个新建的事务中运行。
    PROPAGATION_REQUIRES_NEW 创建新事务,如果已经存在事务则暂停当前事务。
    PROPAGATION_SUPPORTS 支持当前事务,如果不存在事务,则以非事务方式执行。

# TransactionStatus 接口

TransactionStatus 接口提供了一些简单的方法,来控制事务的执行、查询事务的状态,接口定义如下。

public interface TransactionStatus extends SavepointManager {
    boolean isNewTransaction();
    boolean hasSavepoint();
    void setRollbackOnly();
    boolean isRollbackOnly();
    boolean isCompleted();
}
1
2
3
4
5
6
7

该接口中各方法说明如下。

名称 说明
boolean hasSavepoint() 获取是否存在保存点
boolean isCompleted() 获取事务是否完成
boolean isNewTransaction() 获取是否是新事务
boolean isRollbackOnly() 获取事务是否回滚
void setRollbackOnly() 设置事务回滚

# 基于XML实现事务管理

# 引入tx命名空间

Spring 提供了一个 tx 命名空间,借助它可以极大地简化 Spring 中的声明式事务的配置。

<?xml version="1.0" encoding="UTF-8"?>
<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"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd">
</beans>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

**注意:**由于 Spring 提供的声明式事务管理是依赖于 Spring AOP 实现的,因此我们在 XML 配置文件中还应该添加与 aop 命名空间相关的配置。

# 配置事务管理器

接下来,我们就需要借助数据源配置,定义相应的事务管理器实现(PlatformTransactionManager 接口的实现类)的 Bean,配置内容如下。

<!--配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <!--数据库连接地址-->
    <property name="url" value="xxx"/>
    <!--数据库的用户名-->
    <property name="username" value="xxx"/>
    <!--数据库的密码-->
    <property name="password" value="xxx"/>
    <!--数据库驱动-->
    <property name="driverClassName" value="xxx"/>
</bean>

<!--配置事物管理器,以JDBC为例-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在以上配置中,配置的事务管理器实现为 DataSourceTransactionManager,即为 JDBC 和 iBatis 提供的 PlatformTransactionManager 接口实现。

# 配置事务通知

在 Spring 的 XML 配置文件中配置事务通知,指定事务作用的方法以及所需的事务属性。

<!--配置通知-->
<tx:advice id="tx-advice" transaction-manager="transactionManager">
    <!--配置事物参数-->
    <tx:attributes>
        <tx:method name="create*" propagation="REQUIRED" isolation="DEFAULT" read-only="false" timeout="10"/>
    </tx:attributes>
</tx:advice>
1
2
3
4
5
6
7

当我们使用 <tx:advice>来声明事务时,需要通过 transaction-manager 参数来定义一个事务管理器,这个参数的取值默认为 transactionManager。

如果我们自己设置的事务管理器恰好与默认值相同,则可以省略对改参数的配置。

对于<tx:advice>来说,事务属性是被定义在<tx:attributes> 中的,该元素可以包含一个或多个 <tx:method>元素。

<tx:method>元素包含多个属性参数,可以为某个或某些指定的方法(name 属性定义的方法)定义事务属性,如下表所示。

# 配置切点切面

<tx:advice> 元素只是定义了一个 AOP 通知,它并不是一个完整的事务性切面。我们在 <tx:advice>元素中并没有定义哪些 Bean 应该被通知,因此我们需要一个切点来做这件事。

在 Spring 的 XML 配置中,我们可以利用 Spring AOP 技术将事务通知(tx-advice)和切点配置到切面中,配置内容如下。

<!--配置切点和切面-->
<aop:config>
    <!--配置切点-->
    <aop:pointcut id="tx-pt" expression="execution(* top.snake8859.service.impl.OrderServiceImpl.*(..))"/>
    <!--配置切面-->
    <aop:advisor advice-ref="tx-advice" pointcut-ref="tx-pt"></aop:advisor>
</aop:config>
1
2
3
4
5
6
7

# 示例

  1. 创建一个springtx数据库实例,执行以下SQL语句。

    DROP TABLE IF EXISTS `account`;
    CREATE TABLE `account` (
                               `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
                               `user_id` bigint DEFAULT NULL COMMENT '用户id',
                               `total` decimal(10,0) DEFAULT NULL COMMENT '总额度',
                               `used` decimal(10,0) DEFAULT NULL COMMENT '已用余额',
                               `residue` decimal(10,0) DEFAULT '0' COMMENT '剩余可用额度',
                               PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
    INSERT INTO `account` VALUES ('1', '1', '1000', '0', '1000');
    
    DROP TABLE IF EXISTS `order`;
    CREATE TABLE `order` (
                             `id` bigint NOT NULL AUTO_INCREMENT,
                             `order_id` varchar(200) NOT NULL,
                             `user_id` varchar(200) NOT NULL COMMENT '用户id',
                             `product_id` varchar(200) NOT NULL COMMENT '产品id',
                             `count` int DEFAULT NULL COMMENT '数量',
                             `money` decimal(11,0) DEFAULT NULL COMMENT '金额',
                             `status` int DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结',
                             PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    DROP TABLE IF EXISTS `storage`;
    CREATE TABLE `storage` (
                               `id` bigint NOT NULL AUTO_INCREMENT,
                               `product_id` bigint DEFAULT NULL COMMENT '产品id',
                               `total` int DEFAULT NULL COMMENT '总库存',
                               `used` int DEFAULT NULL COMMENT '已用库存',
                               `residue` int DEFAULT NULL COMMENT '剩余库存',
                               PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
    INSERT INTO `storage` VALUES ('1', '1', '100', '0', '100');
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33

    通过以上 SQL 语句,我们共创建三张数据库表:order(订单表)、storage(商品库存表)、account(用户账户表)。

  2. 引入项目依赖。

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.0.RELEASE</version>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-expression -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-expression</artifactId>
            <version>5.2.12.RELEASE</version>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.6</version>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-tx -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.18</version>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>
    
    </dependencies>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
  3. 创建一个名为 Order 的实体类,代码如下。

    package top.snake.pojo;
    
    import java.math.BigDecimal;
    
    public class Order {
        //自增 id
        private Long id;
        //订单 id
        private String orderId;
        //用户 id
        private String userId;
        //商品 id
        private String productId;
        //订单商品数量
        private Integer count;
        //订单金额
        private BigDecimal money;
        //订单状态
        private Integer status;
    
        /**省略getter和setter**/
    
        @Override
        public String toString() {
            return "Order{" +
                    "id=" + id +
                    ", orderId='" + orderId + '\'' +
                    ", userId='" + userId + '\'' +
                    ", productId='" + productId + '\'' +
                    ", count=" + count +
                    ", money=" + money +
                    ", status=" + status +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
  4. 创建一个名为 Account 的实体类,代码如下。

    package top.snake.pojo;
    
    import java.math.BigDecimal;
    
    public class Account {
        //自增 id
        private Long id;
        //用户 id
        private String userId;
        //账户总金额
        private BigDecimal total;
        //已用账户金额
        private BigDecimal used;
        //剩余账户金额
        private BigDecimal residue;
    
       /**省略getter和setter**/
    
        @Override
        public String toString() {
            return "Account{" +
                    "id=" + id +
                    ", userId='" + userId + '\'' +
                    ", total=" + total +
                    ", used=" + used +
                    ", residue=" + residue +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
  5. 创建一个名为 Storage 的实体类,代码如下。

    package top.snake.pojo;
    
    public class Storage {
        //自增 id
        private Long id;
        //商品 id
        private String productId;
        //商品库存总数
        private Integer total;
        //已用商品数量
        private Integer used;
        //剩余商品数量
        private Integer residue;
    
        /**省略getter和setter**/
    
        @Override
        public String toString() {
            return "Storage{" +
                    "id=" + id +
                    ", productId='" + productId + '\'' +
                    ", total=" + total +
                    ", used=" + used +
                    ", residue=" + residue +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
  6. 创建一个名为 OrderDao 的接口和实现类OrderDaoImpl,代码如下。

    package top.snake.dao;
    
    import top.snake.pojo.Order;
    
    public interface OrderDao {
        /**
         * 创建订单
         * @param order
         * @return
         */
        int createOrder(Order order);
        /**
         * 修改订单状态
         * 将订单状态从未完成(0)修改为已完成(1)
         * @param orderId
         * @param status
         */
        void updateOrderStatus(String orderId, Integer status);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package top.snake.dao.impl;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    import top.snake.dao.OrderDao;
    import top.snake.pojo.Order;
    
    @Repository
    public class OrderDaoImpl implements OrderDao {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Override
        public int createOrder(Order order) {
            String sql = "insert into `order` (order_id,user_id, product_id, `count`, money, status) values (?,?,?,?,?,?)";
            int update = jdbcTemplate.update(sql, order.getOrderId(), order.getUserId(), order.getProductId(), order.getCount(), order.getMoney(), order.getStatus());
            return update;
        }
    
        @Override
        public void updateOrderStatus(String orderId, Integer status) {
            String sql = " update `order`  set status = 1 where order_id = ? and status = ?;";
            jdbcTemplate.update(sql, orderId, status);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
  7. 创建一个名为 AccountDao 的接口和实现类 AccountDaoImpl,代码如下。

    package top.snake.dao;
    
    import top.snake.pojo.Account;
    
    import java.math.BigDecimal;
    
    public interface AccountDao {
        /**
         * 根据用户查询账户金额
         * @param userId
         * @return
         */
        Account selectByUserId(String userId);
        /**
         * 扣减账户金额
         * @param userId
         * @param money
         * @return
         */
        int decrease(String userId, BigDecimal money);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package top.snake.dao.impl;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.BeanPropertyRowMapper;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    import top.snake.dao.AccountDao;
    import top.snake.pojo.Account;
    
    import java.math.BigDecimal;
    
    @Repository
    public class AccountDaoImpl implements AccountDao {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Override
        public Account selectByUserId(String userId) {
            String sql = "  select * from account where user_id = ?";
            return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<Account>(Account.class), userId);
        }
    
        @Override
        public int decrease(String userId, BigDecimal money) {
            String sql = "UPDATE account SET residue = residue - ?, used = used + ? WHERE user_id = ?;";
            return jdbcTemplate.update(sql, money, money, userId);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
  8. 创建一个名为 StorageDao 的接口和实现类 StorageDaoImpl,代码如下。

    package top.snake.dao;
    
    import top.snake.pojo.Storage;
    
    public interface StorageDao {
        /**
         * 查询商品的库存
         * @param productId
         * @return
         */
        Storage selectByProductId(String productId);
        /**
         * 扣减商品库存
         * @param record
         * @return
         */
        int decrease(Storage record);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package top.snake.dao.impl;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.BeanPropertyRowMapper;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    import top.snake.dao.StorageDao;
    import top.snake.pojo.Storage;
    
    @Repository
    public class StorageDaoImpl implements StorageDao {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Override
        public Storage selectByProductId(String productId) {
            String sql = "select *   from storage where product_id = ?";
            return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<Storage>(Storage.class), productId);
        }
    
        @Override
        public int decrease(Storage record) {
            String sql = " update storage set  used =? ,residue=? where product_id=?";
            return jdbcTemplate.update(sql, record.getUsed(), record.getResidue(), record.getProductId());
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
  9. 创建一个名为 OrderService 的接口和实现类 OrderServiceImpl,代码如下。

    package top.snake.service;
    
    import top.snake.pojo.Order;
    
    public interface OrderService {
        /**
         * 创建订单
         * @param order
         * @return
         */
        public void createOrder(Order order);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package top.snake.service.impl;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import top.snake.dao.AccountDao;
    import top.snake.dao.OrderDao;
    import top.snake.dao.StorageDao;
    import top.snake.pojo.Account;
    import top.snake.pojo.Order;
    import top.snake.pojo.Storage;
    import top.snake.service.OrderService;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    @Service("orderService")
    public class OrderServiceImpl implements OrderService {
        @Autowired
        private OrderDao orderDao;
        @Autowired
        private AccountDao accountDao;
        @Autowired
        private StorageDao storageDao;
    
        @Override
        public void createOrder(Order order) {
            //自动生成订单id
            SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS");
            String format = df.format(new Date());
            String orderId = order.getUserId() + order.getProductId() + format;
            System.out.println("自动生成的订单 id 为:" + orderId);
            order.setOrderId(orderId);
            System.out.println("开始创建订单数据,订单号为:" + orderId);
            //创建订单数据
            orderDao.createOrder(order);
            System.out.println("订单数据创建完成,订单号为:" + orderId);
    
            System.out.println("开始查询商品库存,商品 id 为:" + order.getProductId());
            Storage storage = storageDao.selectByProductId(order.getProductId());
            if(storage != null && storage.getResidue().intValue() >= order.getCount().intValue()) {
                System.out.println("商品库存充足,正在扣减商品库存");
                storage.setUsed(storage.getUsed() + order.getCount());
                storage.setResidue(storage.getTotal().intValue() - storage.getUsed());
                int decrease = storageDao.decrease(storage);
                System.out.println("商品库存扣减完成");
            } else {
                System.out.println("警告:商品库存不足,正在执行回滚操作!");
                throw new RuntimeException("库存不足");
            }
    
            System.out.println("开始查询用户的账户金额");
            Account account = accountDao.selectByUserId(order.getUserId());
            if(account != null && account.getResidue().intValue() >= order.getMoney().intValue()) {
                System.out.println("账户金额充足,正在扣减账户金额");
                int decrease = accountDao.decrease(order.getUserId(), order.getMoney());
                System.out.println("账户金额扣减完成");
            }else {
                System.out.println("警告:账户余额不足,正在执行回滚操作!");
                throw new RuntimeException("账户余额不足");
            }
    
            System.out.println("开始修改订单状态,未完成》》》》》已完成");
            orderDao.updateOrderStatus(order.getOrderId(), 0);
            System.out.println("修改订单状态完成!");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
  10. 创建一个 db.properties,配置内容如下。

    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://127.0.0.1:3306/springtx?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    jdbc.username=root
    jdbc.password=root
    
    1
    2
    3
    4
  11. 创建一个 Spring 的 XML 配置文件 Beans.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"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx.xsd">
    
        <!--开启组件扫描-->
        <context:component-scan base-package="top.snake"/>
        <!--引入db.properties-->
        <context:property-placeholder location="classpath:db.properties"/>
    
        <!--配置数据源 -->
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <!--数据库连接地址-->
            <property name="url" value="${jdbc.url}"/>
            <!--数据库的用户名-->
            <property name="username" value="${jdbc.username}"/>
            <!--数据库的密码-->
            <property name="password" value="${jdbc.password}"/>
            <!--数据库驱动-->
            <property name="driverClassName" value="${jdbc.driver}"/>
        </bean>
    
        <!--定义 JdbcTemplate Bean-->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <!--将数据源的 Bean 注入到 JdbcTemplate 中-->
            <property name="dataSource" ref="dataSource"></property>
        </bean>
    
        <!--配置事物管理器,以JDBC为例-->
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <!--配置通知-->
        <tx:advice id="tx-advice" transaction-manager="transactionManager">
            <!--配置事物参数-->
            <tx:attributes>
                <!--name 指定哪些方法上添加事务-->
                <tx:method name="create*" propagation="REQUIRED" isolation="DEFAULT" read-only="false" timeout="10"/>
            </tx:attributes>
        </tx:advice>
    
        <!--配置切点和切面-->
        <aop:config>
            <!--配置切点-->
            <aop:pointcut id="tx-pt" expression="execution(* top.snake.service.impl.OrderServiceImpl.*(..))"/>
            <!--配置切面-->
            <aop:advisor advice-ref="tx-advice" pointcut-ref="tx-pt"/>
        </aop:config>
    
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
  12. 创建一个名为 MainApp 的类,代码如下。

    package top.snake;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import top.snake.pojo.Order;
    import top.snake.service.OrderService;
    
    import java.math.BigDecimal;
    
    public class MainApp {
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
            OrderService orderService = context.getBean("orderService", OrderService.class);
    
            Order order = new Order();
            //设置商品id
            order.setProductId("1");
            //商品数量为30
            order.setCount(30);
            //商品金额为600
            order.setMoney(new BigDecimal(600));
            //设置用户id
            order.setUserId("1");
            //订单状态为为未完成
            order.setStatus(0);
            //创建订单
            orderService.createOrder(order);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
  13. 执行 MainApp 类中 main 方法,控制台输出如下。

    自动生成的订单 id 为:1120220312165401943
    开始创建订单数据,订单号为:1120220312165401943
    订单数据创建完成,订单号为:1120220312165401943
    开始查询商品库存,商品 id 为:1
    商品库存充足,正在扣减商品库存
    商品库存扣减完成
    开始查询用户的账户金额
    账户金额充足,正在扣减账户金额
    账户金额扣减完成
    开始修改订单状态,未完成》》》》》已完成
    修改订单状态完成!
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  14. 分别查看订单(order)表、商品库存(storage)表和账户(account)表中的数据,结果如下。

    id order_id user_id product_id count money status
    1 1120220312165401943 1 1 30 600 1

    订单(order)表

    id product_id total used residue
    1 1 100 30 70

    商品库存(storage)表

    id user_id total used residue
    1 1 1000 600 400

    账户(account)表

  15. 再次执行 MainApp 中的 main 方法,控制台输出如下。

    自动生成的订单 id 为:1120220312165739912
    开始创建订单数据,订单号为:1120220312165739912
    订单数据创建完成,订单号为:1120220312165739912
    开始查询商品库存,商品 id 为:1
    商品库存充足,正在扣减商品库存
    商品库存扣减完成
    开始查询用户的账户金额
    警告:账户余额不足,正在执行回滚操作!
    Exception in thread "main" java.lang.RuntimeException: 账户余额不足
    	at top.snake.service.impl.OrderServiceImpl.createOrder(OrderServiceImpl.java:59)
    	at top.snake.MainApp.main(MainApp.java:27)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    再次对数据库表进行查询,发现三张数据库表都没有任何改变,说明在扣减账户发生异常后,事务回滚了。

# 基于注解实现事务管理

在 Spring 中,声明式事务除了可以使用 XML 实现外,还可以使用注解实现,以进一步降低代码之间的耦合度。

# 开启注解事务

tx 命名空间提供了一个 <tx:annotation-driven>元素,用来开启注解事务,简化 Spring 声明式事务的 XML 配置。

<tx:annotation-driven>元素的使用方式也十分的简单,我们只要在 Spring 的 XML 配置中添加这样一行配置即可。

<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
1

<tx:advice>元素一样,<tx:annotation-driven>也需要通过 transaction-manager 属性来定义一个事务管理器,这个参数的取值默认为 transactionManager。如果我们使用的事务管理器的 id 与默认值相同,则可以省略对该属性的配置。

通过 <tx:annotation-driven>元素开启注解事务后,Spring 会自动对容器中的 Bean 进行检查,找到使用 @Transactional 注解的 Bean,并为其提供事务支持。

# 使用@Transactional注解

@Transactional 注解是 Spring 声明式事务编程的核心注解,该注解既可以在类上使用,也可以在方法上使用。

@Transactional
public class XXX {
    @Transactional
    public void A(Order order) {
    ……
    }
    public void B(Order order) {
    ……
    }
}
1
2
3
4
5
6
7
8
9
10

若 @Transactional 注解在类上使用,则表示类中的所有方法都支持事务;若 @Transactional 注解在方法上使用,则表示当前方法支持事务。

Spring 在容器中查找所有使用了 @Transactional 注解的 Bean,并自动为它们添加事务通知,通知的事务属性则是通过 @Transactional 注解的属性来定义的。

@Transactional 注解包含多个属性,其中常用属性如下表。

事务属性 说明
propagation 指定事务的传播行为。
isolation 指定事务的隔离级别。
read-only 指定是否为只读事务。
timeout 表示超时时间,单位为“秒”;声明的事务在指定的超时时间后,自动回滚,避免事务长时间不提交会回滚导致的数据库资源的占用。
rollback-for 指定事务对于那些类型的异常应当回滚,而不提交。
no-rollback-for 指定事务对于那些异常应当继续运行,而不回滚。

# 示例

  1. 创建一个springtx数据库实例。
  2. 引入项目依赖。
  3. 创建一个名为 Order 、Account和Storage的实体类。
  4. 创建一个名为 OrderDao 、AccountDao和StorageDao 的接口。
  5. 创建OrderDaoImpl、AccountDaoImpl和StorageDaoImpl的接口实现类。

前五步与基于XML实现事务示例代码一致。

  1. 创建一个名为 OrderService 的接口和实现类OrderServiceImpl1。

    package top.snake.service;
    
    import top.snake.pojo.Order;
    
    public interface OrderService {
        /**
         * 创建订单
         * @param order
         * @return
         */
        public void createOrder(Order order);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package top.snake.service.impl;
    
    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;
    import top.snake.dao.AccountDao;
    import top.snake.dao.OrderDao;
    import top.snake.dao.StorageDao;
    import top.snake.pojo.Account;
    import top.snake.pojo.Order;
    import top.snake.pojo.Storage;
    import top.snake.service.OrderService;
    
    import java.util.Date;
    import java.text.SimpleDateFormat;
    
    @Service("orderService1")
    public class OrderServiceImpl1 implements OrderService {
        @Autowired
        private OrderDao orderDao;
        @Autowired
        private AccountDao accountDao;
        @Autowired
        private StorageDao storageDao;
    
        /**
         * 在方法上使用 @Transactional 注解,
         *
         * @param order
         */
        @Transactional(isolation = Isolation.DEFAULT, propagation = Propagation.REQUIRED, timeout = 10, readOnly = false)
        @Override
        public void createOrder(Order order) {
            //自动生成订单 id
            SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS");
            String format = df.format(new Date());
            String orderId = order.getUserId() + order.getProductId() + format;
            System.out.println("自动生成的订单 id 为:" + orderId);
            order.setOrderId(orderId);
            System.out.println("开始创建订单数据,订单号为:" + orderId);
            //创建订单数据
            orderDao.createOrder(order);
            System.out.println("订单数据创建完成,订单号为:" + orderId);
            System.out.println("开始查询商品库存,商品 id 为:" + order.getProductId());
            Storage storage = storageDao.selectByProductId(order.getProductId());
            if (storage != null && storage.getResidue().intValue() >= order.getCount().intValue()) {
                System.out.println("商品库存充足,正在扣减商品库存");
                storage.setUsed(storage.getUsed() + order.getCount());
                storage.setResidue(storage.getTotal().intValue() - storage.getUsed());
                int decrease = storageDao.decrease(storage);
                System.out.println("商品库存扣减完成");
            } else {
                System.out.println("警告:商品库存不足,正在执行回滚操作!");
                throw new RuntimeException("库存不足");
            }
            System.out.println("开始查询用户的账户金额");
            Account account = accountDao.selectByUserId(order.getUserId());
            if (account != null && account.getResidue().intValue() >= order.getMoney().intValue()) {
                System.out.println("账户金额充足,正在扣减账户金额");
                accountDao.decrease(order.getUserId(), order.getMoney());
                System.out.println("账户金额扣减完成");
            } else {
                System.out.println("警告:账户余额不足,正在执行回滚操作!");
                throw new RuntimeException("账户余额不足");
            }
            System.out.println("开始修改订单状态,未完成》》》》》已完成");
            orderDao.updateOrderStatus(order.getOrderId(), 0);
            System.out.println("修改订单状态完成!");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
  2. 创建一个配置文件db.properties,配置内容如下。

    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://127.0.0.1:3306/springtx?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    jdbc.username=root
    jdbc.password=root
    
    1
    2
    3
    4
  3. 创建一个 XML 配置文件 Beans1.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"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">
        <!--开启注解事务-->
        <tx:annotation-driven/>
        <!--开启组件扫描-->
        <context:component-scan base-package="top.snake"></context:component-scan>
        <!--引入 jdbc.properties 中的配置-->
        <context:property-placeholder location="classpath:db.properties"></context:property-placeholder>
        <!--定义数据源 Bean-->
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <!--数据库连接地址-->
            <property name="url" value="${jdbc.url}"/>
            <!--数据库的用户名-->
            <property name="username" value="${jdbc.username}"/>
            <!--数据库的密码-->
            <property name="password" value="${jdbc.password}"/>
            <!--数据库驱动-->
            <property name="driverClassName" value="${jdbc.driver}"/>
        </bean>
        <!--定义 JdbcTemplate Bean-->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <!--将数据源的 Bean 注入到 JdbcTemplate 中-->
            <property name="dataSource" ref="dataSource"></property>
        </bean>
        <!--配置事务管理器-->
        <bean id="transactionManager"
              class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"></property>
        </bean>
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
  4. 创建一个名为 MainApp 的类,代码如下。

    package top.snake;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import top.snake.pojo.Order;
    import top.snake.service.OrderService;
    
    import java.math.BigDecimal;
    
    public class MainApp {
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("beans1.xml");
            OrderService orderService = context.getBean("orderService1", OrderService.class);
    
            Order order = new Order();
            //设置商品id
            order.setProductId("1");
            //商品数量为30
            order.setCount(30);
            //商品金额为600
            order.setMoney(new BigDecimal(600));
            //设置用户id
            order.setUserId("1");
            //订单状态为为未完成
            order.setStatus(0);
            //创建订单
            orderService.createOrder(order);
    
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
  5. 执行 MainApp 类中 main 方法,控制台输出如下。

    自动生成的订单 id 为:1120220312172716289
    开始创建订单数据,订单号为:1120220312172716289
    订单数据创建完成,订单号为:1120220312172716289
    开始查询商品库存,商品 id 为:1
    商品库存充足,正在扣减商品库存
    商品库存扣减完成
    开始查询用户的账户金额
    警告:账户余额不足,正在执行回滚操作!
    Exception in thread "main" java.lang.RuntimeException: 账户余额不足
    	at top.snake.service.impl.OrderServiceImpl.createOrder(OrderServiceImpl.java:59)
    	at top.snake.MainApp.main(MainApp.java:27)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    对数据库表进行查询,发现三张数据库表都没有任何改变,说明在扣减账户发生异常后,事务回滚了。

# Spring整合

# Log4j2

对于一款软件而言,日志记录都是十分重要的。它不仅能够监控程序的运行情况,周期性的记录到文件中,还能够跟踪程序中代码的运行轨迹,向文件或控制台打印代码的调试信息。当程序出现错误时,日志记录可以帮助开发人员及时定位问题,因此对开发人员来说,日志记录更是尤为重要。

Spring 5 框架自带了通用的日志封装,但是我们依然可以整合其他的日志框架对日志进行记录,其中最广为人知的就是大名鼎鼎的 Log4j。

Log4j 是 Apache 提供的一款开源的强有力的 Java 日志记录工具。它可以通过配置文件灵活、细致地控制日志的生成过程,例如日志级别、日志的输出类型、日志的输出方式以及输出格式等。

Log4j 共有两个大版本,如下表所示。

版本 时间 说明
Log4j 1.x 1999 年至 2015 年 即我们常说的 Log4j, 它于 1999 年首次发布,就迅速成为有史以来最常用的日志框架。 2015 年 8 月 5 日,Apache Logging Services 宣布 Log4j 1.x 生命周期结束,其代码库不再发布更新,并鼓励用户升级到 Log4j 2.x。
Log4j 2.x 2014 年至今 即我们常说的 Log4j2,2014 年 Log4j 2.x 作为 Log4j 1.x 的替代品发布。 Log4j 2.x 是对 Log4j 1.x 的重大升级,它完全重写了 Log4j 的日志实现,比 Log4j 1.x 效率更高、更可靠且更易于开发和维护。此外,Log4j 2.x 还对 Logback 进行了许多改进,修复了 Logback 架构中的一些固有问题,目前已经更新到 2.17.1 版本。

Spring 5 是基于 Java 8 实现的,其自身作了不少的优化,将许多不建议使用的类和方法从代码库中删除,其中就包括了 Log4jConfigListener(Spring 加载 Log4j 配置的监听器)。因此从 Spring 5 开始就不在支持对 Log4j 的整合,而更加推荐我们通过 Log4j2 进行日志记录。

下面就来介绍下 Spring 是如何整合 Log4j2 的。

  1. 导入Maven依赖

    <dependencies>
            <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-core</artifactId>
                <version>2.17.1</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-api</artifactId>
                <version>2.17.1</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j18-impl -->
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-slf4j18-impl</artifactId>
                <version>2.17.1</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>1.8.0-beta0</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-log4j12</artifactId>
                <version>1.8.0-beta0</version>
            </dependency>
    
        </dependencies>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37

    特别注意:由于 Log4j2 在 2021 年 12 月 10 日被曝存在远程代码执行漏洞,所有 Apache Log4j 2.x <= 2.14.1 版本均受到影响。随后,Log4j2 官方对此漏洞进行了了修复,因此我们在引入 Log4j2 的依赖时,尽量选择最新版本。

  2. 创建一个名为 log4j2.xml 的配置文件,配置内容如下。

    <?xml version="1.0" encoding="UTF-8" ?>
    <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
    <!-- Configuration 后面的 status 用于设置 log4j2 自身内部的信息输出,可以不设置,当设置成 trace 时,可以看到 log4j2 内部各种详细输出-->
    <Configuration status="INFO" xmlns="http://logging.apache.org/log4j/2.0/config">
        <!--先定义所有的 appender-->
        <Appenders>
            <!--输出日志信息到控制台-->
            <Console name="Console" target="SYSTEM_OUT">
                <!--控制日志输出的格式-->
                <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"></PatternLayout>
            </Console>
        </Appenders>
        <!--然后定义 logger,只有定义了 logger 并引入的 appender,appender 才会生效-->
        <Loggers>
            <Root level="info">
                <AppenderRef ref="Console"></AppenderRef>
            </Root>
        </Loggers>
    </Configuration>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
  3. 创建一个名为 HelloLog4j 的 Java 类,代码如下。

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class MainApp {
    
        private static final Logger log = LoggerFactory.getLogger(MainApp.class);
    
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
            log.info("正在从容器中获取 HelloLog4j 的 Bean");
            HelloLog4j obj = context.getBean("helloLog4j", HelloLog4j.class);
            obj.getMessage();
            log.info("代码执行完成!");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  4. 执行 MainApp 中的 main() 方法,控制台输出如下。

    SLF4J: Class path contains multiple SLF4J providers.
    SLF4J: Found provider [org.apache.logging.slf4j.SLF4JServiceProvider@1753acfe]
    SLF4J: Found provider [org.slf4j.log4j12.Log4j12ServiceProvider@7c16905e]
    SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
    SLF4J: Actual provider is of type [org.apache.logging.slf4j.SLF4JServiceProvider@1753acfe]
    2022-03-12 19:11:24.033 [main] INFO  HelloLog4j - 消息为:Hello,Spring!
    2022-03-12 19:11:24.042 [main] INFO  MainApp - 正在从容器中获取 HelloLog4j 的 Bean
    2022-03-12 19:11:24.042 [main] INFO  MainApp - 代码执行完成!
    
    1
    2
    3
    4
    5
    6
    7
    8

# MyBatis

MyBatis-Spring (opens new window)用于将MyBatis整合到Spring中,它能将MyBatis与Spring事务管理整合、将MyBatis的映射器mapper和SqlSession交于Spring容器管理,以及将MyBatis异常转化为Spring的异常。

下面就来介绍下 Spring 是如何整合的Mybatis。

  1. 依赖导入

    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.5</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.7</version>
        </dependency>
    </dependencies>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  2. 创建User类,代码如下。

    package top.snake8859.pojo;
    
    public class User {
        private int id;
        private String name;
        private String pwd;
    
        /**省略getter和setter**/
    
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", pwd='" + pwd + '\'' +
                    '}';
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
  3. 配置数据源和数据库配置文件

    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://127.0.0.1:3306/springjdbc?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    jdbc.username=root
    jdbc.password=root
    
    1
    2
    3
    4
    <?xml version="1.0" encoding="UTF-8"?>
    <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-3.0.xsd
        http://www.springframework.org/schema/context
                http://www.springframework.org/schema/context/spring-context.xsd">
    
        <!--引入db.properties中的配置-->
        <context:property-placeholder location="classpath:db.properties"/>
    
        <!--定义数据源 Bean-->
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <!--数据库连接地址-->
            <property name="url" value="${jdbc.url}"/>
            <!--数据库的用户名-->
            <property name="username" value="${jdbc.username}"/>
            <!--数据库的密码-->
            <property name="password" value="${jdbc.password}"/>
            <!--数据库驱动-->
            <property name="driverClassName" value="${jdbc.driver}"/>
        </bean>
    
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
  4. SqlSessionFactory注入Spring容器。

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    
    1
    2
    3
    @Configuration
    public class MyBatisConfig {
      @Bean
      public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource());
        return factoryBean.getObject();
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  5. 创建Mapper接口,并注入Spring容器。

    package top.snake8859.mapper;
    
    import org.apache.ibatis.annotations.Param;
    import org.apache.ibatis.annotations.Select;
    import top.snake8859.pojo.User;
    
    public interface UserMapper {
        @Select("select * from user where id = #{id}")
        User getUserById(@Param("id") Integer id);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    注意:所指定的映射器类必须是一个接口,而不是具体的实现类。在这个示例中,通过注解来指定 SQL 语句,但是也可以使用 MyBatis 映射器的 XML 配置文件。

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="top.snake8859.mapper.UserMapper">
    
        <!--根据id查询用户-->
        <select id="getUserById" parameterType="java.lang.Integer" resultType="top.snake8859.pojo.User">
            select * from user where id = #{id}
        </select>
    
    </mapper>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Configuration
    public class MyBatisConfig {
      @Bean
      public UserMapper userMapper() throws Exception {
        SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory());
        return sqlSessionTemplate.getMapper(UserMapper.class);
      }
    
    1
    2
    3
    4
    5
    6
    7
  6. 测试

    package top.snake8859;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import top.snake8859.mapper.UserMapper;
    import top.snake8859.pojo.User;
    
    public class MainApp {
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
            UserMapper userMapper = context.getBean("userMapper", UserMapper.class);
            User user = userMapper.getUserById(1);
            System.out.println(user);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  7. 输出

    User{id=1, name='snake', pwd='123'}
    
    1

参考:https://mybatis.org/spring/zh/getting-started.html

# 参考资料

Last Updated: 9/13/2023, 9:34:21 PM