文章

Spring6

1.概述

1.1.Spring是什么?

Spring 是一款主流的 Java EE 轻量级开源框架 ,Spring 由“Spring 之父”Rod Johnson 提出并创立,其目的是用于简化 Java 企业级应用的开发难度和开发周期。Spring的用途不仅限于服务器端的开发。从简单性.可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。Spring 框架除了自己提供功能外,还提供整合其他技术和框架的能力。

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

自 2004 年 4 月,Spring 1.0 版本正式发布以来,Spring 已经步入到了第 6 个大版本,也就是 Spring 6。本课程采用Spring当前最新发布的正式版本6.0.2

image-20221216223135162

1.2.Spring 的狭义和广义

在不同的语境中,Spring 所代表的含义是不同的。下面我们就分别从“广义”和“狭义”两个角度,对 Spring 进行介绍。

广义的 Spring:Spring 技术栈

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

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

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

狭义的 Spring:Spring Framework

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

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

Spring 有两个最核心模块: IoC 和 AOP。

IoC:Inverse of Control 的简写,译为“控制反转”,指把创建对象过程交给 Spring 进行管理。

AOP:Aspect Oriented Programming 的简写,译为“面向切面编程”。AOP 用来封装多个类的公共行为,将那些与业务无关,却为业务模块所共同调用的逻辑封装起来,减少系统的重复代码,降低模块间的耦合度。另外,AOP 还解决一些系统层面上的问题,比如日志.事务.权限等。

1.3.Spring Framework特点

  • 非侵入式:使用 Spring Framework 开发应用程序时,Spring 对应用程序本身的结构影响非常小。对领域模型可以做到零污染;对功能性组件也只需要使用几个简单的注解进行标记,完全不会破坏原有结构,反而能将组件结构进一步简化。这就使得基于 Spring Framework 开发应用程序时结构清晰.简洁优雅。

  • 控制反转:IoC——Inversion of Control,翻转资源获取方向。把自己创建资源.向环境索取资源变成环境将资源准备好,我们享受资源注入。

  • 面向切面编程:AOP——Aspect Oriented Programming,在不修改源代码的基础上增强代码功能。

  • 容器:Spring IoC 是一个容器,因为它包含并且管理组件对象的生命周期。组件享受到了容器化的管理,替程序员屏蔽了组件创建过程中的大量细节,极大的降低了使用门槛,大幅度提高了开发效率。

  • 组件化:Spring 实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用 XML 和 Java 注解组合这些对象。这使得我们可以基于一个个功能明确.边界清晰的组件有条不紊的搭建超大型复杂应用系统。

  • 一站式:在 IoC 和 AOP 的基础上可以整合各种企业应用的开源框架和优秀的第三方类库。而且 Spring 旗下的项目已经覆盖了广泛领域,很多方面的功能性需求可以在 Spring Framework 的基础上全部使用 Spring 来实现。

1.4.Spring模块组成

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

image-20221207142746771

image-2097896352

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

Spring Core(核心容器)

spring core提供了IOC,DI,Bean配置装载创建的核心实现。核心概念: Beans.BeanFactory.BeanDefinitions.ApplicationContext。

  • spring-core :IOC和DI的基本实现

  • spring-beans:BeanFactory和Bean的装配管理(BeanFactory)
  • spring-context:Spring context上下文,即IOC容器(AppliactionContext)
  • spring-expression:spring表达式语言

Spring AOP

  • spring-aop:面向切面编程的应用模块,整合ASM,CGLib,JDK Proxy
  • spring-aspects:集成AspectJ,AOP应用框架
  • spring-instrument:动态Class Loading模块

Spring Data Access

  • spring-jdbc:spring对JDBC的封装,用于简化jdbc操作
  • spring-orm:java对象与数据库数据的映射框架
  • spring-oxm:对象与xml文件的映射框架
  • spring-jms: Spring对Java Message Service(java消息服务)的封装,用于服务之间相互通信
  • spring-tx:spring jdbc事务管理

Spring Web

  • spring-web:最基础的web支持,建立于spring-context之上,通过servlet或listener来初始化IOC容器
  • spring-webmvc:实现web mvc
  • spring-websocket:与前端的全双工通信协议
  • spring-webflux:Spring 5.0提供的,用于取代传统java servlet,非阻塞式Reactive Web框架,异步,非阻塞,事件驱动的服务

Spring Message

  • Spring-messaging:spring 4.0提供的,为Spring集成一些基础的报文传送服务

Spring test

  • spring-test:集成测试支持,主要是对junit的封装

1.5.Spring6特点

1.5.1.版本要求

(1)Spring6要求JDK最低版本是JDK17

image-20221201103138194

2.入门

2.1.运行环境

  • JDK:Java17 + Spring6 要求JDK最低版本是Java17
  • Maven:3.6.x
  • Spring:6.0.2

2.2.构建模块

  1. 构建父模块 Spring6

在IDEA中,依次单击 新建 - > 项目 - > 新建项目 - > 创建

image-20240501221012329

  1. 构建子模块 spring6-first

2.3.入门案例

2.3.1.引入依赖

官方文档:https://spring.io/projects/spring-framework#learn

添加依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependencies>
    <!--spring context依赖-->
    <!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>6.0.2</version>
    </dependency>

    <!--junit5测试-->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.3.1</version>
    </dependency>
</dependencies>

image-20240501222555291

2.3.2.创建Java类

1
2
3
4
5
6
7
8
9
package com.muzhi.spring6.bean;

public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

2.3.3.创建配置文件

在resources目录创建一个 Spring 配置文件 beans.xml(配置文件名称可随意命名,如:springs-bean.xm)

image-20240501223129343

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

    <!--
    配置HelloWorld所对应的bean,即将HelloWorld的对象交给Spring的IOC容器管理
    通过bean标签配置IOC容器所管理的bean
    属性:
        id:设置bean的唯一标识
        class:设置bean所对应类型的全类名
	-->
    <bean id="helloWorld" class="com.muzhi.spring6.bean.HelloWorld"></bean>
</beans>

2.3.4.创建测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.muzhi.test;

import com.muzhi.spring6.bean.HelloWorld;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class HelloWorldTest {

    @Test
    public void testHelloWorld() {
        // 加载Spring 配置文件
        ApplicationContext ac = new ClassPathXmlApplicationContext("spring-bean.xml");
        // 获取Bean对象
        HelloWorld helloWorld = (HelloWorld) ac.getBean("helloWorld");
        // 调用Bean的方法
        helloWorld.sayHello();
    }
}

2.4.案例分析

  1. 底层创建对象是通过反射机制调用无参构造方法。

修改HelloWorld 类:

1
2
3
4
5
6
7
8
9
10
public class HelloWorld {

    public HelloWorld() {
        System.out.println("HelloWorld 无参构造方法");
    }

    public void sayHello(){
        System.out.println("Hello World");
    }
}

执行结果:

image-20240501224637452

测试可知:创建对象时调用了无参构造方法。

  1. Spring如何创建的对象,原理是什么?

    1. 加载bean.xml配置文件
    2. 对xml文件进行解析操作。
    3. 获取xml文件bean标签属性值 id属性值.class属性值
    4. 使用反射根据class属性值 全路径创建对象。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
        @Test
        public void testHelloWorld2() throws Exception {
            // 反射创建对象
            Class aClass = Class.forName("com.muzhi.spring6.bean.HelloWorld");
            // 获取对象
            HelloWorld helloWorld = (HelloWorld) aClass.newInstance();
            helloWorld.sayHello();
            // 或者
            HelloWorld helloWorld2 = (HelloWorld) aClass.getDeclaredConstructor().newInstance();
            helloWorld2.sayHello();
        }
    
  2. 创建后的对象存储在哪里。

bean对象最终存储在spring容器中,在spring源码底层就是一个map集合,存储bean的map在DefaultListableBeanFactory类中:

1
2
3
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
key唯一标识
value类的定义 (描述信息BeanDefinition)    

Spring容器加载到Bean类时 , 会把这个类的描述信息, 以包名加类名的方式存到beanDefinitionMap 中Map<String,BeanDefinition> , 其中 String是Key , 默认是类名首字母小写 , BeanDefinition , 存的是类的定义(描述信息) , 我们通常叫BeanDefinition接口为 : bean的定义对象。

2.5.启用Log4j2日志框架

2.5.1.Log4j2日志概述

在项目开发中,日志十分的重要,不管是记录运行情况还是定位线上问题,都离不开对日志的分析。日志记录了系统行为的时间.地点.状态等相关信息,能够帮助我们了解并监控系统状态,在发生错误或者接近某种危险状态时能够及时提醒我们处理,同时在系统产生问题时,能够帮助我们快速的定位.诊断并解决问题。

Apache Log4j2是一个开源的日志记录组件,使用非常的广泛。在工程中以易用方便代替了 System.out 等打印语句,它是JAVA下最流行的日志输入工具。

Log4j2主要由几个重要的组件构成:

(1)日志信息的优先级,日志信息的优先级从高到低有TRACE < DEBUG < INFO < WARN < ERROR < FATAL TRACE:追踪,是最低的日志级别,相当于追踪程序的执行 DEBUG:调试,一般在开发中,都将其设置为最低的日志级别 INFO:信息,输出重要的信息,使用较多 WARN:警告,输出警告的信息 ERROR:错误,输出错误信息 FATAL:严重错误

这些级别分别用来指定这条日志信息的重要程度;级别高的会自动屏蔽级别低的日志,也就是说,设置了WARN的日志,则INFO.DEBUG的日志级别的日志不会显示

(2)日志信息的输出目的地,日志信息的输出目的地指定了日志将打印到控制台还是文件中

(3)日志信息的输出格式,而输出格式则控制了日志信息的显示内容。

2.5.2.引入Log4j2依赖

1
2
3
4
5
6
7
8
9
10
11
<!--log4j2的依赖-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.19.0</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j2-impl</artifactId>
    <version>2.19.0</version>
</dependency>

2.5.3.加入日志配置文件

在类的根路径下提供log4j2.xml配置文件(文件名固定为:log4j2.xml,文件必须放到类根路径下。)

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
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <loggers>
        <!--
            level指定日志级别,从低到高的优先级:
                TRACE < DEBUG < INFO < WARN < ERROR < FATAL
                trace:追踪,是最低的日志级别,相当于追踪程序的执行
                debug:调试,一般在开发中,都将其设置为最低的日志级别
                info:信息,输出重要的信息,使用较多
                warn:警告,输出警告的信息
                error:错误,输出错误信息
                fatal:严重错误
        -->
        <root level="DEBUG">
            <appender-ref ref="spring6log"/>
            <appender-ref ref="RollingFile"/>
            <appender-ref ref="log"/>
        </root>
    </loggers>

    <appenders>
        <!--输出日志信息到控制台-->
        <console name="spring6log" target="SYSTEM_OUT">
            <!--控制日志输出的格式-->
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/>
        </console>

        <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用-->
        <File name="log" fileName="d:/spring6_log/test.log" append="false">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
        </File>

        <!-- 这个会打印出所有的信息,
            每次大小超过size,
            则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,
            作为存档-->
        <RollingFile name="RollingFile" fileName="d:/spring6_log/app.log"
                     filePattern="log/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz">
            <PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
            <SizeBasedTriggeringPolicy size="50MB"/>
            <!-- DefaultRolloverStrategy属性如不设置,
            则默认为最多同一文件夹下7个文件,这里设置了20 -->
            <DefaultRolloverStrategy max="20"/>
        </RollingFile>
    </appenders>
</configuration>

2.5.4.使用日志

1
2
3
private static final Log log = LogFactory.getLog(HelloWorldTest.class);

log.info("testHelloWorld");

image-20240501231742594

3.容器

IOC,即“控制反转”(Inversion of Control),是一种设计原则,用于减少计算机编程中的耦合度。在传统的程序设计中,组件之间的依赖关系通常是由组件自身在内部创建和维护的。而在IoC容器中,这种创建和管理依赖关系的责任被转移到了外部容器,组件之间的依赖关系由容器在运行时动态注入。

3.1.IOC容器

3.1.1.控制反转 IOC

控制反转是一种设计原则,它的核心思想是将传统上由程序代码所控制的某些方面转交给框架或容器来处理。在软件工程中,IoC 可以应用在多个层面,比如:

  • 对象创建:不是由使用对象的代码来创建对象,而是由容器在运行时创建对象。
  • 对象查找:不是由对象自己查找依赖,而是由容器提供依赖对象。
  • 对象生命周期管理:不是由对象自己管理其生命周期,而是由容器负责创建.初始化.使用.销毁对象。

3.1.2.依赖注入DI

依赖注入是实现控制反转的一种具体方式,它专注于组件之间的依赖关系管理。在依赖注入中,组件不负责创建或查找它的依赖,而是通过构造函数.方法参数.属性或工厂方法等方式,由容器将依赖对象“注入”到组件中。

依赖注入可以进一步细分为几种不同的注入方式:

  1. 构造函数注入:通过对象的构造函数传递依赖。
  2. 设值注入:通过setter方法或属性赋值来注入依赖。
  3. 接口注入:通过接口传递依赖对象。
  4. 方法注入:在组件的某个方法中,通过方法参数传递依赖。

3.1.3.IOC容器在Spring中实现

  1. BeanFactory:是 IoC 容器的基本实现,是 Spring 内部使用的接口。面向 Spring 本身,不提供给开发人员使用
  2. ApplicationContext:BeanFactory 的子接口,提供了更多高级特性。面向 Spring 的使用者,几乎所有场合都使用 ApplicationContext 而不是底层的 BeanFactory。BeanFactory
类型名简介
ClassPathXmlApplicationContext通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象
FileSystemXmlApplicationContext通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象
ConfigurableApplicationContextApplicationContext 的子接口,包含一些扩展方法 refresh() 和 close() ,让 ApplicationContext 具有启动.关闭和刷新上下文的能力。
WebApplicationContext专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。

3.2.基于XML管理Bean

3.2.1.搭建子模块spring6-ioc-xml

  1. 创建方式参考:spring6-first
  2. 引入配置文件:bean.xml.log4j2.xml
  3. 添加依赖
  4. 引入Java类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    <!--log4j2的依赖-->
     <dependency>
         <groupId>org.apache.logging.log4j</groupId>
         <artifactId>log4j-core</artifactId>
         <version>2.19.0</version>
     </dependency>
     <dependency>
         <groupId>org.apache.logging.log4j</groupId>
         <artifactId>log4j-slf4j2-impl</artifactId>
         <version>2.19.0</version>
     </dependency>
    

3.2.2.获取Bean

1.根据ID获取

由于 id 属性指定了 bean 的唯一标识,所以根据 bean 标签的 id 属性可以精确获取到一个组件对象。

1
HelloWorld helloWorld = (HelloWorld) ac.getBean("helloWorld");
2.根据类型获取
1
2
3
4
5
6
7
8
9
    @Test
    public void testHelloWorld() {
        // 加载Spring 配置文件
        ApplicationContext ac = new ClassPathXmlApplicationContext("spring-bean.xml");
        // 获取Bean对象
        HelloWorld helloWorld = ac.getBean(HelloWorld.class);
        // 调用Bean的方法
        helloWorld.sayHello();
    }
3.根据ID和类型
1
2
3
4
5
6
7
8
9
10
	@Test
    public void testHelloWorld3() throws Exception {
        log.info("testHelloWorld3");
        // 加载Spring 配置文件
        ApplicationContext ac = new ClassPathXmlApplicationContext("spring-bean.xml");
        // 获取Bean对象
        HelloWorld helloWorld = ac.getBean("helloWorld", HelloWorld.class);
        // 调用Bean的方法
        helloWorld.sayHello();
    }
4.注意

当根据类型获取bean时,要求IOC容器中指定类型的bean有且只能有一个

当IOC容器中一共配置了两个:

1
2
    <bean id="helloWorld" class="com.muzhi.spring6.bean.HelloWorld"></bean>
    <bean id="helloWorld2" class="com.muzhi.spring6.bean.HelloWorld"></bean>

根据类型获取时会抛出异常:

image-20240502005157015

扩展
  • 如果组件类实现了接口,根据接口类型可以获取 bean,但Bean必须是唯一的。
  • 如果一个接口有多个实现类,这些实现类都配置了 bean,不能根据接口类型获取 bean,因为Bean不是唯一的。

根据类型来获取bean时,在满足bean唯一性的前提下,其实只是看:『对象 instanceof 指定的类型』的返回结果,只要返回的是true就可以认定为和类型匹配,能够获取到。

java中,instanceof运算符用于判断前面的对象是否是后面的类,或其子类.实现类的实例。如果是返回true,否则返回false。也就是说:用instanceof关键字做判断时, instanceof 操作符的左右操作必须有继承或实现关系

3.2.3.依赖注入之setter注入

  • 创建用户类User
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
public class User {
    private String id;
    private String name;
    private int age;
    private String sex;
    public User() {}
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getSex() {
        return sex;
    }
    public void setSex(String sex) {
        this.sex = sex;
    }
    @Override
    public String toString() {
        return "User{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                '}';
    }
}
  • 配置Bean并赋值
1
2
3
4
5
6
7
8
9
10
11
12
    <bean id="user" class="com.muzhi.spring6.bean.User">
        <!--
        使用property标签为bean的属性赋值
        属性:
            name:设置属性名
            value:设置属性值
        -->
        <property name="id" value="1"/>
        <property name="name" value="张三"/>
        <property name="age" value="20"/>
        <property name="sex" value="男"/>
    </bean>
  • 测试并输出
1
2
3
4
5
6
7
8
9
    @Test
    public void testUser() {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-bean.xml");
        User user = context.getBean("user", User.class);
        System.out.println(user.toString());
    }

DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'user'
User{id='1', name='张三', age=20, sex='男'}

3.2.4.依赖注入之构造器注入

  • 在User类中添加有参构造
1
2
3
4
5
6
    public User(String id, String name, int age, String sex) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
  • 配置Bean
1
2
3
4
5
6
7
8
9
10
11
12
    <bean id="user" class="com.muzhi.spring6.bean.User">
        <!--
        使用constructor-arg标签为bean的构造器赋值
        属性:
            name:设置构造器参数名
            value:设置构造器参数值
        -->
        <constructor-arg name="id" value="1"/>
        <constructor-arg name="name" value="张三"/>
        <constructor-arg name="age" value="20"/>
        <constructor-arg name="sex" value="男"/>
    </bean>

注意:

constructor-arg标签还有两个属性可以进一步描述构造器参数:

  • index属性:指定参数所在位置的索引(从0开始)
  • name属性:指定参数名
  • 测试
1
2
3
4
5
6
7
8
9
    @Test
    public void testUser() {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-bean.xml");
        User user = context.getBean("user", User.class);
        System.out.println(user.toString());
    }

DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'user'
User{id='1', name='张三', age=20, sex='男'}

3.2.5.特殊值处理

  • 字面量赋值
1
2
<!-- 使用value属性给bean的属性赋值时,Spring会把value属性的值看做字面量 -->
<property name="name" value="张三"/>
  • null值
1
2
3
4
5
<property name="name">
    <null />
</property>
注意:
<property name="name" value="null"></property> 赋值为字符串 null 非 null
  • xml实体
1
2
3
<!-- 小于号在XML文档中用来定义标签的开始,不能随便使用 -->
<!-- 解决方案一:使用XML实体来代替 -->
<property name="expression" value="a &lt; b"/>
  • CDATA 节
1
2
3
4
5
6
7
<property name="expression">
    <!-- 解决方案二:使用CDATA节 -->
    <!-- CDATA中的C代表Character,是文本.字符的含义,CDATA就表示纯文本数据 -->
    <!-- XML解析器看到CDATA节就知道这里是纯文本,就不会当作XML标签或属性来解析 -->
    <!-- 所以CDATA节中写什么符号都随意 -->
    <value><![CDATA[a < b]]></value>
</property>

3.2.6.为对象类型属性赋值

  • 创建用户身份类:UserIdentity
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
public class UserIdentity {
    private String name;
    private String idCard;

    public UserIdentity() {
    }

    public UserIdentity(String name, String idCard) {
        this.name = name;
        this.idCard = idCard;
    }

    public String getName() {
        return name;
    }

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

    public String getIdCard() {
        return idCard;
    }

    public void setIdCard(String idCard) {
        this.idCard = idCard;
    }

    @Override
    public String toString() {
        return "UserIdentity{" +
                "name='" + name + '\'' +
                ", idCard='" + idCard + '\'' +
                '}';
    }
}
  • 修改User 类
1
2
3
4
5
6
7
8
9
	private UserIdentity userIdentity;

    public UserIdentity getUserIdentity() {
        return userIdentity;
    }

    public void setUserIdentity(UserIdentity userIdentity) {
        this.userIdentity = userIdentity;
    }
3.2.6.1.引用外部Bean
1
2
3
4
5
6
    <bean id="userIdentity" class="com.muzhi.spring6.bean.UserIdentity">
        <property name="name" value="身份证"/>
        <property name="idCard" value="123456789012345678"/>
    </bean>
	
	<property name="userIdentity" ref="userIdentity"/>
3.2.6.2.内部Bean
1
2
3
4
5
6
	<property name="userIdentity">
		<bean id="userIdentity" class="com.muzhi.spring6.bean.UserIdentity">
			<property name="name" value="身份证"/>
			<property name="idCard" value="123456789012345678"/>
		</bean>
	</property>
3.2.6.3.级联属性赋值
1
2
3
	<property name="userIdentity" ref="userIdentity"/>
	<property name="userIdentity.name" value="身份证"/>
	<property name="userIdentity.idCard" value="123456789012345678"/>

3.2.7.为数组类型属性赋值

  • 在User类 添加数组属性。
1
2
3
4
5
6
7
8
9
    private List<String> hobbies;

    public List<String> getHobbies() {
        return hobbies;
    }

    public void setHobbies(List<String> hobbies) {
        this.hobbies = hobbies;
    }
  • 配置Bean
1
2
3
4
5
6
7
8
    <!-- 使用list标签配置集合属性 -->
	<property name="hobbies">
		<list>
			<value>篮球</value>
            <value>足球</value>
            <value>乒乓球</value>
        </list>
	</property>

若为Set集合类型属性赋值,只需要将其中的list标签改为set标签即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--map集合类型的bean-->
<property name="userIdentityMap">
        <map>
            <entry>
                <key>
                    <value>10010</value>
                </key>
                <ref bean="userIdentity"></ref>
            </entry>
            <entry>
                <key>
                    <value>10086</value>
                </key>
                <ref bean="userIdentity"></ref>
            </entry>
        </map>
</property>

3.2.8.为集合类型属性赋值

3.2.8.1. List集合类型属性赋值
  • 在Clazz类中添加以下代码:
1
2
3
4
5
6
7
8
9
private List<Student> students;

