Spring6.0

1.概述

1.1Spring是什么?

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

image-20230612095947200

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

自 2004 年 4 月,Spring 1.0 版本正式发布以来,Spring 已经步入到了第 6 个大版本,也就是 Spring 6。

image-20221216223135162

1.2Spring 的狭义和广义

在不同的语境中,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.3Spring 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.4Spring模块组成

官网地址: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的封装

2.入门

2.1环境要求

  • JDK:Java17+(Spring6要求JDK最低版本是Java17)

  • Maven:3.6+

  • Spring:6.0.2

2.2构建模块

(1)构建父模块spring6

在idea中,依次单击 File -> New -> Project -> New Project

image-20221205201741893

点击“Create”

image-20221205202000198

删除src目录

(2)构建子模块spring6-first

image-20221205202117383

点击 Create 完成

image-20221205202154225

2.3程序开发

2.3.1引入依赖

https://spring.io/projects/spring-framework#learn

添加依赖:

<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-20221201105416558

2.3.2创建java类

package com.atguigu.spring6.bean;

public class HelloWorld {
    
    public void sayHello(){
        System.out.println("helloworld");
    }
}

2.3.3创建配置文件

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

img007

<?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.atguigu.spring6.bean.HelloWorld"></bean>
    
</beans>

2.3.4创建测试类测试

package com.atguigu.spring6.bean;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class HelloWorldTest {

    @Test
    public void testHelloWorld(){
        ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
        HelloWorld helloworld = (HelloWorld) ac.getBean("helloWorld");
        helloworld.sayHello();
    }
}

2.3.5运行测试程序

image-20221031172354535

2.4程序分析

1. 底层是怎么创建对象的,是通过反射机制调用无参数构造方法吗?

修改HelloWorld类:

package com.atguigu.spring6.bean;

public class HelloWorld {

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

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

执行结果:

image-20221031181430720

测试得知:创建对象时确实调用了无参数构造方法。

2. Spring是如何创建对象的呢?原理是什么?

// dom4j解析beans.xml文件,从中获取class属性值,类的全类名
 // 通过反射机制调用无参数构造方法创建对象
 Class clazz = Class.forName("com.atguigu.spring6.bean.HelloWorld");
 //Object obj = clazz.newInstance();
 Object object = clazz.getDeclaredConstructor().newInstance();

3. 把创建好的对象存储到一个什么样的数据结构当中了呢?

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

private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);

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

2.5启用Log4j2日志框架

2.5.1Log4j2日志概述

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

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依赖

<!--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,文件必须放到类根路径下。)

<?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>

3.容器:IoC

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

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

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

3.1IoC容器

3.1.1控制反转(IoC)

  • 控制反转是一种思想。
  • 控制反转是为了降低程序耦合度,提高程序扩展力。
  • 控制反转,即将对象的创建、对象和对象的依赖关系的控制权从开发者转移到了 Spring 框架。
  • 控制反转这种思想如何实现呢?
    • DI(Dependency Injection):依赖注入

3.1.2依赖注入(DI)

DI(Dependency Injection):依赖注入,依赖注入实现了控制反转的思想。

依赖注入:

  • 指Spring创建对象的过程中,将对象依赖属性通过配置进行注入

依赖注入常见的实现方式包括三种:

  • setter注入
  • 构造器注入
  • 属性注入

所以结论是:IOC 就是一种控制反转的思想, 而 DI 是对IoC的一种具体实现。

Bean管理说的是:Bean对象的创建,以及Bean对象中属性的赋值(或者叫做Bean对象之间关系的维护)。

3.1.3IoC容器在Spring的实现

Spring 的 IoC 容器就是 IoC思想的一个落地的产品实现。IoC容器中管理的组件也叫做 bean。在创建 bean 之前,首先需要创建IoC 容器。Spring 提供了IoC 容器的两种实现方式:

①BeanFactory

这是 IoC 容器的核心实现,是 Spring 内部使用的接口。面向 Spring 本身,不提供给开发人员使用。

②ApplicationContext

BeanFactory 的子接口,提供了更多高级特性。面向 Spring 的使用者,几乎所有场合都使用 ApplicationContext 而不是底层的 BeanFactory。

③ApplicationContext的主要实现类

iamges

类型名 简介
ClassPathXmlApplicationContext 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象
FileSystemXmlApplicationContext 通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象
ConfigurableApplicationContext ApplicationContext 的子接口,包含一些扩展方法 refresh() 和 close() ,让 ApplicationContext 具有启动、关闭和刷新上下文的能力。
WebApplicationContext 专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。

3.1.4bean的作用域

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

取值 含义 创建对象的时机
singleton(默认) 在IOC容器中,这个bean的对象始终为单实例 IOC容器初始化时
prototype 这个bean在IOC容器中有多个实例 获取bean时
request - 每个HTTP请求都会创建一个新的实例
session - 每个HTTP会话都会创建一个新的实例
application - 每个ServletContext都会创建一个新的实例
websocket - 每个WebSocket都会创建一个新的实例

image-20230723222354369

3.1.4.1singleton

  • singleton:初始化IoC容器的时候就创建bean,并且bean对象是单实例的。
  1. 创建类Orders

  2. 配置bean

<!-- scope属性:取值singleton(默认值),bean在IOC容器中只有一个实例,IOC容器初始化时创建对象 -->
<!-- scope属性:取值prototype,bean在IOC容器中可以有多个实例,getBean()时创建对象 -->
<bean id="orders" class="com.xha.scope.Orders"></bean>
  1. 加载bean.xml,创建bean
import com.xha.scope.Orders;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class OrdersTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean-scope.xml");
        Orders orders1 = (Orders) context.getBean("orders");
        System.out.println(orders1);
        Orders orders2 = (Orders) context.getBean("orders");
        System.out.println(orders2);
    }
}

image-20230723223812114

3.1.4.2prototype(多实例)

  • prototype:在获取bean的时候才会创建bean,并且对象是多实例的。
  1. 配置bean
<!-- scope属性:取值singleton(默认值),bean在IOC容器中只有一个实例,IOC容器初始化时创建对象 -->
<!-- scope属性:取值prototype,bean在IOC容器中可以有多个实例,getBean()时创建对象 -->
<bean id="orders" class="com.xha.scope.Orders" scope="prototype"></bean>
  1. 加载bean.xml,创建bean
import com.xha.scope.Orders;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class OrdersTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean-scope.xml");
        Orders orders1 = (Orders) context.getBean("orders");
        System.out.println(orders1);
        Orders orders2 = (Orders) context.getBean("orders");
        System.out.println(orders2);
    }
}

image-20230723224217736

3.1.4.3@Scope注解配置Bean的作用域

在基于注解的方式中,可以使用@Scope注解来配置bean的作用域。例如,如果你使用组件扫描来发现和声明Bean,那么你可以在Bean的类上使用@Scope注解,将其声明为原型Bean。例如:

@Component
@Scope(ConfigurableListableBeanFactory.SCOPE_PROTOTYPE)
public class BeanScope {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

在上面的代码中,我们使用了ConfigurableListableBeanFactory类的SCOPE_PROTOTYPE常量来设置类原型作用域。当然也可以直接使用@Scope (“prototype”),但是使用SCOPE_PROTOTYPE常量更加安全并且不易出错。

3.1.5IoC的执行流程

  1. 创建Spring IOC容器BeanFactory是一个根接口,是IoC 容器的核心实现,是管理Bean的工厂类。而ApplicationContextBeanFactory的子接口,使用ApplicationContext的实现类来创建IoC容器,如ClassPathXmlApplicationContext通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象,AnnotationConfigApplicationContext通过读取注解创建 IOC 容器对象。
  2. 使用BeanDefinitionReader解析XML中或者注解的Bean定义,生成BeanDefinition。它是将Bean的类名,属性,依赖等封装成一个BeanDefinition对象,并注册到BeanFactory中。BeanDefinitionReader有多种实现类,如XmlBeanDefinitionReader, AnnotationBeanDefinitionReader等。
  3. 后置处理器BeanFacotryPostProcesser处理增强Bean。这是一个用于在BeanFactory初始化前后对Bean进行修改或增强的接口等。
  4. 通过反射,得到实例化后的Bean。这是一个用于根据BeanDefinition中的类名来创建对象的过程,它使用Java反射机制来调用构造方法或工厂方法来生成实例。
  5. Bean的初始化,包括填充属性,执行Aware方法,Bean的前后PostProcesser的处理,得到完整的Bean。这是一个用于对实例化后的Bean进行进一步配置和初始化的过程,它包括以下几个步骤:
    • 填充属性。根据BeanDefinition中的属性信息,使用反射或者setter方法来为Bean设置属性值。
    • 执行Aware方法。如果Bean实现了一些Aware接口,如ApplicationContextAware, BeanNameAware等,则调用相应的方法来注入一些Spring容器相关的对象或信息。
    • Bean的前后PostProcesser的处理。如果容器中存在一些实现了BeanPostProcessor接口的对象,则在初始化前后分别调用其postProcessBeforeInitialization和postProcessAfterInitialization方法来对Bean进行修改或增强。这也是一个可以实现一些高级功能的接口,如AOP代理,事务管理等。BeanPostProcessor有多种实现类,如DefaultAdvisorAutoProxyCreator, TransactionInterceptor等。
  6. 得到完整的Bean。经过以上步骤后,就得到了一个完全初始化并可用的Bean对象。获得对象。这是一个用于从容器中获取已经注册和初始化好的Bean对象的过程,它可以通过getBean方法或者注解来实现。

3.1.6bean生命周期

3.1.6.1bean生命周期概述

Bean的生命周期主要分为4个阶段,其中初始化完成后,就代表Bean可以使用了:

image-20230816143948861

具体说明:

  1. 实例化:通过反射或者工厂方法创建对象。
  2. 属性填充:根据配置文件或者注解为对象设置属性值。
  3. 初始化前处理:如果存在BeanPostProcessor,调用其postProcessBeforeInitialization方法。
  4. 初始化:如果对象实现了InitializingBean接口,调用其afterPropertiesSet方法。或者如果配置了init-method属性,调用指定的方法。
  5. 初始化后处理:如果存在BeanPostProcessor,调用其postProcessAfterInitialization方法。
  6. 使用:从容器中获取对象并使用。
  7. 销毁:如果对象实现了DisposableBean接口,调用其destroy方法。或者如果配置了destroy-method属性,调用指定的方法。

3.1.12.2案例演示

  1. User类
  • 添加无参方法
  • 添加初始化方法,在bean.xml配置文件配置
  • 添加销毁方法,在bean.xml配置文件配置
public class User {
    private String name;

    public User() {
        System.out.println("第一步:User的无参构造");
    }

    public User(String name) {
        System.out.println("User的有参构造");
        this.name = name;
    }

    public void init() {
        System.out.println("第四步:User的init方法");
    }

    public void destroy() {
        System.out.println("第七步:User的destroy方法");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        System.out.println("第二步:User的setName方法");
        this.name = name;
    }
}

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

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

创建bean的后置处理器:

/**
 * BeanPostProcessor接口,bean的后置处理器,
 * 需要实现两个方法,postProcessBeforeInitialization和postProcessAfterInitialization,
 * 作用是在bean的初始化前后进行一些处理工作
 *
 * @author Xu huaiang
 * @date 2023/07/23
 */
public class BeanPost implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("第三步:初始化之前的方法");
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("第五步:初始化之后的方法");
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
}
  1. 配置bean

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

<?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="user" class="com.xha.life.User"
          scope="singleton"
          init-method="init"
          destroy-method="destroy">
        <property name="name" value="小明"></property>
    </bean>
<!--    bean的后置处理器要放入IOC容器才能生效-->
    <bean id="BeanPost" class="com.xha.life.BeanPost"></bean>
</beans>
  1. 加载配置文件,创建bean对象
  • ApplicationContext对象没有close()方法,所以需要使用ApplicationContext接口的实现类ClassPathXmlApplicationContext来调用close()方法来对bean进行销毁。
public class Test {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean-life.xml");
        User user = (User) context.getBean("user");
        System.out.println("第六步:bean对象创建完成:" + user);
        context.close();
    }
}

image-20230723232249924

3.1.6bean的循环依赖和三级缓存

3.1.6.1bean的循环依赖

循环依赖是指在Spring框架中,两个或多个Bean之间相互依赖,形成一个循环引用的情况。

  1. Bean A依赖于Bean B,而Bean B又依赖于Bean A,这就形成了一个循环依赖。

image-20230817221349904

  1. 间接依赖,两个以上的 Bean 存在间接依赖关系造成循环调用。

image-20230817221421981

  1. 自我依赖,自己依赖自己造成了循环依赖

image-20230817221530056

3.1.6.2三级缓存

Spring框架使用三级缓存来解决Bean循环依赖的问题。这三个缓存分别是singletonObjectsearlySingletonObjectssingletonFactories,都存在于DefaultSinglonBeanRegistry中。

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

Spring框架在创建Bean时,会先检查一级缓存中是否已经存在该Bean。如果不存在,则会开始创建该Bean。在创建过程中,如果发现该Bean依赖于另一个Bean,则会先创建另一个Bean。如果另一个Bean又依赖于第一个Bean,则会形成循环依赖。