public List<Student> getStudents() {
    return students;
}

public void setStudents(List<Student> students) {
    this.students = students;
}
  • 引用集合类型的bean
1
2
3
4
5
6
7
8
9
10
11
<bean id="clazzTwo" class="com.atguigu.spring6.bean.Clazz">
    <property name="clazzId" value="4444"></property>
    <property name="clazzName" value="Javaee0222"></property>
    <property name="students">
        <list>
            <ref bean="studentOne"></ref>
            <ref bean="studentTwo"></ref>
            <ref bean="studentThree"></ref>
        </list>
    </property>
</bean>
3.2.8.2.为Map集合类型属性赋值
  • 创建教师类Teacher:
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
package com.muzhi.spring6.bean;
public class Teacher {

    private Integer teacherId;

    private String teacherName;

    public Integer getTeacherId() {
        return teacherId;
    }

    public void setTeacherId(Integer teacherId) {
        this.teacherId = teacherId;
    }

    public String getTeacherName() {
        return teacherName;
    }

    public void setTeacherName(String teacherName) {
        this.teacherName = teacherName;
    }

    public Teacher(Integer teacherId, String teacherName) {
        this.teacherId = teacherId;
        this.teacherName = teacherName;
    }

    public Teacher() {

    }
    
    @Override
    public String toString() {
        return "Teacher{" +
                "teacherId=" + teacherId +
                ", teacherName='" + teacherName + '\'' +
                '}';
    }
}
  • 在Student类中添加以下代码:
1
2
3
4
5
6
7
8
9
private Map<String, Teacher> teacherMap;

public Map<String, Teacher> getTeacherMap() {
    return teacherMap;
}

public void setTeacherMap(Map<String, Teacher> teacherMap) {
    this.teacherMap = teacherMap;
}
  • 配置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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<bean id="teacherOne" class="com.muzhi.spring6.bean.Teacher">
    <property name="teacherId" value="10010"></property>
    <property name="teacherName" value="大宝"></property>
</bean>

<bean id="teacherTwo" class="com.muzhi.spring6.bean.Teacher">
    <property name="teacherId" value="10086"></property>
    <property name="teacherName" value="二宝"></property>
</bean>

<bean id="studentFour" class="com.muzhi.spring6.bean.Student">
    <property name="id" value="1004"></property>
    <property name="name" value="赵六"></property>
    <property name="age" value="26"></property>
    <property name="sex" value="女"></property>
    <!-- ref属性:引用IOC容器中某个bean的id,将所对应的bean为属性赋值 -->
    <property name="clazz" ref="clazzOne"></property>
    <property name="hobbies">
        <array>
            <value>唱歌</value>
            <value>跳舞</value>
            <value>上天</value>
        </array>
    </property>
    <property name="teacherMap">
        <map>
            <entry>
                <key>
                    <value>10010</value>
                </key>
                <ref bean="teacherOne"></ref>
            </entry>
            <entry>
                <key>
                    <value>10086</value>
                </key>
                <ref bean="teacherTwo"></ref>
            </entry>
        </map>
    </property>
</bean>

使用util:list.util:map标签必须引入相应的命名空间

1
2
3
4
5
6
7
8
<?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:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation="http://www.springframework.org/schema/util
    http://www.springframework.org/schema/util/spring-util.xsd
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">

3.2.9.P 命名空间

引入 p 命名空间

1
2
3
4
5
6
7
8
9
<?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:util="http://www.springframework.org/schema/util"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/util
       http://www.springframework.org/schema/util/spring-util.xsd
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

引入p命名空间后,可以通过以下方式为bean的各个属性赋值

1
2
3
<bean id="studentSix" class="com.muzhi.spring6.bean.Student"
    p:id="1006" p:name="小明" p:clazz-ref="clazzOne" p:teacherMap-ref="teacherMap">
</bean>

3.2.10.引入外部属性文件

  1. 加入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
     <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.30</version>
    </dependency>
       
    <!-- 数据源 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.15</version>
    </dependency>
    
  2. 创建外部属性文件

    1
    2
    3
    4
    
    jdbc.username=root
    jdbc.password=root
    jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC
    jdbc.driver=com.mysql.cj.jdbc.Driver
    
  3. 引入属性文件

    1. 引入context名称空间
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    <?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.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd">
       
    </beans>
    
    1. 引入外部属性文件
    1
    2
    
    <!-- 引入外部属性文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    

    注意:在使用 元素加载外包配置文件功能前,首先需要在 XML 配置的一级标签 中添加 context 相关的约束。

  4. 配置Bean

    1
    2
    3
    4
    5
    6
    
    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    
  5. 测试

    1
    2
    3
    4
    5
    6
    7
    
    @Test
    public void testDataSource() throws SQLException {
        ApplicationContext ac = new ClassPathXmlApplicationContext("spring-datasource.xml");
        DataSource dataSource = ac.getBean(DataSource.class);
        Connection connection = dataSource.getConnection();
        System.out.println(connection);
    }
    

3.2.11.bean作用域

  1. 概念

    在Spring中可以通过配置bean标签的scope属性来指定bean的作用域范围,各取值含义参加下表:

    取值含义创建对象的时机
    singleton(默认)在IOC容器中,这个bean的对象始终为单实例IOC容器初始化时
    prototype这个bean在IOC容器中有多个实例获取bean时

    如果是在WebApplicationContext环境下还会有另外几个作用域(但不常用):

    取值含义
    request在一个请求范围内有效
    session在一个会话范围内有效
  2. 创建User类

    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
    
    package com.muzhi.spring6.bean;
    public class User {
       
        private Integer id;
       
        private String username;
       
        private String password;
       
        private Integer age;
       
        public User() {
        }
       
        public User(Integer id, String username, String password, Integer age) {
            this.id = id;
            this.username = username;
            this.password = password;
            this.age = age;
        }
       
        public Integer getId() {
            return id;
        }
       
        public void setId(Integer id) {
            this.id = id;
        }
       
        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 Integer getAge() {
            return age;
        }
       
        public void setAge(Integer age) {
            this.age = age;
        }
       
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", username='" + username + '\'' +
                    ", password='" + password + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    
  3. 配置Bean

    1
    2
    3
    
    <!-- scope属性:取值singleton(默认值),bean在IOC容器中只有一个实例,IOC容器初始化时创建对象 -->
    <!-- scope属性:取值prototype,bean在IOC容器中可以有多个实例,getBean()时创建对象 -->
    <bean class="com.atguigu.spring6.bean.User" scope="prototype"></bean>
    
  4. 测试

    1
    2
    3
    4
    5
    6
    7
    
    @Test
    public void testBeanScope(){
        ApplicationContext ac = new ClassPathXmlApplicationContext("spring-scope.xml");
        User user1 = ac.getBean(User.class);
        User user2 = ac.getBean(User.class);
        System.out.println(user1==user2);
    }
    

3.2.12.bean生命周期

  1. 具体的生命周期过程

    • bean对象创建(调用无参构造器)
    • 给bean对象设置属性
    • bean的后置处理器(初始化之前)
    • bean对象初始化(需在配置bean时指定初始化方法)
    • bean的后置处理器(初始化之后)
    • bean对象就绪可以使用
    • bean对象销毁(需在配置bean时指定销毁方法)
    • IOC容器关闭
  2. 修改User类

    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
    
    public class User {
       
        private Integer id;
       
        private String username;
       
        private String password;
       
        private Integer age;
       
        public User() {
            System.out.println("生命周期:1.创建对象");
        }
       
        public User(Integer id, String username, String password, Integer age) {
            this.id = id;
            this.username = username;
            this.password = password;
            this.age = age;
        }
       
        public Integer getId() {
            return id;
        }
       
        public void setId(Integer id) {
            System.out.println("生命周期:2.依赖注入");
            this.id = id;
        }
       
        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 Integer getAge() {
            return age;
        }
       
        public void setAge(Integer age) {
            this.age = age;
        }
       
        public void initMethod(){
            System.out.println("生命周期:3.初始化");
        }
       
        public void destroyMethod(){
            System.out.println("生命周期:5.销毁");
        }
       
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", username='" + username + '\'' +
                    ", password='" + password + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    

    注意其中的initMethod()和destroyMethod(),可以通过配置bean指定为初始化和销毁的方法

  3. 配置bean

    1
    2
    3
    4
    5
    6
    7
    8
    
    <!-- 使用init-method属性指定初始化方法 -->
    <!-- 使用destroy-method属性指定销毁方法 -->
    <bean class="com.atguigu.spring6.bean.User" scope="prototype" init-method="initMethod" destroy-method="destroyMethod">
        <property name="id" value="1001"></property>
        <property name="username" value="admin"></property>
        <property name="password" value="123456"></property>
        <property name="age" value="23"></property>
    </bean>
    
  4. 测试

    1
    2
    3
    4
    5
    6
    7
    
    @Test
    public void testLife(){
        ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-lifecycle.xml");
        User bean = ac.getBean(User.class);
        System.out.println("生命周期:4.通过IOC容器获取bean并使用");
        ac.close();
    }
    
  5. bean的后置处理器

    bean的后置处理器会在生命周期的初始化前后添加额外的操作,需要实现BeanPostProcessor接口,且配置到IOC容器中,需要注意的是,bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中所有bean都会执行

    创建bean的后置处理器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    package com.muzhi.spring6.process;
           
    import org.springframework.beans.BeansException;
    import org.springframework.beans.factory.config.BeanPostProcessor;
       
    public class MyBeanProcessor implements BeanPostProcessor {
           
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            System.out.println("☆☆☆" + beanName + " = " + bean);
            return bean;
        }
           
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            System.out.println("★★★" + beanName + " = " + bean);
            return bean;
        }
    }
    

    在IOC容器中配置后置处理器:

    1
    2
    
    <!-- bean的后置处理器要放入IOC容器才能生效 -->
    <bean id="myBeanProcessor" class="com.muzhi.spring6.process.MyBeanProcessor"/>
    

3.2.13.FactoryBean

  1. 简介

    FactoryBean是Spring提供的一种整合第三方框架的常用机制。和普通的bean不同,配置一个FactoryBean类型的bean,在获取bean的时候得到的并不是class属性中配置的这个类的对象,而是getObject()方法的返回值。通过这种机制,Spring可以帮我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来,只把最简洁的使用界面展示给我们。

    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
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    
    /*
     * 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.beans.factory;
    
    import org.springframework.lang.Nullable;
    
    /**
     * Interface to be implemented by objects used within a {@link BeanFactory} which
     * are themselves factories for individual objects. If a bean implements this
     * interface, it is used as a factory for an object to expose, not directly as a
     * bean instance that will be exposed itself.
     *
     * <p><b>NB: A bean that implements this interface cannot be used as a normal bean.</b>
     * A FactoryBean is defined in a bean style, but the object exposed for bean
     * references ({@link #getObject()}) is always the object that it creates.
     *
     * <p>FactoryBeans can support singletons and prototypes, and can either create
     * objects lazily on demand or eagerly on startup. The {@link SmartFactoryBean}
     * interface allows for exposing more fine-grained behavioral metadata.
     *
     * <p>This interface is heavily used within the framework itself, for example for
     * the AOP {@link org.springframework.aop.framework.ProxyFactoryBean} or the
     * {@link org.springframework.jndi.JndiObjectFactoryBean}. It can be used for
     * custom components as well; however, this is only common for infrastructure code.
     *
     * <p><b>{@code FactoryBean} is a programmatic contract. Implementations are not
     * supposed to rely on annotation-driven injection or other reflective facilities.</b>
     * {@link #getObjectType()} {@link #getObject()} invocations may arrive early in the
     * bootstrap process, even ahead of any post-processor setup. If you need access to
     * other beans, implement {@link BeanFactoryAware} and obtain them programmatically.
     *
     * <p><b>The container is only responsible for managing the lifecycle of the FactoryBean
     * instance, not the lifecycle of the objects created by the FactoryBean.</b> Therefore,
     * a destroy method on an exposed bean object (such as {@link java.io.Closeable#close()}
     * will <i>not</i> be called automatically. Instead, a FactoryBean should implement
     * {@link DisposableBean} and delegate any such close call to the underlying object.
     *
     * <p>Finally, FactoryBean objects participate in the containing BeanFactory's
     * synchronization of bean creation. There is usually no need for internal
     * synchronization other than for purposes of lazy initialization within the
     * FactoryBean itself (or the like).
     *
     * @author Rod Johnson
     * @author Juergen Hoeller
     * @since 08.03.2003
     * @param <T> the bean type
     * @see org.springframework.beans.factory.BeanFactory
     * @see org.springframework.aop.framework.ProxyFactoryBean
     * @see org.springframework.jndi.JndiObjectFactoryBean
     */
    public interface FactoryBean<T> {
    
        /**
         * The name of an attribute that can be
         * {@link org.springframework.core.AttributeAccessor#setAttribute set} on a
         * {@link org.springframework.beans.factory.config.BeanDefinition} so that
         * factory beans can signal their object type when it can't be deduced from
         * the factory bean class.
         * @since 5.2
         */
        String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";
    
        /**
         * Return an instance (possibly shared or independent) of the object
         * managed by this factory.
         * <p>As with a {@link BeanFactory}, this allows support for both the
         * Singleton and Prototype design pattern.
         * <p>If this FactoryBean is not fully initialized yet at the time of
         * the call (for example because it is involved in a circular reference),
         * throw a corresponding {@link FactoryBeanNotInitializedException}.
         * <p>As of Spring 2.0, FactoryBeans are allowed to return {@code null}
         * objects. The factory will consider this as normal value to be used; it
         * will not throw a FactoryBeanNotInitializedException in this case anymore.
         * FactoryBean implementations are encouraged to throw
         * FactoryBeanNotInitializedException themselves now, as appropriate.
         * @return an instance of the bean (can be {@code null})
         * @throws Exception in case of creation errors
         * @see FactoryBeanNotInitializedException
         */
        @Nullable
        T getObject() throws Exception;
    
        /**
         * Return the type of object that this FactoryBean creates,
         * or {@code null} if not known in advance.
         * <p>This allows one to check for specific types of beans without
         * instantiating objects, for example on autowiring.
         * <p>In the case of implementations that are creating a singleton object,
         * this method should try to avoid singleton creation as far as possible;
         * it should rather estimate the type in advance.
         * For prototypes, returning a meaningful type here is advisable too.
         * <p>This method can be called <i>before</i> this FactoryBean has
         * been fully initialized. It must not rely on state created during
         * initialization; of course, it can still use such state if available.
         * <p><b>NOTE:</b> Autowiring will simply ignore FactoryBeans that return
         * {@code null} here. Therefore it is highly recommended to implement
         * this method properly, using the current state of the FactoryBean.
         * @return the type of object that this FactoryBean creates,
         * or {@code null} if not known at the time of the call
         * @see ListableBeanFactory#getBeansOfType
         */
        @Nullable
        Class<?> getObjectType();
    
        /**
         * Is the object managed by this factory a singleton? That is,
         * will {@link #getObject()} always return the same object
         * (a reference that can be cached)?
         * <p><b>NOTE:</b> If a FactoryBean indicates to hold a singleton object,
         * the object returned from {@code getObject()} might get cached
         * by the owning BeanFactory. Hence, do not return {@code true}
         * unless the FactoryBean always exposes the same reference.
         * <p>The singleton status of the FactoryBean itself will generally
         * be provided by the owning BeanFactory; usually, it has to be
         * defined as singleton there.
         * <p><b>NOTE:</b> This method returning {@code false} does not
         * necessarily indicate that returned objects are independent instances.
         * An implementation of the extended {@link SmartFactoryBean} interface
         * may explicitly indicate independent instances through its
         * {@link SmartFactoryBean#isPrototype()} method. Plain {@link FactoryBean}
         * implementations which do not implement this extended interface are
         * simply assumed to always return independent instances if the
         * {@code isSingleton()} implementation returns {@code false}.
         * <p>The default implementation returns {@code true}, since a
         * {@code FactoryBean} typically manages a singleton instance.
         * @return whether the exposed object is a singleton
         * @see #getObject()
         * @see SmartFactoryBean#isPrototype()
         */
        default boolean isSingleton() {
            return true;
        }
    }
    

    整合MyBatis时,Spring就是通过FactoryBean机制来帮我们创建SqlSessionFactory对象的。

  2. 创建类 UserFactoryBean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    package com.muzhi.spring6.bean;
    public class UserFactoryBean implements FactoryBean<User> {
        @Override
        public User getObject() throws Exception {
            return new User();
        }
       
        @Override
        public Class<?> getObjectType() {
            return User.class;
        }
    }
    
  3. 配置Bean

    1
    
    <bean id="user" class="com.muzhi.spring6.bean.UserFactoryBean"></bean>
    
  4. 测试

    1
    2
    3
    4
    5
    6
    7
    
    @Test
    public void testUserFactoryBean(){
        //获取IOC容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("spring-factorybean.xml");
        User user = (User) ac.getBean("user");
        System.out.println(user);
    }
    

3.2.14.基于XML自动装配

自动装配:根据指定的策略,在IOC容器中匹配某一个bean,自动为指定的bean中所依赖的类类型或接口类型属性赋值

  1. 场景模拟

    1. 创建UserController
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    package com.muzhi.spring6.autowire.controller
    public class UserController {
       
        private UserService userService;
       
        public void setUserService(UserService userService) {
            this.userService = userService;
        }
       
        public void saveUser(){
            userService.saveUser();
        }
       
    }
    
    1. 创建接口UserService
    1
    2
    3
    4
    5
    
    package com.muzhi.spring6.autowire.service
    public interface UserService {
       
        void saveUser();
    }
    
    1. 创建类UserServiceImpl实现接口UserService
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    package com.muzhi.spring6.autowire.service.impl
    public class UserServiceImpl implements UserService {
       
        private UserDao userDao;
           
        public void setUserDao(UserDao userDao) {
            this.userDao = userDao;
        }
        @Override
        public void saveUser() {
            userDao.saveUser();
        }
    }
    
    1. 创建接口UserDao
    1
    2
    3
    4
    5
    
    package com.muzhi.spring6.autowire.dao
    public interface UserDao {
       
        void saveUser();
    }
    
    1. 创建类UserDaoImpl实现接口UserDao
    1
    2
    3
    4
    5
    6
    7
    8
    
    package com.muzhi.spring6.autowire.dao.impl
    public class UserDaoImpl implements UserDao {
       
        @Override
        public void saveUser() {
            System.out.println("保存成功");
        }
    }
    
  2. 配置Bean

    使用bean标签的autowire属性设置自动装配效果

    自动装配方式:byType

    byType:根据类型匹配IOC容器中的某个兼容类型的bean,为属性自动赋值

    若在IOC中,没有任何一个兼容类型的bean能够为属性赋值,则该属性不装配,即值为默认值null

    若在IOC中,有多个兼容类型的bean能够为属性赋值,则抛出异常NoUniqueBeanDefinitionException

    1
    2
    3
    
    <bean id="userController" class="com.muzhi.spring6.autowire.controller.UserController" autowire="byType"></bean>
    <bean id="userService" class="com.muzhi.spring6.autowire.service.impl.UserServiceImpl" autowire="byType"></bean>
    <bean id="userDao" class="com.muzhi.spring6.autowire.dao.impl.UserDaoImpl"></bean>
    

    自动装配方式:byName

    byName:将自动装配的属性的属性名,作为bean的id在IOC容器中匹配相对应的bean进行赋值

    1
    2
    3
    4
    5
    6
    7
    
    <bean id="userController" class="com.muzhi.spring6.autowire.controller.UserController" autowire="byName"></bean>
       
    <bean id="userService" class="com.muzhi.spring6.autowire.service.impl.UserServiceImpl" autowire="byName"></bean>
    <bean id="userServiceImpl" class="com.muzhi.spring6.autowire.service.impl.UserServiceImpl" autowire="byName"></bean>
       
    <bean id="userDao" class="com.muzhi.spring6.autowire.dao.impl.UserDaoImpl"></bean>
    <bean id="userDaoImpl" class="com.muzhi.spring6.autowire.dao.impl.UserDaoImpl"></bean>
    
  3. 测试

    1
    2
    3
    4
    5
    6
    
    @Test
    public void testAutoWireByXML(){
        ApplicationContext ac = new ClassPathXmlApplicationContext("autowire-xml.xml");
        UserController userController = ac.getBean(UserController.class);
        userController.saveUser();
    }
    

3.3.基于注解管理Bean ☆

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

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

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

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

3.3.1.搭建子模块spring6-ioc-annotation

  1. 搭建模块

    搭建方式如:spring6-ioc-xml

  2. 引入配置文件

    引入log4j2.xml

  3. 添加依赖

    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
    
    <dependencies>
        <!--spring context依赖-->
        <!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.3</version>
        </dependency>
       
        <!--junit5测试-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
        </dependency>
       
        <!--log4j2的依赖-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.19.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j2-impl</artifactId>
            <version>2.19.0</version>
        </dependency>
    </dependencies>
    

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

3.3.2.开启组件扫描

3.3.2.1.最基本的扫描方式
1
2
<context:component-scan base-package="com.muzhi.spring6">
</context:component-scan>
3.3.2.2.指定排除的组件
1
2
3
4
5
6
7
8
9
10
<context:component-scan base-package="com.muzhi.spring6">
    <!-- context:exclude-filter标签:指定排除规则 -->
    <!-- 
 		type:设置排除或包含的依据
		type="annotation",根据注解排除,expression中设置要排除的注解的全类名
		type="assignable",根据类型排除,expression中设置要排除的类型的全类名
	-->
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        <!--<context:exclude-filter type="assignable" expression="com.atguigu.spring6.controller.UserController"/>-->
</context:component-scan>
3.3.2.3.仅扫描指定组件
1
2
3
4
5
6
7
8
9
10
11
12
<context:component-scan base-package="com.muzhi" use-default-filters="false">
    <!-- context:include-filter标签:指定在原有扫描规则的基础上追加的规则 -->
    <!-- use-default-filters属性:取值false表示关闭默认扫描规则 -->
    <!-- 此时必须设置use-default-filters="false",因为默认规则即扫描指定包下所有类 -->
    <!-- 
 		type:设置排除或包含的依据
		type="annotation",根据注解排除,expression中设置要排除的注解的全类名
		type="assignable",根据类型排除,expression中设置要排除的类型的全类名
	-->
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
	<!--<context:include-filter type="assignable" expression="com.atguigu.spring6.controller.UserController"/>-->
</context:component-scan>

3.3.3.使用注解定义Bean

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

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

3.3.4.@Autowired注入

单独使用@Autowired注解,默认根据类型装配。【默认是byType】

查看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.springframework.beans.factory.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
    boolean required() default true;
}