在创建一个Bean的过程中,首先会将其放入三级缓存singletonFactories中。然后,在依赖注入时需要另一个Bean时,会开始创建另一个Bean实例,并将其也放入三级缓存singletonFactories中。当另一个Bean在依赖注入时需要第一个Bean时,它会从三级缓存singletonFactories中查找到第一个Bean并将其放入二级缓存earlySingletonObjects中,并从三级缓存singletonFactories中移除。然后把不完整的第一个Bean注入到另一个Bean中,完成另一个Bean的属性填充并执行完初始化,最后将其放入一级缓存singletonObjects。接着,第一个Bean再注入另一个Bean实例,从一级缓存singletonObjects中找到另一个Bean实例并完成属性填充,执行完初始化并放入一级缓存singletonObjects。这样就解决了循环依赖的问题。

image-20230817223918864

3.1.7FactoryBean

3.1.7.1FactoryBean概述

一般情况下,我们将 bean 的创建和管理都交给 Spring IoC 容器,Spring 会利用 bean 的 class 属性指定的类来实例化 bean。

但是如果我们想自己实现 bean 的创建操作,可以实现吗?答案是可以的,FactoryBean 就可以实现这个需求。

FactoryBean 是一种特殊的 bean,它是个工厂 bean,可以自己创建 bean 实例,如果一个类实现了 FactoryBean 接口,则该类可以自己定义创建实例对象的方法,只需要实现它的 getObject() 方法即可。

  1. 查看FactoryBean接口
/*
 * 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.
 * ......
 */
public interface FactoryBean<T> {

    String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

    @Nullable
    T getObject() throws Exception;

    @Nullable
    Class<?> getObjectType();

    default boolean isSingleton() {
        return true;
    }
}
  1. User类

  2. 创建类UserFactoryBean

实现FactoryBean接口,重写getObject()方法

package com.atguigu.spring6.bean;
public class UserFactoryBean implements FactoryBean<User> {
    @Override
    public User getObject() throws Exception {
        return new User();
    }

    @Override
    public Class<?> getObjectType() {
        return User.class;
    }
}
  1. 配置bean
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="user" class="com.xha.factorybean.User"></bean>
</beans>
  1. 测试
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean-factory.xml");
        User user = (User) context.getBean("user");
        System.out.println(user instanceof User);
    }
}

结果为:true

3.1.7.2BeanFactory和FactoryBean的区别

  • BeanFactory是 IoC 容器的核心实现,是 Spring 内部使用的接口。它是一个管理Bean的工厂,负责创建和管理Bean。它提供了一种从配置源(如XML文件)中读取Bean定义,使用反射机制将其转换为实际的Bean实例的机制。BeanFactory还负责管理这些Bean实例,包括它们的创建、配置和销毁。
  • FactoryBean是一个接口,它定义了一种特殊类型的Bean,这种Bean不是直接用来作为其他对象的依赖,而是用来创建其他对象。当一个Bean实现了FactoryBean接口时,它就成为了一个工厂Bean,Spring IoC容器会调用它的getObject()方法来创建实际的对象。这种方式可以用来创建复杂的对象,或者在创建对象时需要进行一些特殊处理。
  • 总之,BeanFactory是一个管理Bean的工厂,而FactoryBean是一种特殊类型的Bean,用来创建其他对象。两者之间没有直接的关系,但都是Spring框架中重要的组成部分。

3.2基于XML管理Bean

3.2.1搭建子模块spring6-ioc-xml

  1. 搭建模块

搭建方式如:spring-first

  1. 添加依赖
<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>
        <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>
  1. 引入java类

引入spring-first模块java及test目录下实体类

package com.atguigu.spring6.bean;

public class HelloWorld {

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

    public void sayHello(){
        System.out.println("helloworld");
    }
}
  1. 引入配置文件

引入spring-first模块配置文件:beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="user" class="com.xha.pojo.User"></bean>
</beans>

3.2.2获取bean的方式

image-20230723175004998

3.2.2.1根据id获取

由于 id 属性指定了 bean 的唯一标识,所以根据 bean 标签的 id 属性可以精确获取到一个组件对象。上个实验中我们使用的就是这种方式。

public class UserTest {
    public static void main(String[] args) {
//      1.加载spring配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
//        2.1.通过id获取
        User user1 = (User) context.getBean("user");
        System.out.println("user = " + user1);
    }
}
3.2.2.2根据类型获取
public class UserTest {
    public static void main(String[] args) {
//      1.加载spring配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
//        2.1.通过id获取
        User user1 = (User) context.getBean("user");
        System.out.println("user = " + user1);
//        2.2.通过类型获取
        User user2 = context.getBean(User.class);
        System.out.println("user = " + user2);
    }
}
3.2.2.3根据id和类型
public class UserTest {
    public static void main(String[] args) {
//      1.加载spring配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
//        2.1.通过id获取
        User user1 = (User) context.getBean("user");
        System.out.println("user = " + user1);
//        2.2.通过类型获取
        User user2 = context.getBean(User.class);
        System.out.println("user = " + user2);
//        2.3.通过类型和id获取
        User user3 = context.getBean(User.class, "user");
        System.out.println("user = " + user3);
    }
}

image-20230723175411259

3.2.2.4注意的地方

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

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

<bean id="helloworldOne" class="com.atguigu.spring6.bean.HelloWorld"></bean>
<bean id="helloworldTwo" class="com.atguigu.spring6.bean.HelloWorld"></bean>

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

Exception in thread “main” org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type ‘com.xha.pojo.User’ available: expected single matching bean but found 2: user,user1。

线程“main”中的异常 org.springframework.beans.factory.NoUniqueBeanDefinitionException: 没有类型为 ‘com.xha.pojo.User’ 的合格 bean:预期的单个匹配 bean,但找到 2:user,user1

3.2.2.5扩展知识

如果组件类实现了接口,根据接口类型可以获取 bean 吗?

可以,前提是bean唯一

如果一个接口有多个实现类,这些实现类都配置了 bean,根据接口类型可以获取 bean 吗?

不行,因为bean不唯一

结论

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

java中,instanceof关键字的作用是用来判断一个对象是否是一个类的实例对象或者是一个接口的实现类对象。如果是返回true,否则返回false。也就是说:用instanceof关键字做判断时, instanceof 操作符的左右操作必须有继承或实现关系。

3.2.3依赖注入之setter注入

  1. 创建类,定义属性,添加属性的set方法
  2. 在Spring配置文件中配置

①创建图书类Book

public class Book {
    private String name;

    private String author;
    
    public Book() {

    }

    public String getName() {
        return name;
    }

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

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }
}

②配置bean时为属性赋值

spring-di.xml

<bean id="book" class="com.xha.pojo.Book">
    <!-- property标签:通过组件类的setXxx()方法给组件对象设置属性 -->
    <!-- name属性:指定属性名(这个属性名是getXxx()、setXxx()方法定义的,和成员变量无关) -->
    <!-- value属性:指定属性值 -->
    <property name="name" value="码出高效"></property>
    <property name="author" value="alibaba"></property>
</bean>

③测试

public class BookTest {
    public static void main(String[] args) {
//        1.加载spring配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
//        2.1.通过id获取
        Book book = (Book) context.getBean("book");
        System.out.println(book.getName() + "->" + book.getAuthor());
    }
}

image-20230723182222053

3.2.4依赖注入之构造器注入

  1. 创建类,定义属性,添加属性的set方法,同时添加有参构造
  2. 在Spring配置文件中配置

①在Book类中添加有参构造

public class Book {
    private String name;

    private String author;

    public Book(String name, String author) {
        this.name = name;
        this.author = author;
    }

    public Book() {

    }

    public String getName() {
        return name;
    }

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

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }
}

②配置bean

spring-di.xml

<bean id="bookByConstruct" class="com.xha.pojo.Book">
    <constructor-arg name="name" value="码出高效"></constructor-arg>
    <constructor-arg name="author" value="alibaba"></constructor-arg>
</bean>

注意:

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

  • index属性:指定参数所在位置的索引(从0开始)
  • name属性:指定参数名

③测试

public class BookTest {
    public static void main(String[] args) {
//        1.加载spring配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
//        2.1.通过id获取
        Book book = (Book) context.getBean("bookByConstruct");
        System.out.println(book.getName() + "->" + book.getAuthor());
    }
}

image-20230723182222053

3.2.5特殊值处理

3.2.5.1字面量赋值

什么是字面量?

int a = 10;

声明一个变量a,初始化为10,此时a就不代表字母a了,而是作为一个变量的名字。当我们引用a的时候,我们实际上拿到的值是10。

而如果a是带引号的:’a’,那么它现在不是一个变量,它就是代表a这个字母本身,这就是字面量。所以字面量没有引申含义,就是我们看到的这个数据本身。

<!-- 使用value属性给bean的属性赋值时,Spring会把value属性的值看做字面量 -->
<property name="name" value="张三"/>
3.2.5.2null值
<property name="name">
    <null />
</property>

注意:

<property name="name" value="null"></property>

以上写法不是null值,为name所赋的值是字符串null

3.2.5.3xml实体
<!-- 小于号在XML文档中用来定义标签的开始,不能随便使用 -->
<!-- 解决方案一:使用XML实体来代替 -->
<property name="expression" value="a &lt; b"/>

3.2.6对象类型属性赋值

①创建班级类Classes

public class Classes {
    private int id;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public void info(){
        System.out.println("Classes info:" + id + ":" + name);
    }
}

②修改Student类

在Student类中添加以下代码:

Student类当中存在Classes对象属性

public class Student {
    private String name;
    private Classes classes;

    public String getName() {
        return name;
    }

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

    public Classes getClasses() {
        return classes;
    }

    public void setClasses(Classes classes) {
        this.classes = classes;
    }

    public void study(){
        System.out.println(name + " is studying");
        classes.info();
    }
}
3.2.6.1方式一:引用外部bean
  1. 在Bean.xml文件中配置Classes类型的bean:
<bean id="classes" class="com.xha.pojo.Classes">
    <property name="id" value="1"></property>
    <property name="name" value="一班"></property>
</bean>
  1. 为Student中的classes属性赋值:
    <bean id="student" class="com.xha.pojo.Student">
<!--        普通属性注入-->
        <property name="name" value="小明"></property>
<!--        对象属性注入,使用property标签的ref属性,引用IOC容器中某个bean的id,将所对应的bean为属性赋值-->
        <property name="classes" ref="classes"></property>
    </bean>

如果错把ref属性写成了value属性,会抛出异常: Caused by: java.lang.IllegalStateException: Cannot convert value of type ‘java.lang.String’ to required type ‘com.atguigu.spring6.bean.Clazz’ for property ‘clazz’: no matching editors or conversion strategy found

意思是不能把String类型转换成我们要的Clazz类型,说明我们使用value属性时,Spring只把这个属性看做一个普通的字符串,不会认为这是一个bean的id,更不会根据它去找到bean来赋值。

  1. 测试
public class StudentTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        Student student = (Student) context.getBean("student");
        student.study();
    }
}

3.2.6.2方式二:内部bean
<bean id="student" class="com.xha.pojo.Student">
    <!--        普通属性注入-->
    <property name="name" value="小明"></property>
    <property name="classes">
        <bean id="classes" class="com.xha.pojo.Classes">
            <property name="id" value="1"></property>
            <property name="name" value="一班"></property>
        </bean>
    </property>
</bean>

image-20230723191455806

3.2.6.3方式三:级联属性赋值

Bean.xml配置文件

<bean id="classes" class="com.xha.pojo.Classes">
    <property name="name" value="一班"></property>
</bean>

<bean id="student" class="com.xha.pojo.Student">
    <property name="name" value="小明"></property>
    <property name="classes" ref="classes"></property>
    <property name="classes.name" value="二班"></property>
</bean>

执行结果:

image-20230723191654569

3.2.7数组类型属性赋值

  1. 修改Student类,添加hobby属性

在Student类中添加以下代码:

import java.util.Arrays;

public class Student {
    private String name;
    private Classes classes;
    private String[] hobby;



    public String getName() {
        return name;
    }

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

    public Classes getClasses() {
        return classes;
    }

    public void setClasses(Classes classes) {
        this.classes = classes;
    }

    public void setHobby(String[] hobby) {
        this.hobby = hobby;
    }

    public void study(){
        System.out.println(name + " is studying");
        classes.info();
        System.out.println(Arrays.toString(hobby));
    }
}
  1. 配置bean
  • 在Bean.xml配置文件中的student的bean注入array类型属性
<bean id="classes" class="com.xha.pojo.Classes">
    <property name="name" value="一班"></property>
</bean>

<bean id="student" class="com.xha.pojo.Student">
    <property name="name" value="小明"></property>
    <property name="classes" ref="classes"></property>
    <property name="hobby">
        <array>
            <value>篮球</value>
            <value>足球</value>
            <value>乒乓球</value>
        </array>
    </property>
</bean>

image-20230723192536305

3.2.8集合类型属性赋值

3.2.8.1为List集合类型属性赋值
  1. Student类
public class Student {
    private String name;
    
    public String getName() {
        return name;
    }

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

    public void study(){
        System.out.println(name + " is studying");
    }
}
  1. Classes类,含有Student类型的List
import java.util.List;

public class Classes {
    private int id;
    private String name;
    private List<Student> students;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public void setStudents(List<Student> students) {
        this.students = students;
    }

    public void info() {
        System.out.println("Classes info:" + id + ":" + name);
        for (Student student : students) {
            System.out.println(student.getName());
        }
    }
}
  1. 配置bean
  • 在Bean.xml配置文件中的classes的bean注入List集合类型属性
    <bean id="student1" class="com.xha.pojo.Student">
        <property name="name" value="张三"></property>
    </bean>
    <bean id="student2" class="com.xha.pojo.Student">
        <property name="name" value="李四"></property>
    </bean>
    <bean id="student3" class="com.xha.pojo.Student">
        <property name="name" value="王五"></property>
    </bean>


    <bean id="classes" class="com.xha.pojo.Classes">
        <property name="id" value="1"></property>
        <property name="name" value="一班"></property>
        <property name="students">
 <!--                注入list集合类型的属性-->           
            <list>
<!--                通过ref的方式引入外部bean-->
                <ref bean="student1"></ref>
                <ref bean="student2"></ref>
                <ref bean="student3"></ref>
            </list>
        </property>
    </bean>

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

  1. 测试类
public class ClassesTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        Classes classes = (Classes) context.getBean("classes");
        classes.info();
    }
}

image-20230723194115539

3.2.8.2为Map集合类型属性赋值
  1. Teacher类
public class Teacher {
    private String name;


    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "Teacher{" +
                "name='" + name + '\'' +
                '}';
    }
}
  1. Student类
import java.util.Map;

public class Student {
    private String name;

    private Map<String, Teacher> teachers;

    public String getName() {
        return name;
    }

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

    public void setTeachers(Map<String, Teacher> teachers) {
        this.teachers = teachers;
    }

    public void study() {
        System.out.println(name + " is studying");
        for (Map.Entry<String,Teacher> teacher : teachers.entrySet()){
            System.out.println(teacher);
        }
    }
}
  1. 配置bean
  • 在Bean.xml配置文件中的student的bean注入map集合类型属性
<bean id="teacher1" class="com.xha.pojo.Teacher">
    <property name="name" value="张老师"></property>
</bean>
<bean id="teacher2" class="com.xha.pojo.Teacher">
    <property name="name" value="徐老师"></property>
</bean>
<bean id="teacher3" class="com.xha.pojo.Teacher">
    <property name="name" value="赵老师"></property>
</bean>

<bean id="student" class="com.xha.pojo.Student">
    <property name="name" value="小明"></property>
    <property name="teachers">
        <map>
            <entry key="语文老师" value-ref="teacher1"></entry>
            <entry key="数学老师" value-ref="teacher2"></entry>
            <entry key="英语老师" value-ref="teacher3"></entry>
        </map>
    </property>
</bean>

image-20230723195858431

3.2.8.3引用集合类型的bean
  1. Lesson类
public class Lesson {
    private String name;

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

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Lesson{" +
                "name='" + name + '\'' +
                '}';
    }
}
  1. Teacher类
public class Teacher {
    private String name;


    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "Teacher{" +
                "name='" + name + '\'' +
                '}';
    }
}
  1. Student类

Student类中存在List集合和Map集合。

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

public class Student {
    private String name;

    private List<Lesson> lessons;

    private Map<String, Teacher> teachers;

    public String getName() {
        return name;
    }

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

    public void setTeachers(Map<String, Teacher> teachers) {
        this.teachers = teachers;
    }

    public void setLessons(List<Lesson> lessons) {
        this.lessons = lessons;
    }

    public void study() {
        System.out.println(name + " is studying");
        for (Map.Entry<String, Teacher> teacher : teachers.entrySet()) {
            System.out.println(teacher);
        }
        for (Lesson lesson :lessons){
            System.out.println(lesson);
        }
    }
}
  1. 添加util命名空间
<?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">
  1. 使用util命名空间对List类型和Map类型赋值
    <bean id="lesson1" class="com.xha.pojo.Lesson">
        <property name="name" value="数据结构与算法"></property>
    </bean>
    <bean id="lesson2" class="com.xha.pojo.Lesson">
        <property name="name" value="数据结构与算法"></property>
    </bean>

    <bean id="teacher1" class="com.xha.pojo.Teacher">
        <property name="name" value="张老师"></property>
    </bean>
    <bean id="teacher2" class="com.xha.pojo.Teacher">
        <property name="name" value="赵老师"></property>
    </bean>

    <util:list id="lessonList">
        <ref bean="lesson1"></ref>
        <ref bean="lesson2"></ref>
    </util:list>

    <util:map id="teacherMap">
        <entry>
            <key>
                <value>1</value>
            </key>
            <ref bean="teacher1"></ref>
        </entry>
        <entry>
            <key>
                <value>2</value>
            </key>
            <ref bean="teacher2"></ref>
        </entry>
    </util:map>

    <bean id="student" class="com.xha.pojo.Student">
        <property name="name" value="张三"></property>
<!--        注入list和map类型的属性-->
        <property name="teachers" ref="teacherMap"></property>
        <property name="lessons" ref="lessonList"></property>
    </bean>

image-20230723212914594

  1. 测试类
import com.xha.pojo.Student;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class StudentTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        Student student = (Student) context.getBean("student");
        student.study();
    }
}

image-20230723214250550

3.2.9p命名空间

Spring提供了更加简洁的p-命名空间,作为元素的替代方案。它是XML快捷方式,替代了标记的子元素。要启用p-namespace功能,我们需要将xmlns:p="http://www.springframework.org/schema/p"添加到XML文件中。

引入p命名空间

<?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的各个属性赋值

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

3.2.10引入外部属性文件

  1. 加入依赖
 <!-- 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>
  1. 创建外部属性文件

images

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

引入context 名称空间

<?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/beans"
       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>
<!-- 引入外部属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>

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

  1. 配置bean
<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>
  1. 测试
@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基于xml自动装配

自动装配:

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

  1. UserController
import com.xha.auto.service.UserService;

public class UserController {

    private UserService userService;

    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void addUserController() {
        System.out.println("UserController add...");
        userService.addUserService();
    }
}
  1. 创建接口UserService
public interface UserService {
    void addUserService();
}
  1. 创建类UserServiceImpl实现接口UserService
import com.xha.auto.dao.UserDao;
import com.xha.auto.service.UserService;

public class UserServiceImpl implements UserService {

    private UserDao userDao;

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public void addUserService() {
        System.out.println("UserServiceImpl add...");
        userDao.addUserDao();
    }
}
  1. 创建接口UserDao
public interface UserDao {
     void addUserDao();
}
  1. 创建类UserDaoImpl实现接口UserDao
public class UserDaoImpl implements UserDao {

    @Override
    public void addUserDao() {
        System.out.println("UserDaoImpl add...");
    }
}
  1. 配置bean

自动装配方式:byType

使用bean标签的autowire属性设置自动装配效果 ,自动装配使用的也是setter方法

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

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

image-20230724000233842

<?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="userController" class="com.xha.auto.controller.UserController" autowire="byType"></bean>
    <bean id="userService" class="com.xha.auto.service.impl.UserServiceImpl" autowire="byType"></bean>
    <bean id="userDao" class="com.xha.auto.dao.UserDaoImpl"></bean>

</beans>

根据类型自动注入的总结:

userController bean的autowire属性被设置为byType。这意味着Spring IoC容器会尝试根据类型自动装配该bean的依赖。

在这种情况下,userController bean是一个com.xha.auto.controller.UserController类型的对象,它有一个名为setUserService的方法,该方法接受一个UserService类型的参数。因此,Spring IoC容器会在其内部寻找与UserService类型兼容的bean,并将其注入到userController bean中。这是通过调用setUserService方法来实现的。

在XML配置文件中,有一个名为userService的bean,它是一个com.xha.auto.service.impl.UserServiceImpl类型的对象。由于该类型与UserService类型兼容,因此该bean会被自动注入到userController bean中。

自动装配方式:byName

byName:根据setter方法注入,将自动装配的属性的属性名,也就是setUserService -> userService作为bean的id在IOC容器中匹配相对应的bean进行赋值

public class UserController {

 private UserService userService;

 public void setUserService(UserService userService) {
     this.userService = userService;
 }

 public void addUserController() {
     System.out.println("UserController add...");
     userService.addUserService();
 }
}
<?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="userController" class="com.xha.auto.controller.UserController" autowire="byName"></bean>
    <bean id="userService" class="com.xha.auto.service.impl.UserServiceImpl" autowire="byName"></bean>
    <bean id="userDao" class="com.xha.auto.dao.UserDaoImpl"></bean>

</beans>

根据名称自动注入总结:

如果将userController bean的autowire属性从byType更改为byName,则Spring IoC容器会尝试根据属性名称自动装配该bean的依赖。

在这种情况下,userController bean有一个名为userService的属性,因此Spring IoC容器会在其内部寻找一个与该属性名称相同的bean,并将其注入到该属性中。如果找不到与该属性名称相同的bean,则不会进行自动装配。

在你提供的XML配置文件中,有一个名为userService的bean,因此该bean会被自动注入到userController bean的userService属性中。

  1. 测试
public class Application {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean-auto.xml");
        UserController userController = (UserController) context.getBean("userController");
        userController.addUserController();
    }
}

image-20230724001906785

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

  1. 引入配置文件

引入spring-ioc-xml模块日志log4j2.xml

  1. 添加依赖
<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>

3.3.2开启组件扫描

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

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">

    <!--    开启组件扫描-->
    <context:component-scan base-package="com.xha"></context:component-scan>
</beans>

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

情况一:最基本的扫描方式

<context:component-scan base-package="com.atguigu.spring6">
</context:component-scan>

情况二:指定要排除的组件

<context:component-scan base-package="com.atguigu.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>

情况三:仅扫描指定组件

<context:component-scan base-package="com.atguigu" 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 相同。

Spring还提供了@Bean注解,@Bean注解是一个方法级别的注解,用于指定一个方法的返回对象交由Spring容器管理。它通常在配置类中声明,用于创建Spring Bean定义。

如下面的Spring配置文件,在其中定义了3个bean

  • getDataSource():这个方法创建并配置了一个DruidDataSource对象,用于连接到MySQL数据库。
  • getJdbcTemplate(DataSource dataSource):这个方法创建并配置了一个JdbcTemplate对象,用于执行JDBC操作。
  • getDataSourceTransactionManager(DataSource dataSource):这个方法创建并配置了一个DataSourceTransactionManager对象,用于管理事务。
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.xha")
@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;
    }
}

@Bean注解的属性如下:

@Bean注解有几个属性,可以用来指定bean的名称、初始化方法和销毁方法。

  • name:可以使用此属性指定bean的名称。默认情况下,bean的名称与方法名称相同。
  • initMethod:可以使用此属性指定在创建bean时调用的初始化方法。
  • destroyMethod:可以使用此属性指定在销毁bean时调用的方法。

其中的属性和XML配置bean期间是相同的。

3.3.4@Autowired注入

@Autowired注解是Spring框架中用于实现依赖注入的一种方式。它允许开发人员在字段、构造函数、方法或配置方法参数上声明依赖关系,而不需要执行显式的绑定。Spring IoC容器会自动检测到这些依赖关系,并在运行时自动装配它们。@Autowired注解,默认根据类型装配。【默认是byType】

查看源码:

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 {

	/**
	 * Declares whether the annotated dependency is required.
	 * <p>Defaults to {@code true}.
	 */
	boolean required() default true;

}
  • @Target注解指定了@Autowired注解可以用在哪些元素上。

    • 构造函数
    • 方法
    • 参数
    • 字段
    • 注解类型
  • @Retention注解指定了@Autowired注解的保留策略。在这种情况下,它的保留策略为RUNTIME,这意味着该注解会被保留到运行时。

  • @Documented注解指示将此注解包含在Javadoc中。

  • @Autowired注解有一个名为required的属性,它用于指定被注解的依赖是否是必需的。默认情况下,该属性的值为true,这意味着被注解的依赖是必需的。如果Spring IoC容器无法找到与被注解的依赖兼容的bean,则会抛出异常。如果required属性设置为false,表示注入的Bean存在或者不存在都没关系,存在的话就注入,不存在的话,也不报错。

3.3.4.1属性注入
  1. UserDao接口
public interface UserDao {
     void addUserDao();
}
  1. UserDaoImpl实现
import com.xha.dao.UserDao;
import org.springframework.stereotype.Service;

@Service
public class UserDaoImpl implements UserDao {

    @Override
    public void addUserDao() {
        System.out.println("UserDaoImpl add...");
    }
}
  1. UserService接口
public interface UserService {
    void addUserService();
}
  1. UserServiceImpl实现类
import com.xha.dao.UserDao;
import com.xha.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 addUserService() {
        System.out.println("UserServiceImpl add...");
        userDao.addUserDao();
    }
}
  1. UserController类
import com.xha.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

    //根据类型找到对应的bean进行注入(根据接口找到实现类对象的bean)
    @Autowired
    private UserService userService;

    public void addUserController() {
        System.out.println("UserController add...");
        userService.addUserService();
    }
}
  1. 测试
import com.xha.controller.UserController;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Application {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        UserController userController = (UserController) context.getBean("userController");
        userController.addUserController();
    }
}

测试结果:

image-20230724161607768

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

3.3.4.2set注入
  • 如下修改UserController中的内容,将原来的由属性注入更改为setter方法注入。
import com.xha.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {
    
    private UserService userService;

    @Autowired  //根据类型找到对应的bean进行注入(根据接口找到实现类对象的bean)
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void addUserController() {
        System.out.println("UserController add...");
        userService.addUserService();
    }
}
3.3.4.3构造方法注入
  • 如下修改UserController中的内容,将原来的由属性注入更改为构造方法注入。
import com.xha.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 addUserController() {
        System.out.println("UserController add...");
        userService.addUserService();
    }
}
3.3.4.4形参上注入
  • 如下修改UserController中的内容,将原来的由属性注入更改为形参注入。
import com.xha.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 addUserController() {
        System.out.println("UserController add...");
        userService.addUserService();
    }
}
3.3.4.5只有一个构造函数,注解可以省略
  • 如下修改UserController中的内容,只有一个构造函数,所以@Autowired注解可以省略
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.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层执行结束");
    }
}
3.3.4.6属性、构造函数、Setter注入的区别