源码中有两处需要注意:

  • 第一处:该注解可以标注在哪里?
    • 构造方法上
    • 方法上
    • 形参上
    • 属性上
    • 注解上
  • 第二处:该注解有一个required属性,默认值是true,表示在注入的时候要求被注入的Bean必须是存在的,如果不存在则报错。如果required属性设置为false,表示注入的Bean存在或者不存在都没关系,存在的话就注入,不存在的话,也不报错。
3.3.4.1.属性注入
  1. 创建UserDao接口

    1
    2
    3
    4
    5
    6
    
    package com.muzhi.spring6.dao;
       
    public interface UserDao {
       
        public void print();
    }
    
  2. 创建UserDaoImpl实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    package com.muzhi.spring6.dao.impl;
       
    import com.atguigu.spring6.dao.UserDao;
    import org.springframework.stereotype.Repository;
       
    @Repository
    public class UserDaoImpl implements UserDao {
       
        @Override
        public void print() {
            System.out.println("Dao层执行结束");
        }
    }
    
  3. 创建UserService接口

    1
    2
    3
    4
    5
    6
    
    package com.atguigu.spring6.service;
       
    public interface UserService {
       
        public void out();
    }
    
  4. 创建UserServiceImpl实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    package com.atguigu.spring6.service.impl;
       
    import com.atguigu.spring6.dao.UserDao;
    import com.atguigu.spring6.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
       
    @Service
    public class UserServiceImpl implements UserService {
       
        @Autowired
        private UserDao userDao;
       
        @Override
        public void out() {
            userDao.print();
            System.out.println("Service层执行结束");
        }
    }
    
  5. 创建UserController

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    package com.muzhi.spring6.controller;
       
    import com.atguigu.spring6.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
       
    @Controller
    public class UserController {
       
        @Autowired
        private UserService userService;
       
        public void out() {
            userService.out();
            System.out.println("Controller层执行结束。");
        }
       
    }
    
  6. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    package com.muzhi.spring6.bean;
       
    import com.muzhi.spring6.controller.UserController;
    import org.junit.jupiter.api.Test;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
       
    public class UserTest {
       
        private Logger logger = LoggerFactory.getLogger(UserTest.class);
       
        @Test
        public void testAnnotation(){
            ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
            UserController userController = context.getBean("userController", UserController.class);
            userController.out();
            logger.info("执行成功");
        }
    }
    
  7. 测试结果:

    image-20240502233130676

    以上构造方法和setter方法都没有提供,经过测试,仍然可以注入成功。

3.3.4.2.set注入
  1. 修改UserServiceImpl类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    package com.muzhi.spring6.service.impl;
       
    import com.muzhi.spring6.dao.UserDao;
    import com.muzhi.spring6.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
       
    @Service
    public class UserServiceImpl implements UserService {
       
        private UserDao userDao;
       
        @Autowired
        public void setUserDao(UserDao userDao) {
            this.userDao = userDao;
        }
       
        @Override
        public void out() {
            userDao.print();
            System.out.println("Service层执行结束");
        }
    }
    
  2. 修改UserController类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    package com.muzhi.spring6.controller;
       
    import com.muzhi.spring6.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
       
    @Controller
    public class UserController {
       
        private UserService userService;
       
        @Autowired
        public void setUserService(UserService userService) {
            this.userService = userService;
        }
       
        public void out() {
            userService.out();
            System.out.println("Controller层执行结束。");
        }
    }
    
3.3.4.3.构造方法注入
  1. 修改UserServiceImpl类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    package com.muzhi.spring6.service.impl;
       
    import com.muzhi.spring6.dao.UserDao;
    import com.muzhi.spring6.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
       
    @Service
    public class UserServiceImpl implements UserService {
       
        private UserDao userDao;
       
        @Autowired
        public UserServiceImpl(UserDao userDao) {
            this.userDao = userDao;
        }
       
        @Override
        public void out() {
            userDao.print();
            System.out.println("Service层执行结束");
        }
    }
    
  2. 修改UserController类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    package com.muzhi.spring6.controller;
       
    import com.muzhi.spring6.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
       
    @Controller
    public class UserController {
       
        private UserService userService;
       
        @Autowired
        public UserController(UserService userService) {
            this.userService = userService;
        }
       
        public void out() {
            userService.out();
            System.out.println("Controller层执行结束。");
        }
       
    }
    
3.3.4.4.形参注入
  1. 修改UserServiceImpl类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    package com.muzhi.spring6.service.impl;
       
    import com.muzhi.spring6.dao.UserDao;
    import com.muzhi.spring6.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
       
    @Service
    public class UserServiceImpl implements UserService {
       
        private UserDao userDao;
       
        public UserServiceImpl(@Autowired UserDao userDao) {
            this.userDao = userDao;
        }
       
        @Override
        public void out() {
            userDao.print();
            System.out.println("Service层执行结束");
        }
    }
    
  2. 修改UserController类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    package com.muzhi.spring6.controller;
       
    import com.muzhi.spring6.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
       
    @Controller
    public class UserController {
       
        private UserService userService;
       
        public UserController(@Autowired UserService userService) {
            this.userService = userService;
        }
       
        public void out() {
            userService.out();
            System.out.println("Controller层执行结束。");
        }
       
    }
    
3.3.4.5.只有一个构造函数,无注解
  1. 修改UserServiceImpl类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    package com.muzhi.spring6.service.impl;
       
    import com.muzhi.spring6.dao.UserDao;
    import com.muzhi.spring6.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.stereotype.Service;
       
    @Service
    public class UserServiceImpl implements UserService {
       
        @Autowired
        private UserDao userDao;
       
        public UserServiceImpl(UserDao userDao) {
            this.userDao = userDao;
        }
       
        @Override
        public void out() {
            userDao.print();
            System.out.println("Service层执行结束");
        }
    }
    

    当有参数的构造方法只有一个时,@Autowired注解可以省略。

    说明:有多个构造方法时呢?大家可以测试(再添加一个无参构造函数),测试报错

3.3.4.6.@Autowired注解和@Qualifier注解联合
  1. 添加dao层实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    package com.muzhi.spring6.dao.impl;
       
    import com.muzhi.spring6.dao.UserDao;
    import org.springframework.stereotype.Repository;
       
    @Repository
    public class UserDaoRedisImpl implements UserDao {
       
        @Override
        public void print() {
            System.out.println("Redis Dao层执行结束");
        }
    }
    
  2. 测试:测试异常

    1
    
    Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'myUserService' defined in file [\spring6\spring6-ioc-annotation\target\classes\com\muzhi\spring6\resource\service\UserServiceImpl.class]: Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'com.muzhi.spring6.resource.dao.UserDao' available: expected single matching bean but found 2: myUserDao,myUserRedisDao
    

    错误信息中说:不能装配,UserDao这个Bean的数量等于2

    怎么解决这个问题呢?当然要byName,根据名称进行装配了。

  3. 修改UserServiceImpl类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    package com.muzhi.spring6.service.impl;
       
    import com.muzhi.spring6.dao.UserDao;
    import com.muzhi.spring6.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
       
    @Service
    public class UserServiceImpl implements UserService {
       
        @Autowired
        @Qualifier("userDaoImpl") // 指定bean的名字
        private UserDao userDao;
       
        @Override
        public void out() {
            userDao.print();
            System.out.println("Service层执行结束");
        }
    }
    
  4. 总结

    • @Autowired注解可以出现在:属性上.构造方法上.构造方法的参数上.setter方法上。
    • 当带参数的构造方法只有一个,@Autowired注解可以省略。()
    • @Autowired注解默认根据类型注入。如果要根据名称注入的话,需要配合@Qualifier注解一起使用。

3.3.5.@Resource注入

@Resource注解也可以完成属性注入。那它和@Autowired注解有什么区别?

  • @Resource注解是JDK扩展包中的,也就是说属于JDK的一部分。所以该注解是标准注解,更加具有通用性。(JSR-250标准中制定的注解类型。JSR是Java规范提案。)
  • @Autowired注解是Spring框架自己的。
  • @Resource注解默认根据名称装配byName,未指定name时,使用属性名作为name。通过name找不到的话会自动启动通过类型byType装配。
  • @Autowired注解默认根据类型装配byType,如果想根据名称装配,需要配合@Qualifier注解一起用。
  • @Resource注解用在属性上.setter方法上。
  • @Autowired注解用在属性上.setter方法上.构造方法上.构造方法参数上。

@Resource注解属于JDK扩展包,所以不在JDK当中,需要额外引入以下依赖:

如果是JDK8的话不需要额外引入依赖。高于JDK11或低于JDK8需要引入以下依赖。

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

源码:

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
package jakarta.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Resources.class)
public @interface Resource {
    String name() default "";

    String lookup() default "";

    Class<?> type() default Object.class;

    Resource.AuthenticationType authenticationType() default Resource.AuthenticationType.CONTAINER;

    boolean shareable() default true;

    String mappedName() default "";

    String description() default "";

    public static enum AuthenticationType {
        CONTAINER,
        APPLICATION;

        private AuthenticationType() {
        }
    }
}
3.3.5.1.根据name注入
  1. 修改UserDaoImpl类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    package com.muzhi.spring6.dao.impl;
       
    import com.muzhi.spring6.dao.UserDao;
    import org.springframework.stereotype.Repository;
       
    @Repository("myUserDao")
    public class UserDaoImpl implements UserDao {
       
        @Override
        public void print() {
            System.out.println("Dao层执行结束");
        }
    }
    
  2. 修改UserServiceImpl类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    package com.atguigu.spring6.service.impl;
       
    import com.atguigu.spring6.dao.UserDao;
    import com.atguigu.spring6.service.UserService;
    import jakarta.annotation.Resource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.stereotype.Service;
       
    @Service
    public class UserServiceImpl implements UserService {
       
        @Resource(name = "myUserDao")
        private UserDao myUserDao;
       
        @Override
        public void out() {
            myUserDao.print();
            System.out.println("Service层执行结束");
        }
    }
    
3.3.5.2.name未知注入
  1. 修改UserDaoImpl类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    package com.muzhi.spring6.dao.impl;
       
    import com.muzhi.spring6.dao.UserDao;
    import org.springframework.stereotype.Repository;
       
    @Repository("myUserDao")
    public class UserDaoImpl implements UserDao {
       
        @Override
        public void print() {
            System.out.println("Dao层执行结束");
        }
    }
    
  2. 修改UserServiceImpl类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    package com.muzhi.spring6.service.impl;
       
    import com.muzhi.spring6.dao.UserDao;
    import com.muzhi.spring6.service.UserService;
    import jakarta.annotation.Resource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.stereotype.Service;
       
    @Service
    public class UserServiceImpl implements UserService {
       
        @Resource
        private UserDao myUserDao;
       
        @Override
        public void out() {
            myUserDao.print();
            System.out.println("Service层执行结束");
        }
    }
    

    当@Resource注解使用时没有指定name的时候,还是根据name进行查找,这个name是属性名。

3.3.5.3.其他情况
  1. 修改UserServiceImpl类,userDao1属性名不存在。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    package com.muzhi.spring6.service.impl;
       
    import com.muzhi.spring6.dao.UserDao;
    import com.muzhi.spring6.service.UserService;
    import jakarta.annotation.Resource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.stereotype.Service;
       
    @Service
    public class UserServiceImpl implements UserService {
       
        @Resource
        private UserDao userDao1;
       
        @Override
        public void out() {
            userDao1.print();
            System.out.println("Service层执行结束");
        }
    }
    
  2. 测试异常

    根据异常信息得知:显然当通过name找不到的时候,自然会启动byType进行注入,以上的错误是因为UserDao接口下有两个实现类导致的。所以根据类型注入就会报错。

    @Resource的set注入可以自行测试

  3. 总结:

    @Resource注解:默认byName注入,没有指定name时把属性名当做name,根据name找不到时,才会byType注入。byType注入时,某种类型的Bean只能有一个。

3.3.6.Spring全注解开发

  1. 全注解开发就是不再使用spring配置文件了,写一个配置类来代替配置文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    package com.muzhi.spring6.config;
       
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
       
    @Configuration
    //@ComponentScan({"com.muzhi.spring6.controller", "com.muzhi.spring6.service","com.muzhi.spring6.dao"})
    @ComponentScan("com.atguigu.spring6")
    public class Spring6Config {
    }
    
  2. 测试

    1
    2
    3
    4
    5
    6
    7
    
    @Test
    public void testAllAnnotation(){
        ApplicationContext context = new AnnotationConfigApplicationContext(Spring6Config.class);
        UserController userController = context.getBean("userController", UserController.class);
        userController.out();
        logger.info("执行成功");
    }
    

4.原理:手写IOC

Spring框架的IOC是基于Java反射机制实现的。

4.1.Java反射

Java反射(Reflection)是一种强大的机制,它允许程序在运行时访问.检查和操作类的对象。通过反射,你可以获取类的构造器.方法.字段等信息,甚至可以调用方法和访问字段。反射在动态加载类.实现框架.动态代理等方面有着广泛的应用。

以下是Java反射的一些关键概念和用法:

  1. Class类:每个类都有一个Class对象,它代表了类在Java虚拟机中的运行时状态。可以通过Class.forName.object.getClass()等方式获取Class对象。

  2. Constructor对象:表示类的构造器。可以通过Class对象获取构造器,并使用它来创建类的实例。

  3. Method对象:表示类的方法。可以通过Class对象获取方法,并调用它。

  4. Field对象:表示类的成员变量。可以通过Class对象获取字段,并读取或修改它的值。

  5. Modifier类:用于检测成员变量.方法.构造器的修饰符。

  6. Array类:提供了创建和操作多维数组的方法。

  7. Proxy类和InvocationHandler接口:用于创建动态代理。

以下是使用Java反射的一些基本示例:

获取Class对象

1
Class<?> clazz = Class.forName("java.lang.String");

获取构造器并创建实例

1
2
Constructor<?> constructor = clazz.getConstructor();
Object instance = constructor.newInstance();

获取并调用方法

1
2
Method method = clazz.getMethod("substring", int.class, int.class);
Object result = method.invoke("Hello World", 1, 5); // 返回 "ello"

获取并操作字段

1
2
Field field = clazz.getField("length");
int length = field.getInt("Hello World"); // 返回 11

创建动态代理

1
2
3
4
5
6
InvocationHandler handler = new MyInvocationHandler();
Object proxyInstance = Proxy.newProxyInstance(
   clazz.getClassLoader(),
   new Class<?>[]{Interface.class},
   handler
);

使用反射时需要注意的是,它会降低程序的性能,因为它需要在运行时进行类型检查和解析。此外,反射可能会破坏封装性,因为它允许访问私有成员。

反射在Java中非常有用,但应当谨慎使用,以避免潜在的安全问题和性能问题。

4.2.实现Spring的IOC