@Autowired 注解可以用来实现基于属性、基于构造方法、Setter方法的依赖注入。这两种方式的主要区别在于注入时机和依赖关系的处理方式。

  • 基于属性的依赖注入:当使用 @Autowired 注解来实现基于属性的依赖注入时,Spring 框架会在 Bean 实例化之后,通过反射机制来设置 Bean 的属性值。这种方式简单易用,但也有一些缺点。例如,它不能很好地处理循环依赖的问题,因为当两个 Bean 互相依赖时,它们都需要等待对方实例化完成才能进行属性注入,这会导致死锁。
  • 基于构造方法的依赖注入:当使用 @Autowired 注解来实现基于构造方法的依赖注入时,Spring 框架会在 Bean 实例化之前,通过调用构造方法来传递依赖关系。这种方式可以更好地处理循环依赖的问题,因为它可以保证在 Bean 实例化之前就完成了依赖关系的传递。
  • 基于 Setter 方法的依赖注入:当使用 @Autowired 注解来实现基于 Setter 方法的依赖注入时,Spring 框架会在 Bean 实例化之后,通过调用 Setter 方法来设置 Bean 的属性值。不是通过反射机制来设置。
3.3.4.7多bean同类型问题

假设我们有两个Employee类型的bean,分别为JohnEmployee()和TonyEmployee():

@Configuration
public class Config {
    @Bean
    public Employee JohnEmployee() {
        return new Employee("John");
    }

    @Bean
    public Employee TonyEmployee() {
        return new Employee("Tony");
    }
}

如果我们尝试运行应用程序,Spring会抛出NoUniqueBeanDefinitionException异常。

对于以上问题,可以有以下解决方案:

  • 当发现有多种类型的Bean时,@Primary注解会通知IoC容器优先使用它所标注的Bean进行注入;

  • @Quelifier注解可以与@AutoWired注解组合使用,达到通过类型和名称一起筛选Bean的效果。

  • 当前也可以直接使用@Resourse注解按照名称进行注入。

对于出现的问题:

  • NoUniqueBeanDefinitionException 异常是当同时对多个相同类型的bean注入到IoC容器当中时出现的错误。
  • 而UnsatisfiedDependencyException时出现在当使用Autowire注解进行依赖注入时,发现IoC容器当中存在两个相同类型的bean而出现的错误
3.3.4.8@Autowired注解和@Qualifier注解联合

因为@Autowired是根据类型进行注入,则当一个接口有多个实现类的时候,出现UnsatisfiedDependencyException

如为UserService添加新的实现类——UserServiceDoubleImpl

image-20230724163839329

使用@Autowired的属性注入测试:

image-20230724164114124

此时可以使用@Autowired注解和@Qualifier根据名称进行注入

  • 名称就是类的名称(首字母小写)

添加dao层实现

import com.xha.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

    @Autowired
//    使用@Qualifier指定注入的bean的名称
    @Qualifier("userServiceImpl")
    private UserService userService;

    public void addUserController() {
        System.out.println("UserController add...");
        userService.addUserService();
    }
}
3.3.4.9@Autowired总结
  • @Autowired注解可以出现在:属性上、构造方法上、构造方法的参数上、setter方法上。
  • 当带参数的构造方法只有一个,@Autowired注解可以省略。
  • @Autowired注解默认根据类型注入。如果要根据名称注入的话,需要配合@Qualifier注解一起使用。

3.3.5@Resource注入

3.3.5.1@Autowired注解和@Resource注解的区别

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

  1. @Autowired是Spring定义的注解,而@Resource是JDK定义的注解。

  2. @Autowired默认按byType进行依赖注入。@Resource默认按byName进行依赖注入,如果无法通过名称匹配到对应的实现类的话,注入方式会变为byType。。

  3. @Autowired能够用在:属性、构造器、方法、参数和注解上,而@Resource能用在:类、属性和方法上。

  4. @Autowired只包含一个参数:required,表示是否开启自动注入,默认是true。而@Resource包含七个参数,其中最重要的两个参数是:name 和 type。

  5. @Autowired如果要使用byName,需要使用@Qualifier一起配合。而@Resource如果指定了name,则用byName自动装配,如果指定了type,则用byType自动装配。

  6. 二者装配顺序不同

    • Autowried

    image-20230724174715272

    • Resource

    image-20230724174758695

@Resource注解属于JDK扩展包,所以不在JDK当中,需要额外引入以下依赖:【如果是JDK8的话不需要额外引入依赖。高于JDK11或低于JDK8需要引入以下依赖。

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

@Resource源码:

import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;


@Target({TYPE, FIELD, METHOD})
@Retention(RUNTIME)
@Repeatable(Resources.class)
public @interface Resource {

    String name() default "";


    String lookup() default "";

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

    enum AuthenticationType {
	    CONTAINER,
	    APPLICATION
    }

    AuthenticationType authenticationType() default AuthenticationType.CONTAINER;

    boolean shareable() default true;

    String mappedName() default "";
    
    String description() default "";
}

3.3.5.2根据名称注入

  • 根据byName自动注入,会自动在容器中找到名字为userServiceImpl的bean
import com.xha.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

//    根据byName自动注入,会自动在容器中找到名字为userServiceImpl的bean
    @Resource(name = "userServiceImpl")
    private UserService userService;

    public void addUserController() {
        System.out.println("UserController add...");
        userService.addUserService();
    }
}

3.3.5.3根据类型注入

import com.xha.service.UserService;
import com.xha.service.impl.UserServiceImpl;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {


//    根据类型注入
    @Resource(type = UserServiceImpl.class)
    private UserService userService;

    public void addUserController() {
        System.out.println("UserController add...");
        userService.addUserService();
    }
}

3.3.6Spring全注解开发

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

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

// @Configuration: 作为配置类,替代xml配置文件
@Configuration
// @ComponentScan: 扫描包
@ComponentScan("com.xha")
public class SpringConfig {
}

同时在启动类上,不再通过加载配置文件的方式来进行配置包扫描了,而是通过加载配置类的方式。

import com.xha.config.SpringConfig;
import com.xha.controller.UserController;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Application {
    public static void main(String[] args) {
//        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
//        UserController userController = (UserController) context.getBean("userController");
//        userController.addUserController();
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        UserController userController = (UserController) context.getBean("userController");
        userController.addUserController();
    }
}

4.原理-手写IoC

我们都知道,Spring框架的IOC是基于Java反射机制实现的,下面我们先回顾一下java反射。

4.1回顾Java反射

4.1.1反射的概念

Java反射机制允许在程序运行时能够获取到类的信息。可以创建类的实例,调用类的方法,访问和修改类的属性。

要想解剖一个类,必须先要获取到该类的Class对象。而剖析一个类或用反射解决具体的问题就是使用相关API(1)java.lang.Class(2)java.lang.reflect,所以,Class对象是反射的根源

4.1.2反射案例

  1. Car类
public class Car {
    private String name;

    private int age;

    private String color;

    public Car() {
    }

    public Car(String name, int age, String color) {
        this.name = name;
        this.age = age;
        this.color = color;
    }

    private void init() {
        System.out.println("私有init...");
    }

    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 getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }
}
  1. 反射具体案例
  • 获取到Class对象
  • 通过反射创建类的实例
  • 获取到类的所有public构造函数
  • 获取到类的所有构造函数
  • 获取到类的有参构造函数
  • 获取属性
  • 获取方法
  • 获取指定的方法
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectDemo {
    public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException {
//        1.获取Class对象
//          1.1通过类名获取
        Class<Car> carClass1 = Car.class;
//          1.2通过对象获取
        Class<? extends Car> carClass2 = new Car().getClass();
//          1.3通过全类名获取
        Class<?> carClass3 = Class.forName("com.xha.pojo.Car");

//        2.创建类的实例
        Car car = carClass1.getDeclaredConstructor().newInstance();
        System.out.println("创建实例------------->");
        System.out.println(car);
        System.out.println("--------------------");

//        3.获取到类的所有public构造函数
        Constructor<?>[] constructors = carClass1.getConstructors();
        System.out.println("获取到类的所有public构造函数------------->");
        for (Constructor<?> constructor : constructors) {
            System.out.println(constructor);
        }
        System.out.println("--------------------");

//        4.获取到类的所有构造函数
        Constructor<?>[] declaredConstructors = carClass1.getDeclaredConstructors();
        System.out.println("获取到类的所有构造函数------------->");
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            System.out.println(declaredConstructor);
        }
        System.out.println("--------------------");

//        5.获取到类的有参构造函数
        Constructor<Car> constructor = carClass1.getConstructor(String.class, int.class, String.class);
        Car car1 = constructor.newInstance("宝马", 3, "黑色");
        System.out.println("获取到类的有参构造函数------------->");
        System.out.println(car1);
        System.out.println("--------------------");

//        6.获取属性
//          6.1获取所有public属性
        Field[] fields = carClass1.getFields();
//          6.2获取所有属性
        Field[] declaredFields = carClass1.getDeclaredFields();
        System.out.println("获取所有属性------------->");
        for (Field declaredField : declaredFields) {
            if (declaredField.equals("name")) {
//                6.1设置属性可访问
                declaredField.setAccessible(true);
                declaredField.set(car1, "奔驰");
            }
            System.out.println(declaredField);
        }
        System.out.println("--------------------");

//        7.获取方法
//          7.1获取所有public方法
        Method[] methods = carClass1.getMethods();
        System.out.println("获取所有public方法------------->");
        for (Method method : methods) {
            System.out.println(method);
        }
        System.out.println("--------------------");
//          7.2获取指定的方法
        Method[] declaredMethods = carClass1.getDeclaredMethods();
        System.out.println("2获取指定的方法------------->");
        for (Method declaredMethod : declaredMethods) {
//            7.3设置方法可访问
            declaredMethod.setAccessible(true);
            if (declaredMethod.getName().equals("init")) {
                declaredMethod.invoke(car1);
            }
        }
        System.out.println("--------------------");
    }
}

测试结果:

创建实例------------->
Car{name='null', age=0, color='null'}
--------------------
获取到类的所有public构造函数------------->
public com.xha.pojo.Car()
public com.xha.pojo.Car(java.lang.String,int,java.lang.String)
--------------------
获取到类的所有构造函数------------->
public com.xha.pojo.Car()
public com.xha.pojo.Car(java.lang.String,int,java.lang.String)
--------------------
获取到类的有参构造函数------------->
Car{name='宝马', age=3, color='黑色'}
--------------------
获取所有属性------------->
private java.lang.String com.xha.pojo.Car.name
private int com.xha.pojo.Car.age
private java.lang.String com.xha.pojo.Car.color
--------------------
获取所有public方法------------->
public java.lang.String com.xha.pojo.Car.getName()
public java.lang.String com.xha.pojo.Car.toString()
public void com.xha.pojo.Car.setName(java.lang.String)
public void com.xha.pojo.Car.setColor(java.lang.String)
public int com.xha.pojo.Car.getAge()
public java.lang.String com.xha.pojo.Car.getColor()
public void com.xha.pojo.Car.setAge(int)
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
--------------------
2获取指定的方法------------->
私有init...
--------------------

Process finished with exit code 0

4.2实现Spring的IoC

4.2.1思路分析

  1. 自定义注解@Bean@DI用于实现创建bean依赖注入
  2. 创建bean容器接口Application,定义方法返回对象(抽象工厂)
  3. 创建bean容器接口Application的实现类ApplicationContext
    • 定义HashMap集合用于存储bean,key为Class实例,value为对应类的实例对象(bean)
    • private void loadBean(File file)方法:根据包扫描规则扫描对应的包和子包的所有子类,查找类上是否有@Bean注解,如果有把这个类通过反射进行实例化。 完成bean的创建。
    • private void loadDi()方法:遍历beanFactory中的所有bean对象,获取到Class实例,通过反射获取到类的属性属性,判断属性上面是否含有@DI注解,如果有就将HashMap当中的value(bean)作为该属性的属性值。完成依赖注入。

image-20230725170341729

4.2.2代码实现

项目结构:

image-20230725174744481

4.2.2.1自定义注解

@Bean注解

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 {
}

@DI注解

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 {
}

4.2.2.2定义测试类

  1. UserController类
  • 在使用@Bean注解将UserController注册进IoC容器,使用@DI注解注入UserService类型的依赖。
import com.xha.annotation.Bean;
import com.xha.annotation.DI;
import com.xha.service.UserService;

@Bean
public class UserController {


//    根据类型注入
    @DI
    private UserService userService;

    public void addUserController() {
        System.out.println("UserController add...");
        userService.addUserService();
    }
}
  1. UserService接口
public interface UserService {
    void addUserService();
}
  1. UserServiceImpl类

在使用@Bean注解将UserServiceImpl注册进IoC容器

import com.xha.annotation.Bean;
import com.xha.service.UserService;

@Bean
public class UserServiceImpl implements UserService {

    @Override
    public void addUserService() {
        System.out.println("UserServiceImpl add...");
    }
}

4.2.2.3创建Bean容器接口

  • bean容器接口Application,定义方法返回对象(抽象工厂)
public interface ApplicationContext {


    /**
     * 定义获取bean的方法
     *
     * @param clazz clazz
     * @return {@link Object}
     */
    Object getBean(Class clazz);
}

4.2.2.4创建Bean容器接口实现类

  • 定义HashMap集合用于存储bean,key为Class实例,value为对应类的实例对象(bean)
  • private void loadBean(File file)方法:根据包扫描规则扫描对应的包和子包的所有子类,查找类上是否有@Bean注解,如果有把这个类通过反射进行实例化。 完成bean的创建。
  • private void loadDi()方法:遍历beanFactory中的所有bean对象,获取到Class实例,通过反射获取到类的属性属性,判断属性上面是否含有@DI注解,如果有就将HashMap当中的value(bean)作为该属性的属性值。完成依赖注入。
import com.xha.annotation.Bean;
import com.xha.annotation.DI;
import com.xha.bean.ApplicationContext;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class AnnotationApplicationContext implements ApplicationContext {

    //    1.创建map集合,用来存储bean对象
    private static Map<Class, Object> beanFactory = new HashMap<>();

    private static String basePath;

    /**
     * 返回bean对象:和ApplicationContext.getBean()方法一样,bean是存储在beanFactory中的,
     * 而beanFactory是一个Map集合,key是Class类型,value是Object类型,所以这里的参数是Class类型
     *
     * @param clazz clazz
     * @return {@link Object}
     */
    @Override
    public Object getBean(Class clazz) {
        return beanFactory.get(clazz);
    }


    /**
     * 注释应用程序上下文
     *
     * @param basePackage 基本包
     */
    public AnnotationApplicationContext(String basePackage) {
//        1.将包路径中的.替换为\
        String packagePath = basePackage.replaceAll("\\.", "\\\\");
        try {
//            2.获取到当前线程的类加载器,获取到字节码包的绝对路径
            Enumeration<URL> resources = Thread.currentThread().getContextClassLoader().getResources(packagePath);
            while (resources.hasMoreElements()) {
//                4.获取到字节码包的绝对路径
//                file:/D:/Idea_project/Spring6/spring-ioc-soure-code/target/classes/com%5cxha
                URL url = resources.nextElement();
//                5.格式化路径
//                /D:/Idea_project/Spring6/spring-ioc-soure-code/target/classes/com\xha
                String filePath = URLDecoder.decode(url.getPath(), "UTF-8");
//                6.将绝对路径截取除去包路径的部分
//                /D:/Idea_project/Spring6/spring-ioc-soure-code/target/classes/
                basePath = filePath.substring(0, filePath.length() - packagePath.length());
//                7.调用loadBean方法,进行包扫描,将bean对象存储到beanFactory中
                loadBean(new File(filePath));
                loadDi();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 进行包扫描,将bean对象存储到beanFactory中
     *
     * @param file 文件
     */
    private void loadBean(File file) {
//        1.判断当前file对象是否是文件夹
        if (file.isDirectory()) {
//        2.获取到文件夹下的所有文件夹或者文件
            File[] childrenFiles = file.listFiles();
            if (childrenFiles == null || childrenFiles.length <= 0) {
                return;
            }
            for (File children : childrenFiles) {
//        3.如果是文件夹,则递归遍历
                if (children.isDirectory()) {
                    loadBean(children);
                } else {
//        4.如果是文件,则截取除类之外的路径(com\xha\annotation\Bean.class)
                    String pathWithClass = children.getAbsolutePath().substring(basePath.length() - 1);
//        5.判断是否是class文件,如果是class文件,把路径中的\替换成.,把.class替换成空字符串
                    try {
                        if (pathWithClass.contains(".class")) {
                            //        6.将路径中的\替换成.,把.class替换成空字符串(com.xha.annotation.Bean)
                            String className = pathWithClass.replaceAll("\\\\", ".").replace(".class", "");
                            //        7.获取到Class对象
                            Class<?> clazz = null;
                            clazz = Class.forName(className);
                            //        8.如果不是接口类型,则查找是否有注解
                            if (!clazz.isInterface()) {
                                //        9.如果类上有@Bean注解
                                if (clazz.isAnnotationPresent(Bean.class)) {
                                    //        10.实例化
                                    Object instance = null;
                                    try {
                                        instance = clazz.getDeclaredConstructor().newInstance();
                                    } catch (Exception e) {
                                        throw new RuntimeException(e);
                                    }
                                   //        11.如果当前类有接口,就让类的接口作为Class对象,如果没有接口,就让当前类作为Class对象
                                    if (clazz.getInterfaces().length > 0) {
                                        beanFactory.put(clazz.getInterfaces()[0], instance);
                                    } else {
                                        beanFactory.put(clazz, instance);
                                    }
                                }
                            }
                        }
                    } catch (ClassNotFoundException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }

    /**
     * 进行依赖注入
     */
    private void loadDi() {
//        1.遍历beanFactory中的所有bean对象
        Set<Map.Entry<Class, Object>> entries = beanFactory.entrySet();
        for (Map.Entry<Class, Object> entry : entries) {
//        3.获取到bean的Class实例
            Class clazz = entry.getKey();
//        4.通过反射获取到bean的所有属性
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
//        5.如果对应的属性上有@DI注解
                if (field.isAnnotationPresent(DI.class)) {
//                    6.设置属性可以被访问
                    field.setAccessible(true);
                    try {
//                      7.获取到bean(实例对象)
                        Object obj = entry.getValue();
//                      8.给属性赋值,从beanFactory中获取到属性对应的对象,完成依赖注入
                        field.set(obj, beanFactory.get(field.getType()));
                    } catch (IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
}

4.2.2.5测试IoC容器和依赖注入(DI)

配置包扫描规则:com.xha

import com.xha.bean.ApplicationContext;
import com.xha.bean.impl.AnnotationApplicationContext;
import com.xha.controller.UserController;

public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationApplicationContext("com.xha");
        UserController userController = (UserController) context.getBean(UserController.class);
        userController.addUserController();
    }
}

image-20230725175227993

5.面向切面:AOP

5.1场景模拟

模拟计算器场景

5.1.1声明接口

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

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创建实现类

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创建带日志功能的实现类

images

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. 现有代码缺陷

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

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

解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。

  1. 困难

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

5.2代理模式

5.2.1 概述

代理模式通过创建一个代理对象来控制对实际对象的访问。

Java中的代理按照代理类生成时机不同又分为静态代理和动态代理静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。

使用代理前:

images

使用代理后:

images

images

5.2.2 结构

代理(Proxy)模式分为三种角色:

  • 抽象主题(Subject)定义了真实主题和代理主题的共同接口,这样代理和真实主题可以互换使用。
  • 真实主题(Real Subject):定义代理对象所代表的真实对象。
  • 代理(Proxy)持有一个真实主题对象的引用,并实现了与真实主题相同的接口,可以在接口方法的调用前后添加额外的处理逻辑。

5.2.3 静态代理

静态代理是一种在编译时就已经确定代理关系的代理模式。在静态代理中,代理类和真实类是在编译期间就已经确定的,代理类在编写代码时就已经存在。

在静态代理中,代理类通过实现与真实类相同的接口或继承与真实类相同的父类,来实现对真实类方法的代理。当客户端调用代理对象的方法时,代理对象会执行额外的处理,例如调用真实对象的方法之前或之后添加日志记录、权限验证等功能。

静态代理的实现步骤如下:

  1. 创建接口:定义被代理类和代理类共同实现的接口,即被代理接口
  2. 创建真实类:实现被代理接口的类,即真实对象。
  3. 创建代理类:实现被代理接口,并在代理类中维护一个指向真实对象的引用。在代理类的方法中添加额外的处理逻辑,并在适当的时机调用真实对象的相应方法。
  4. 客户端调用:通过代理类来调用方法,代理类会将请求转发给真实对象进行处理。

静态代理的优点是简单易用,对于简单的代理需求可以较快实现。但是静态代理的缺点是扩展性较差,每当有新的代理需求时,需要重新编写新的代理类。当代理类较多时,会产生大量的重复代码,导致代码冗余。

【例】火车站卖票

如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的代理模式,火车站是真实主题,代售点是代理类。类图如下:

  1. SellTickets接口(抽象主题
public interface SellTickets {
    void sell();
}
  1. TrainStation类(真实主题
public class TrainStation implements SellTickets{
    @Override
    public void sell() {
        System.out.println("火车站卖票");
    }
}
  1. ProxyPoint类(代理
public class ProxyPoint implements SellTickets{

//    声明火车站对象
    private TrainStation trainStation = new TrainStation();

    @Override
    public void sell() {
        System.out.println("代售点收取一些服务费用");
//        调用火车站卖票方法
        trainStation.sell();
    }
}
  1. 测试类
public class TestDemo {
    public static void main(String[] args) {
//        1.创建代售点对象
        ProxyPoint proxyPoint = new ProxyPoint();
//        2.调用代售点的卖票方法
        proxyPoint.sell();
    }
}

从上面代码中可以看出测试类直接访问的是ProxyPoint类对象,也就是说ProxyPoint作为访问对象和目标对象的中介。同时也对sell方法进行了增强(代理点收取一些服务费用)。

5.2.4 动态代理

5.2.4.1动态代理概述

动态代理允许在运行时创建和使用代理对象,而无需在编译时显式地指定代理类。代理类会在运行时根据需求动态生成,通过代理类可以间接地访问实际对象,并在访问前后执行一些额外的操作。

动态代理可以分为两种主要类型:基于接口的动态代理和基于类的动态代理。

  1. 基于接口的动态代理:这种代理方式是在运行时,动态生成一个实现指定接口的代理类,代理类通过实现接口的方法来执行真正的业务逻辑。Java中常用的基于接口的动态代理是使用Java的反射机制实现的,例如 java.lang.reflect.Proxy 类和 InvocationHandler 接口。
  2. 基于类的动态代理:这种代理方式是在运行时,动态生成一个继承自指定类的代理类,代理类通过继承和重写父类的方法来执行真正的业务逻辑。基于类的动态代理通常使用字节码操作库来生成代理类,例如Java字节码操作库CGLIB、ASM等。

5.2.4.2JDK动态代理

动态代理是一种基于反射机制的代理模式,它可以在运行时动态地创建代理类和代理实例,而无需在编译时就确定代理类的关系。

在动态代理中,代理类是根据被代理的接口信息动态生成的。当客户端调用代理对象的方法时,实际上是通过代理对象转发给了真实对象进行处理。代理对象可以在真实对象的方法调用前后添加额外的逻辑,例如日志记录、性能监控等。

动态代理主要涉及到以下几个组成部分:

  1. 接口:定义了真实对象和代理对象共同实现的方法。
  2. InvocationHandler(调用处理器)接口:它是实现动态代理的关键部分,代理对象的所有方法调用都会经过该调用处理器的invoke()方法进行处理。
  3. Proxy类负责动态生成代理类和代理实例的类。

具体实现动态代理的步骤如下:

  1. 定义接口:定义需要被代理的接口,该接口包含一组要被代理的方法。
  2. 通过Proxy类的静态方法newProxyInstance()动态生成代理类和代理实例:该方法需要三个参数,第一个参数是类加载器,第二个参数是实现了接口的数组,第三个参数是InvocationHandler的实例。
  3. 实现InvocationHandler接口:创建一个调用处理器的实现类,实现invoke()方法,在invoke()方法中可以添加额外的逻辑。
  4. 使用代理对象:通过代理对象调用方法,实际上会通过InvocationHandlerinvoke()方法转发给真实对象进行处理。

动态代理的优点是灵活性和可扩展性强,可以在运行时动态地添加和修改代理类的行为,而无需改动被代理的真实对象。它提供了一种统一的方式来处理各种不同的代理需求,例如日志记录、事务管理、权限控制等。

代码如下:

  1. ProxyFactory(**获取代理对象类**)
public class ProxyFactory {
//    声明火车站对象
    private TrainStation trainStation = new TrainStation();

//    获取代理对象
    public SellTickets getProxyObject() {
        //在程序运行期间,动态的创建TrainStation接口的代理对象
        /*
            newProxyInstance()方法参数说明:
                ClassLoader loader : 类加载器,用于加载代理类,可以通过被代理对象获取类加载器
                Class<?>[] interfaces : 被代理类实现的接口字节码对象数组
                InvocationHandler h : 代理对象的调用处理程序
         */
        SellTickets proxyObject = (SellTickets) Proxy.newProxyInstance(
                trainStation.getClass().getClassLoader(), //类加载器
                trainStation.getClass().getInterfaces(), //真实对象所实现的接口的字节码对象数组
                new InvocationHandler() { //调用处理程序
                    /*
                        InvocationHandler中invoke方法参数说明:
                            proxy : 代理对象,和proxyObject对象是同一个对象
                            method : 代理对象调用的方法.对接口中的方法进行封装成method对象
                            args : 代理对象调用接口方法时传递的实际参数
                    */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//                        在invoke()方法中可以添加额外的逻辑
                        System.out.println("代售点收取一些服务费用");
//                        通过反射调用真实对象的方法
                        Object obj = method.invoke(trainStation, args);
                        return obj;
                    }
                }
        );
        return proxyObject;
    }
}

测试类:

public class TestDemo {
    public static void main(String[] args) {
//        1.创建火车站对象
        ProxyFactory proxyFactory = new ProxyFactory();
//        2.调用代理工厂的方法获取代理对象
        SellTickets proxyObject = proxyFactory.getProxyObject();
//        3.调用代理对象的卖票方法
        proxyObject.sell();
    }
}

image-20230713191834680

总结:动态代理的核心是使用了 Java 的Proxy类和 InvocationHandler 接口。Proxy 类提供了一个静态方法 newProxyInstance,通过调用该方法并传入被代理对象的类加载器、实现的接口和 InvocationHandler 对象,可以创建代理对象。

InvocationHandler 接口是一个函数式接口,其中定义了一个 invoke 方法,该方法在代理对象上调用被代理的方法时被触发。invoke方法中,我们可以编写自定义的逻辑来处理被代理方法的调用,例如在方法调用前后进行额外的操作。

总而言之,动态代理利用反射机制在运行时动态地创建代理对象,并通过InvocationHandler来处理代理对象的方法调用。这样可以实现对被代理对象的透明代理,而无需手动编写每个方法的代理代码。

5.2.4.3JDK实现动态代理分析

使用了动态代理,我们思考下面问题:

  1. 定义的ProxyFactory类是代理类吗?

刚刚定义的ProxyFactory不是代理模式中所说的代理类,而代理类是程序在运行过程中动态的在内存中生成的类。通过阿里巴巴开源的 Java 诊断工具(Arthas【阿尔萨斯】)查看代理类的结构:

package com.sun.proxy;

import com.itheima.proxy.dynamic.jdk.SellTickets;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements SellTickets {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            return;
        }
        catch (NoSuchMethodException noSuchMethodException) {
            throw new NoSuchMethodError(noSuchMethodException.getMessage());
        }
        catch (ClassNotFoundException classNotFoundException) {
            throw new NoClassDefFoundError(classNotFoundException.getMessage());
        }
    }

    public final boolean equals(Object object) {
        try {
            return (Boolean)this.h.invoke(this, m1, new Object[]{object});
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final String toString() {
        try {
            return (String)this.h.invoke(this, m2, null);
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final int hashCode() {
        try {
            return (Integer)this.h.invoke(this, m0, null);
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final void sell() {
        try {
            this.h.invoke(this, m3, null);
            return;
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
}

从上面的类中,我们可以看到以下几个信息:

  • 代理类($Proxy0)实现了SellTickets。这也就印证了我们之前说的真实类和代理类实现同样的接口。
  • 代理类($Proxy0)将我们提供了的匿名内部类对象传递给了父类。
  1. 动态代理的执行流程是什么样?

下面是摘取的重点代码:

//程序运行过程中动态生成的代理类
public final class $Proxy0 extends Proxy implements SellTickets {
    private static Method m3;

    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }

    static {
        m3 = Class.forName("com.xha.model.construct.proxy.jdk_proxy.SellTickets").getMethod("sell", new Class[0]);
    }

    public final void sell() {
        this.h.invoke(this, m3, null);
    }
}

//Java提供的动态代理相关类
public class Proxy implements java.io.Serializable {
	protected InvocationHandler h;
	 
	protected Proxy(InvocationHandler h) {
        this.h = h;
    }
}

//代理工厂类
public class ProxyFactory {

    private TrainStation station = new TrainStation();

    public SellTickets getProxyObject() {
        SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(station.getClass().getClassLoader(),
                station.getClass().getInterfaces(),
                new InvocationHandler() {
                    
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        System.out.println("代理点收取一些服务费用(JDK动态代理方式)");
                        Object result = method.invoke(station, args);
                        return result;
                    }
                });
        return sellTickets;
    }
}


//测试访问类
public class Client {
    public static void main(String[] args) {
        //获取代理对象
        ProxyFactory factory = new ProxyFactory();
        SellTickets proxyObject = factory.getProxyObject();
        proxyObject.sell();
    }
}

执行流程如下:

1. 在测试类中通过代理对象调用`sell()`方法
2. 根据多态的特性,执行的是代理类(`$Proxy0`)中的sell()方法
3. 代理类($Proxy0)中的sell()方法中又调用了`InvocationHandler`接口的子实现类对象的`invoke`方法
4. `invoke`方法通过反射执行了真实对象所属类`(TrainStation)中的sell()`方法

5.2.4.3 CGLIB动态代理

同样是上面的案例,我们再次使用CGLIB代理实现。

如果没有定义SellTickets接口,只定义了TrainStation(火车站类)。很显然JDK代理是无法使用了,因为JDK动态代理要求必须定义接口,对接口进行代理。

CGLIB(Code Generation Library)是一个功能强大的第三方库,用于在Java中创建动态代理。与标准的JDK动态代理(基于接口)不同,CGLIB动态代理可以基于类来创建代理对象。它通过生成目标类的子类,并重写其中的方法来实现代理功能。下面是CGLIB动态代理的执行流程:

  1. 创建目标类:首先,我们需要创建一个目标类(即要被代理的类),该类通常是一个普通的Java类,它包含我们希望代理的方法。
  2. 创建代理工厂:我们需要创建一个代理工厂(ProxyFactory)对象,用于生成代理类和代理对象。
  3. 设置代理目标:在代理工厂中,我们需要设置目标类(Target Class)和方法拦截器(MethodInterceptor)。目标类是我们希望代理的类,而方法拦截器则是在代理对象上调用方法时被调用的拦截器。
  4. 生成代理类:通过代理工厂的create方法,CGLIB根据目标类和方法拦截器生成一个代理类。这个代理类会继承目标类并重写目标类中的方法。
  5. 创建代理对象:在生成代理类后,我们可以使用其构造方法或静态工厂方法来创建代理对象。代理对象是代理类的实例,具有与目标类相同的方法和属性。
  6. 方法拦截:当我们在代理对象上调用方法时,代理对象会将方法调用转发给方法拦截器方法拦截器可以在方法调用前后执行自定义的逻辑,例如修改方法参数、记录日志、执行额外的操作等。

CGLIB是第三方提供的包,所以需要引入jar包的坐标:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2.2</version>
</dependency>

代码如下:

  1. TrainStation类(目标类
public class TrainStation {
    public void sell(){
        System.out.println("火车站卖票");
    }
}
  1. ProxyFactory类(代理工厂
/**
 * 代理工厂
 *
 * @author Xu huaiang
 * @date 2023/07/13
 */
public class ProxyFactory implements MethodInterceptor {
//    声明火车站对象
    private TrainStation trainStation = new TrainStation();

    public TrainStation getProxyObject(){
//        1.创建Enhancer对象,类似于JDK代理中的Proxy类,下一步就是设置几个参数
        Enhancer enhancer = new Enhancer();
//        2.由于是继承代理,所以需要设置父类,父类就是目标类
        enhancer.setSuperclass(TrainStation.class);
//        3.设置回调函数,MethodInterceptor接口等效于JDK代理中的InvocationHandler接口
        enhancer.setCallback(this);
//        4.创建代理对象
        return (TrainStation) enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("代售点收取一些服务费用");
        Object obj = method.invoke(trainStation, objects);
        return obj;
    }
}
  1. 测试类
public class TestDemo {
    public static void main(String[] args) {
//        1.创建代理工厂对象
        ProxyFactory proxyFactory = new ProxyFactory();
//        2.调用代理工厂的方法获取代理对象,代理对象就是TrainStation的子类对象
        TrainStation proxyObject = proxyFactory.getProxyObject();
//        3.调用代理对象的卖票方法
        proxyObject.sell();
    }
}

image-20230713203955131

5.2.6 三种代理的对比

  • jdk代理和CGLIB代理

    JDK动态代理:JDK动态代理要求目标对象必须实现至少一个接口。代理对象是目标对象的子类,它们共享相同的接口。JDK代理是基于Java的反射机制实现的。
    CGLIB动态代理:CGLIB动态代理不要求目标对象实现接口。它通过生成目标对象的子类,并覆盖其中的方法来创建代理对象。这种方式允许代理非接口类型的类。

    在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理。

  • 动态代理和静态代理

    动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法(InvocationHandler.invoke)中处理。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。

    如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题

5.2.7 优缺点

优点:

  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
  • 代理对象可以扩展目标对象的功能;
  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;

缺点:

  • 增加了系统的复杂度;

5.2.8 使用场景

  • 远程(Remote)代理

    本地服务通过网络请求远程服务。为了实现本地到远程的通信,我们需要实现网络通信,处理其中可能的异常。为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节。

  • 防火墙(Firewall)代理

    当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。

  • 保护(Protect or Access)代理

    控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。

5.3AOP概念及相关术语

5.3.1概述

AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善。AOP通过预编译运行期动态代理方式实现,在不修改源代码的情况下,为程序动态添加额外功能的一种技术。面向切面编程(AOP)旨在提高模块化,使开发人员能够更好地将横切关注点(如日志记录,安全性和事务管理)与业务逻辑分离。

5.3.2相关术语

5.3.2.1横切关注点

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

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

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

images

5.3.2.2通知(增强)

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

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

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

images

5.3.2.3切面

封装通知方法的类。

images

5.3.2.4目标

被代理的目标对象。

5.3.2.5代理

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

5.3.2.6连接点

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

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

images

5.3.2.7切入点

定位连接点的方式。

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

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

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

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

5.4基于注解的AOP(AspectJ框架实现)

5.4.1AspectJ技术说明

images

AspectJ是一个基于Java语言的面向切面(AOP)框架。它定义了AOP语法,并且有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。在Spring 2.0以后,新增了对AspectJ框架的支持。在Spring框架中建议使用AspectJ框架开发AOP。

5.4.2各种通知

名称 说明
前置通知(@Before) 在调用目标方法之前自动执行
后置通知(@After) 在调用目标方法之后自动执行
异常通知(@AfterThrowing) 在目标方法抛出异常时自动执行
环绕通知(@Around) 在调用目标方法之前与之后自动执行
返回通知(@AfterReturning) 当目标方法没有抛出任何异常并正常返回时,将执行返回通知

各种通知的执行顺序:

  • 前置通知(@Before
  • 环绕通知(@Around)(在目标方法执行之前)
  • 目标方法执行
  • 环绕通知(@Around)(在目标方法执行之后)
  • 后置通知(@After
  • 返回通知(@AfterReturning)或异常通知(@AfterThrowing

5.4.3切入点表达式语法

  1. 作用

images

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

execution(访问权限符 返回值类型 方法全类名(参数列表))

images

5.4.4重用切入点表达式

  1. 声明
@Pointcut("execution(* com.atguigu.aop.annotation.*.*(..))")
public void pointCut(){}
  1. 在同一个切面类中使用
@Before("pointCut()")
public void beforeMethod(JoinPoint joinPoint){
    String methodName = joinPoint.getSignature().getName();
    String args = Arrays.toString(joinPoint.getArgs());
    System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}
  1. 在不同切面类中使用
@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.5获取通知的相关信息

  1. 获取连接点信息

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

@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);
}
  1. 获取目标方法的返回值

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

@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);
}
  1. 获取目标方法的异常

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

@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);
}

5.4.6环绕通知

@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;
}

5.4.7切面的优先级

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

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

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

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

images

5.4.8准备工作

  1. 添加依赖

在IOC所需依赖基础上再加入下面依赖即可:

<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>
</dependencies>
  1. 准备被代理的目标资源

接口:

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);
    
}

实现类:

@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.9创建切面类并配置

  1. 创建切面类
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

/**
 * 切面类
 */
@Aspect
@Component
public class LogAspect {

    //前置通知,方法执行前执行,切入点表达式作用是指定在哪个方法切入
    //切入点表达式:execution(访问权限符 返回值类型 方法全类名(参数列表))
    //* com.xha.annotation.service.impl.CalculatorImpl.add(..)表示不限制访问权限、返回值类型,
    // 切入CalculatorImpl类的add方法,参数列表不限制
    @Before(value = "execution(* com.xha.annotation.service.impl.CalculatorImpl.add(..))")
    public void before() {
        System.out.println("前置通知");
    }

    public void after() {
        System.out.println("后置通知");
    }

    public void afterThrowing() {
        System.out.println("异常通知");
    }

    public void afterReturning() {
        System.out.println("返回通知");
    }

    public void around() {
        System.out.println("环绕通知");
    }
}
  1. 创建配置类进行配置
  • 开启包扫描
  • 开启aspectj自动代理,为目标对象生成代理
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
//开启组件扫描
@ComponentScan("com.xha.annotation")
//开启AOP的自动代理
@EnableAspectJAutoProxy
public class SpringConfig {
}
  1. 执行测试
import com.xha.calculator.Calculator;
import com.xha.calculator.impl.CalculatorImpl;
import com.xha.proxy.ProxyFactory;

public class ProxyDemo {
    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl());
        Calculator proxy = (Calculator) proxyFactory.getProxy();
        int add = proxy.add(1, 2);
    }
}

执行结果:

image-20230725225039094

5.5基于XML的AOP

5.5.1准备工作

参考基于注解的AOP环境

5.5.2实现

<context:component-scan base-package="com.atguigu.aop.xml"></context:component-scan>

<aop:config>
    <!--配置切面类-->
    <aop:aspect ref="loggerAspect">
        <aop:pointcut id="pointCut" 
                   expression="execution(* com.atguigu.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

在之前的测试方法中,几乎都能看到以下的两行代码:

ApplicationContext context = new ClassPathXmlApplicationContext("xxx.xml");
Xxxx xxx = context.getBean(Xxxx.class);

这两行代码的作用是创建Spring容器,最终获取到对象,但是每次测试都需要重复编写。针对上述问题,我们需要的是程序能自动帮我们创建容器。我们都知道JUnit无法知晓我们是否使用了 Spring 框架,更不用说帮我们创建 Spring 容器了。Spring提供了一个运行器,可以读取配置文件(或注解)来创建容器。我们只需要告诉它配置文件位置就可以了。这样一来,我们通过Spring整合JUnit可以使程序创建spring容器了

6.1整合JUnit5

6.1.1搭建子模块

搭建spring-junit模块

6.1.2引入依赖

<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>

6.1.3添加配置文件

beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="com.atguigu.spring6.bean"/>
</beans>

copy日志文件:log4j2.xml

6.1.4添加java类

package com.atguigu.spring6.bean;

import org.springframework.stereotype.Component;

@Component
public class User {

    public User() {
        System.out.println("run user");
    }
}

6.1.5测试

import com.atguigu.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);
    }
}

6.2整合JUnit4

JUnit4在公司也会经常用到,在此也学习一下

6.2.1添加依赖

<!-- junit测试 -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

6.2.2测试

import com.atguigu.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);
    }
}

7.数据绑定

7.1JdbcTemplate

7.1.1简介

Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作

7.1.2准备工作

  1. 搭建子模块

搭建子模块:spring-jdbc-tx

  1. 加入依赖
<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>
  1. 创建jdbc.properties
jdbc.user=root
jdbc.password=root
jdbc.url=jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false
jdbc.driver=com.mysql.cj.jdbc.Driver
  1. 配置Spring的配置文件

beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.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>
  1. 准备数据库与测试表
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

创建测试类,整合JUnit,注入JdbcTemplate

package com.atguigu.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测试增删改功能
@Test
//测试增删改功能
public void testUpdate(){
    //添加功能
	String sql = "insert into t_emp values(null,?,?,?)";
	int result = jdbcTemplate.update(sql, "张三", 23, "男");
    
    //修改功能
	//String sql = "update t_emp set name=? where id=?";
    //int result = jdbcTemplate.update(sql, "张三atguigu", 1);

    //删除功能
	//String sql = "delete from t_emp where id=?";
	//int result = jdbcTemplate.update(sql, 1);
}
7.1.3.2查询数据返回对象
public class Emp {

    private Integer id;
    private String name;
    private Integer age;
    private String sex;

    //生成get和set方法
    //......

    @Override
    public String toString() {
        return "Emp{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                '}';
    }
}
//查询:返回对象
@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 sql = "select * from t_emp where id=?";
    Emp emp = jdbcTemplate.queryForObject(sql,
                  new BeanPropertyRowMapper<>(Emp.class),1);
    System.out.println(emp);
}
7.1.3.2查询数据返回list集合
@Test
//查询多条数据为一个list集合
public void testSelectList(){
    String sql = "select * from t_emp";
    List<Emp> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Emp.class));
    System.out.println(list);
}
7.1.3.2查询返回单个的值
@Test
//查询单行单列的值
public void selectCount(){
    String sql = "select count(id) from t_emp";
    Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
    System.out.println(count);
}