image-20240504074025431

  1. 搭建子模块 muzhi-spring

  2. 添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    
    <dependencies>
        <!--junit5测试-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.3.1</version>
        </dependency>
    </dependencies>
    
  3. 创建UserDao接口

    1
    2
    3
    4
    5
    6
    
    package com.atguigu.spring6.test.dao;
       
    public interface UserDao {
       
        public void print();
    }
    
  4. 创建UserDaoImpl实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    package com.atguigu.spring6.test.dao.impl;
       
    import com.atguigu.spring.dao.UserDao;
       
    public class UserDaoImpl implements UserDao {
       
        @Override
        public void print() {
            System.out.println("Dao层执行结束");
        }
    }
    
  5. 创建UserService接口

    1
    2
    3
    4
    5
    6
    
    package com.atguigu.spring6.test.service;
       
    public interface UserService {
       
        public void out();
    }
    
  6. 创建UserServiceImpl实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    package com.atguigu.spring.test.service.impl;
       
    import com.atguigu.spring.core.annotation.Bean;
    import com.atguigu.spring.service.UserService;
       
    @Bean
    public class UserServiceImpl implements UserService {
       
    //    private UserDao userDao;
       
        @Override
        public void out() {
            //userDao.print();
            System.out.println("Service层执行结束");
        }
    }
    
  7. 定义注释

    1. bean注解

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      
      package com.muzhi.spring.core.annotation;
            
      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;
            
      @Target(ElementType.TYPE)
      @Retention(RetentionPolicy.RUNTIME)
      public @interface Bean {
      }
      
    2. 依赖注入注解

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      
      package com.muzhi.spring.core.annotation;
            
      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;
            
      @Target({ElementType.FIELD})
      @Retention(RetentionPolicy.RUNTIME)
      public @interface Di {
      }
      
  8. 定义bean容器接口

    1
    2
    3
    4
    5
    6
    
    package com.muzhi.spring.core;
       
    public interface ApplicationContext {
       
        Object getBean(Class clazz);
    }
    
  9. 编写注解bean容器接口实现

    AnnotationApplicationContext基于注解扫描bean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    package com.muzhi.spring.core;
       
    import java.util.HashMap;
       
    public class AnnotationApplicationContext implements ApplicationContext {
       
        //存储bean的容器
        private HashMap<Class, Object> beanFactory = new HashMap<>();
       
        @Override
        public Object getBean(Class clazz) {
            return beanFactory.get(clazz);
        }
       
        /**
         * 根据包扫描加载bean
         * @param basePackage
         */
        public AnnotationApplicationContext(String basePackage) {
               
        }
    }
    
  10. 编写扫描bean逻辑

    通过构造方法传入包的base路径,扫描被@Bean注解的java对象,完整代码如下:

    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
    80
    81
    82
    83
    84
    85
    
    package com.muzhi.spring.core;
        
    import com.muzhi.spring.core.annotation.Bean;
        
    import java.io.File;
    import java.util.HashMap;
        
    public class AnnotationApplicationContext implements ApplicationContext {
        
        //存储bean的容器
        private HashMap<Class, Object> beanFactory = new HashMap<>();
        private static String rootPath;
        
        @Override
        public Object getBean(Class clazz) {
            return beanFactory.get(clazz);
        }
        
        /**
         * 根据包扫描加载bean
         * @param basePackage
         */
        public AnnotationApplicationContext(String basePackage) {
           try {
                String packageDirName = basePackage.replaceAll("\\.", "\\\\");
                Enumeration<URL> dirs =Thread.currentThread().getContextClassLoader().getResources(packageDirName);
                while (dirs.hasMoreElements()) {
                    URL url = dirs.nextElement();
                    String filePath = URLDecoder.decode(url.getFile(),"utf-8");
                    rootPath = filePath.substring(0, filePath.length()-packageDirName.length());
                    loadBean(new File(filePath));
                }
        
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        
        private  void loadBean(File fileParent) {
            if (fileParent.isDirectory()) {
                File[] childrenFiles = fileParent.listFiles();
                if(childrenFiles == null || childrenFiles.length == 0){
                    return;
                }
                for (File child : childrenFiles) {
                    if (child.isDirectory()) {
                        //如果是个文件夹就继续调用该方法,使用了递归
                        loadBean(child);
                    } else {
                        //通过文件路径转变成全类名,第一步把绝对路径部分去掉
                        String pathWithClass = child.getAbsolutePath().substring(rootPath.length() - 1);
                        //选中class文件
                        if (pathWithClass.contains(".class")) {
                            //    com.xinzhi.dao.UserDao
                            //去掉.class后缀,并且把 \ 替换成 .
                            String fullName = pathWithClass.replaceAll("\\\\", ".").replace(".class", "");
                            try {
                                Class<?> aClass = Class.forName(fullName);
                                //把非接口的类实例化放在map中
                                if(!aClass.isInterface()){
                                    Bean annotation = aClass.getAnnotation(Bean.class);
                                    if(annotation != null){
                                        Object instance = aClass.newInstance();
                                        //判断一下有没有接口
                                        if(aClass.getInterfaces().length > 0) {
                                            //如果有接口把接口的class当成key,实例对象当成value
                                            System.out.println("正在加载【"+ aClass.getInterfaces()[0] +"】,实例对象是:" + instance.getClass().getName());
                                            beanFactory.put(aClass.getInterfaces()[0], instance);
                                        }else{
                                            //如果有接口把自己的class当成key,实例对象当成value
                                            System.out.println("正在加载【"+ aClass.getName() +"】,实例对象是:" + instance.getClass().getName());
                                            beanFactory.put(aClass, instance);
                                        }
                                    }
                                }
                            } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
        
    }
    
  11. Bean注解

    1
    2
    
    @Bean
    public class UserServiceImpl implements UserService
    
    1
    2
    
    @Bean
    public class UserDaoImpl implements UserDao 
    
  12. 测试Bean加载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    package com.muzhi.spring;
        
    import com.muzhi.spring.core.AnnotationApplicationContext;
    import com.muzhi.spring.core.ApplicationContext;
    import com.muzhi.spring.test.service.UserService;
    import org.junit.jupiter.api.Test;
        
    public class SpringIocTest {
        
        @Test
        public void testIoc() {
            ApplicationContext applicationContext = new AnnotationApplicationContext("com.muzhi.spring.test");
            UserService userService = (UserService)applicationContext.getBean(UserService.class);
            userService.out();
            System.out.println("run success");
        }
    }
    
  13. 依赖注入

    1. 只要userDao.print();调用成功,说明就注入成功
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    package com.muzhi.spring.test.service.impl;
        
    import com.muzhi.spring.core.annotation.Bean;
    import com.muzhi.spring.core.annotation.Di;
    import com.muzhi.spring.dao.UserDao;
    import com.muzhi.spring.service.UserService;
        
    @Bean
    public class UserServiceImpl implements UserService {
        
        @Di
        private UserDao userDao;
        
        @Override
        public void out() {
            userDao.print();
            System.out.println("Service层执行结束");
        }
    }
    
  14. 依赖注入

    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
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    
    package com.m.spring.core;
        
    import com.muzhi.spring.core.annotation.Bean;
    import com.muzhi.spring.core.annotation.Di;
        
    import java.io.File;
    import java.lang.reflect.Field;
    import java.util.HashMap;
    import java.util.Map;
        
    public class AnnotationApplicationContext implements ApplicationContext {
        
        //存储bean的容器
        private HashMap<Class, Object> beanFactory = new HashMap<>();
        private static String rootPath;
        
        @Override
        public Object getBean(Class clazz) {
            return beanFactory.get(clazz);
        }
        
        /**
         * 根据包扫描加载bean
         * @param basePackage
         */
        public AnnotationApplicationContext(String basePackage) {
            try {
                String packageDirName = basePackage.replaceAll("\\.", "\\\\");
                Enumeration<URL> dirs =Thread.currentThread().getContextClassLoader().getResources(packageDirName);
                while (dirs.hasMoreElements()) {
                    URL url = dirs.nextElement();
                    String filePath = URLDecoder.decode(url.getFile(),"utf-8");
                    rootPath = filePath.substring(0, filePath.length()-packageDirName.length());
                    loadBean(new File(filePath));
                }
        
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
                
            //依赖注入
            loadDi();
        }
            
        private  void loadBean(File fileParent) {
            if (fileParent.isDirectory()) {
                File[] childrenFiles = fileParent.listFiles();
                if(childrenFiles == null || childrenFiles.length == 0){
                    return;
                }
                for (File child : childrenFiles) {
                    if (child.isDirectory()) {
                        //如果是个文件夹就继续调用该方法,使用了递归
                        loadBean(child);
                    } else {
                        //通过文件路径转变成全类名,第一步把绝对路径部分去掉
                        String pathWithClass = child.getAbsolutePath().substring(rootPath.length() - 1);
                        //选中class文件
                        if (pathWithClass.contains(".class")) {
                            //    com.xinzhi.dao.UserDao
                            //去掉.class后缀,并且把 \ 替换成 .
                            String fullName = pathWithClass.replaceAll("\\\\", ".").replace(".class", "");
                            try {
                                Class<?> aClass = Class.forName(fullName);
                                //把非接口的类实例化放在map中
                                if(!aClass.isInterface()){
                                    Bean annotation = aClass.getAnnotation(Bean.class);
                                    if(annotation != null){
                                        Object instance = aClass.newInstance();
                                        //判断一下有没有接口
                                        if(aClass.getInterfaces().length > 0) {
                                            //如果有接口把接口的class当成key,实例对象当成value
                                            System.out.println("正在加载【"+ aClass.getInterfaces()[0] +"】,实例对象是:" + instance.getClass().getName());
                                            beanFactory.put(aClass.getInterfaces()[0], instance);
                                        }else{
                                            //如果有接口把自己的class当成key,实例对象当成value
                                            System.out.println("正在加载【"+ aClass.getName() +"】,实例对象是:" + instance.getClass().getName());
                                            beanFactory.put(aClass, instance);
                                        }
                                    }
                                }
                            } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
        
        private void loadDi() {
            for(Map.Entry<Class,Object> entry : beanFactory.entrySet()){
                //就是咱们放在容器的对象
                Object obj = entry.getValue();
                Class<?> aClass = obj.getClass();
                Field[] declaredFields = aClass.getDeclaredFields();
                for (Field field : declaredFields){
                    Di annotation = field.getAnnotation(Di.class);
                    if( annotation != null ){
                        field.setAccessible(true);
                        try {
                            System.out.println("正在给【"+obj.getClass().getName()+"】属性【" + field.getName() + "】注入值【"+ beanFactory.get(field.getType()).getClass().getName() +"】");
                            field.set(obj,beanFactory.get(field.getType()));
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        
    }
    

5.面向切面:AOP

image-20240504074002991

面向切面编程(Aspect-Oriented Programming,简称 AOP)是一种程序编程范式,它允许开发者将横切关注点(如日志记录.事务管理.安全性和缓存等)与业务逻辑分离,从而提高代码的模块化和可维护性。AOP 特别适用于那些在多个地方重复出现的模式。

以下是 AOP 的一些关键概念:

  1. 切面(Aspect):切面是 AOP 中的核心概念,它将应用中横切关注点的代码封装在一起。例如,一个日志记录的切面可能会在方法执行前后记录日志。
  2. 连接点(Join Point):在程序执行过程中,可以在特定方法的调用或特定异常的处理过程中插入切面,这些点被称为连接点。在 Java 中,连接点通常是方法的调用。
  3. 切入点(Pointcut):切入点是一组匹配连接点的条件,它定义了切面应该应用到何处。例如,可以定义一个切入点来匹配特定包下的所有方法调用。
  4. 通知(Advice):通知是切面在特定连接点上执行的动作,它在切入点匹配时执行。通知有几种类型,包括:
    • 前置通知(Before):在方法执行前执行。
    • 后置通知(After):在方法执行后执行。
    • 返回通知(After returning):在方法成功返回后执行。
    • 异常通知(After throwing):在方法抛出异常后执行。
    • 环绕通知(Around):在方法调用前后执行,并且可以修改方法的调用过程。
  5. 目标对象(Target Object):被通知的对象,即包含连接点的对象。
  6. 织入(Weaving):将切面应用到目标对象的过程称为织入。这可以在编译时.类加载时或运行时进行。
  7. 代理(Proxy):AOP 框架通常使用代理模式来实现织入。在 Java 中,这通常是通过动态代理来完成的,为目标对象创建一个代理对象,代理对象包含额外的切面逻辑。

5.1. 测试示例

  1. 搭建子模块 spring6-aop

5.1.1. 创建接口

声明计算器接口Calculator,包含加减乘除的抽象方法

1
2
3
4
5
6
7
8
9
10
public interface Calculator {
    
    int add(int i, int j);
    
    int sub(int i, int j);
    
    int mul(int i, int j);
    
    int div(int i, int j);
}

5.1.2. 创建实现类

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
public class CalculatorImpl implements Calculator {
    
    @Override
    public int add(int i, int j) {
    
        int result = i + j;
    
        System.out.println("方法内部 result = " + result);
    
        return result;
    }
    
    @Override
    public int sub(int i, int j) {
    
        int result = i - j;
    
        System.out.println("方法内部 result = " + result);
    
        return result;
    }
    
    @Override
    public int mul(int i, int j) {
    
        int result = i * j;
    
        System.out.println("方法内部 result = " + result);
    
        return result;
    }
    
    @Override
    public int div(int i, int j) {
    
        int result = i / j;
    
        System.out.println("方法内部 result = " + result);
    
        return result;
    }
}

5.1.3. 创建带日志功能的实现类

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
public class CalculatorLogImpl implements Calculator {
    
    @Override
    public int add(int i, int j) {
    
        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
    
        int result = i + j;
    
        System.out.println("方法内部 result = " + result);
    
        System.out.println("[日志] add 方法结束了,结果是:" + result);
    
        return result;
    }
    
    @Override
    public int sub(int i, int j) {
    
        System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);
    
        int result = i - j;
    
        System.out.println("方法内部 result = " + result);
    
        System.out.println("[日志] sub 方法结束了,结果是:" + result);
    
        return result;
    }
    
    @Override
    public int mul(int i, int j) {
    
        System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j);
    
        int result = i * j;
    
        System.out.println("方法内部 result = " + result);
    
        System.out.println("[日志] mul 方法结束了,结果是:" + result);
    
        return result;
    }
    
    @Override
    public int div(int i, int j) {
    
        System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j);
    
        int result = i / j;
    
        System.out.println("方法内部 result = " + result);
    
        System.out.println("[日志] div 方法结束了,结果是:" + result);
    
        return result;
    }
}

5.1.4. 示例问题

  1. 示例代码缺陷

    针对带日志功能的实现类,缺陷如下:

    • 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
    • 附加功能分散在各个业务功能方法中,不利于统一维护
  2. 解决思路

    解耦,把附加功能从业务功能中抽取出来。

  3. 解决困难

    解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。

5.2. 代理模式

5.2.1. 代理模式概念

代理模式(Proxy Pattern)是一种常用的软件设计模式,它为其他对象提供一个代理或占位符,以控制对原对象的访问。代理模式可以在不改变原对象代码的情况下,为原对象添加额外的功能,例如访问控制、延迟初始化、日志记录、缓存等。

代理模式有几个关键角色:

  1. 主题(Subject):定义了真实对象和代理对象共有的接口,这样代理可以用来代替真实对象。
  2. 真实主题(Real Subject):定义了代理所代表的真实对象,实现了主题接口的具体业务逻辑。
  3. 代理(Proxy):包含对真实主题的引用,实现了与真实主题相同的接口,并在访问真实主题之前或之后添加额外的处理。
  4. 客户端(Client):与代理对象交互,它并不知道代理对象是代理还是真实对象。

代理模式有几种不同的形式,包括:

  • 远程代理(Remote Proxy):为远程对象(如网络服务)提供代理。
  • 虚拟代理(Virtual Proxy):延迟创建开销较大的真实对象,直到真正需要时才创建。
  • 保护代理(Protection Proxy):提供权限检查,控制对敏感对象的访问。
  • 智能引用(Smart Reference):在访问对象之前执行额外的操作,如检查空值、计数等。

5.2.2. 静态代理

静态代理是代理模式的一种实现方式,它在代码编译时就已经确定代理类和目标类的关系。在Java中,静态代理的实现通常涉及以下步骤:

  1. 定义接口:首先定义一个接口(或抽象类),该接口由目标类和代理类共同实现。

  2. 实现目标类:创建一个实际执行操作的目标类,这个类将被代理。

  3. 创建代理类:编写一个代理类,它也实现了相同的接口,并在内部维护了对目标类的引用。代理类的方法将在适当的时候调用目标类的方法。

  4. 客户端调用:客户端通过代理对象来访问目标对象,代理对象控制对目标对象的访问,并可以添加额外的处理逻辑。

下面是一个简单的静态代理示例:

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
// 定义一个接口
interface Service {
    void serve();
}

// 目标类实现接口
class RealService implements Service {
    @Override
    public void serve() {
        System.out.println("RealService is serving.");
    }
}

// 代理类也实现相同的接口,并持有对目标类的引用
class ServiceProxy implements Service {
    private Service service;

    public ServiceProxy(Service service) {
        this.service = service;
    }

    @Override
    public void serve() {
        // 代理逻辑,例如访问控制、日志记录等
        System.out.println("Before serving.");

        // 调用目标对象的方法
        service.serve();

        // 代理逻辑,例如清理、后续处理等
        System.out.println("After serving.");
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        // 创建目标对象
        Service service = new RealService();

        // 创建代理对象,并传入目标对象
        ServiceProxy proxy = new ServiceProxy(service);

        // 通过代理对象调用方法
        proxy.serve();
    }
}

在这个示例中,Service 是一个接口,RealService 是实现了 Service 接口的目标类。ServiceProxy 是代理类,它也实现了 Service 接口,并在内部持有对 RealService 实例的引用。客户端通过 ServiceProxy 创建的代理对象来调用 serve 方法,而不是直接调用 RealService 对象的 serve 方法。

静态代理的主要优点是可以在不修改目标对象的情况下,控制对目标对象的访问,并且可以在代理对象中添加额外的逻辑,如访问验证、延迟初始化、日志记录等。然而,静态代理也有其局限性,例如,如果要为多个目标对象创建代理,可能需要编写大量的代理类代码,这增加了系统的复杂性。在这种情况下,动态代理可能是一个更好的选择。

静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。

提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。

5.2.3. 动态代理

image-20240504170511264

  1. 生产代理对象的工厂类:

    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
    
    public class ProxyFactory {
       
        private Object target;
       
        public ProxyFactory(Object target) {
            this.target = target;
        }
       
        public Object getProxy(){
       
            /**
             * newProxyInstance():创建一个代理实例
             * 其中有三个参数:
             * 1、classLoader:加载动态生成的代理类的类加载器
             * 2、interfaces:目标对象实现的所有接口的class对象所组成的数组
             * 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法
             */
            ClassLoader classLoader = target.getClass().getClassLoader();
            Class<?>[] interfaces = target.getClass().getInterfaces();
            InvocationHandler invocationHandler = new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    /**
                     * proxy:代理对象
                     * method:代理对象需要实现的方法,即其中需要重写的方法
                     * args:method所对应方法的参数
                     */
                    Object result = null;
                    try {
                        System.out.println("[动态代理][日志] "+method.getName()+",参数:"+ Arrays.toString(args));
                        result = method.invoke(target, args);
                        System.out.println("[动态代理][日志] "+method.getName()+",结果:"+ result);
                    } catch (Exception e) {
                        e.printStackTrace();
                        System.out.println("[动态代理][日志] "+method.getName()+",异常:"+e.getMessage());
                    } finally {
                        System.out.println("[动态代理][日志] "+method.getName()+",方法执行完毕");
                    }
                    return result;
                }
            };
       
            return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
        }
    }
    
  2. 测试

    1
    2
    3
    4
    5
    6
    7
    
    @Test
    public void testDynamicProxy(){
        ProxyFactory factory = new ProxyFactory(new CalculatorLogImpl());
        Calculator proxy = (Calculator) factory.getProxy();
        proxy.div(1,0);
        //proxy.div(1,1);
    }
    

5.3. AOP相关术语

5.3.1. 横切关注点

分散在每个各个模块中解决同一样的问题,如用户验证、日志管理、事务处理、数据缓存都属于横切关注点。

从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。

这个概念不是语法层面的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。

images

5.3.2. 增强通知

增强,通俗说,就是你想要增强的功能,比如 安全,事务,日志等。

每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。

  • 前置通知:在被代理的目标方法执行
  • 返回通知:在被代理的目标方法成功结束后执行(寿终正寝
  • 异常通知:在被代理的目标方法异常结束后执行(死于非命
  • 后置通知:在被代理的目标方法最终结束后执行(盖棺定论
  • 环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

images

5.3.3. 切面

封装通知方法的类。

images

5.3.4. 目标

被代理的目标

5.3.5. 代理

向目标对象应用通知之后创建的代理对象。

5.3.6.连接点

这也是一个纯逻辑概念,不是语法定义的。

把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。通俗说,就是spring允许你使用通知的地方

images

5.3.7. 切入点

定位连接点的方式。

每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。

如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。

Spring 的 AOP 技术可以通过切入点定位到特定的连接点。通俗说,要实际去增强的方法

切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

5.3.8. 作用

  • 简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。
  • 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了。

5.4. 基于注解的AOP

5.4.1. 技术说明

img023

image-20240504172145872

  • 动态代理分为JDK动态代理和cglib动态代理
  • 当目标类有接口的情况使用JDK动态代理和cglib动态代理,没有接口时只能使用cglib动态代理
  • JDK动态代理动态生成的代理类会在com.sun.proxy包下,类名为$proxy1,和目标类实现相同的接口
  • cglib动态代理动态生成的代理类会和目标在在相同的包下,会继承目标类
  • 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
  • cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
  • AspectJ:是AOP思想的一种实现。本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。

5.4.2. 测试示例

  1. 添加依赖

    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
    
    <dependencies>
        <!--spring context依赖-->
        <!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.2</version>
        </dependency>
       
        <!--spring aop依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>6.0.2</version>
        </dependency>
        <!--spring aspects依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>6.0.2</version>
        </dependency>
       
        <!--junit5测试-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.3.1</version>
        </dependency>
       
        <!--log4j2的依赖-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.19.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j2-impl</artifactId>
            <version>2.19.0</version>
        </dependency>
    </dependencies>
    
  2. 被代理的目标资源

    接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    public interface Calculator {
           
        int add(int i, int j);
           
        int sub(int i, int j);
           
        int mul(int i, int j);
           
        int div(int i, int j); 
    }
    
  3. 实现类

    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
    
    @Component
    public class CalculatorImpl implements Calculator {
           
        @Override
        public int add(int i, int j) {
           
            int result = i + j;
           
            System.out.println("方法内部 result = " + result);
           
            return result;
        }
           
        @Override
        public int sub(int i, int j) {
           
            int result = i - j;
           
            System.out.println("方法内部 result = " + result);
           
            return result;
        }
           
        @Override
        public int mul(int i, int j) {
           
            int result = i * j;
           
            System.out.println("方法内部 result = " + result);
           
            return result;
        }
           
        @Override
        public int div(int i, int j) {
           
            int result = i / j;
           
            System.out.println("方法内部 result = " + result);
           
            return result;
        }
    }
    

5.4.3. 创建切面类并配置

  1. 创建切面类

    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
    
    // @Aspect表示这个类是一个切面类
    @Aspect
    // @Component注解保证这个切面类能够放入IOC容器
    @Component
    public class LogAspect {
           
        @Before("execution(public int com.atguigu.aop.annotation.CalculatorImpl.*(..))")
        public void beforeMethod(JoinPoint joinPoint){
            String methodName = joinPoint.getSignature().getName();
            String args = Arrays.toString(joinPoint.getArgs());
            System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
        }
       
        @After("execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))")
        public void afterMethod(JoinPoint joinPoint){
            String methodName = joinPoint.getSignature().getName();
            System.out.println("Logger-->后置通知,方法名:"+methodName);
        }
       
        @AfterReturning(value = "execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))", returning = "result")
        public void afterReturningMethod(JoinPoint joinPoint, Object result){
            String methodName = joinPoint.getSignature().getName();
            System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result);
        }
       
        @AfterThrowing(value = "execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))", throwing = "ex")
        public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
            String methodName = joinPoint.getSignature().getName();
            System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
        }
           
        @Around("execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))")
        public Object aroundMethod(ProceedingJoinPoint joinPoint){
            String methodName = joinPoint.getSignature().getName();
            String args = Arrays.toString(joinPoint.getArgs());
            Object result = null;
            try {
                System.out.println("环绕通知-->目标对象方法执行之前");
                //目标对象(连接点)方法的执行
                result = joinPoint.proceed();
                System.out.println("环绕通知-->目标对象方法返回值之后");
            } catch (Throwable throwable) {
                throwable.printStackTrace();
                System.out.println("环绕通知-->目标对象方法出现异常时");
            } finally {
                System.out.println("环绕通知-->目标对象方法执行完毕");
            }
            return result;
        }
           
    }
    
  2. 配置Spring中配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    <?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"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.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">
        <!--
            基于注解的AOP的实现:
            1、将目标对象和切面交给IOC容器管理(注解+扫描)
            2、开启AspectJ的自动代理,为目标对象自动生成代理
            3、将切面类通过注解@Aspect标识
        -->
        <context:component-scan base-package="com.muzhi.aop.annotation"></context:component-scan>
       
        <aop:aspectj-autoproxy />
    </beans>
    
  3. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    public class CalculatorTest {
       
        private Logger logger = LoggerFactory.getLogger(CalculatorTest.class);
       
        @Test
        public void testAdd(){
            ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
            Calculator calculator = ac.getBean( Calculator.class);
            int add = calculator.add(1, 1);
            logger.info("执行成功:"+add);
        }
    }
    

5.4.4. 通知

  • 前置通知:使用@Before注解标识,在被代理的目标方法执行
  • 返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行(寿终正寝
  • 异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行(死于非命
  • 后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行(盖棺定论
  • 环绕通知:使用@Around注解标识,使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

5.4.5. 切入点表达式语法

  1. 作用

    image-20240504173325461

  2. 语法细节

    • 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
    • 在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。
      • 例如:*.Hello匹配com.Hello,不匹配com.atguigu.Hello
    • 在包名的部分,使用“*..”表示包名任意、包的层次深度任意
    • 在类名的部分,类名部分整体用*号代替,表示类名任意
    • 在类名的部分,可以使用*号代替类名的一部分
      • 例如:*Service匹配所有名称以Service结尾的类或接口
    • 在方法名部分,可以使用*号表示方法名任意
    • 在方法名部分,可以使用*号代替方法名的一部分
      • 例如:*Operation匹配所有方法名以Operation结尾的方法
    • 在方法参数列表部分,使用(..)表示参数列表任意
    • 在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头
    • 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
      • 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
    • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
      • 例如:execution(public int ..Service.(.., int)) 正确 例如:execution( int ..Service.*(.., int)) 错误

    images

5.4.6. 切入点表达式

  1. 声明

    1
    2
    
    @Pointcut("execution(* com.muzhi.aop.annotation.*.*(..))")
    public void pointCut(){}
    
  2. 在同一个切面中使用

    1
    2
    3
    4
    5
    6
    
    @Before("pointCut()")
    public void beforeMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        String args = Arrays.toString(joinPoint.getArgs());
        System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
    }
    
  3. 在不同切面中使用

    1
    2
    3
    4
    5
    6
    
    @Before("com.atguigu.aop.CommonPointCut.pointCut()")
    public void beforeMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        String args = Arrays.toString(joinPoint.getArgs());
        System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
    }
    

5.4.7. 获取通知的相关信息

  1. 获取连接点信息

    获取连接点信息可以在通知方法的参数位置设置JoinPoint类型的形参

    1
    2
    3
    4
    5
    6
    7
    8
    
    @Before("execution(public int com.muzhi.aop.annotation.CalculatorImpl.*(..))")
    public void beforeMethod(JoinPoint joinPoint){
        //获取连接点的签名信息
        String methodName = joinPoint.getSignature().getName();
        //获取目标方法到的实参信息
        String args = Arrays.toString(joinPoint.getArgs());
        System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
    }
    
  2. 获取目标方法的返回值

    @AfterReturning中的属性returning 用来将通知方法的某个形参,接收目标方法的返回值。

    1
    2
    3
    4
    5
    
    @AfterReturning(value = "execution(* com.muzhi.aop.annotation.CalculatorImpl.*(..))", returning = "result")
    public void afterReturningMethod(JoinPoint joinPoint, Object result){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result);
    }
    
  3. 获取目标方法的异常

    @AfterThrowing中的属性throwing,用来将通知方法的某个形参,接收目标方法的异常。

    1
    2
    3
    4
    5
    
    @AfterThrowing(value = "execution(* com.muzhi.aop.annotation.CalculatorImpl.*(..))", throwing = "ex")
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
    }
    

5.4.8. 环绕通知

  1. 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    @Around("execution(* com.muzhi.aop.annotation.CalculatorImpl.*(..))")
    public Object aroundMethod(ProceedingJoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        String args = Arrays.toString(joinPoint.getArgs());
        Object result = null;
        try {
            System.out.println("环绕通知-->目标对象方法执行之前");
            //目标方法的执行,目标方法的返回值一定要返回给外界调用者
            result = joinPoint.proceed();
            System.out.println("环绕通知-->目标对象方法返回值之后");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("环绕通知-->目标对象方法出现异常时");
        } finally {
            System.out.println("环绕通知-->目标对象方法执行完毕");
        }
        return result;
    }
    

5.4.9. 切面的优先级

相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。

  • 优先级高的切面:外面
  • 优先级低的切面:里面

使用@Order注解可以控制切面的优先级:

  • @Order(较小的数):优先级高
  • @Order(较大的数):优先级低

image-20240504174626503

5.5. 基于XML的AOP

5.5.1. 测试示例

  1. 修改Spring配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    <context:component-scan base-package="com.muzhi.aop.xml"></context:component-scan>
       
    <aop:config>
        <!--配置切面类-->
        <aop:aspect ref="loggerAspect">
            <aop:pointcut id="pointCut" 
                       expression="execution(* com.muzhi.aop.xml.CalculatorImpl.*(..))"/>
            <aop:before method="beforeMethod" pointcut-ref="pointCut"></aop:before>
            <aop:after method="afterMethod" pointcut-ref="pointCut"></aop:after>
            <aop:after-returning method="afterReturningMethod" returning="result" pointcut-ref="pointCut"></aop:after-returning>
            <aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut-ref="pointCut"></aop:after-throwing>
            <aop:around method="aroundMethod" pointcut-ref="pointCut"></aop:around>
        </aop:aspect>
    </aop:config>
    

6.单元测试:JUnit

JUnit 是一个广泛使用的开源框架,用于编写和运行单元测试。它主要用于 Java 语言,但也存在其他语言的类似实现,如 C# 的 NUnit 或 JavaScript 的 Jest。单元测试是软件开发过程中的重要部分,它们允许开发者对最小的可测试部分(通常是方法或函数)进行验证。

以下是 JUnit 的一些关键特性和概念:

  1. 断言(Assertions):JUnit 使用断言来验证测试用例的期望结果与实际结果是否一致。如果断言失败,测试也会失败。
  2. 注解(Annotations):JUnit 利用 Java 注解来标识测试方法和测试类,常见的注解包括 @Test.@Before.@After.@BeforeClass@AfterClass
  3. 测试方法:任何用 @Test 注解的方法都是一个测试用例。这些方法应该简短.独立,并且只测试一个特定的行为。
  4. 测试运行器(Test Runner):JUnit 需要一个测试运行器来执行测试。在 IDE(如 IntelliJ IDEA 或 Eclipse)中,通常有内建的测试运行器支持。
  5. 测试套件(Test Suite):可以使用 @Suite 注解或 JUnit 的 @RunWith 注解来定义测试套件,将多个测试类组合在一起进行批量测试。
  6. 参数化测试:JUnit 支持参数化测试,允许用不同的输入参数多次运行同一个测试方法。
  7. 异常测试:可以测试代码是否抛出了特定的异常。
  8. 模拟和存根(Mocking and Stubs):JUnit 常与 Mockito 或 EasyMock 等模拟框架结合使用,以创建测试中使用的模拟对象和存根。
  9. 测试监听器(Test Listeners):JUnit 允许开发者实现 TestListener 接口来监听测试事件,例如测试开始.测试失败或测试结束。
  10. 断言实用工具:JUnit 提供了一系列的断言方法,如 assertEquals.assertTrue.assertFalse.assertNotNull.assertNull 等。
  11. 测试覆盖率:虽然 JUnit 本身不提供测试覆盖率报告,但可以与其他工具(如 JaCoCo)结合使用来生成这些报告。
  12. JUnit 版本:存在多个 JUnit 版本,如 JUnit 4(也称为 JUnit Classic)和 JUnit 5,它们在注解.测试用例结构和扩展机制上有所不同。
  13. 集成与扩展:JUnit 可以很容易地与构建工具(如 Maven 或 Gradle)和持续集成(CI)服务器集成。

6.1.整合JUnit4

  1. 搭建子模块 spring6-junit

  2. 引入依赖

    1
    2
    3
    4
    5
    6
    
    <!-- junit4测试 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
    
  3. junit4测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    import com.muzhi.spring6.bean.User;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
       
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration("classpath:beans.xml")
    public class SpringJUnit4Test {
       
        @Autowired
        private User user;
       
        @Test
        public void testUser(){
            System.out.println(user);
        }
    }
    

6.2.整合JUnit5

  1. 引入依赖

    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
    
    <dependencies>
        <!--spring context依赖-->
        <!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.2</version>
        </dependency>
       
        <!--spring对junit的支持相关依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>6.0.2</version>
        </dependency>
       
        <!--junit5测试-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.9.0</version>
        </dependency>
       
        <!--log4j2的依赖-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.19.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j2-impl</artifactId>
            <version>2.19.0</version>
        </dependency>
    </dependencies>
    
  2. 添加配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    
    <?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.xsd
                               http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
        <context:component-scan base-package="com.muzhi.spring6.bean"/>
    </beans>
    
  3. 引入日志文件:log4j2.xml

  4. 添加Java类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    package com.muzhi.spring6.bean;
       
    import org.springframework.stereotype.Component;
       
    @Component
    public class User {
       
        public User() {
            System.out.println("run user");
        }
    }
    
  5. JUnit5测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    import com.muzhi.spring6.bean.User;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.extension.ExtendWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit.jupiter.SpringExtension;
    import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
       
    //两种方式均可
    //方式一
    //@ExtendWith(SpringExtension.class)
    //@ContextConfiguration("classpath:beans.xml")
    //方式二
    @SpringJUnitConfig(locations = "classpath:beans.xml")
    public class SpringJUnit5Test {
       
        @Autowired
        private User user;
       
        @Test
        public void testUser(){
            System.out.println(user);
        }
    }
    

7.JDBC和事务

7.1.JdbcTemplate

image-20240504115745888

7.1.1.JdbcTemplate 简介

JdbcTemplate 是 Spring 框架提供的一个用于简化数据库操作的工具类。它位于 org.springframework.jdbc.core 包中,封装了 JDBC API 的大部分复杂性,提供了一种更加面向对象的方式来执行数据库操作。以下是 JdbcTemplate 的一些关键特性:

  1. 模板方法模式JdbcTemplate 使用了模板方法设计模式,提供了一个抽象的模板方法 execute,子类可以重写这个方法来定义具体的数据库操作逻辑。

  2. 声明式事务管理:可以与 Spring 的声明式事务管理集成,使得数据库操作可以自动地参与到事务管理中。

  3. SQL 语句执行:支持执行静态 SQL 语句,包括查询和更新操作。

  4. 自动的资源管理JdbcTemplate 会自动管理 Connection.StatementResultSet 等 JDBC 资源的创建和释放,减少了资源泄露的风险。

  5. 异常转换:将 JDBC 检查型异常(如 SQLException)转换为 Spring 的数据访问异常(DataAccessException),便于统一处理。

  6. 批量操作:支持批量更新操作,可以通过 batchUpdate 方法执行批量 SQL 语句。

  7. 回调接口:提供了回调接口(如 RowMapper.PreparedStatementSetter 等),允许开发者提供具体的操作逻辑,提高了代码的可读性和可维护性。

  8. 查询和更新操作:提供了多种方法来执行查询和更新操作,如 queryForObject.query.update 等。

  9. 参数化查询:支持使用 PreparedStatement 设置参数化查询,可以防止 SQL 注入攻击。

  10. 事务管理:可以与 Spring 的 TransactionTemplate@Transactional 注解结合使用,实现声明式事务管理。

  11. 扩展性:允许开发者通过实现 RowMapper.ResultSetExtractor 等接口来自定义数据处理逻辑。

  12. 简单易用:通过提供简洁的方法和回调接口,使得数据库操作变得简单,降低了 JDBC 编程的复杂性。

下面是一个简单的使用 JdbcTemplate 执行查询操作的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Autowired
private JdbcTemplate jdbcTemplate;

public List<User> findAllUsers() {
    String sql = "SELECT * FROM users";
    return jdbcTemplate.query(sql, new RowMapper<User>() {
        @Override
        public User mapRow(ResultSet rs, int rowNum) throws SQLException {
            User user = new User();
            user.setId(rs.getLong("id"));
            user.setName(rs.getString("name"));
            // ... 设置其他属性
            return user;
        }
    });
}

在这个例子中,我们通过 query 方法执行了一个查询,并提供了一个 RowMapper 实现来将 ResultSet 中的数据映射到 User 对象。

JdbcTemplate 是 Spring 中非常有用的一个组件,它极大地简化了 JDBC 编程,提高了开发效率,并且通过异常转换和事务管理等功能,提高了程序的健壮性。

7.1.2.JDBC 示例

  1. 搭建子模块spring6-jdbc-tx

  2. 加入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    <dependencies>
        <!--spring jdbc  Spring 持久化层支持jar包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.0.2</version>
        </dependency>
        <!-- MySQL驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
        <!-- 数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.15</version>
        </dependency>
    </dependencies>
    
  3. 创建jdbc.properties

    1
    2
    3
    4
    
    jdbc.user=root
    jdbc.password=root
    jdbc.url=jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false
    jdbc.driver=com.mysql.cj.jdbc.Driver
    
  4. 配置Spring配置文件

    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
    
    <?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.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd">
       
        <!-- 导入外部属性文件 -->
        <context:property-placeholder location="classpath:jdbc.properties" />
       
        <!-- 配置数据源 -->
        <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="url" value="${jdbc.url}"/>
            <property name="driverClassName" value="${jdbc.driver}"/>
            <property name="username" value="${jdbc.user}"/>
            <property name="password" value="${jdbc.password}"/>
        </bean>
       
        <!-- 配置 JdbcTemplate -->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <!-- 装配数据源 -->
            <property name="dataSource" ref="druidDataSource"/>
        </bean>
    </beans>
    
  5. 数据库与测试表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    CREATE DATABASE `spring`;
       
    use `spring`;
       
    CREATE TABLE `t_emp` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(20) DEFAULT NULL COMMENT '姓名',
      `age` int(11) DEFAULT NULL COMMENT '年龄',
      `sex` varchar(2) DEFAULT NULL COMMENT '性别',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    

7.1.3.实现CURD

7.1.3.1.装配JdbcTemplate
  1. 创建测试类,整合JUnit,注入JdbcTemplate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    package com.muzhi.spring6;
       
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
       
    @SpringJUnitConfig(locations = "classpath:beans.xml")
    public class JDBCTemplateTest {
       
        @Autowired
        private JdbcTemplate jdbcTemplate;
    }
    
7.1.3.2.测试增删
  1. 创建测试类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    @Test
    //测试增删改功能
    public void testJdbcByInsertAndUpdateOrDelete(){
    	// 1 添加操作
    	// 第一步 编写sql语句
    	String sql = "INSERT INTO t_emp VALUES(NULL,?,?,?)";
    	// 第二步 调用jdbcTemplate的方法,传入相关参数
    	Object[] params = {"张三", 20, "未知"};
    	int rows = jdbcTemplate.update(sql,params);
    	System.out.println(rows);
       
    	// 2 修改操作
    	String sql1 = "update t_emp set name=? where id=?";
    	int rows1 = jdbcTemplate.update(sql1, "李四", 3);
    	System.out.println(rows1);
       
    	// 3 删除操作
    	String sql2 = "delete from t_emp where id=?";
    	int rows2 = jdbcTemplate.update(sql2, 3);
    	System.out.println(rows2);
    }
    
7.1.3.3.测试查询
  1. 创建测试方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    @Test
    public void testSelectObject() {
    	// 写法一
    	String sql = "select * from t_emp where id=?";
    	Emp empResult = jdbcTemplate.queryForObject(sql,
                    (rs, rowNum) -> {
                        Emp emp = new Emp();
                        emp.setId(rs.getInt("id"));
                        emp.setName(rs.getString("name"));
                        emp.setAge(rs.getInt("age"));
                        emp.setSex(rs.getString("sex"));
                        return emp;
                    }, 1);
    	System.out.println(empResult);
       
    	// 写法二
    	String sql1 = "select * from t_emp where id=?";
    	Emp emp = jdbcTemplate.queryForObject(sql1,
                    new BeanPropertyRowMapper<>(Emp.class), 1);
    	System.out.println(emp);
    }
    
  2. 查询数据返回List集合

    1
    2
    3
    4
    5
    6
    7
    8
    
    // 查询:返回list集合
    @Test
    public void testSelectList() {
    	String sql = "select * from t_emp";
    	List<Emp> list = jdbcTemplate.query(sql,
                    new BeanPropertyRowMapper<>(Emp.class));
    	System.out.println(list);
        }
    
  3. 查询返回单个值

    1
    2
    3
    4
    5
    6
    7
    
    // 查询:返回单个值
    @Test
    public void testSelectValue() {
    	String sql = "select count(*) from t_emp";
    	Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
    	System.out.println(count);
        }
    

7.2.事务

image-20240504073742975

7.2.1.事务基本概念

事务(Transaction)是计算机科学中的一个概念,尤其在数据库管理系统中非常重要。它表示一组不可分割的操作,这些操作要么全部成功执行,要么全部不执行。事务的概念主要用于确保数据的完整性和一致性。

以下是事务的一些关键特性,通常被称为ACID属性:

  1. 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不做。如果事务中的某个操作失败,整个事务将回滚到事务开始前的状态,就像这个事务从未执行过一样。

  2. 一致性(Consistency):事务必须使数据库从一个一致的状态转移到另一个一致的状态。一致的状态意味着数据库中的数据应满足所有预定义的规则和约束,如实体完整性.参照完整性等。

  3. 隔离性(Isolation):并发执行的事务需要以某种方式被隔离,以防止数据不一致。不同的数据库系统为事务提供不同级别的隔离性,以避免脏读.不可重复读和幻读。

  4. 持久性(Durability):一旦事务提交,则其结果就是永久性的,即使系统发生故障也不会丢失已提交的事务的结果。

事务在编程和数据库管理中非常关键,它们确保了即使在系统故障的情况下,数据的完整性也不会受到影响。在企业级应用程序中,事务通常用于复杂的业务逻辑,以确保所有相关数据的变更能够“一起成功”或“一起失败”。

在编程中,事务可以通过不同的方式进行管理:

  • 声明式事务管理:在 Spring 框架中,可以通过 @Transactional 注解实现声明式事务管理,它允许你在方法或类级别通过注解来控制事务的边界和行为。

  • 编程式事务管理:开发者需要在代码中明确地开始(begin)和结束(commit 或 rollback)事务。

  • 容器管理的事务:在 EJB (Enterprise JavaBeans) 等容器环境中,事务由容器自动管理。

事务管理是并发编程和数据管理中的一个核心概念,正确使用事务对于维护数据的完整性和系统的稳定性至关重要。

7.2.2.声明式事务

声明式事务(Declarative Transaction Management)是一种在应用程序中实现事务管理的方式,它允许你通过注解或XML配置来声明事务的边界和特性,而不是在代码中硬编码事务逻辑。Spring框架提供了两种主要的声明式事务管理方式:

  1. 基于注解的声明式事务:使用Spring的@Transactional注解来声明事务。你可以将这个注解添加到方法或类级别上,以声明该方法或类中的所有方法都在事务的上下文中执行。

  2. 基于XML的声明式事务:在Spring的配置文件中使用XML来声明事务管理器和事务属性,这通常涉及到配置<tx:advice><aop:config>

以下是声明式事务的一些关键点:

  • 非入侵性:声明式事务不修改业务逻辑代码,使得业务代码不受事务管理的入侵,提高了代码的可读性和可维护性。

  • 灵活性:可以轻松地通过改变注解参数或XML配置来调整事务的属性,如传播行为.隔离级别.超时时间等。

  • AOP支持:声明式事务基于Spring的AOP(面向切面编程)功能,允许将事务管理逻辑从业务逻辑中分离出来。

  • 事务属性:可以为不同的方法或类指定不同的事务属性,如只读事务.事务的隔离级别等。

  • 回滚规则:可以定义基于异常类型的回滚规则,例如,你可以声明当方法抛出特定异常时,事务应该回滚。

  • 编程式事务管理:尽管声明式事务非常强大和方便,但有时候你可能需要更细粒度的控制,这时可以使用编程式事务管理,它允许你在代码中明确地管理事务的生命周期。

一个简单的基于注解的声明式事务示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@EnableTransactionManagement
public class AppConfig {
    // 其他配置
}

@Service
public class MyService {

    @Transactional(readOnly = true)
    public MyObject findSomething(Long id) {
        // 业务逻辑
    }

    @Transactional
    public void doSomething() {
        // 业务逻辑,事务将在这里开始和结束
    }
}

在这个例子中,@Transactional注解声明了findSomething方法是一个只读事务,而doSomething方法则是一个普通的事务。

声明式事务极大地简化了事务管理的复杂性,使得开发者可以更专注于业务逻辑的实现。

7.2.3.编程式事务

编程式事务管理是 Spring 框架提供的一种事务控制方式,它允许你通过编码的方式直接管理事务的生命周期,包括开启事务.提交事务和回滚事务等操作。与声明式事务管理相比,编程式事务管理提供了更细粒度的控制,但在代码中需要编写更多的事务管理代码。

以下是编程式事务管理的关键点:

  1. TransactionTemplate:Spring 提供了 TransactionTemplate 类,它是一个模板类,用于简化编程式事务管理。你可以通过它来控制事务的执行。

  2. 显式事务管理:在编程式事务中,事务的开始.提交和回滚都是显式调用的,你需要在代码中明确地调用相应的方法。

  3. 事务属性:可以为事务配置属性,如隔离级别.传播行为和超时时间等。

  4. 回滚策略:可以自定义回滚策略,明确指定哪些异常会导致事务回滚。

  5. 灵活性:编程式事务管理提供了更高的灵活性,允许在复杂的业务逻辑中进行细粒度的事务控制。

  6. 侵入性:与声明式事务相比,编程式事务会侵入业务逻辑代码,因此可能会使代码的可读性降低。

  7. 事务管理器:需要一个事务管理器(如 DataSourceTransactionManagerJtaTransactionManager),它负责与底层事务系统(如 JDBC 或 JTA)进行交互。

下面是一个使用 TransactionTemplate 的编程式事务管理示例:

1
2
3
4
5
6
7
8
9
10
11
12
@Autowired
private TransactionTemplate transactionTemplate;

public void someServiceOperation() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus status) {
            // 业务逻辑代码
            // 如果抛出异常,事务将被回滚
        }
    });
}

在这个例子中,someServiceOperation 方法中的业务逻辑被包装在 TransactionTemplateexecute 方法中。doInTransactionWithoutResult 方法内的代码将在事务的上下文中执行。如果在执行过程中抛出异常,事务将被回滚。

编程式事务管理适用于需要复杂事务控制的场景,但在大多数情况下,声明式事务管理因其简单性和非入侵性而更受推荐。

7.3.基于注解的声明事务

7.3.1.测试示例

  1. 添加配置

    1
    2
    
    <!--扫描组件-->
    <context:component-scan base-package="com.muzhi.spring6"></context:component-scan>
    
  2. 创建表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    CREATE TABLE `t_book` (
      `book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
      `book_name` varchar(20) DEFAULT NULL COMMENT '图书名称',
      `price` int(11) DEFAULT NULL COMMENT '价格',
      `stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)',
      PRIMARY KEY (`book_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
    insert  into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'斗破苍穹',80,100),(2,'斗罗大陆',50,100);
    CREATE TABLE `t_user` (
      `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
      `username` varchar(20) DEFAULT NULL COMMENT '用户名',
      `balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)',
      PRIMARY KEY (`user_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
    insert  into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);
    
  3. 创建组件

    1. 创建BookController

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      
      package com.muzhi.spring6.controller;
            
      @Controller
      public class BookController {
            
          @Autowired
          private BookService bookService;
            
          public void buyBook(Integer bookId, Integer userId){
              bookService.buyBook(bookId, userId);
          }
      }
      
    2. 创建接口BookService

      1
      2
      3
      4
      
      package com.muzhi.spring6.service;
      public interface BookService {
          void buyBook(Integer bookId, Integer userId);
      }
      
    3. 创建接口实现类BookServiceImpl

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      
      package com.muzhi.spring6.service.impl;
      @Service
      public class BookServiceImpl implements BookService {
            
          @Autowired
          private BookDao bookDao;
            
          @Override
          public void buyBook(Integer bookId, Integer userId) {
              //查询图书的价格
              Integer price = bookDao.getPriceByBookId(bookId);
              //更新图书的库存
              bookDao.updateStock(bookId);
              //更新用户的余额
              bookDao.updateBalance(userId, price);
          }
      }
      
    4. 创建接口BookDao

      1
      2
      3
      4
      5
      6
      7
      8
      
      package com.muzhi.spring6.dao;
      public interface BookDao {
          Integer getPriceByBookId(Integer bookId);
            
          void updateStock(Integer bookId);
            
          void updateBalance(Integer userId, Integer price);
      }
      
    5. 创建实现类BookDaoImpl

      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
      
      package com.muzhi.spring6.dao.impl;
      @Repository
      public class BookDaoImpl implements BookDao {
            
          @Autowired
          private JdbcTemplate jdbcTemplate;
            
          @Override
          public Integer getPriceByBookId(Integer bookId) {
              String sql = "select price from t_book where book_id = ?";
              return jdbcTemplate.queryForObject(sql, Integer.class, bookId);
          }
            
          @Override
          public void updateStock(Integer bookId) {
              String sql = "update t_book set stock = stock - 1 where book_id = ?";
              jdbcTemplate.update(sql, bookId);
          }
            
          @Override
          public void updateBalance(Integer userId, Integer price) {
              String sql = "update t_user set balance = balance - ? where user_id = ?";
              jdbcTemplate.update(sql, price, userId);
          }
      }
      

7.3.2.测试无事务情况

  1. 创建测试类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
       
    @SpringJUnitConfig(locations = "classpath:beans.xml")
    public class TxByAnnotationTest {
       
        @Autowired
        private BookController bookController;
       
        @Test
        public void testBuyBook(){
            bookController.buyBook(1, 1);
        }
    }
    
  2. 模拟测试

    场景:

    用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额

    假设用户id为1的用户,购买id为1的图书

    用户余额为50,而图书价格为80

    购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段

    此时执行sql语句会抛出SQLException

    结果:

    因为没有添加事务,图书的库存更新了,但是用户的余额没有更新

    显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败

7.3.3.加入事务

  1. 添加事务配置 在Spring配置文件中引入tx命名空间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    <?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.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">
    
  2. 在Spring的配置文件中添加配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="druidDataSource"></property>
    </bean>
       
    <!--
        开启事务的注解驱动
        通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务
    -->
    <!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 -->
    <tx:annotation-driven transaction-manager="transactionManager" />
    
  3. 添加事务注解

    因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理

    在BookServiceImpl的buybook()添加注解@Transactional

  4. 测试结果

    由于使用了Spring的声明式事务,更新库存和更新余额都没有执行

7.3.4.@Transactional注解标识的位置

@Transactional标识在方法上,则只会影响该方法

@Transactional标识的类上,则会影响类中所有的方法

7.3.5.事务属性:只读

在 Spring 框架中,@Transactional 注解的 readOnly 属性用来指定事务是否为只读事务。当你将事务设置为 readOnly = true 时,你告诉事务管理器这个事务不会修改任何数据,它将只执行读取操作。

以下是 @Transactional(readOnly = true) 的一些详细说明:

  1. 数据库优化:许多数据库系统能够识别只读事务,并对其进行优化。例如,数据库可能不会为只读事务获取写入锁,从而减少锁定争用并提高并发性能。

  2. 脏读保护:在某些数据库系统中,将事务设置为只读可以防止脏读,因为数据库知道事务不会进行任何写入操作,所以它可以应用更宽松的隔离级别。

  3. 事务管理器行为:事务管理器可能会利用事务的只读属性来优化事务的执行。例如,Hibernate 等 ORM 框架在只读事务中不会跟踪实体的更改,从而减少不必要的资源消耗。

  4. Spring 声明式事务:在使用 Spring 的声明式事务管理时,你可以通过在 @Transactional 注解中设置 readOnly = true 来声明一个只读事务。

  5. 示例

1
2
3
4
5
@Transactional(readOnly = true)
public List<User> findAllUsers() {
    // 这里执行的是对数据库的读取操作
    return userRepository.findAll();
}

在这个示例中,findAllUsers 方法被声明为只读事务,这意味着方法内部执行的所有数据库操作都将被视为只读。

  1. 限制:虽然 readOnly = true 可以防止应用程序在事务中执行写入操作,但它仅仅是一个提示,并不能强制事务管理器或数据库系统执行只读操作。因此,正确的应用程序设计和开发实践是确保事务只读的关键。

  2. 警告:即使事务被声明为只读,开发者也应该注意不在事务中执行任何可能导致数据更改的操作,如更新.插入或删除。

  3. 事务回滚:如果你在只读事务中执行了写入操作,某些 ORM 框架(如 Hibernate)可能会抛出异常,并且事务将不会回滚,因为事务管理器认为事务是只读的,不会对数据的完整性构成威胁。

使用 @Transactional(readOnly = true) 可以提高事务的性能并减少不必要的资源消耗,特别是在执行复杂的查询或报告生成任务时。然而,开发者应当确保事务中确实不包含任何写入操作,以维护数据的一致性和事务的正确性。

注意:

对增删改操作设置只读会抛出下面异常:

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

7.3.6.事务属性:超时

在 Spring 的 @Transactional 注解中,timeout 属性用于指定一个事务允许执行的最长时间,单位通常是秒。如果事务在这个时间之后还没有完成,它将被回滚。

当你使用 @Transactional(timeout=) 时,你需要提供具体的超时时间值。例如,如果你想设置一个 5 秒的超时,你应该这样写:

1
@Transactional(timeout = 5)

以下是一些关于使用 @Transactional(timeout=) 的要点:

  1. 必须提供值timeout 属性后面必须跟一个整数值,这个值表示超时限制的时间(以秒为单位)。

  2. 事务回滚:如果事务在超时时间结束时仍然活跃,事务管理器将回滚事务。

  3. 数据库支持:事务超时是一个数据库功能,所以确保你使用的数据库支持事务超时。

  4. 编程式事务管理:在使用编程式事务管理时,可以通过 TransactionTemplatesetTimeout 方法设置超时时间。

  5. 声明式事务管理:在使用声明式事务管理时,可以在 @Transactional 注解上设置 timeout 属性。

  6. 方法级别的超时:可以为每个使用 @Transactional 注解的方法设置不同的超时时间。

  7. 事务管理器:确保你的事务管理器(如 DataSourceTransactionManagerJpaTransactionManager)支持超时属性。

  8. 示例

1
2
3
4
@Transactional(timeout = 5)
public void someServiceMethod() {
    // 执行数据库操作,如果在5秒内没有完成,事务将被回滚
}
  1. 注意:设置合适的超时时间对于防止资源锁定和提高系统响应性非常重要。超时时间太短可能导致不必要的回滚,而超时时间太长可能无法有效防止资源长时间锁定。

  2. 事务隔离级别:超时属性与事务的隔离级别是两个不同的概念,不要混淆。隔离级别控制事务间的可见性,而超时属性控制事务的执行时间。

使用 @Transactional(timeout=) 是一种很好的做法,它可以帮助避免长时间运行事务导致的数据库锁定问题,但务必根据实际业务逻辑和性能需求来设置合理的超时时间。

超时回滚,释放资源

7.3.7.事务属性:回滚策略

@Transactional 注解允许你定义哪些异常会导致事务的回滚。Spring 事务的默认行为是,当抛出未检查异常(即继承自 RuntimeException 的异常)时,事务将回滚。而抛出检查型异常(即继承自 Exception 的异常)时,事务不会回滚。

可以通过 @Transactional 注解的 rollbackFornoRollbackFor 属性来自定义这个行为:

  1. rollbackFor:这个属性允许你指定一个异常类型数组,当抛出这些类型的异常时,事务将回滚。你可以使用任何未检查异常类型,或者通过实现 org.springframework.core.NestedException 的异常类型。

    1
    
    @Transactional(rollbackFor = {DataAccessException.class, MyCustomException.class})
    

    在这个例子中,如果抛出 DataAccessExceptionMyCustomException 类型的异常,事务将回滚。

  2. noRollbackFor:这个属性指定了一个异常类型数组,当抛出这些类型的异常时,事务将不会回滚。这通常用于指定一些检查型异常。

    1
    
    @Transactional(noRollbackFor = {MyCheckedException.class})
    

    在这个例子中,即使抛出 MyCheckedException 类型的异常,事务也不会回滚。

  3. exception透明性:Spring 事务的一个关键特性是异常的透明性。这意味着业务代码不需要知道事务是如何被管理的。你只需要抛出异常,事务管理器将根据定义的回滚策略来处理。

  4. 默认回滚规则:如果没有指定 rollbackFornoRollbackFor 属性,Spring 将根据异常的类型来决定是否回滚事务。默认情况下,抛出未检查异常将导致事务回滚,而抛出检查型异常则不会。

  5. 编程式事务管理:在使用编程式事务管理时,你可以通过编程的方式显式地回滚事务,而不是依赖于异常驱动的回滚。

  6. 事务管理器:确保你的事务管理器(如 DataSourceTransactionManagerJpaTransactionManager)支持自定义的回滚规则。

  7. 事务传播行为@Transactionalpropagation 属性定义了事务的传播行为,它与回滚策略是正交的,可以根据需要独立配置。

通过合理配置 @Transactional 的回滚策略,你可以更精确地控制事务的行为,以适应复杂的业务需求。然而,应当谨慎使用自定义回滚规则,确保它们与你的业务逻辑和数据完整性要求相匹配。

使用方式:

1
2
3
4
5
6
7
8
9
10
11
@Transactional(noRollbackFor = ArithmeticException.class)
//@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
public void buyBook(Integer bookId, Integer userId) {
    //查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    //更新图书的库存
    bookDao.updateStock(bookId);
    //更新用户的余额
    bookDao.updateBalance(userId, price);
    System.out.println(1/0);
}

结果:

虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当出现ArithmeticException不发生回滚,因此购买图书的操作正常执行

7.3.8.事务属性:隔离级别

@Transactional 注解的 isolation 属性用于指定事务的隔离级别。事务隔离级别定义了在并发执行事务时数据的一致性和完整性如何得到保证。不同的隔离级别解决了不同级别的并发问题,如脏读.不可重复读和幻读。

以下是 @Transactional 注解中 isolation 属性可以设置的一些值:

  1. ISOLATION_DEFAULT:使用底层数据库的默认隔离级别。

  2. ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许脏读,即允许读取未提交的事务数据。

  3. ISOLATION_READ_COMMITTED:允许读取未提交的事务数据,但不允许脏读。这是大多数情况下的默认隔离级别。

  4. ISOLATION_REPEATABLE_READ:保证在一个事务的执行期间看到的数据保持不变,即使另一个事务在该事务期间尝试修改了数据。这通常用于解决不可重复读的问题。

  5. ISOLATION_SERIALIZABLE:最高的隔离级别,完全串行化的事务执行,避免了脏读.不可重复读和幻读,但可能会降低并发性能。

  6. 示例

1
2
3
4
@Transactional(isolation = Isolation.READ_COMMITTED)
public void someServiceMethod() {
    // 执行数据库操作
}

在这个示例中,someServiceMethod 方法被声明为具有 READ_COMMITTED 隔离级别的事务。

  1. 数据库支持:不同的数据库对隔离级别的支持和实现可能有所不同。确保你的数据库支持你选择的隔离级别。

  2. 事务管理器:确保你的事务管理器(如 DataSourceTransactionManagerJpaTransactionManager)能够正确地应用指定的隔离级别。

  3. 事务属性isolation@Transactional 注解的一个属性,与 propagation.timeout.readOnly 等属性一起,共同定义了事务的行为。

  4. 注意:提高隔离级别通常可以增强数据的一致性,但可能会降低系统的并发性能。因此,选择隔离级别时需要在数据一致性和性能之间做出权衡。

通过使用 @Transactional(isolation = Isolation.READ_COMMITTED) 或其他隔离级别,你可以控制事务在并发执行时的行为,以满足应用程序对数据一致性的要求。然而,应当谨慎选择隔离级别,因为它可能对系统的性能和并发能力产生影响。

各个隔离级别解决并发问题的能力见下表:

隔离级别脏读不可重复读幻读
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE

各种数据库产品对事务隔离级别的支持程度:

隔离级别OracleMySQL
READ UNCOMMITTED×
READ COMMITTED√(默认)
REPEATABLE READ×√(默认)
SERIALIZABLE

使用方式:

1
2
3
4
5
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化

7.3.9.事务属性:传播行为

@Transactional 注解的 propagation 属性定义了事务的传播行为。传播行为决定了在事务上下文中如何创建和使用事务。以下是 Spring 支持的事务传播行为:

  1. Propagation.REQUIRED:这是默认的传播行为。如果存在一个事务,方法将加入该事务;如果不存在事务,将创建一个新的事务。

  2. Propagation.SUPPORTS:如果存在一个事务,方法将加入该事务;如果不存在事务,方法将非事务性地执行。

  3. Propagation.MANDATORY:如果存在一个事务,方法将加入该事务;如果不存在事务,将抛出异常 IllegalStateException

  4. Propagation.REQUIRES_NEW:无论是否存在一个事务,总是创建一个新的事务,并暂停现有的事务。

  5. Propagation.NOT_SUPPORTED:方法应该以非事务性的方式执行;如果存在一个活动的事务,该事务将被暂停。

  6. Propagation.NEVER:方法应该以非事务性的方式执行;如果存在一个活动的事务,将抛出异常 IllegalStateException

  7. Propagation.NESTED:如果支持嵌套事务,当前的事务将被挂起,并创建一个新的事务。如果不支持嵌套事务,这与 REQUIRED 行为相同。

  8. 示例

1
2
3
4
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void someServiceMethod() {
    // 这个方法将总是运行在自己的事务中,与外部事务无关
}

在这个示例中,someServiceMethod 将始终在一个新事务中执行,无论调用它的环境是否已经有事务在进行。

  1. 嵌套事务:在使用 NESTED 传播行为时,需要注意事务管理器是否支持 JDBC 3 规范的 savepoint 操作。如果支持,嵌套事务可以通过保存点来实现。

  2. 事务管理器:确保你的事务管理器(如 DataSourceTransactionManagerJpaTransactionManager)能够正确地应用指定的传播行为。

  3. 事务属性propagation@Transactional 注解的一个属性,与 isolation.timeout.readOnly 等属性一起,共同定义了事务的行为。

通过合理配置 @Transactional 的传播行为,你可以更精确地控制事务的使用,以适应不同的业务场景和并发需求。然而,应当谨慎选择传播行为,因为它可能对系统的事务管理和性能产生影响。

测试:

  1. 创建接口CheckoutService

    1
    2
    3
    4
    5
    
    package com.muzhi.spring6.service;
       
    public interface CheckoutService {
        void checkout(Integer[] bookIds, Integer userId);
    }
    
  2. 创建实现类CheckoutServiceImpl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    package com.muzhi.spring6.service.impl;
       
    @Service
    public class CheckoutServiceImpl implements CheckoutService {
       
        @Autowired
        private BookService bookService;
       
        @Override
        @Transactional
        //一次购买多本图书
        public void checkout(Integer[] bookIds, Integer userId) {
            for (Integer bookId : bookIds) {
                bookService.buyBook(bookId, userId);
            }
        }
    }
    
  3. BookController中添加方法:

    1
    2
    3
    4
    5
    6
    
    @Autowired
    private CheckoutService checkoutService;
       
    public void checkout(Integer[] bookIds, Integer userId){
        checkoutService.checkout(bookIds, userId);
    }
    
  4. 测试场景

    在数据库中将用户的余额修改为100元

    结果:

    可以通过@Transactional中的propagation属性设置事务传播行为

    修改BookServiceImpl中buyBook()上,注解@Transactional的propagation属性

    @Transactional(propagation = Propagation.REQUIRED),默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。经过观察,购买图书的方法buyBook()在checkout()中被调用,checkout()上有事务注解,因此在此事务中执行。所购买的两本图书的价格为80和50,而用户的余额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了

    @Transactional(propagation = Propagation.REQUIRES_NEW),表示不管当前线程上是否有已经开启的事务,都要开启新事务。同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本。

7.3.10.全注解配置事务

  1. 创建配置类SpringConfig

    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
    
    package com.atguigu.spring6.config;
       
    import com.alibaba.druid.pool.DruidDataSource;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    import javax.sql.DataSource;
       
    @Configuration
    @ComponentScan("com.muzhi.spring6")
    @EnableTransactionManagement
    public class SpringConfig {
       
        @Bean
        public DataSource getDataSource(){
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
            dataSource.setUrl("jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false");
            dataSource.setUsername("root");
            dataSource.setPassword("root");
            return dataSource;
        }
       
        @Bean(name = "jdbcTemplate")
        public JdbcTemplate getJdbcTemplate(DataSource dataSource){
            JdbcTemplate jdbcTemplate = new JdbcTemplate();
            jdbcTemplate.setDataSource(dataSource);
            return jdbcTemplate;
        }
       
        @Bean
        public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
            DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
            dataSourceTransactionManager.setDataSource(dataSource);
            return dataSourceTransactionManager;
        }
    }
    
  2. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    import com.muzhi.spring6.config.SpringConfig;
    import com.muzhi.spring6.controller.BookController;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
       
    public class TxByAllAnnotationTest {
       
        @Test
        public void testTxAllAnnotation(){
            ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
            BookController accountService = applicationContext.getBean("bookController", BookController.class);
            accountService.buyBook(1, 1);
        }
    }
    

7.4.基于XML的声明式事务

7.4.1.测试示例

参考基于注解的声明式事务

7.4.2.修改Spring配置文件

  1. 将Spring配置文件中去掉tx:annotation-driven 标签,并添加配置:

    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
    
    <aop:config>
        <!-- 配置事务通知和切入点表达式 -->
        <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.muzhi.spring.tx.xml.service.impl.*.*(..))"></aop:advisor>
    </aop:config>
    <!-- tx:advice标签配置事务通知 -->
    <!-- id属性给事务通知标签设置唯一标识便于引用 -->
    <!-- transaction-manager属性关联事务管理器 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!-- tx:method标签配置具体的事务方法 -->
            <!-- name属性指定方法名可以使用星号代表多个字符 -->
            <tx:method name="get*" read-only="true"/>
            <tx:method name="query*" read-only="true"/>
            <tx:method name="find*" read-only="true"/>
           
            <!-- read-only属性设置只读属性 -->
            <!-- rollback-for属性设置回滚的异常 -->
            <!-- no-rollback-for属性设置不回滚的异常 -->
            <!-- isolation属性设置事务的隔离级别 -->
            <!-- timeout属性设置事务的超时属性 -->
            <!-- propagation属性设置事务的传播行为 -->
            <tx:method name="save*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
            <tx:method name="update*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
            <tx:method name="delete*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
        </tx:attributes>
    </tx:advice>
    
  2. 引入依赖

    1
    2
    3
    4
    5
    
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>6.0.2</version>
    </dependency>
    

8.资源操作:Resource

image-20240504073649705

8.1.介绍

Java’s standard class and standard handlers for various URL prefixes, unfortunately, are not quite adequate enough for all access to low-level resources. For example, there is no standardized implementation that may be used to access a resource that needs to be obtained from the classpath or relative to a . While it is possible to register new handlers for specialized prefixes (similar to existing handlers for prefixes such as ), this is generally quite complicated, and the interface still lacks some desirable functionality, such as a method to check for the existence of the resource being pointed to.java.net.URL URL ServletContext URL http: URL

8.2.官方文档

资源 :: Spring Framework

8.3.Resource 接口

Spring的接口位于软件包中 旨在成为一个功能更强大的接口,用于抽象对低级资源的访问。这 以下列表提供了接口的概述。有关更多详细信息,请参阅资源 javadoc。Resource org.springframework.core.io. Resource

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
public interface Resource extends InputStreamSource {

	boolean exists();

	boolean isReadable();

	boolean isOpen();

	boolean isFile();

	URL getURL() throws IOException;

	URI getURI() throws IOException;

	File getFile() throws IOException;

	ReadableByteChannel readableChannel() throws IOException;

	long contentLength() throws IOException;

	long lastModified() throws IOException;

	Resource createRelative(String relativePath) throws IOException;

	String getFilename();

	String getDescription();
}

如接口的定义所示,它扩展了接口。以下列表显示了接口的定义:Resource InputStreamSource InputStreamSource

1
2
3
4
public interface InputStreamSource {

	InputStream getInputStream() throws IOException;
}

界面中一些最重要的方法是:Resource

  • getInputStream():查找并打开资源,返回 for 从资源中读取。预计每次调用都会返回一个新的 .调用方负责关闭流。InputStream InputStream
  • exists():返回一个指示此资源是否实际存在于 物理形式。boolean
  • isOpen():返回指示此资源是否表示句柄 使用开放流。如果 ,则不能多次读取,并且 必须只读取一次,然后关闭以避免资源泄漏。返回 所有常用的资源实现,但 .boolean true InputStream false InputStreamResource
  • getDescription():返回此资源的说明,用于错误 使用资源时的输出。这通常是完全限定的文件名或 资源的实际 URL。

其他方法允许您获得表示 资源(如果基础实现兼容并支持该实现 功能)。URL File

该接口的某些实现还实现了扩展的 WritableResource 接口 用于支持写入的资源。Resource

Spring 本身广泛使用抽象,作为 需要资源时的许多方法签名。某些 Spring API 中的其他方法 (例如各种实现的构造函数)采用一种以朴素或简单的形式用于创建适当的 该上下文实现,或者通过路径上的特殊前缀,让 调用方指定必须创建和使用特定的实现。Resource ApplicationContext String Resource String Resource

虽然该接口在 Spring 和 Spring 中被大量使用,但它实际上是 非常方便地在自己的代码中单独用作通用实用程序类,以便访问 资源,即使你的代码不知道或不关心 Spring 的任何其他部分。 虽然这会将您的代码耦合到 Spring,但它实际上只将其耦合到这一小集合 实用程序类,它可以作为更有能力的替代品,并且可以 被认为等同于您将用于此目的的任何其他库。Resource URL

8.4.Resource的实现类

Resource 接口是 Spring 资源访问策略的抽象,它本身并不提供任何资源访问实现,具体的资源访问由该接口的实现类完成——每个实现类代表一种资源访问策略。Resource一般包括这些实现类:UrlResource.ClassPathResource.FileSystemResource.ServletContextResource.InputStreamResource.ByteArrayResource

8.4.1.UrlResource访问网络资源

Resource的一个实现类,用来访问网络资源,它支持URL的绝对路径。

http:——该前缀用于访问基于HTTP协议的网络资源。

ftp:——该前缀用于访问基于FTP协议的网络资源

file: ——该前缀用于从文件系统中读取资源

实验:访问基于HTTP协议的网络资源

创建一个maven子模块spring6-resources,配置Spring依赖(参考前面)

image-20240504093347353

  1. 访问网络资源

    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
    
    package com.muzhi.spring6.resources;
       
    import org.springframework.core.io.UrlResource;
       
    public class UrlResourceDemo {
       
        public static void loadAndReadUrlResource(String path){
            // 创建一个 Resource 对象
            UrlResource url = null;
            try {
                url = new UrlResource(path);
                // 获取资源名
                System.out.println(url.getFilename());
                System.out.println(url.getURI());
                // 获取资源描述
                System.out.println(url.getDescription());
                //获取资源内容
                System.out.println(url.getInputStream().read());
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
           
        public static void main(String[] args) {
            //访问网络资源
            loadAndReadUrlResource("http://www.baidu.com");
        }
    }
    
  2. 访问文件资源

    1
    2
    
        //访问文件系统资源
        loadAndReadUrlResource("file:atguigu.txt");
    

8.4.2.ClassPathResource 访问类路径下资源

ClassPathResource 用来访问类加载路径下的资源,相对于其他的 Resource 实现类,其主要优势是方便访问类加载路径里的资源,尤其对于 Web 应用,ClassPathResource 可自动搜索位于 classes 下的资源文件,无须使用绝对路径访问。

实验:在类路径下创建文件 muzhi.txt,使用ClassPathResource 访问 可参考上图

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
package com.muzhi.spring6.resources;

import org.springframework.core.io.ClassPathResource;
import java.io.InputStream;

public class ClassPathResourceDemo {

    public static void loadAndReadUrlResource(String path) throws Exception{
        // 创建一个 Resource 对象
        ClassPathResource resource = new ClassPathResource(path);
        // 获取文件名
        System.out.println("resource.getFileName = " + resource.getFilename());
        // 获取文件描述
        System.out.println("resource.getDescription = "+ resource.getDescription());
        //获取文件内容
        InputStream in = resource.getInputStream();
        byte[] b = new byte[1024];
        while(in.read(b)!=-1) {
            System.out.println(new String(b));
        }
    }

    public static void main(String[] args) throws Exception {
        loadAndReadUrlResource("muzhi.txt");
    }
}

ClassPathResource实例可使用ClassPathResource构造器显式地创建,但更多的时候它都是隐式地创建的。当执行Spring的某个方法时,该方法接受一个代表资源路径的字符串参数,当Spring识别该字符串参数中包含classpath:前缀后,系统会自动创建ClassPathResource对象。

8.4.3.FileSystemResource 访问文件系统资源

Spring 提供的 FileSystemResource 类用于访问文件系统资源,使用 FileSystemResource 来访问文件系统资源并没有太大的优势,因为 Java 提供的 File 类也可用于访问文件系统资源。

实验:使用FileSystemResource 访问文件系统资源

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
package com.muzhi.spring6.resources;

import org.springframework.core.io.FileSystemResource;

import java.io.InputStream;

public class FileSystemResourceDemo {

    public static void loadAndReadUrlResource(String path) throws Exception{
        //相对路径
        FileSystemResource resource = new FileSystemResource("muzhi.txt");
        //绝对路径
        //FileSystemResource resource = new FileSystemResource("C:\\muzhi.txt");
        // 获取文件名
        System.out.println("resource.getFileName = " + resource.getFilename());
        // 获取文件描述
        System.out.println("resource.getDescription = "+ resource.getDescription());
        //获取文件内容
        InputStream in = resource.getInputStream();
        byte[] b = new byte[1024];
        while(in.read(b)!=-1) {
            System.out.println(new String(b));
        }
    }

    public static void main(String[] args) throws Exception {
        loadAndReadUrlResource("muzhi.txt");
    }
}

FileSystemResource实例可使用FileSystemResource构造器显示地创建,但更多的时候它都是隐式创建。执行Spring的某个方法时,该方法接受一个代表资源路径的字符串参数,当Spring识别该字符串参数中包含file:前缀后,系统将会自动创建FileSystemResource对象。

8.4.4.ServletContextResource

这是ServletContext资源的Resource实现,它解释相关Web应用程序根目录中的相对路径。它始终支持流(stream)访问和URL访问,但只有在扩展Web应用程序存档且资源实际位于文件系统上时才允许java.io.File访问。无论它是在文件系统上扩展还是直接从JAR或其他地方(如数据库)访问,实际上都依赖于Servlet容器。

8.4.5.InputStreamResource

InputStreamResource 是给定的输入流(InputStream)的Resource实现。它的使用场景在没有特定的资源实现的时候使用(感觉和@Component 的适用场景很相似)。与其他Resource实现相比,这是已打开资源的描述符。 因此,它的isOpen()方法返回true。如果需要将资源描述符保留在某处或者需要多次读取流,请不要使用它。

8.4.6.ByteArrayResource

字节数组的Resource实现类。通过给定的数组创建了一个ByteArrayInputStream。它对于从任何给定的字节数组加载内容非常有用,而无需求助于单次使用的InputStreamResource。

8.5.Resource类图

image-20240504094809073

8.6.ResourceLoader 接口

8.6.1.ResourceLoader 概述

Spring 提供如下两个标志性接口:

1. ResourceLoader : 该接口实现类的实例可以获得一个Resource实例。

2. ResourceLoaderAware : 该接口实现类的实例将获得一个ResourceLoader的引用。

在ResourceLoader接口里有如下方法:

  1. Resource getResource(String location) : 该接口仅有这个方法,用于返回一个Resource实例。ApplicationContext实现类都实现ResourceLoader接口,因此ApplicationContext可直接获取Resource实例。

8.6.2.使用演示

  1. ClassPathXmlApplicationContext获取Resource实例

    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
    
    package com.muzhi.spring6.resource;
       
    import org.springframework.core.io.ClassPathResource;
       
    import java.io.IOException;
    import java.io.InputStream;
       
    //访问类路径下资源
    public class ClassPathResourceDemo {
       
        public static void loadClasspathResource(String path) {
            //创建对象ClassPathResource
            ClassPathResource resource = new ClassPathResource(path);
       
            System.out.println(resource.getFilename());
            System.out.println(resource.getDescription());
            //获取文件内容
            try {
                InputStream in = resource.getInputStream();
                byte[] b = new byte[1024];
                while(in.read(b)!=-1) {
                    System.out.println(new String(b));
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
       
        public static void main(String[] args) {
            loadClasspathResource("muzhi.txt");
        }
    }
    
  2. FileSystemApplicationContext获取Resource实例

    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
    
    package com.muzhi.spring6.resource;
       
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import org.springframework.core.io.FileSystemResource;
    import org.springframework.core.io.Resource;
       
    import java.io.IOException;
    import java.io.InputStream;
       
    //访问系统资源
    public class FileSystemResourceDemo {
       
        public static void main(String[] args) {
            ApplicationContext ctx = new ClassPathXmlApplicationContext();
    //        通过ApplicationContext访问资源
    //        ApplicationContext实例获取Resource实例时,
    //        默认采用与ApplicationContext相同的资源访问策略
            Resource res = ctx.getResource("muzhi.txt");
            System.out.println("=="+res);
            System.out.println(res.getFilename());
       
    //        loadFileResource("c:\\muzhi.txt");
    //        loadFileResource("muzhi.txt");
        }
       
        public static void loadFileResource(String path) {
            //创建对象
            FileSystemResource resource = new FileSystemResource(path);
       
            System.out.println(resource.getFilename());
            System.out.println(resource.getDescription());
            try {
                InputStream in = resource.getInputStream();
                byte[] b = new byte[1024];
                while(in.read(b)!=-1) {
                    System.out.println(new String(b));
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
       
        }
    }
    

8.6.3.ResourceLoader 总结

Spring将采用和ApplicationContext相同的策略来访问资源。也就是说,如果ApplicationContext是FileSystemXmlApplicationContext,res就是FileSystemResource实例;如果ApplicationContext是ClassPathXmlApplicationContext,res就是ClassPathResource实例

当Spring应用需要进行资源访问时,实际上并不需要直接使用Resource实现类,而是调用ResourceLoader实例的getResource()方法来获得资源,ReosurceLoader将会负责选择Reosurce实现类,也就是确定具体的资源访问策略,从而将应用程序和具体的资源访问策略分离开来

另外,使用ApplicationContext访问资源时,可通过不同前缀指定强制使用指定的ClassPathResource.FileSystemResource等实现类

1
2
3
Resource res = ctx.getResource("calsspath:bean.xml");
Resrouce res = ctx.getResource("file:bean.xml");
Resource res = ctx.getResource("http://localhost:8080/beans.xml");

8.7.ResourceLoaderAware 接口

ResourceLoaderAware接口实现类的实例将获得一个ResourceLoader的引用,ResourceLoaderAware接口也提供了一个setResourceLoader()方法,该方法将由Spring容器负责调用,Spring容器会将一个ResourceLoader对象作为该方法的参数传入。

如果把实现ResourceLoaderAware接口的Bean类部署在Spring容器中,Spring容器会将自身当成ResourceLoader作为setResourceLoader()方法的参数传入。由于ApplicationContext的实现类都实现了ResourceLoader接口,Spring容器自身完全可作为ResorceLoader使用。

  1. ResourceLoaderAware使用

    1. 创建类,实现ResourceLoaderAware接口

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
      package com.muzhi.spring6.resourceloaderaware;
            
      import org.springframework.context.ResourceLoaderAware;
      import org.springframework.core.io.ResourceLoader;
            
      public class TestBean implements ResourceLoaderAware {
            
          private ResourceLoader resourceLoader;
          @Override
          public void setResourceLoader(ResourceLoader resourceLoader) {
              this.resourceLoader = resourceLoader;
          }
            
          public ResourceLoader getResourceLoader() {
              return this.resourceLoader;
          }
      }
            
      
    2. 创建bean.xml文件,配置TestBean

      1
      2
      3
      4
      5
      6
      7
      
      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
            
          <bean id="testBean" class="com.muzhi.spring6.resourceloaderaware.TestBean"></bean>
      </beans>
      
    3. 测试

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      
      package com.muzhi.spring6.resouceloader;
            
      import org.springframework.context.ApplicationContext;
      import org.springframework.context.support.ClassPathXmlApplicationContext;
      import org.springframework.core.io.Resource;
      import org.springframework.core.io.ResourceLoader;
            
      public class TestDemo {
            
          public static void main(String[] args) {
              //Spring容器会将一个ResourceLoader对象作为该方法的参数传入
              ApplicationContext ctx = new ClassPathXmlApplicationContext("bean.xml");
              TestBean testBean = ctx.getBean("testBean",TestBean.class);
              //获取ResourceLoader对象
              ResourceLoader resourceLoader = testBean.getResourceLoader();
              System.out.println("Spring容器将自身注入到ResourceLoaderAware Bean 中 ? :" + (resourceLoader == ctx));
              //加载其他资源
              Resource resource = resourceLoader.getResource("muzhi.txt");
              System.out.println(resource.getFilename());
              System.out.println(resource.getDescription());
          }
      }
      

8.8.使用Resource 作为属性

前面介绍了 Spring 提供的资源访问策略,但这些依赖访问策略要么需要使用 Resource 实现类,要么需要使用 ApplicationContext 来获取资源。实际上,当应用程序中的 Bean 实例需要访问资源时,Spring 有更好的解决方法:直接利用依赖注入。从这个意义上来看,Spring 框架不仅充分利用了策略模式来简化资源访问,而且还将策略模式和 IoC 进行充分地结合,最大程度地简化了 Spring 资源访问。

归纳起来,如果 Bean 实例需要访问资源,有如下两种解决方案:

  • 代码中获取 Resource 实例。
  • 使用依赖注入。

对于第一种方式,当程序获取 Resource 实例时,总需要提供 Resource 所在的位置,不管通过 FileSystemResource 创建实例,还是通过 ClassPathResource 创建实例,或者通过 ApplicationContext 的 getResource() 方法获取实例,都需要提供资源位置。这意味着:资源所在的物理位置将被耦合到代码中,如果资源位置发生改变,则必须改写程序。因此,通常建议采用第二种方法,让 Spring 为 Bean 实例依赖注入资源。

让Spring为Bean实例依赖注入资源

  1. 创建依赖注入类,定义属性和方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    package com.muzhi.spring6.di;
       
    import org.springframework.core.io.Resource;
       
    public class ResourceBean {
       
        private Resource resource;
       
        public void setResource(Resource resource) {
            this.resource = resource;
        }
        public Resource getResource() {
            return resource;
        }
       
        public void parse() {
            System.out.println(resource.getFilename());
            System.out.println(resource.getDescription());
        }
    }
    
  2. 创建Spring配置文件,配置依赖注入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
       
        <bean id="resourceBean" class="com.muzhi.spring6.resouceloader.ResourceBean" >
          <!-- 可以使用file:.http:.ftp:等前缀强制Spring采用对应的资源访问策略 -->
          <!-- 如果不采用任何前缀,则Spring将采用与该ApplicationContext相同的资源访问策略来访问资源 -->
            <property name="res" value="classpath:muzhi.txt"/>
        </bean>
    </beans>
    
  3. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    package com.muzhi.spring6.resouceloader;
       
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
       
    public class TestBeanDemo {
       
        public static void main(String[] args) {
            ApplicationContext ctx =
                    new ClassPathXmlApplicationContext("bean.xml");
            ResourceBean resourceBean = ctx.getBean("resourceBean",ResourceBean.class);
            resourceBean.parse();
        }
    }
    

8.9.应用程序上下文和资源路径

8.9.1.概述

不管以怎样的方式创建ApplicationContext实例,都需要为ApplicationContext指定配置文件,Spring允许使用一份或多分XML配置文件。当程序创建ApplicationContext实例时,通常也是以Resource的方式来访问配置文件的,所以ApplicationContext完全支持ClassPathResource.FileSystemResource.ServletContextResource等资源访问方式。

ApplicationContext确定资源访问策略通常有两种方法:

  1. 使用ApplicationContext实现类指定访问策略。
  2. 使用前缀指定访问策略。

8.9.2.ApplicationContext实现类指定访问策略

创建ApplicationContext对象时,通常可以使用如下实现类:

  1. ClassPathXMLApplicationContext : 对应使用ClassPathResource进行资源访问。

  2. FileSystemXmlApplicationContext : 对应使用FileSystemResource进行资源访问。

  3. XmlWebApplicationContext : 对应使用ServletContextResource进行资源访问。

当使用ApplicationContext的不同实现类时,就意味着Spring使用响应的资源访问策略。

8.9.3.使用前缀指定访问策略

8.9.3.1.classpath 前缀使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.muzhi.spring6.context;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.core.io.Resource;

public class ClasspathDemo {

    public static void main(String[] args) {
        /*
         * 通过搜索文件系统路径下的xml文件创建ApplicationContext,
         * 但通过指定classpath:前缀强制搜索类加载路径
         * classpath:bean.xml
         * */
        ApplicationContext ctx =
                new ClassPathXmlApplicationContext("classpath:bean.xml");
        System.out.println(ctx);
        Resource resource = ctx.getResource("muzhi.txt");
        System.out.println(resource.getFilename());
        System.out.println(resource.getDescription());
    }
}
8.9.3.2.classpath通配符使用

classpath * :前缀提供了加载多个XML配置文件的能力,当使用classpath*:前缀来指定XML配置文件时,系统将搜索类加载路径,找到所有与文件名匹配的文件,分别加载文件中的配置定义,最后合并成一个ApplicationContext。

1
2
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:bean.xml");
System.out.println(ctx);

当使用classpath * :前缀时,Spring将会搜索类加载路径下所有满足该规则的配置文件。

如果不是采用classpath * :前缀,而是改为使用classpath:前缀,Spring则只加载第一个符合条件的XML文件

注意 :

classpath * : 前缀仅对ApplicationContext有效。实际情况是,创建ApplicationContext时,分别访问多个配置文件(通过ClassLoader的getResource方法实现)。因此,classpath * :前缀不可用于Resource。

8.9.3.3.通配符其他使用

一次性加载多个配置文件的方式:指定配置文件时使用通配符

1
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:bean*.xml");

Spring允许将classpath*:前缀和通配符结合使用:

1
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:bean*.xml");

9.国际化:i18n

image-20240504073604876

9.1.i18n概述

i18n 是 “internationalization”(国际化)的缩写,其中 “i” 和 “n” 分别代表单词的首字母和末字母,而数字 “18” 表示首尾字母之间的字符数。国际化是指设计和开发软件.文档或系统,以便它们可以轻松地适应不同的语言和文化,而无需进行重大的修改。

以下是 i18n 的一些关键概念和组成部分:

  1. 本地化(Localization):简称 L10n,是 i18n 的过程的一部分,涉及将软件.文档或系统调整为特定语言和文化环境。
  2. 字符编码:使用 Unicode 等字符编码标准,确保软件能够支持多种语言和字符集。
  3. 文本外键:将所有用户可见的文本(如界面元素.消息.文档等)移至代码之外,以便于翻译和维护。
  4. 日期和时间格式:支持多种日期和时间格式,以适应不同地区的习俗。
  5. 数字.货币和度量单位:根据目标地区的标准格式化数字.货币和度量单位。
  6. 从右到左(RTL)语言支持:一些语言(如阿拉伯语和希伯来语)是从右到左阅读的,软件界面需要能够支持这种布局。
  7. 文化习俗:考虑不同文化的社会习俗和偏好,避免设计和内容上的文化不敏感。
  8. 可扩展性:设计数据结构和算法时,考虑到将来可能会添加新的地区和语言。
  9. 本地化测试:在不同的语言和地区设置中测试软件,确保其正确性和可用性。
  10. 法律和合规性:遵守目标地区的法律法规,如数据保护法规.版权法和商标法。
  11. 多语言搜索和排序:支持不同语言的搜索和排序规则。
  12. 用户偏好设置:允许用户选择自己的语言和地区设置。
  13. 国际化框架和库:使用如 ICU(International Components for Unicode)等国际化框架和库来简化开发过程。
  14. 国际化最佳实践:遵循行业标准和最佳实践,如使用 gettext 或其它翻译管理工具来管理翻译资源。

9.2.Java国际化

9.2.1.Java国际化概述

Java 国际化(Internationalization,简称 i18n)是设计和开发软件的过程,使其能够适应不同的语言和地区而无需进行重大的修改。国际化的目的是创建一个可以轻松本地化(Localization,简称 L10n)的软件框架,本地化是针对特定地区进行的软件调整,包括语言.货币.日期和时间格式等。

以下是 Java 国际化的一些关键概念和实践:

  1. Locale:Java 中 Locale 类表示了国际化设置,如语言.国家和变体。它用于向格式化和本地化相关的类提供信息。

    1
    2
    3
    4
    5
    6
    7
    8
    
        /**
         * This method must be called only for creating the Locale.*
         * constants due to making shortcuts.
         */
        private static Locale createConstant(String lang, String country) {
            BaseLocale base = BaseLocale.createInstance(lang, country);
            return getInstance(base, null);
        }
    
  2. ResourceBundle:这个类用于加载属性文件,这些文件包含了特定于区域的字符串和其他资源,如错误消息和用户界面元素。

  3. 属性文件:国际化应用程序通常使用属性文件来存储本地化字符串。这些文件根据语言和国家有不同版本,例如 messages_en_US.propertiesmessages_zh_CN.properties

    配置文件命名规则: basename_language_country.properties 必须遵循以上的命名规则,java才会识别。其中,basename是必须的,语言和国家是可选的。这里存在一个优先级概念,如果同时提供了messages.properties和messages_zh_CN.propertes两个配置文件,如果提供的locale符合en_CN,那么优先查找messages_en_CN.propertes配置文件,如果没查找到,再查找messages.properties配置文件。最后,提示下,所有的配置文件必须放在classpath中,一般放在resources目录下

  4. 格式化:Java 提供了多个与 Locale 相关的类,如 NumberFormat.DateFormatMessageFormat,用于根据不同地区的习俗来格式化数字.日期和消息。

  5. 国际化工具类:Java API 提供了一些工具类来支持国际化,包括 Collator(用于字符串比较和排序)和 Currency(用于货币格式化)。

  6. 字符编码:Java 推荐使用 Unicode 编码(如 UTF-8)来支持国际化文本。

  7. 消息外部化:将所有用户可见的文本放在外部资源文件中,这样在不修改代码的情况下就可以切换不同的语言。

  8. 动态本地化:在运行时根据用户的偏好设置或浏览器信息动态选择 Locale

  9. 测试:对应用程序进行测试,确保它在不同的语言和地区设置下都能正确工作。

  10. 兼容性:确保应用程序在不同的 Java 版本和平台上都能正确国际化。

  11. 工具和框架:使用如 Spring Framework 提供的国际化支持,可以更简单地实现复杂的国际化需求。

  12. 本地化测试:使用模拟或实际的 Locale 设置进行测试,以验证应用程序的行为是否符合预期。

  13. 最佳实践:遵循 Java 国际化的最佳实践,如使用意义明确的资源文件键.避免硬编码字符串等。

9.2.2.Java国际化示例

  1. 创建子模块spring6-i18n,引入Spring依赖

    image-20240504104922341

  2. 在resource目录下创建两个配置文件:messages_zh_CN.propertes和messages_en_GB.propertes

  3. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    package com.muzhi.spring6.javai18n;
       
    import java.util.Locale;
    import java.util.ResourceBundle;
       
    public class ResourceI18n {
       
        public static void main(String[] args) {
            ResourceBundle bundle1 = ResourceBundle.getBundle("messages",
                    new Locale("zh", "CN"));
            String value1 = bundle1.getString("test");
            System.out.println(value1);
       
            ResourceBundle bundle2 = ResourceBundle.getBundle("messages",
                    new Locale("en","GB"));
            String value2 = bundle2.getString("test");
            System.out.println(value2);
        }
    }
    

9.3.Spring6国际化

9.3.1.MessageSource接口

spring中国际化是通过MessageSource这个接口来支持的

常见实现类

ResourceBundleMessageSource

这个是基于Java的ResourceBundle基础类实现,允许仅通过资源名加载国际化资源

ReloadableResourceBundleMessageSource

这个功能和第一个类的功能类似,多了定时刷新功能,允许在不重启系统的情况下,更新资源的信息

StaticMessageSource

它允许通过编程的方式提供国际化信息,一会我们可以通过这个来实现db中存储国际化信息的功能。

9.3.2.使用Spring6国际化

  1. 创建资源文件

    国际化文件命名格式:基本名称 _ 语言 _ 国家.properties

    创建muzhi_en_US.propertiesmuzhi_zh_CN.properties

    www.muzhi.com=welcome {0},时间:{1}

    www.atguigu.com=欢迎 {0},时间:{1}

  2. 创建spring配置文件,配置MessageSource

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd">
       
        <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
            <property name="basenames">
                <list>
                    <value>muzhi</value>
                </list>
            </property>
            <property name="defaultEncoding">
                <value>utf-8</value>
            </property>
        </bean>
    </beans>
    
  3. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    package com.muzhi.spring6.springi18n;
       
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
       
    import java.util.Date;
    import java.util.Locale;
       
    public class ResourceI18n {
       
        public static void main(String[] args) {
            ApplicationContext context =
                    new ClassPathXmlApplicationContext("bean.xml");
       
            Object[] objs = new Object[]{"muzhi",new Date().toString()};
            String value = context.getMessage("www.muzhi.com", objs, Locale.UK);
            System.out.println(value);
        }
    }
    

10.数据校验:Validation

image-20240504073416152

Spring Validation 是 Spring 框架提供的一种数据验证机制,它基于 JavaBean 规范的约束,允许你通过注解的方式来验证数据。Spring Validation 通常用于对用户输入的数据进行校验,确保数据的合法性和应用的健壮性。以下是 Spring Validation 的一些关键点:

  1. 注解驱动的校验:Spring 通过 JSR 380(Bean Validation 2.0)规范提供的注解来实现校验。常用的校验注解包括 @NotNull.@Size.@Min.@Max 等。
  2. 验证组:可以为不同的校验注解指定验证组(Group),以便于在不同的场景下执行不同的校验逻辑。
  3. 自定义注解:除了使用标准的校验注解,Spring 也支持自定义注解,以满足特定的校验需求。
  4. 验证错误信息:校验失败时,Spring 可以生成默认的错误信息,也可以自定义错误信息。
  5. 方法参数校验:Spring 支持对方法参数进行校验,这通常用于 Controller 层,确保接收到的参数满足业务规则。
  6. 对象属性校验:可以对 Java 对象的属性进行校验,确保对象状态的合法性。
  7. Spring MVC 集成:在 Spring MVC 中,可以使用 @Valid@Validated 注解来对传入的参数进行校验。
  8. 消息源:Spring Validation 支持国际化,可以通过配置 MessageSource 来自定义校验错误信息。
  9. 编程式校验:除了使用注解,Spring 也提供了编程式的校验 API,可以在代码中手动触发校验过程。
  10. 非注解方式:除了在 Java 类上使用注解,也可以使用 XML 配置或 Java 配置来定义校验逻辑。
  11. 整合 Hibernate Validator:Spring Validation 通常与 Hibernate Validator 整合使用,Hibernate Validator 是一个实现了 JSR 380 规范的校验器。
  12. 异步校验:支持异步校验,可以对耗时的校验逻辑进行优化。
  13. 继承和组合:可以使用组合注解(如 @Email@Pattern 的组合)和继承的方式来简化校验注解的使用。

10.1.通过Validator接口实现

  1. 创建子模块spring6-validator

    image-20240504110324562

  2. 引入相关依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    <dependencies>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>7.0.5.Final</version>
        </dependency>
       
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>jakarta.el</artifactId>
            <version>4.0.1</version>
        </dependency>
    </dependencies>
    
  3. 创建Person

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    package com.muzhi.spring6.validator.one;
       
    public class Person {
       
        private String name;
        private int age;
       
        public String getName() {
            return name;
        }
       
        public void setName(String name) {
            this.name = name;
        }
       
        public int getAge() {
            return age;
        }
       
        public void setAge(int age) {
            this.age = age;
        }
    }
    
  4. 创建类实现Validator接口,并实现接口指定校验规则

    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
    
    package com.muzhi.spring6.validator.one;
       
    import org.springframework.validation.Errors;
    import org.springframework.validation.ValidationUtils;
    import org.springframework.validation.Validator;
       
    public class PersonValidator implements Validator {
       
        @Override
        public boolean supports(Class<?> clazz) {
            return Person.class.equals(clazz);
        }
       
        //校验规则
        @Override
        public void validate(Object target, Errors errors) {
            //name不能为空
            ValidationUtils.rejectIfEmpty(errors,
                    "name", "name.empty","name is null");
            //age 不能小于0,不能大于200
            Person p = (Person)target;
            if(p.getAge() < 0) {
                errors.rejectValue("age","age.value.error","age < 0");
            } else if(p.getAge() > 200) {
               errors.rejectValue("age","age.value.error.old","age > 200");
            }
        }
    }
       
    

    supports方法用来表示此校验用在哪个类型上,validate是设置校验逻辑的地点,其中ValidationUtils,是Spring封装的校验工具类,帮助快速实现校验。

  5. 测试

    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
    
    package com.muzhi.spring6.validator.one;
       
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.DataBinder;
       
    //校验测试
    public class TestPerson {
       
        public static void main(String[] args) {
            //创建person对象
            Person person = new Person();
            person.setName("lucy");
            person.setAge(250);
       
            //创建person对应databinder
            DataBinder binder = new DataBinder(person);
       
            //设置校验器
            binder.setValidator(new PersonValidator());
       
            //调用方法执行校验
            binder.validate();
       
            //输出校验结果
            BindingResult result = binder.getBindingResult();
            System.out.println(result.getAllErrors());
        }
    }
    

10.2.Bean Validation注解实现

使用Bean Validation校验方式,就是如何将Bean Validation需要使用的javax.validation.ValidatorFactory 和javax.validation.Validator注入到容器中。spring默认有一个实现类LocalValidatorFactoryBean,它实现了上面Bean Validation中的接口,并且也实现了org.springframework.validation.Validator接口。

  1. 创建配置类,配置LocalValidatorFactoryBean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    package com.muzhi.spring6.validator.three;
       
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
       
    @Configuration
    @ComponentScan("com.muzhi.spring6.validator.three")
    public class ValidationConfig {
       
        @Bean
        public MethodValidationPostProcessor validationPostProcessor() {
            return new MethodValidationPostProcessor();
        }
    }
    
  2. 使用注解定义校验规则

    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
    
    package com.muzhi.spring6.validator.three;
       
    import com.muzhi.spring6.validator.four.CannotBlank;
    import jakarta.validation.constraints.*;
       
    public class User {
       
        @NotNull
        private String name;
       
        @Min(0)
        @Max(150)
        private int age;
       
        @Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$",message = "手机号码格式错误")
        @NotBlank(message = "手机号码不能为空")
        private String phone;
       
        @CannotBlank
        private String message;
       
        public String getMessage() {
            return message;
        }
       
        public void setMessage(String message) {
            this.message = message;
        }
       
        public String getName() {
            return name;
        }
       
        public void setName(String name) {
            this.name = name;
        }
       
        public int getAge() {
            return age;
        }
       
        public void setAge(int age) {
            this.age = age;
        }
       
        public String getPhone() {
            return phone;
        }
       
        public void setPhone(String phone) {
            this.phone = phone;
        }
    }
    

    常用注解说明 @NotNull 限制必须不为null @NotEmpty 只作用于字符串类型,字符串不为空,并且长度不为0 @NotBlank 只作用于字符串类型,字符串不为空,并且trim()后不为空串 @DecimalMax(value) 限制必须为一个不大于指定值的数字 @DecimalMin(value) 限制必须为一个不小于指定值的数字 @Max(value) 限制必须为一个不大于指定值的数字 @Min(value) 限制必须为一个不小于指定值的数字 @Pattern(value) 限制必须符合指定的正则表达式 @Size(max,min) 限制字符长度必须在min到max之间 @Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式

  3. 使用两种不同的校验器实现

  4. 使用jakarta.validation.Validator校验

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    package com.muzhi.spring6.validation.method2;
          
    import jakarta.validation.ConstraintViolation;
    import jakarta.validation.Validator;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import java.util.Set;
          
    @Service
    public class MyService1 {
          
        @Autowired
        private Validator validator;
          
        public  boolean validator(User user){
            Set<ConstraintViolation<User>> sets =  validator.validate(user);
            return sets.isEmpty();
        }
    }
    
  5. 使用org.springframework.validation.Validator校验

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    package com.muzhi.spring6.validation.method2;
          
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.validation.BindException;
    import org.springframework.validation.Validator;
          
    @Service
    public class MyService2 {
          
        @Autowired
        private Validator validator;
          
        public boolean validaPersonByValidator(User user) {
            BindException bindException = new BindException(user, user.getName());
            validator.validate(user, bindException);
            return bindException.hasErrors();
        }
    }
    
  6. 测试

    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
    
    package com.muzhi.spring6.validation.method2;
       
    import org.junit.jupiter.api.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
       
    public class TestMethod2 {
       
        @Test
        public void testMyService1() {
            ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfig.class);
            MyService1 myService = context.getBean(MyService1.class);
            User user = new User();
            user.setAge(-1);
            boolean validator = myService.validator(user);
            System.out.println(validator);
        }
       
        @Test
        public void testMyService2() {
            ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfig.class);
            MyService2 myService = context.getBean(MyService2.class);
            User user = new User();
            user.setName("lucy");
            user.setAge(130);
            user.setAge(-1);
            boolean validator = myService.validaPersonByValidator(user);
            System.out.println(validator);
        }
    }
    

10.3.基于方法实现校验

  1. 创建配置类,配置MethodValidationPostProcessor

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    package com.muzhi.spring6.validation.method3;
       
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
    import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
       
    @Configuration
    @ComponentScan("com.muzhi.spring6.validation.method3")
    public class ValidationConfig {
       
        @Bean
        public MethodValidationPostProcessor validationPostProcessor() {
            return new MethodValidationPostProcessor();
        }
    }
    
  2. 创建实体类,使用注解设置校验规则

    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
    
    package com.muzhi.spring6.validation.method3;
       
    import jakarta.validation.constraints.*;
       
    public class User {
       
        @NotNull
        private String name;
       
        @Min(0)
        @Max(120)
        private int age;
       
        @Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$",message = "手机号码格式错误")
        @NotBlank(message = "手机号码不能为空")
        private String phone;
       
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
        public String getPhone() {
            return phone;
        }
        public void setPhone(String phone) {
            this.phone = phone;
        }
    }
    
  3. 定义Service类,通过注解操作对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   package com.muzhi.spring6.validation.method3;
   
   import jakarta.validation.Valid;
   import jakarta.validation.constraints.NotNull;
   import org.springframework.stereotype.Service;
   import org.springframework.validation.annotation.Validated;
   
   @Service
   @Validated
   public class MyService {
       
       public String testParams(@NotNull @Valid User user) {
           return user.toString();
       }
   
   }
  1. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    package com.muzhi.spring6.validation.method3;
       
    import org.junit.jupiter.api.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
       
    public class TestMethod3 {
       
        @Test
        public void testMyService1() {
            ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfig.class);
            MyService myService = context.getBean(MyService.class);
            User user = new User();
            user.setAge(-1);
            myService.testParams(user);
        }
    }
    

10.4.实现自定义校验

  1. 自定义校验注解

    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
    
    package com.muzhi.spring6.validation.method4;
       
    import jakarta.validation.Constraint;
    import jakarta.validation.Payload;
    import java.lang.annotation.*;
       
    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Constraint(validatedBy = {CannotBlankValidator.class})
    public @interface CannotBlank {
        //默认错误消息
        String message() default "不能包含空格";
       
        //分组
        Class<?>[] groups() default {};
       
        //负载
        Class<? extends Payload>[] payload() default {};
       
        //指定多个时使用
        @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        @interface List {
            CannotBlank[] value();
        }
    }
    
  2. 编写校验类

    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
    
    package com.muzhi.spring6.validation.method4;
       
    import jakarta.validation.ConstraintValidator;
    import jakarta.validation.ConstraintValidatorContext;
       
    public class CannotBlankValidator implements ConstraintValidator<CannotBlank, String> {
       
            @Override
            public void initialize(CannotBlank constraintAnnotation) {
            }
       
            @Override
            public boolean isValid(String value, ConstraintValidatorContext context) {
                    //null时不进行校验
                    if (value != null && value.contains(" ")) {
                            //获取默认提示信息
                            String defaultConstraintMessageTemplate = context.getDefaultConstraintMessageTemplate();
                            System.out.println("default message :" + defaultConstraintMessageTemplate);
                            //禁用默认提示信息
                            context.disableDefaultConstraintViolation();
                            //设置提示语
                            context.buildConstraintViolationWithTemplate("can not contains blank").addConstraintViolation();
                            return false;
                    }
                    return true;
            }
    }
    

11.提前编译:AOT

image-20240504073528443

11.1.AOT 概述

image-20240504073921732

AOT(Ahead-Of-Time)编译是一种编译策略,它在应用程序运行之前就将源代码或中间字节码编译成目标机器的机器码。与JIT(Just-In-Time)编译相反,JIT编译是在应用程序运行时根据需要即时编译代码。AOT编译的主要优点是它可以提高应用程序的启动速度和性能,因为它消除了运行时编译的需要。

以下是AOT编译的一些关键特点:

  1. 预编译:在应用程序部署之前,源代码或中间字节码被编译成目标机器的机器码。
  2. 启动时间:由于所有代码已经被编译,应用程序的启动时间可以显著减少。
  3. 性能:AOT编译的代码通常比JIT编译的代码更快,因为JIT编译需要在运行时进行优化。
  4. 内存使用:AOT编译的应用程序可能会占用更多的内存,因为它们需要存储更多的机器码。
  5. 平台特定:AOT编译生成的机器码是针对特定平台的,这意味着它们不能在不同的硬件或操作系统上运行,除非重新编译。
  6. 安全性:AOT编译可以提高安全性,因为它减少了运行时代码生成的可能性,这可以防止某些类型的攻击。
  7. 调试难度:AOT编译的代码可能更难调试,因为源代码和机器码之间的映射可能不那么直接。
  8. 优化:AOT编译器可以在编译时进行更多的优化,因为它们有更多的时间来分析代码。

AOT编译在许多场景中都非常有用,特别是在需要快速启动和高性能的应用程序中,如移动应用程序.桌面应用程序和嵌入式系统。然而,它也有一些局限性,如平台依赖性和可能增加的内存使用。

在某些编程语言和环境中,如C/C++,AOT编译是默认的编译策略。而在其他语言,如Java和C#,AOT编译是一种可选的策略,可以在特定场景下使用。

11.2.AOT与JIT的区别

AOT(Ahead-of-Time Compilation,预编译)和JIT(Just-In-Time Compilation,即时编译)是两种不同的编译策略,它们在编译时间.执行性能.内存占用以及灵活性等方面有显著的区别。

  1. 编译时间

    • AOT:在程序运行之前,整个源代码或字节码文件被一次性编译成本地机器码。这意味着在程序运行时不需要再进行额外的编译工作,因此AOT编译的程序通常具有更快的执行速度,但编译时间较长。
    • JIT:在程序运行过程中,根据实际的执行情况动态地将热点代码(频繁执行的代码)从字节码即时编译成本地机器码。JIT编译器会根据实际的执行情况来选择需要编译的代码,并且可以根据优化策略对代码进行优化,因此编译时间相对较短。
  2. 执行性能
    • AOT:由于在程序运行前已经生成了机器码,因此执行速度较快,但通常无法达到JIT编译器所能达到的最高性能。
    • JIT:在程序运行初期可能性能较低,因为需要时间来编译热点代码,但随着编译的进行,性能会逐渐提升并可能达到更高的峰值性能。
  3. 内存占用
    • AOT:生成的机器码需要占用额外的存储空间,因为整个程序都被编译成了机器码。
    • JIT:只对热点代码进行编译,所以占用的内存相对较少。
  4. 灵活性
    • AOT:是静态的,一次性地将整个代码编译成机器码,无法根据实际情况进行动态调整。
    • JIT:是动态的,在运行时根据实际的执行情况选择需要编译的代码,并进行优化,提供了更大的灵活性。
  5. 安全性
    • AOT:生成的机器码难以反编译,因此相比JIT编译的代码更难被破解。
  6. 优化
    • JIT:支持更多动态特性,如Profile-Guided Optimizations (PGO),可以根据运行时信息进行深层次的优化。
    • AOT:由于是在程序运行前进行编译,无法利用运行时信息进行优化,因此优化程度通常不如JIT。
  7. 启动时间
    • AOT:由于不需要运行时编译,所以启动时间更短,适用于对启动时间要求较高的应用场景。
    • JIT:需要在运行时编译程序,所以启动时需要一段时间的预热才能达到峰值性能。
  8. 适用场景
    • AOT:适用于对启动时间要求较高.稳定性要求较高的应用场景,如移动应用程序或桌面应用程序。
    • JIT:适用于对执行性能要求较高.灵活性要求较高的应用场景,如服务器端应用程序。
  9. 结合使用
    • AOT和JIT也可以结合使用,以发挥各自的优势。例如,在某些语言或框架中,可以使用静态AOT编译来提前将整个应用程序或库编译成机器码,同时,使用动态JIT编译来对热点代码进行优化,以提高执行性能。
  10. JIT, Just-in-time,动态(即时)编译,边运行边编译:

在程序运行时,根据算法计算出热点代码,然后进行 JIT 实时编译,这种方式吞吐量高,有运行时性能加成,可以跑得更快,并可以做到动态生成代码等,但是相对启动速度较慢,并需要一定时间和调用频率才能触发 JIT 的分层机制。JIT 缺点就是编译需要占用运行时资源,会导致进程卡顿。

  1. AOT,Ahead Of Time,指运行前编译,预先编译。

    AOT 编译能直接将源代码转化为机器码,内存占用低,启动速度快,可以无需 runtime 运行,直接将 runtime 静态链接至最终的程序中,但是无运行时性能加成,不能根据程序运行情况做进一步的优化,AOT 缺点就是在程序运行前编译会使程序安装的时间增加。

    简单来讲:JIT即时编译指的是在程序的运行过程中,将字节码转换为可在硬件上直接运行的机器码,并部署至托管环境中的过程。而 AOT 编译指的则是,在程序运行之前,便将字节码转换为机器码的过程。

    .java -> .class -> (使用jaotc编译工具) -> .so(程序函数库,即编译好的可以供其他程序使用的代码和数据)

    image-20221207113544080

  2. AOT的优点

    简单来讲,Java 虚拟机加载已经预编译成二进制库,可以直接执行。不必等待及时编译器的预热,减少 Java 应用给人带来“第一次运行慢” 的不良体验。

    在程序运行前编译,可以避免在运行时的编译性能消耗和内存消耗 可以在程序运行初期就达到最高性能,程序启动速度快 运行产物只有机器码,打包体积小。

  3. AOT的缺点

    由于是静态提前编译,不能根据硬件情况或程序运行情况择优选择机器指令序列,理论峰值性能不如JIT 没有动态能力,同一份产物不能跨平台运行

    第一种即时编译 (JIT) 是默认模式,Java Hotspot 虚拟机使用它在运行时将字节码转换为机器码。后者提前编译 (AOT)由新颖的 GraalVM 编译器支持,并允许在构建时将字节码直接静态编译为机器码。

    现在正处于云原生,降本增效的时代,Java 相比于 Go.Rust 等其他编程语言非常大的弊端就是启动编译和启动进程非常慢,这对于根据实时计算资源,弹性扩缩容的云原生技术相冲突,Spring6 借助 AOT 技术在运行时内存占用低,启动速度快,逐渐的来满足 Java 在云原生时代的需求,对于大规模使用 Java 应用的商业公司可以考虑尽早调研使用 JDK17,通过云原生技术为公司实现降本增效。

11.3.Graalvm

GraalVM 是一个高性能的虚拟机,它是由 Oracle Labs 开发的,旨在提供一个可以运行多种编程语言的统一运行时环境。GraalVM 支持 Java 和其他基于 JVM 的语言,并且通过 Truffle 框架支持 JavaScript.Python.Ruby 以及其他语言。GraalVM 的核心特性包括:

  1. 即时编译(JIT):GraalVM 提供了一个先进的 JIT 编译器,用于在运行时优化 Java 应用程序的性能。这个 JIT 编译器可以增量地进行编译,并对频繁执行的代码段进行额外的优化,以提高应用程序的峰值吞吐量和降低延迟。
  2. 静态编译(AOT):GraalVM 的 Native Image 功能允许开发者将 Java 字节码提前编译成本地机器代码,生成原生可执行文件。这些可执行文件启动速度快,内存占用小,并且由于它们只包含应用程序所需的类和方法,因此体积也更小。
  3. 多语言支持:GraalVM 通过 Truffle 框架支持多语言互操作性,允许不同语言编写的程序共享相同的运行时环境,提高了开发效率。
  4. 性能提升:GraalVM 通过高级优化器提高应用程序的性能,减少对象分配,降低垃圾回收时间,从而优化内存使用。
  5. 云原生应用构建:GraalVM 的 Native Image 功能特别适合构建云原生应用,因为它可以生成快速启动且资源占用低的原生可执行文件,这对于云环境和微服务架构非常有用。
  6. 企业支持:Oracle 提供了 GraalVM 的企业版,包括 24/7 支持和额外的性能提升特性,如 G1 垃圾回收器和压缩指针。
  7. 社区和生态系统:GraalVM 拥有活跃的社区和生态系统,提供了丰富的文档.工具和框架支持,使得开发者可以更容易地采用和扩展 GraalVM。
  8. 免费和开源:GraalVM 有社区版(CE),它在开源 GNU General Public License (GPL) 下可用,而企业版(EE)则提供了额外的性能特性和商业支持。
  9. 版本更新:GraalVM 通常与 Java 的版本更新同步,以确保用户可以体验到最新的 Java 特性和 GraalVM 的性能优势。

11.4.Native Image

GraalVM Native Image 是一种技术,它允许将 Java 代码提前编译(AOT)成独立的可执行文件,这些文件不依赖于 Java 虚拟机(JVM)即可运行。这种原生可执行文件通常具有更快的启动时间和更低的运行时内存开销。

以下是 GraalVM Native Image 的一些关键特性和概念:

  1. 静态分析:Native Image 在构建时执行静态分析,以确定在应用程序执行期间哪些类和方法是可访问的。
  2. 资源效率:生成的原生可执行文件仅包含运行时所需的代码,因此相比 JVM,它们使用更少的资源,启动时间更快,并且能够立即达到峰值性能。
  3. 平台特定:每个原生可执行文件都是为特定的操作系统和架构编译的,这意味着你需要为每个目标平台生成一个独立的可执行文件。
  4. 闭环优化:由于 Native Image 在构建时进行静态分析,它需要一个封闭的世界假设,即所有在运行时可访问的类和字节码在构建时都已知。
  5. 限制:某些 Java 特性,如动态类加载和反射,可能不受 Native Image 支持,除非在构建时提供了适当的元数据。
  6. 构建配置:用户可以通过 JSON 格式的配置文件或通过代码中的注解来提供额外的元数据,以支持反射.代理等动态特性。
  7. 工具链依赖:Native Image 工具链依赖于本地环境的特定组件,如 C 库的头文件.gcc 等,这些组件可以通过包管理器安装。
  8. Maven 和 Gradle 插件:存在用于 Native Image 的 Maven 和 Gradle 插件,可以自动化构建.测试和配置原生可执行文件的过程。
  9. 多语言支持:Native Image 不仅支持 JVM 语言,还可以执行动态语言,如 JavaScript.Ruby.R 或 Python,并可以将多语言嵌入编译为原生可执行文件。
  10. 共享库:除了可执行文件,Native Image 还可以构建共享库,这些库可以通过 C 代码进行调用。
  11. 版本确定性:可以通过特定的命令行参数或配置文件来确定用于生成原生映像的 GraalVM 版本。
  12. 兼容性和优化:Native Image 可能需要额外的配置来兼容某些应用程序,特别是那些使用 JNI.反射或其他动态特性的应用程序。

11.5.Native Image 演示

GraalVM Native Image 的构建过程涉及将 Java 应用程序转换为一个独立的可执行文件,这个文件在运行时不依赖于 Java 虚拟机(JVM)。以下是使用 GraalVM Native Image 构建过程的基本步骤:

11.5.1. 准备工作

确保你已经安装了 GraalVM,并且已经安装了 native-image 组件。你可以通过以下命令安装 native-image

1
gu install native-image

11.5.2. 编写你的 Java 应用程序

创建一个简单的 Java 应用程序,例如一个 “Hello World” 应用程序。

11.5.3. 构建 Java 字节码

使用标准的 Java 构建工具(如 Maven 或 Gradle)构建你的应用程序。这将生成一个包含所有类文件的 JAR 文件。

11.5.4. 使用 Native Image 构建原生可执行文件

运行 native-image 命令来编译 JAR 文件并生成原生可执行文件。例如:

1
native-image -jar path/to/your/app.jar -o your-app

这里,-jar 参数后面跟着 JAR 文件的路径,-o 参数后面跟着输出的可执行文件的名称。

11.5.5. 运行原生可执行文件

在生成可执行文件后,你可以直接在命令行中运行它,而不需要 JVM:

1
./your-app

11.5.6. 处理动态特性(可选)

如果你的应用程序使用了 Java 的动态特性,如反射.动态代理等,你可能需要提供额外的元数据。这可以通过使用 GraalVM 提供的配置文件或注解来完成。

11.5.7. 使用 Maven 或 Gradle 插件(可选)

为了简化构建过程,你可以使用 Maven 或 Gradle 插件来自动化构建原生可执行文件的过程。以下是使用 Maven 插件的一个例子:

pom.xml 文件中添加 Native Image Maven 插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    <version>${native.maven.plugin.version}</version>
    <extensions>true</extensions>
    <executions>
        <execution>
            <id>build-native</id>
            <goals>
                <goal>build</goal>
            </goals>
            <phase>package</phase>
        </execution>
    </executions>
    <configuration>
        <skip>false</skip>
        <imageName>${exe.file.name}</imageName>
        <mainClass>${app.main.class}</mainClass>
        <buildArgs>
            <buildArg>--no-fallback</buildArg>
            <buildArg>--report-unsupported-elements-at-runtime</buildArg>
        </buildArgs>
    </configuration>
</plugin>

然后运行 Maven 构建命令来生成原生可执行文件:

1
mvn clean package -Pnative

这个命令会触发 Maven 插件来使用 native-image 工具构建你的应用程序。

注意事项

  • 确保在构建系统的环境中设置了 GRAALVM_HOME 环境变量,并将其 bin 目录添加到 PATH 中。
  • 如果你的应用程序依赖于特定的 Java 动态特性,可能需要额外的步骤来生成所需的元数据。
  • 构建原生可执行文件可能需要一些时间,具体取决于应用程序的复杂性和你的硬件性能。

通过上述步骤,你可以将 Java 应用程序转换为一个高效的原生可执行文件,从而提高启动速度和运行时性能。

本文由作者按照 CC BY 4.0 进行授权