8.事务

8.1事务基本概念

数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。

8.2事务的ACID特性

A:原子性(Atomicity)

一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

C:一致性(Consistency)

事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。

如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。

如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

I:隔离性(Isolation)

指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

D:持久性(Durability)

指的是只要事务成功结束,它对数据库所做的更新就必须保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

8.3编程式事务和声明式事务

编程式事务和声明式事务是两种不同的事务管理方式。

  • 编程式事务是指在代码中显式地管理事务。这意味着您需要在代码中手动启动、提交和回滚事务。
  • 声明式事务是指通过声明来管理事务,而不是在代码中显式地管理事务。这意味着您只需要在类或方法上使用注解(如@Transactional)来声明事务管理,而不需要在代码中手动启动、提交和回滚事务。

8.3@Transactional注解介绍

@Transactional是Spring框架中的一个注解,它用于声明事务管理。当您在类或方法上使用@Transactional注解时,Spring将自动为该类或方法创建一个代理,以便在调用该类或方法时自动启动和提交事务。

@Transactional注解可以用在类或方法上。

当在类上使用@Transactional注解时,该类中的所有公共方法都将被声明为事务方法。当在方法上使用@Transactional注解时,只有该方法被声明为事务方法。

8.4声明式事务的属性

在Spring框架中,使用@Transactional注解声明的事务具有以下几个属性:

  1. 传播行为(Propagation):定义了事务的传播方式,即在多个事务嵌套调用时,如何管理事务。例如,PROPAGATION_REQUIRED表示如果当前存在事务,则加入该事务;如果当前不存在事务,则创建一个新事务。
  2. 隔离级别(Isolation):定义了事务的隔离级别,即多个事务并发执行时,如何隔离它们之间的数据访问。例如,ISOLATION_READ_COMMITTED表示一个事务只能读取另一个已提交的事务所修改的数据。
  3. 超时时间(Timeout):定义了事务的超时时间,即事务在指定时间内未完成,则自动回滚。
  4. 只读标志(Read-only):定义了事务是否为只读事务。只读事务不会修改数据,因此可以进行一些优化。
  5. 回滚规则(Rollback rules):定义了哪些异常会导致事务回滚。默认情况下,所有运行时异常都会导致事务回滚。

您可以在使用@Transactional注解时指定这些属性的值。例如,下面是一个简单的例子,它指定了传播行为、隔离级别和超时时间:

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, timeout = 30)
public void createUser(String username, String password) {
    // 在这里执行数据库操作
}

在这个例子中,我们指定了传播行为为Propagation.REQUIRED,隔离级别为Isolation.READ_COMMITTED,超时时间为30秒。

8.4.1传播行为

  1. 介绍

事务的传播行为定义了在多个事务嵌套调用时,如何管理事务。font>在Spring框架中,您可以使用@Transactional注解的propagation属性来指定事务的传播行为。

如在service类中有a()方法和b()方法,a()方法上有事务,b()方法上也有事务,当a()方法执行过程中调用了b()方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。

一共有七种传播行为:

  • PROPAGATION_REQUIRED(required):如果当前存在事务,则加入该事务;如果当前不存在事务,则创建一个新事务。这是默认的传播行为。(存在加入,不存在新建
  • PROPAGATION_REQUIRES_NEW(requires_new):创建一个新事务,并暂停当前事务(如果存在)。
  • PROPAGATION_SUPPORTS(supports):如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务方式执行。(存在加入,不存在非事务执行
  • PROPAGATION_NOT_SUPPORTED(not_supported):以非事务方式执行,并暂停当前事务(如果存在)。
  • PROPAGATION_MANDATORY(mandatory):如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
  • PROPAGATION_NEVER(never):以非事务方式执行,如果当前存在事务,则抛出异常。
  • PROPAGATION_NESTED(nested):如果当前存在事务,则在嵌套事务中执行;如果当前不存在事务,新建事务。
  1. 测试

创建接口CheckoutService:

package com.atguigu.spring6.service;

public interface CheckoutService {
    void checkout(Integer[] bookIds, Integer userId);
}

创建实现类CheckoutServiceImpl:

package com.atguigu.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);
        }
    }
}

在BookController中添加方法:

@Autowired
private CheckoutService checkoutService;

public void checkout(Integer[] bookIds, Integer userId){
    checkoutService.checkout(bookIds, userId);
}

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

  1. 观察结果

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

修改BookServiceImplbuyBook()上,注解@Transactional的propagation属性

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

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

8.4.2隔离级别

  1. 介绍

数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。font>

隔离级别一共有四种:

  • 读未提交:READ UNCOMMITTED

    允许Transaction01读取Transaction02未提交的修改。

  • 读已提交:READ COMMITTED、

    要求Transaction01只能读取Transaction02已提交的修改。

  • 可重复读:REPEATABLE READ

    确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。

  • 串行化:SERIALIZABLE

    确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。

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

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

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

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

②使用方式

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

8.4.3超时时间

  1. 介绍

事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。超时回滚,释放资源。

默认为-1,timeout = -1,表示永不超时。

  1. 使用方式
//超时时间单位秒
@Transactional(timeout = 3)
public void buyBook(Integer bookId, Integer userId) {
    try {
        TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    //更新图书的库存
    bookDao.updateStock(bookId);
    //更新用户的余额
    bookDao.updateBalance(userId, price);
    //System.out.println(1/0);
}
  1. 观察结果

执行过程中抛出异常:

org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Fri Jun 04 16:25:39 CST 2022

8.4.4只读标志

  1. 介绍

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

  1. 使用方式
@Transactional(readOnly = true)
public void buyBook(Integer bookId, Integer userId) {
    //查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    //更新图书的库存
    bookDao.updateStock(bookId);
    //更新用户的余额
    bookDao.updateBalance(userId, price);
    //System.out.println(1/0);
}
  1. 注意

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

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

8.4.5回滚策略

  1. 介绍

声明式事务默认只针对运行时异常回滚,编译时异常不回滚。font>

可以通过@Transactional中相关属性设置回滚策略

  • rollbackFor属性:需要设置一个Class类型的对象

  • rollbackForClassName属性:需要设置一个字符串类型的全类名

  • noRollbackFor属性:需要设置一个Class类型的对象

  • rollbackFor属性:需要设置一个字符串类型的全类名

  1. 使用方式
@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);
}
  1. 观察结果

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

8.6基于注解的声明式事务

8.6.1准备工作

  1. 添加配置

在beans.xml添加配置

<!--扫描组件-->
<context:component-scan base-package="com.atguigu.spring6"></context:component-scan>
  1. 创建表
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);
  1. 创建组件

创建BookController:

package com.atguigu.spring6.controller;

@Controller
public class BookController {

    @Autowired
    private BookService bookService;

    public void buyBook(Integer bookId, Integer userId){
        bookService.buyBook(bookId, userId);
    }
}

创建接口BookService:

package com.atguigu.spring6.service;
public interface BookService {
    void buyBook(Integer bookId, Integer userId);
}

创建实现类BookServiceImpl:

package com.atguigu.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);
    }
}

创建接口BookDao:

package com.atguigu.spring6.dao;
public interface BookDao {
    Integer getPriceByBookId(Integer bookId);

    void updateStock(Integer bookId);

    void updateBalance(Integer userId, Integer price);
}

创建实现类BookDaoImpl:

package com.atguigu.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);
    }
}

8.6.2测试无事务情况

  1. 创建测试类
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);
    }

}
  1. 模拟场景

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

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

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

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

此时执行sql语句会抛出SQLException

  1. 观察结果

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

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

8.6.3加入事务

1. 添加事务配置

在spring配置文件中引入tx命名空间

<?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">

在Spring的配置文件中添加配置:

<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" />
2. 添加事务注解

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

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

3. 观察结果

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

8.6.4全注解配置事务

  1. 添加配置类
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.atguigu.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;
    }
}
  1. 测试
import com.atguigu.spring6.config.SpringConfig;
import com.atguigu.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);
    }
}

8.7基于XML的声明式事务

8.7.1场景模拟

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

8.7.2修改Spring配置文件

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

<aop:config>
    <!-- 配置事务通知和切入点表达式 -->
    <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.atguigu.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>

注意:基于xml实现的声明式事务,必须引入aspectJ的依赖

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>6.0.2</version>
</dependency>

9.资源操作:Resources

9.1Spring Resources概述

Java的标准java.net.URL类和各种URL前缀的标准处理程序无法满足所有对low-level资源的访问,比如:没有标准化的 URL 实现可用于访问需要从类路径或相对于 ServletContext 获取的资源。并且缺少某些Spring所需要的功能,例如检测某资源是否存在等。而Spring的Resource声明了访问low-level资源的能力。

9.2Resource接口

Spring 的 Resource 接口位于 org.springframework.core.io 中。 旨在成为一个更强大的接口,用于抽象对低级资源的访问。以下显示了Resource接口定义的方法

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所没有的方法。InputStreamSource接口,只有一个方法:

public interface InputStreamSource {

    InputStream getInputStream() throws IOException;

}

其中一些重要的方法:

  • getInputStream(): 找到并打开资源,返回一个InputStream以从资源中读取。预计每次调用都会返回一个新的InputStream(),调用者有责任关闭每个流
  • exists(): 返回一个布尔值,表明某个资源是否以物理形式存在
  • isOpen: 返回一个布尔值,指示此资源是否具有开放流的句柄。如果为true,InputStream就不能够多次读取,只能够读取一次并且及时关闭以避免内存泄漏。对于所有常规资源实现,返回false,但是InputStreamResource除外。
  • getDescription(): 返回资源的描述,用来输出错误的日志。这通常是完全限定的文件名或资源的实际URL。

其他方法:

  • isReadable(): 表明资源的目录读取是否通过getInputStream()进行读取。
  • isFile(): 表明这个资源是否代表了一个文件系统的文件。
  • getURL(): 返回一个URL句柄,如果资源不能够被解析为URL,将抛出IOException
  • getURI(): 返回一个资源的URI句柄
  • getFile(): 返回某个文件,如果资源不能够被解析称为绝对路径,将会抛出FileNotFoundException
  • lastModified(): 资源最后一次修改的时间戳
  • createRelative(): 创建此资源的相关资源
  • getFilename(): 资源的文件名是什么 例如:最后一部分的文件名 myfile.txt

9.3Resource的实现类

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

9.3.1UrlResource访问网络资源

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

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

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

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

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

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

image-20221207102315185

package com.atguigu.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");
    }
}

实验二:在项目根路径下创建文件,从文件系统中读取资源

方法不变,修改调用传递路径

public static void main(String[] args) {
    //1 访问网络资源
	//loadAndReadUrlResource("http://www.atguigu.com");
    
    //2 访问文件系统资源
    loadAndReadUrlResource("file:atguigu.txt");
}

9.3.2ClassPathResource 访问类路径下资源

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

实验:在类路径下创建文件atguigu.txt,使用ClassPathResource 访问

image-20221207103020854

package com.atguigu.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("atguigu.txt");
    }
}

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

9.3.3FileSystemResource 访问文件系统资源

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

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

package com.atguigu.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("atguigu.txt");
        //绝对路径
        //FileSystemResource resource = new FileSystemResource("C:\\atguigu.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("atguigu.txt");
    }
}

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

9.3.4ServletContextResource

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

9.3.5InputStreamResource

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

9.3.6ByteArrayResource

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

9.4Resource类图

上述Resource实现类与Resource顶级接口之间的关系可以用下面的UML关系模型来表示

image-20221206232920494

9.5ResourceLoader 接口

9.5.1ResourceLoader 概述

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

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

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

在ResourceLoader接口里有如下方法:

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

9.5.2使用演示

实验一:ClassPathXmlApplicationContext获取Resource实例

package com.atguigu.spring6.resouceloader;

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

public class Demo1 {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext();
//        通过ApplicationContext访问资源
//        ApplicationContext实例获取Resource实例时,
//        默认采用与ApplicationContext相同的资源访问策略
        Resource res = ctx.getResource("atguigu.txt");
        System.out.println(res.getFilename());
    }
}

实验二:FileSystemApplicationContext获取Resource实例

package com.atguigu.spring6.resouceloader;

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

public class Demo2 {

    public static void main(String[] args) {
        ApplicationContext ctx = new FileSystemXmlApplicationContext();
        Resource res = ctx.getResource("atguigu.txt");
        System.out.println(res.getFilename());
    }
}

9.5.3ResourceLoader 总结

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

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

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

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

9.6ResourceLoaderAware 接口

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

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

实验:演示ResourceLoaderAware使用

第一步 创建类,实现ResourceLoaderAware接口

package com.atguigu.spring6.resouceloader;

import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ResourceLoader;

public class TestBean implements ResourceLoaderAware {

    private ResourceLoader resourceLoader;

    //实现ResourceLoaderAware接口必须实现的方法
	//如果把该Bean部署在Spring容器中,该方法将会有Spring容器负责调用。
	//SPring容器调用该方法时,Spring会将自身作为参数传给该方法。
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    //返回ResourceLoader对象的应用
    public ResourceLoader getResourceLoader(){
        return this.resourceLoader;
    }

}

第二步 创建bean.xml文件,配置TestBean

<?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.atguigu.spring6.resouceloader.TestBean"></bean>
</beans>

第三步 测试

package com.atguigu.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 Demo3 {

    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("atguigu.txt");
        System.out.println(resource.getFilename());
        System.out.println(resource.getDescription());
    }
}

9.7使用Resource 作为属性

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

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

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

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

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

第一步 创建依赖注入类,定义属性和方法

package com.atguigu.spring6.resouceloader;

import org.springframework.core.io.Resource;

public class ResourceBean {
    
    private Resource res;
    
    public void setRes(Resource res) {
        this.res = res;
    }
    public Resource getRes() {
        return res;
    }
    
    public void parse(){
        System.out.println(res.getFilename());
        System.out.println(res.getDescription());
    }
}

第二步 创建spring配置文件,配置依赖注入

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="resourceBean" class="com.atguigu.spring6.resouceloader.ResourceBean" >
      <!-- 可以使用file:、http:、ftp:等前缀强制Spring采用对应的资源访问策略 -->
      <!-- 如果不采用任何前缀,则Spring将采用与该ApplicationContext相同的资源访问策略来访问资源 -->
        <property name="res" value="classpath:atguigu.txt"/>
    </bean>
</beans>

第三步 测试

package com.atguigu.spring6.resouceloader;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Demo4 {

    public static void main(String[] args) {
        ApplicationContext ctx =
                new ClassPathXmlApplicationContext("bean.xml");
        ResourceBean resourceBean = ctx.getBean("resourceBean",ResourceBean.class);
        resourceBean.parse();
    }
}

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

9.8.1概述

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

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

(1)使用ApplicationContext实现类指定访问策略。

(2)使用前缀指定访问策略。

9.8.2ApplicationContext实现类指定访问策略

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

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

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

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

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

效果前面已经演示

9.8.3使用前缀指定访问策略

实验一:classpath前缀使用

package com.atguigu.spring6.context;

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

public class Demo1 {

    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("atguigu.txt");
        System.out.println(resource.getFilename());
        System.out.println(resource.getDescription());
    }
}

实验二:classpath通配符使用

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

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

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

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

注意 :

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

使用三:通配符其他使用

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

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

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

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

10.国际化:i18n

10.1i18n概述

国际化也称作i18n,其来源是英文单词 internationalization的首末字符i和n,18为中间的字符数。由于软件发行可能面向多个国家,对于不同国家的用户,软件显示不同语言的过程就是国际化。通常来讲,软件中的国际化是通过配置文件来实现的,假设要支撑两种语言,那么就需要两个版本的配置文件。

10.2Java国际化

  1. Java自身是支持国际化的,java.util.Locale用于指定当前用户所属的语言环境等信息,java.util.ResourceBundle用于查找绑定对应的资源文件。Locale包含了language信息和country信息,Locale创建默认locale对象时使用的静态方法:
/**
 * 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);
}
  1. 配置文件命名规则

    basename_language_country.properties

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

  2. 演示Java国际化

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

image-20230726175755367

​ 文件内容:

​ messages_en_GB.properties

country=China

​ messages_zh_CN.properties

country=中国
  1. 测试
import java.util.Locale;
import java.util.ResourceBundle;

public class JavaI18n {
    public static void main(String[] args) {
        ResourceBundle bundle1 = ResourceBundle.getBundle("messages", new Locale("zh", "CN"));
        String country1 = bundle1.getString("country");
        System.out.println(country1);
        ResourceBundle bundle2 = ResourceBundle.getBundle("messages", new Locale("en", "GB"));
        String country2 = bundle2.getString("country");
        System.out.println(country2);
    }
}

10.3Spring6国际化

10.3.1MessageSource接口

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

常见实现类

  • ResourceBundleMessageSource

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

  • ReloadableResourceBundleMessageSource

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

  • StaticMessageSource

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

10.3.2使用Spring6国际化

  1. 创建资源文件

    国际化文件命名格式:基本名称 _ 语言 _ 国家.properties。{0},{1}这样内容,就是动态参数。

image-20230726175755367

  1. 创建atguigu_en_US.properties
tip=Welcome {0},now time is {1}
  1. 创建atguigu_zh_CN.properties
tip=欢迎您{0},当前时间为{1}
  1. 配置MessageSource

  2. 通过配置类

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;

@Configuration
public class SpringConfig {

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages");
        messageSource.setDefaultEncoding("UTF8");
        return messageSource;
    }
}
  1. 通过配置文件
<?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>atguigu</value>
            </list>
        </property>
        <property name="defaultEncoding">
            <value>utf-8</value>
        </property>
    </bean>
</beans
  1. 创建测试类
import com.xha.config.SpringConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.Date;
import java.util.Locale;

public class JavaI18n {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        Object[] objects = {"zhagnsan", new Date().toString()};
//        使用context.getMessage()方法获取国际化信息
        String message1 = context.getMessage("tip", objects, Locale.UK);
        String message2 = context.getMessage("tip", objects, Locale.CHINA);
        System.out.println(message1);
        System.out.println(message2);
    }
}

image-20230726181254320

11.数据校验:Validation

11.1Spring Validation概述

在开发中,我们经常遇到参数校验的需求,比如用户注册的时候,要校验用户名不能为空、用户名长度不超过20个字符、手机号是合法的手机号格式等等。如果使用普通方式,我们会把校验的代码和真正的业务处理逻辑耦合在一起,而且如果未来要新增一种校验逻辑也需要在修改多个地方。而spring validation允许通过注解的方式来定义对象校验规则,把校验和业务逻辑分离开,让代码编写更加方便。Spring Validation其实就是对Hibernate Validator进一步的封装,方便在Spring中使用。

在Spring中有多种校验的方式

第一种是通过实现org.springframework.validation.Validator接口,然后在代码中调用这个类

第二种是按照Bean Validation方式来进行校验,即通过注解的方式。

第三种是基于方法实现校验

除此之外,还可以实现自定义校验

11.2通过Validator接口实现

  1. 引入相关依赖
<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>
  1. 创建实体类,定义属性和方法
package com.atguigu.spring6.validation.method1;

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;
    }
}
  1. 创建类实现Validator接口,实现接口方法指定校验规则
package com.atguigu.spring6.validation.method1;

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 object, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "name", "name.empty");
        Person p = (Person) object;
        if (p.getAge() < 0) {
            errors.rejectValue("age", "error value < 0");
        } else if (p.getAge() > 110) {
            errors.rejectValue("age", "error value too old");
        }
    }
}

上面定义的类,其实就是实现接口中对应的方法,

supports方法用来表示此校验用在哪个类型上,

validate是设置校验逻辑的地点,其中ValidationUtils,是Spring封装的校验工具类,帮助快速实现校验。

  1. 使用上述Validator进行测试
package com.atguigu.spring6.validation.method1;

import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;

public class TestMethod1 {

    public static void main(String[] args) {
        //创建person对象
        Person person = new Person();
        person.setName("lucy");
        person.setAge(-1);
        
        // 创建Person对应的DataBinder
        DataBinder binder = new DataBinder(person);

        // 设置校验
        binder.setValidator(new PersonValidator());

        // 由于Person对象中的属性为空,所以校验不通过
        binder.validate();

        //输出结果
        BindingResult results = binder.getBindingResult();
        System.out.println(results.getAllErrors());
    }
}

11.3Bean Validation注解实现

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

  1. 创建配置类,配置LocalValidatorFactoryBean
@Configuration
@ComponentScan("com.atguigu.spring6.validation.method2")
public class ValidationConfig {

    @Bean
    public LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean();
    }
}
  1. 创建实体类,使用注解定义校验规则
package com.atguigu.spring6.validation.method2;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;

public class User {

    @NotNull
    private String name;

    @Min(0)
    @Max(120)
    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;
    }
}
  1. 常用注解说明
  • @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格式
  1. 使用两种不同的校验器实现

(1)使用jakarta.validation.Validator校验

package com.atguigu.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();
    }

}

(2)使用org.springframework.validation.Validator校验

package com.atguigu.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();
    }
}
  1. 测试
package com.atguigu.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);
    }
}

11.4基于方法实现校验

  1. 创建配置类,配置MethodValidationPostProcessor
package com.atguigu.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.atguigu.spring6.validation.method3")
public class ValidationConfig {

    @Bean
    public MethodValidationPostProcessor validationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}
  1. 创建实体类,使用注解设置校验规则
package com.atguigu.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;
    }
}
  1. 定义Service类,通过注解操作对象
package com.atguigu.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. 测试
package com.atguigu.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);
    }
}

11.5实现自定义校验

  1. 自定义校验注解
package com.atguigu.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();
    }
}
  1. 编写真正的校验类
package com.atguigu.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;
        }
}

12.提前编译:AOT

12.1AOT概述

12.1.1JIT与AOT的区别

JIT和AOT 这个名词是指两种不同的编译方式,这两种编译方式的主要区别在于是否在“运行时”进行编译

(1)JIT, Just-in-time,动态(即时)编译,边运行边编译;

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

(2)AOT,Ahead Of Time,指运行前编译,预先编译。

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

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

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

image-20221207113544080

(3)AOT的优点

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

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

AOT的缺点

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

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

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

12.1.2Graalvm

Spring6 支持的 AOT 技术,这个 GraalVM 就是底层的支持,Spring 也对 GraalVM 本机映像提供了一流的支持。GraalVM 是一种高性能 JDK,旨在加速用 Java 和其他 JVM 语言编写的应用程序的执行,同时还为 JavaScript、Python 和许多其他流行语言提供运行时。 GraalVM 提供两种运行 Java 应用程序的方法:在 HotSpot JVM 上使用 Graal 即时 (JIT) 编译器或作为提前 (AOT) 编译的本机可执行文件。 GraalVM 的多语言能力使得在单个应用程序中混合多种编程语言成为可能,同时消除了外语调用成本。GraalVM 向 HotSpot Java 虚拟机添加了一个用 Java 编写的高级即时 (JIT) 优化编译器。

GraalVM 具有以下特性:

(1)一种高级优化编译器,它生成更快、更精简的代码,需要更少的计算资源

(2)AOT 本机图像编译提前将 Java 应用程序编译为本机二进制文件,立即启动,无需预热即可实现最高性能

(3)Polyglot 编程在单个应用程序中利用流行语言的最佳功能和库,无需额外开销

(4)高级工具在 Java 和多种语言中调试、监视、分析和优化资源消耗

总的来说对云原生的要求不算高短期内可以继续使用 2.7.X 的版本和 JDK8,不过 Spring 官方已经对 Spring6 进行了正式版发布。

12.1.3Native Image

目前业界除了这种在JVM中进行AOT的方案,还有另外一种实现Java AOT的思路,那就是直接摒弃JVM,和C/C++一样通过编译器直接将代码编译成机器代码,然后运行。这无疑是一种直接颠覆Java语言设计的思路,那就是GraalVM Native Image。它通过C语言实现了一个超微缩的运行时组件 —— Substrate VM,基本实现了JVM的各种特性,但足够轻量、可以被轻松内嵌,这就让Java语言和工程摆脱JVM的限制,能够真正意义上实现和C/C++一样的AOT编译。这一方案在经过长时间的优化和积累后,已经拥有非常不错的效果,基本上成为Oracle官方首推的Java AOT解决方案。
Native Image 是一项创新技术,可将 Java 代码编译成独立的本机可执行文件或本机共享库。在构建本机可执行文件期间处理的 Java 字节码包括所有应用程序类、依赖项、第三方依赖库和任何所需的 JDK 类。生成的自包含本机可执行文件特定于不需要 JVM 的每个单独的操作系统和机器体系结构。


Spring6.0
https://xhablog.online/2022/06/15/Spring6/
作者
Xu huaiang
发布于
2022年6月15日
许可协议