SSM框架学习(二:SpringFramework实战指南)

目录

一、SpringFramework介绍

1.总体技术体系

(1)单一架构

(2) 分布式架构

2.Spring 和 SpringFramework概念

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

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

二、Spring IoC容器和核心概念 

1.组件和组件管理概念

2.Spring IoC容器和容器实现 

(1)普通容器和复杂容器

(2)Spring IoC容器介绍

(3)Spring IoC容器具体接口和实现类

(4)Spring IoC容器管理配置方式 

3.Spring IoC / DI概念总结 

(1)IoC(Inversion of Control)控制反转

 (2)DI (Dependency Injection) 依赖注入

三、Spring IoC 实践和应用 

1.Spring IoC / DI 实现步骤 

 2.基于XML配置方式组件管理

(1)实验一: 组件(Bean)信息声明配置(IoC) 

Ⅰ.基于无参数构造函数

 Ⅱ.基于静态工厂方法实例化

Ⅲ.基于非静态(实例)工厂方法实例化

(2)实验二:组件(Bean)依赖注入配置(DI) 

 Ⅰ.基于构造函数的依赖注入(单个构造参数)

Ⅱ. 基于构造函数的依赖注入(多构造参数解析)

Ⅲ.基于Setter方法依赖注入(重点)

(3)实验三: IoC容器创建和使用

Ⅰ.容器实例化

 Ⅱ.Bean对象读取

(4)实验四:高级特性:组件(Bean)作用域和周期方法配置

Ⅰ.组件周期方法配置

Ⅱ.作用域配置

(5)实验五: 高级特性:FactoryBean特性和使用 

(6) 实验六: 基于XML方式整合三层架构组

3.基于注解方式管理 Bean

(1)实验一: Bean注解标记和扫描 (IoC)

(2)实验二:组件(Bean)作用域和周期方法注解

Ⅰ. 组件周期方法配置

Ⅱ. 组件作用域配置

(3)实验三: Bean属性赋值:引用类型自动装配 (DI)

 (4)实验四: Bean属性赋值:基本类型属性赋值 (DI)

(5)实验五: 基于注解+XML方式整合三层架构组件

4.基于 配置类 方式管理 Bean 

(1)实验一:配置类和扫描注解 

 (2)实验二:@Bean定义组件

(3)实验三:高级特性:@Bean注解细节

Ⅰ. @Bean生成BeanId问题

Ⅱ. @Bean 周期方法指定

Ⅲ. @Bean Scope作用域

Ⅳ. @Bean 引用其他组件

(4)实验四:高级特性:@Import扩展

(5)实验五:基于注解+配置类方式整合三层架构组件

5.整合Spring5-Test5搭建测试环境

四、Spring Aop 面向切面编程

1.引言

 2.解决技术代理模式

(1)静态代理

(2)动态代理

3.面向切面编程思维(AOP) 

(1)概念

(2)AOP术语名词介绍

  4.Spring AOP框架介绍和关系梳理

5.Spring AOP基于注解方式实现和细节

(1)Spring AOP底层技术组成

(2)初步实现

(3)获取通知细节信息

Ⅰ. JointPoint接口

Ⅱ. 方法返回值 

Ⅲ. 异常对象捕捉

(4)切点表达式语法

(5)切点表达式的提取和复用

(6)环绕通知

(7)切面优先级设置

 6.Spring AOP基于XML方式实现(了解)

五、Spring 声明式事务

1.声明式事务概念

(1)编程式事务

(2)声明式事务

 (3)Spring事务管理器

2.基于注解的声明式事务

(1)准备工作

(2)基本事务控制

(3)事务属性

Ⅰ.只读

 Ⅱ.超时时间

Ⅲ. 事务异常

Ⅳ. 事务隔离级别 

Ⅴ. 事务传播行为


一、SpringFramework介绍

1.总体技术体系

(1)单一架构

一个项目,一个工程,导出为一个war包,在一个Tomcat上运行,即 all in one。

单一架构的项目主要应用技术框架为:Spring , SpringMVC , Mybatis 

(2) 分布式架构

一个项目(对应 IDEA 中的一个 project),拆分成很多个模块(module)

每一个工程都是运行在自己的 Tomcat 上。模块之间可以互相调用。

每一个模块内部可以看成是一个单一架构的应用

分布式架构的项目主要应用技术框架为:SpringBoot (SSM), SpringCloud , 中间件等

2.Spring 和 SpringFramework概念

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

经过十多年的发展,Spring 已经不再是一个单纯的应用框架。

而是逐渐发展成为一个由多个不同子项目(模块)组成的成熟技术,例如:

Spring Framework、Spring MVC、SpringBoot、Spring Cloud、Spring Data、Spring Security 等

其中 Spring Framework 是其他子项目的基础。 

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

Spring Framework(Spring 框架)是一个开源的应用程序框架,由SpringSource 公司开发。

最初是为了解决企业级开发中各种常见问题而创建的。它提供了很多功能。

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

SpringFramework框架结构图:

功能模块功能介绍
Core Container核心容器,在 Spring 环境下使用任何功能都必须基于 IOC 容器。
AOP&Aspects面向切面编程
TX声明式事务管理。
Spring MVC提供了面向Web应用程序的集成功能。

 spring 使创建 Java 企业应用程序变得容易。它提供了在企业环境中采用 Java 语言所需的一切。

注意:从Spring Framework 6.0.6开始,Spring 需要 Java 17+。 

二、Spring IoC容器和核心概念 

1.组件和组件管理概念

常规的三层架构处理请求流程:

而整个项目,其实就是由各种组件搭建而成的。

组件:映射到应用程序中所有可复用Java对象 

所以,组件一定是对象,而对象不一定是组件。


在之前,我们都是手动 new 一个 BookDao 的全局变量, 再调用该对象的方法,过于麻烦。

而现在,组件可以完全交给 Spring 框架进行管理,Spring 框架替代了程序员原有的 new 对象和对象属性赋值动作等。

Spring 充当组件管理角色(IoC),具体的组件管理动作包含:

① 组件对象实例化

② 组件属性属性赋值

③ 组件对象之间引用

④ 组件对象存活周期管理

.......

我们只需要编写元数据(配置文件)告知 Spring 管理哪些类组件和他们的关系即可!

Spring 充当一个组件容器,创建、管理、存储组件,减少了我们的编码压力,让我们更加专注进行业务编写。

优点:

① 降低了组件之间的耦合性:Spring IoC容器通过依赖注入机制,将组件之间的依赖关系削弱,减少了程序组件之间的耦合性,使得组件更加松散地耦合。

② 提高了代码的可重用性和可维护性:将组件的实例化过程、依赖关系的管理等功能交给Spring IoC容器处理,使得组件代码更加模块化、可重用、更易于维护。

③ 方便了配置和管理:Spring IoC容器通过XML文件或者注解,轻松的对组件进行配置和管理,使得组件的切换、替换等操作更加的方便和快捷。

④ 交给Spring管理的对象(组件),方可享受Spring框架的其他功能(AOP,声明事务管理)等

2.Spring IoC容器和容器实现 

(1)普通容器和复杂容器

普通容器:只用来存储,没有更多功能。

程序中的普通容器,如:数组(Array),集合(List、Set、Map)

复杂容器:不仅能够存储数据(对象),还能够管理数据(调用对象的方法)

程序中的复杂容器,入:Servlet 容器 --> 能够管理 Servlet(init,service,destroy)、Filter、Listener 这样的组件的一生,如下:

名称时机次数
创建对象(无参构造)默认情况:接收到第一次请求 修改启动顺序后:Web应用启动过程中一次
初始化操作(init)创建对象之后一次
处理请求(service)接收到请求多次
销毁操作(destory)Web应用卸载之前一次

SpringIoC 容器也是一个复杂容器,不仅要负责创建组件的对象、存储组件的对象,还要负责调用组件的方法让它们工作,最终在特定情况下销毁组件

(2)Spring IoC容器介绍

Spring IoC 容器:负责实例化、配置和组装 bean(组件)。

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

配置元数据(配置文件)的表现形式:XML、Java 注解或 Java 代码形式

它允许表达组成应用程序的组件以及这些组件之间丰富的相互依赖关系。

 应用程序类与配置元数据相结合,才拥有完全配置且可执行的系统或应用程序。

(3)Spring IoC容器具体接口和实现类

Spring Ioc容器接口:

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

ApplicationContext 接口:是 BeanFactory 的子接口。它扩展了以下功能:

① 更容易与 Spring 的 AOP 功能集成

② 消息资源处理(用于国际化)

③ 特定于应用程序给予此接口实现,例如Web 应用程序的 WebApplicationContext


 ApplicationContext容器常用实现类:

类型名简介
ClassPathXmlApplicationContext通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象
FileSystemXmlApplicationContext通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象
AnnotationConfigApplicationContext通过读取Java配置类创建 IOC 容器对象
WebApplicationContext专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。

(4)Spring IoC容器管理配置方式 

Spring框架提供了多种配置方式:

① XML配置方式:是Spring框架最早的配置方式之一,通过在XML文件中定义Bean及其依赖关系、Bean的作用域等信息,让Spring IoC容器来管理Bean之间的依赖关系。该方式从Spring框架的第一版开始提供支持。

② 注解方式:从Spring 2.5版本开始提供支持,可以通过在Bean类上使用注解来代替XML配置文件中的配置信息。通过在Bean类上加上相应的注解(如@Component, @Service, @Autowired等),将Bean注册到Spring IoC容器中,这样Spring IoC容器就可以管理这些Bean之间的依赖关系。

③ Java配置类方式:从Spring 3.0版本开始提供支持,通过Java类来定义Bean、Bean之间的依赖关系和配置信息,从而代替XML配置文件的方式。Java配置类是一种使用Java编写配置信息的方式,通过@Configuration、@Bean等注解来实现Bean和依赖关系的配置。

XML方式由于配置时标签过多,解析复杂,已经逐渐被淘汰。当前主流的配置方式:注解+配置类

3.Spring IoC / DI概念总结 

spring 核心容器(也称 IoC 容器)有两个主要功能:IOC 和 DI

(1)IoC(Inversion of Control)控制反转

IoC 主要是针对对象的创建和调用控制而言的,也就是说:

当应用程序需要使用一个对象时,不再是应用程序(程序员)直接创建该对象,而是由 IoC 容器来创建和管理,即控制权由应用程序转移到 IoC 容器中,也就是 “反转” 了控制权

这种方式基本上是通过依赖查找的方式来实现的,即 IoC 容器维护着构成应用程序的对象,并负责创建这些对象。

 (2)DI (Dependency Injection) 依赖注入

DI 是指在组件之间传递依赖关系的过程中,将依赖关系在容器内部进行处理,这样就不必在应用程序代码中硬编码对象之间的依赖关系,实现了对象之间的解耦合

在 Spring 中,DI 是通过 XML 配置文件或注解的方式实现的。它提供了三种形式的依赖注入:构造函数注入、Setter 方法注入和接口注入。

注意:IoC 和 DI 都是在spring IoC 容器内部发生的,我们只需要写配置文件即可。 

三、Spring IoC 实践和应用 

1.Spring IoC / DI 实现步骤 

组件交给Spring IoC容器管理,并且获取和使用的基本步骤分 3 步:

① 配置元数据(配置)

配置元数据,既是编写交给SpringIoC容器管理组件的信息,配置方式有三种。

其中,基于 XML 的配置元数据的基本结构为:

<bean id="..." [1] class="..." [2]>

        <!-- collaborators and configuration for this bean go here -->

</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
    https://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="..." [1] class="..." [2]>  
    <!-- collaborators and configuration for this bean go here -->
  </bean>

  <bean id="..." class="...">
    <!-- collaborators and configuration for this bean go here -->
  </bean>
  <!-- more bean definitions go here -->
</beans>

 Spring IoC 容器管理一个或多个组件。

这些组件是使用你提供给容器的配置元数据(例如,以 XML <bean/> 定义的形式)创建的。

<bean /> 标签 --> 组件信息声明:

        id 属性:标识单个 Bean 定义的字符串

        class 属性:定义 Bean 的类型并使用完全限定的类名

② 实例化IoC容器

提供给 ApplicationContext 构造函数的位置路径是资源字符串地址,允许容器从各种外部资源(如本地文件系统、Java CLASSPATH 等)加载配置元数据。

我们应该选择一个合适的容器实现类,进行IoC容器的实例化工作:

//实例化ioc容器,读取外部配置文件,最终会在容器内进行ioc和di动作
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

 ③ 获取Bean(组件)

ApplicationContext 是一个高级工厂的接口,能够维护不同 bean 及其依赖项的注册表。

通过使用方法 T getBean(String name, Class<T> requiredType) ,您可以检索 bean 的实例。

允许读取 Bean 定义并访问它们,如以下示例所示:

//创建ioc容器对象,指定配置文件,ioc也开始实例组件对象
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
//获取ioc容器的组件对象
PetStoreService service = context.getBean("petStore", PetStoreService.class);
//使用组件对象
List<String> userList = service.getUsernameList();

 2.基于XML配置方式组件管理

(1)实验一: 组件(Bean)信息声明配置(IoC) 

Question:如何通过定义XML配置文件,声明组件类信息,交给 Spring 的 IoC 容器进行组件管理?

 注意:针对组件(对象)不同的实例化方式,IOC 的配置方式也不同。

① 首先,创建一个 maven 工程,在 pom.xml 中导入spring ioc 相关依赖

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

② 基于不同的实例化方式,来编写不同的配置信息

(有参构造实例化中,不仅涉及 ioc,还会涉及到 DI,所以会放在后面进行介绍)

Ⅰ.基于无参数构造函数

a. 准备组件类

package com.mihoyo.ioc_01;

public class HappyComponent {
    //默认包含无参数构造函数

    public void doWork() {
        System.out.println("HappyComponent.doWork");
    }
}

b. xml配置文件编写

创建携带spring约束的xml配置文件

编写配置文件:

    <!-- 1.可以使用无参构造函数实例化的组件,如何进行ioc配置呢?
        <bean>  一个组件信息[默认是单列模式],也就是一个组件对象
            id  组件的唯一标识,方便后期读取
          class 组件的类的全限定符
    -->
    <!-- 将一个组件类,声明两个组件信息,会实例化两个组件对象   -->
    <bean id="happyComponent1" class="com.mihoyo.ioc_01.HappyComponent"/>
    <bean id="happyComponent2" class="com.mihoyo.ioc_01.HappyComponent"/>

注意:该方式仅要求当前组件类必须包含无参数构造函数即可,即使无参构造私有也无所谓,底层反射机制会自动打破访问修饰符的限制。 

 Ⅱ.基于静态工厂方法实例化

 a. 准备组件类

public class ClientService {
  private static ClientService clientService = new ClientService();
  private ClientService() {}

  public static ClientService createInstance() {
    return clientService;
  }
}

b. xml配置文件编写 

    <!-- 2.静态工厂类如何声明工厂方法进行ioc配置呢?
        <bean>
             id = "组件的唯一标识"
          class = "工厂类的全限定符"
          factory-method = "静态工厂方法"
    -->
    <bean id="clientService" class="com.mihoyo.ioc_01.ClientService" factory-method="createInstance"/>

注意:

class 的值是工厂类的全限定符(工厂类类名不一定和创建对象的类的类名相同)

② factory-method 指定的是静态工厂方法,必须要是 static 方法

Ⅲ.基于非静态(实例)工厂方法实例化

a. 准备组件类

public class DefaultServiceLocator {

    private static ClientServiceImpl clientService = new ClientServiceImpl();

    public ClientServiceImpl createClientServiceInstance() {
        return clientService;
    }
}

class ClientServiceImpl {
}

b. xml配置文件编写 

    <!-- 3.非静态工厂如何声明ioc配置 -->
    <!-- ① 配置工厂类的组件信息 -->
    <bean id="defaultServiceLocator" class="com.mihoyo.ioc_01.DefaultServiceLocator"/>

    <!-- ② 通过工厂对象的工厂方法进行实例化组件对象 -->
    <bean id="clientServiceImpl" factory-bean="defaultServiceLocator" factory-method="createClientServiceInstance"/>

注意:

① class 的值是工厂类的全限定符

factory-bean属性:指定当前容器中非静态工厂 bean 的 id

factory-method: 指定非静态(实例)工厂方法名,实例方法必须是非 static 的!


IoC配置流程: 

(2)实验二:组件(Bean)依赖注入配置(DI) 

Question:通过配置文件,实现 IoC 容器中 Bean 之间的引用(依赖注入DI配置)

  注意:针对组件不同的注入方式,DI 的配置方式也不同。

 Ⅰ.基于构造函数的依赖注入(单个构造参数)

a.准备组件类

public class UserDao {
}


public class UserService {
    
    private UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
}

b.编写配置文件

    <!-- 1.单个构造参数注入 -->
    <!-- ① 进行ioc配置:将引用类和被引用类都存放进ioc容器 -->
    <bean id="userDao" class="com.mihoyo.ioc_02.UserDao"/>

    <bean id="userService" class="com.mihoyo.ioc_02.UserService">
        <!-- ② 进行di配置 -->
        <!--
            <constructor-arg>  构造参数传值的di配置
            value = 直接属性值 -> String name = "zhangsan" / int age = 18
            ref   = 引用其他的bean -> beanId值
        -->
        <constructor-arg ref="userDao"/>
    </bean>

注意:

引用和被引用的组件必须全部在 ioc 容器中,绝不能少!

引用和被引用的组件在代码中的先后顺序可以任意,因为 spring ioc 容器是一个高级容器,内部会有缓存动作:a.先创建对象[ ioc ],b.再进行属性赋值[ di ]。

③ constructor-arg标签:构造函数方式引用的标识,可以指定构造参数和对应的值。

Ⅱ. 基于构造函数的依赖注入(多构造参数解析)

a.准备组件类

public class UserDao {
}


public class UserService {
    
    private UserDao userDao;
    
    private int age;
    
    private String name;

    public UserService(int age , String name ,UserDao userDao) {
        this.userDao = userDao;
        this.age = age;
        this.name = name;
    }
}

b.编写配置文件

    <!-- 2.多个构造参数注入 -->
    <!-- ① ioc配置:将引用类和被引用类都存放进ioc容器 -->
    <bean id="userDao" class="com.mihoyo.ioc_02.UserDao"/>

    <bean id="userService1" class="com.mihoyo.ioc_02.UserService">
        <!-- ② di配置 -->
        <!-- 方案一:按照构造参数的顺序填写(value:直接属性值, ref:引用其他的beanId)-->
        <constructor-arg value="18"/>
        <constructor-arg value="张三"/>
        <constructor-arg ref="userDao"/>
    </bean>

    <bean id="userService2" class="com.mihoyo.ioc_02.UserService">
        <!--方案二:按照构造参数的名称填写,无需考虑顺序(name:构造参数的名称)-->
        <constructor-arg name="name" value="张三"/>
        <constructor-arg name="age" value="18"/>
        <constructor-arg name="userDao" ref="userDao"/>
    </bean>

    <bean id="userService3" class="com.mihoyo.ioc_02.UserService">
        <!--方案二:按照构造参数的索引填写(index:构造参数的索引)-->
        <constructor-arg index="1" value="张三"/>
        <constructor-arg index="0" value="18"/>
        <constructor-arg index="2" ref="userDao"/>
    </bean>

注意:三种方案中,我们最推荐第二种

Ⅲ.基于Setter方法依赖注入(重点)

开发中,除了构造函数注入(DI)更多的使用的 Setter 方法进行注入!

a.准备组件类

public Class MovieFinder{

}

public class SimpleMovieLister {

  private MovieFinder movieFinder;
  
  private String movieName;

  public void setMovieFinder(MovieFinder movieFinder) {
    this.movieFinder = movieFinder;
  }
  
  public void setMovieName(String movieName){
    this.movieName = movieName;
  }
}

b.编写配置文件

    <!--  3.触发setter方法进行注入 -->
    <bean id="movieFinder" class="com.mihoyo.ioc_02.MovieFinder"/>
    <bean id="simpleMovieLister" class="com.mihoyo.ioc_02.SimpleMovieLister">
        <!--
            name -> "属性名" -> setter方法中去掉 set,再首字母小写的值(setMovieFinder -> movieFinder)
            value | ref 二选一  value="直接属性值"  ref="引用其他bean的Id"
         -->
        <property name="movieFinder" ref="movieFinder"/>
        <property name="movieName" value="消失的她"/>
    </bean>

注意:

property标签: setter方法引用方式的标识,可以给setter方法对应的属性赋值

(3)实验三: IoC容器创建和使用

实验一和实验二讲解了如何在 XML 格式的配置文件编写 IoC 和 DI 配置。

想要配置文件中声明组件类信息真正的进行实例化成 Bean 对象和形成 Bean 之间的引用关系,我们需要声明IoC容器对象,读取配置文件。

Ⅰ.容器实例化
// ioc容器创建和读取组件的测试类
public class SpringIoCTest {
    //如何创建ioc容器,并且读取配置文件
    public void createIoC() {
        //创建容器,选择合适的容器实现即可
        /*
         * 接口
         *   BeanFactory
         *   ApplicationContext
         * 实现类
         *   ClassPathXmlApplicationContext      读取类路径下的xml配置方式
         *   FileSystemXmlApplicationContext     读取指定文件下的xml配置方式
         *   AnnotationConfigApplicationContext  读取配置类方式的ioc容器
         *   WebApplicationContext               web项目专属的配置的ioc容器
         * */

        //方式一:直接创建容器并指定配置文件[推荐]
        //构造函数(String...配置文件)   可以填写一个或多个
        ApplicationContext applicationContext1 = new ClassPathXmlApplicationContext("spring-03.xml");

        //方式二:先创建ioc容器对象,再指定配置文件,最后刷新配置!(源码的配置过程)
        ClassPathXmlApplicationContext applicationContext2 = new ClassPathXmlApplicationContext();
        applicationContext2.setConfigLocation("spring-03.xml");//外部配置文件的设置,参数为可变参数
        applicationContext2.refresh();//触发刷新配置,调用ioc和di的流程
    }
}

注意:

① 类路径指的是target/classes目录,resource目录下的文件通过编译后,会生成在classes目录下

方式二通常是源码的配置过程,后续ssm整合时,创建容器对象由spring自动完成。

而配置文件 spring 不知道在哪,需要我们手动设置。

方式一内部也和方式二一样,调用了 setConfigLocation 和 refresh 方法。

 Ⅱ.Bean对象读取
    //如何在IoC容器中获取组件(bean)
    @Test
    public void getBeanFromIoC() {
        //1.创建ioc容器对象
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext();
        applicationContext.setConfigLocation("spring-03.xml");
        applicationContext.refresh();//触发刷新,执行ioc和di的动作

        //2.读取ioc容器的组件
        //方案一:直接根据beanId获取即可,返回值类型是Object,需要强转[不推荐]
        HappyComponent happyComponent1 = (HappyComponent) applicationContext.getBean("happyComponent");

        //方案二:根据beanId,同时指定bean的类型(class)
        HappyComponent happyComponent2 = applicationContext.getBean("happyComponent", HappyComponent.class);

        //方案三:直接根据类型获取
        HappyComponent happyComponent3 = applicationContext.getBean(HappyComponent.class);

        happyComponent3.doWork();

        System.out.println(happyComponent1==happyComponent2);//true
        System.out.println(happyComponent2==happyComponent3);//true
    }

注意:

① 创建ioc容器对象,外部设置配置文件时,必须要刷新,否则不会执行ioc和di的动作。

通过三种方式获取的都是同一个对象!地址值相同!

③ 方案三中根据bean的类型获取,要求配置文件中,同一个类型在ioc容器中有且只有一个bean。

如果ioc容器中存在多个同类型的bean,会出现不唯一异常:NoUniqueBeanDefinitionException。

 ④ ioc的配置一定是实现类,但获取 bean 可以根据实现的接口类型来获取(接口的实现类必须唯一)

底层:getBean(xxx.class)中的接口类型xxx  instanceof   ioc 容器中的类型 == true

public void getBeanFromIoC() {
        //1.创建ioc容器对象
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext();
        applicationContext.setConfigLocation("spring-03.xml");
        applicationContext.refresh();//触发刷新,执行ioc和di的动作

        //2.读取ioc容器的组件
        A happyComponent = applicationContext.getBean(A.class);
        happyComponent.doWork();
    }

(4)实验四:高级特性:组件(Bean)作用域和周期方法配置

Ⅰ.组件周期方法配置

周期方法:到了对应的时间节点就会主动被调用的方法,我们只需等待和编写业务逻辑即可。

ioc容器是一个高级容器,不仅会实例化组件,也会进行周期管理!

我们可以在组件类中定义方法,然后当IoC容器实例化和销毁组件对象的时候进行调用!

两个方法我们称为生命周期方法! 

a.周期方法声明

public class JavaBean {
    //初始化方法 --> 编写初始化业务逻辑即可
    public void init(){
        System.out.println("JavaBean.init");
    }

    //销毁方法
    public void clear(){
        System.out.println("JavaBean.clear");
    }
}

注意:

方法命名随意,但是由于底层依靠反射寻找,所以要求方法必须是 public,必须是void,必须是无形参的!

b.周期方法配置 

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

    <!--
        init-method = "初始化方法名"
        destroy-method = "销毁方法名"
        指定好后,spring ioc容器会在对应的时间节点回调对应的方法
    -->
    <bean id="javaBean" class="com.mihoyo.ioc_04.JavaBean" init-method="init" destroy-method="clear"/>
</beans>

c.测试

    //测试ioc配置和销毁方法的触发
    @Test
    public void test_04(){
        //1.创建ioc容器(refresh方法后),就会进行组件对象的实例化 -> init
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-04.xml");

        //2.正常结束ioc容器
        applicationContext.close();
    }

注意:

方法执行完,ioc容器会立即被释放(意外释放),也就来不及去调用组件的destroy方法。

所以需要手动调用close方法,正常结束ioc容器。

Ⅱ.作用域配置

<bean 标签声明 Bean,只是将Bean的信息配置给 SpringIoC 容器。

在IoC容器中,这些<bean 标签对应的信息转成 Spring 内部 BeanDefinition 对象。

BeanDefinition 对象内,包含定义的信息(id,class,属性等等)。

所以,BeanDefinition 与类概念一样,SpringIoC 容器可以可以根据 BeanDefinition 对象反射创建多个 Bean 对象实例。

而具体创建多少个Bean的实例对象,由Bean的作用域 Scope 属性指定。

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

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

scope 取值含义创建对象的时机默认值
request请求范围内有效的实例每次请求
session会话范围内有效的实例每次会话

a.单例 

    <!-- scope属性:取值singleton(默认值),bean在IOC容器中只有一个实例,IOC容器初始化时创建对象 -->
    <bean id="javaBean2" class="com.mihoyo.ioc_04.JavaBean2" scope="singleton"/>
    @Test
    public void test_04() {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-04.xml");

        JavaBean2 bean1 = applicationContext.getBean(JavaBean2.class);
        JavaBean2 bean2 = applicationContext.getBean(JavaBean2.class);
        System.out.println(bean1 == bean2);//true
    }

b.多例

<!-- scope属性:取值prototype,bean在IOC容器中可以有多个实例,getBean()一次就会创建一个组件对象 -->
    <bean id="javaBean2" class="com.mihoyo.ioc_04.JavaBean2" scope="prototype"/>
    @Test
    public void test_04() {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-04.xml");

        JavaBean2 bean1 = applicationContext.getBean(JavaBean2.class);
        JavaBean2 bean2 = applicationContext.getBean(JavaBean2.class);
        System.out.println(bean1 == bean2);//false
    }

(5)实验五: 高级特性:FactoryBean特性和使用 

 FactoryBean 接口是Spring IoC容器实例化逻辑的可插拔性点。

 用于配置复杂的Bean对象,可以将创建过程存储在FactoryBean 的getObject方法!

FactoryBean 接口提供三种方法:

T getObject()

返回此工厂创建的对象的实例,该返回值会被存储到IoC容器。
boolean isSingleton()如果此 FactoryBean 返回单例,则返回 true ,否则返回 false 。此方法的默认实现返回 true。
Class getObjectType()返回 getObject() 方法返回的对象类型,如果事先不知道类型,则返回 null 。

使用场景:

① 代理类的创建

② 第三方框架整合

③ 复杂对象实例化等

应用:

a. JavaBean类:

public class JavaBean {
    private String name;

    public String getName() {
        return name;
    }

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

 b. FactoryBean实现类:

//制造JavaBean的工厂bean对象
/*
* 步骤:
*   1.实现FactoryBean接口<返回值泛型>
    2.重写getObject 和 getObjectType 方法*/
public class JavaBeanFactoryBean implements FactoryBean<JavaBean> {
    @Override
    public JavaBean getObject() throws Exception {
        //使用自己的方式实例化对象
        JavaBean javaBean = new JavaBean();
        return javaBean;
    }

    @Override
    public Class<?> getObjectType() {
        return JavaBean.class;
    }
}

注意:isSingleton 方法是默认方法,所以不强制重写。

c. 配置FactoryBean实现类:

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

    <!-- 
        id -> getObject方法返回的对象的标识
        class -> factoryBean标准化工厂类
    -->
    <bean id="javaBean" class="com.mihoyo.ioc_05.JavaBeanFactoryBean"/>
</beans>

d. 测试

    //读取使用factoryBean工厂配置的组件对象
    @Test
    public void test_05() {
        //1.创建ioc容器
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-05.xml");

        //2.读取组件
        JavaBean javaBean = applicationContext.getBean("javaBean", JavaBean.class);
        System.out.println("javaBean = " + javaBean);

        Object factoryBean = applicationContext.getBean("&javaBean");
        System.out.println("factoryBean = " + factoryBean);

        //3.正常结束ioc容器
        applicationContext.close();
    }

细节:

① JavaBean会放到ioc容器中,同时Factorybean工厂也会放入ioc容器中,标识为 &id值

②  JavaBean 组件中的 name 属性如何赋值?

方式一:getObejct方法中赋值

方式二:在FactoryBean工厂中添加一个value属性,xml中配置di,间接给JavaBean组件赋值

注意:

<bean id="javaBean" class="com.mihoyo.ioc_05.JavaBeanFactoryBean">

该位置配置的属性:JavaBean工厂类配置,而不是 getObject 方法,所以不能直接设置 name 属性。

③ BeanFactory 和 FactoryBean 区别 :

BeanFactory:是 Spring 框架的基础,其作为一个顶级接口定义了容器的基本行为,例如管理 bean 的生命周期、配置文件的加载和解析、bean 的装配和依赖注入等。BeanFactory 接口提供了访问 bean 的方式,例如 getBean() 方法获取指定的 bean 实例。它可以从不同的来源(例如 Mysql 数据库、XML 文件、Java 配置类等)获取 bean 定义,并将其转换为 bean 实例。同时,BeanFactory 还包含很多子类(例如,ApplicationContext 接口)提供了额外的强大功能。

FactoryBean:是 Spring 中一种特殊的 bean,可以在 getObject() 工厂方法自定义的逻辑创建Bean!是一种能够生产其他 Bean 的 Bean。FactoryBean 在容器启动时被创建,而在实际使用时则是通过调用 getObject() 方法来得到其所生产的 Bean。因此,FactoryBean 可以自定义任何所需的初始化逻辑,生产出一些定制化的 bean。

一般情况下,整合第三方框架,都是通过定义FactoryBean实现!!!

BeanFactory 和 FactoryBean 都是接口,BeanFactory是ioc容器最大的接口,相当于ioc容器;FactoryBean是标准化组件工厂的接口,相当于组件。

(6) 实验六: 基于XML方式整合三层架构组

需求:搭建一个三层架构案例,模拟查询全部学生(学生表)信息,持久层使用JdbcTemplate和Druid技术,使用XML方式进行组件管理。

 a.数据库准备

create database jdbc;

use jdbc;

CREATE TABLE students (
  id INT PRIMARY KEY,
  name VARCHAR(50) NOT NULL,
  gender VARCHAR(10) NOT NULL,
  age INT,
  class VARCHAR(50)
);

INSERT INTO students (id, name, gender, age, class)
VALUES
  (1, '张三', '男', 20, '高中一班'),
  (2, '李四', '男', 19, '高中二班'),
  (3, '王五', '女', 18, '高中一班'),
  (4, '赵六', '女', 20, '高中三班'),
  (5, '刘七', '男', 19, '高中二班'),
  (6, '陈八', '女', 18, '高中一班'),
  (7, '杨九', '男', 20, '高中三班'),
  (8, '吴十', '男', 19, '高中二班');

b.创建项目,导入依赖

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

      <!-- 数据库驱动和连接池-->
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>8.0.25</version>
      </dependency>

      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid</artifactId>
          <version>1.2.8</version>
      </dependency>

      <!-- spring-jdbc(使用jdbcTemplate) -->
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-jdbc</artifactId>
          <version>6.0.6</version>
      </dependency>

</dependencies> 

c.准备实体类

public class Student {

    private Integer id;
    private String name;
    private String gender;
    private Integer age;
    private String classes;

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public Integer getAge() {
        return age;
    }

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

    public String getClasses() {
        return classes;
    }

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

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                ", age=" + age +
                ", classes='" + classes + '\'' +
                '}';
    }
}

d.三层架构搭建和实现

① 持久层

//接口
public interface StudentDao {

    /**
     * 查询全部学生数据
     * @return
     */
    List<Student> queryAll();
}

//实现类
public class StudentDaoImpl implements StudentDao {
    private JdbcTemplate jdbcTemplate;

    //注入我们的jdbcTemplate对象
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public List<Student> queryAll() {
        //jdbcTemplate进行数据库的查询(需要先在ioc容器中进行装配jdbcTemplate,不要自己去实例化)
        String sql = "select id,name,gender,age,class as classes from students";
        List<Student> students = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Student.class));
        System.out.println("studentDao:" + students);
        return students;
    }
}

② 业务层

//接口
public interface StudentService {

    /**
     * 查询全部学员业务
     * @return
     */
    List<Student> findAll();

}

//实现类
public class StudentServiceImpl  implements StudentService {
    
    private StudentDao studentDao;

    public void setStudentDao(StudentDao studentDao) {
        this.studentDao = studentDao;
    }

    /**
     * 查询全部学员业务
     * @return
     */
    @Override
    public List<Student> findAll() {
        List<Student> studentList = studentDao.queryAll();
        System.out.println("studentService:" + studentList);
        return studentList;
    }
}

③ 控制层

public class StudentController {
    
    private StudentService studentService;

    public void setStudentService(StudentService studentService) {
        this.studentService = studentService;
    }
    
    public void findAll(){
        List<Student> all = studentService.findAll();
        System.out.println("最终学员数据:"+all);
    }
}

e. 准备数据库连接信息

mihoyo.url=jdbc:mysql://localhost:3306/jdbc
mihoyo.driver=com.mysql.cj.jdbc.Driver
mihoyo.username=root
mihoyo.password=123456

f. 三层架构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"
       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 https://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="${mihoyo.url}"/>
        <property name="driverClassName" value="${mihoyo.driver}"/>
        <property name="username" value="${mihoyo.username}"/>
        <property name="password" value="${mihoyo.password}"/>
    </bean>

    <!-- 配置 JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 装配数据源 -->
        <property name="dataSource" ref="druidDataSource"/>
    </bean>

    <!-- dao 配置 di -> jdbcTemplate -->
    <bean id="studentDao" class="com.mihoyo.dao.impl.StudentDaoImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"/>
    </bean>

    <!-- service 配置 di -> studentDao -->
    <bean id="studentService" class="com.mihoyo.service.impl.StudentServiceImpl">
        <property name="studentDao" ref="studentDao"/>
    </bean>

    <!-- controller 配置 di -> studentService -->
    <bean id="studentController" class="com.mihoyo.controller.StudentController">
        <property name="studentService" ref="studentService"/>
    </bean>
</beans>

g. 测试

public class jdbcTemplateTest {
    //从ioc容器中获取controller并且调用业务,内部都是ioc容器进行组装
    @Test
    public void TestQueryAll(){
        //1.创建ioc容器
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-ioc.xml");
        //2.获取组件对象
        StudentController studentController = applicationContext.getBean(StudentController.class);
        //3.使用组件对象
        studentController.findAll();
        //4.关闭容器
        applicationContext.close();
    }
}

XMLIoC方式问题总结:

① 注入的属性必须添加setter方法、代码结构乱!

② 配置文件和 Java 代码分离、编写不是很方便!

③ XML配置文件解析效率低 

3.基于注解方式管理 Bean

(1)实验一: Bean注解标记和扫描 (IoC)

注解:和 XML 配置文件一样,注解本身并不能执行,注解本身仅仅只是做一个标记,具体的功能是框架检测到注解标记的位置,然后针对这个位置按照注解标记的功能来执行具体操作

Spring 为了知道程序员在哪些地方标记了什么注解,就需要通过扫描的方式来进行检测,然后根据注解进行后续操作。

步骤:

a. 准备组件类

普通组件

/**
 * projectName: com.mihoyo.ioc_01
 *
 * description: 普通的组件
 */
public class CommonComponent {
}

Controller组件

/**
 * projectName: com.mihoyo.ioc_01
 *
 * description: controller类型组件
 */
public class XxxController {
}

Service组件

/**
 * projectName: com.mihoyo.ioc_01
 *
 * description: service类型组件
 */
public class XxxService {
}

Dao组件

/**
 * projectName: com.mihoyo.ioc_01
 *
 * description: dao类型组件
 */
public class XxxDao {
}

b. 组件添加标记注解

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 相同。

 通过查看源码我们得知,@Controller、@Service、@Repository这三个注解只是在@Component注解的基础上起了三个新的名字。

注意:

对于Spring使用IOC容器管理这些组件来说没有区别,也就是语法层面没有区别。

@Controller、@Service、@Repository这三个注解只是给开发人员看的,让我们能够便于分辨组件的作用。

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

普通组件标记

import org.springframework.stereotype.Component;

/**
 * projectName: com.mihoyo.ioc_01
 *
 * description: 普通的组件
 */

//标记后,等价于 <bean id="commonComponent" class="com.mihoyo.ioc_01.CommonComponent"/>
@Component
public class CommonComponent {
}

 Controller组件标记

import org.springframework.stereotype.Controller;

/**
 * projectName: com.mihoyo.ioc_01
 *
 * description: controller类型组件
 */

@Controller
public class XxxController {
}

Service组件标记

import org.springframework.stereotype.Service;

/**
 * projectName: com.mihoyo.ioc_01
 *
 * description: service类型组件
 */
@Service
public class XxxService {
}

Dao组件标记

import org.springframework.stereotype.Repository;

/**
 * projectName: com.mihoyo.ioc_01
 *
 * description: dao类型组件
 */
@Repository
public class XxxDao {
}

c. 配置文件确定扫描范围(告诉ioc容器去哪些包下扫描 ioc 注解)

情况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"
       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 https://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 1.普通配置包扫描 -->
    <!-- base-package 指定ioc容器去哪些包下查找注解类
         会扫描指定的包和子包内容
         一个或多个包 例如: com.mihoyo..ioc_01.controller,com.mihoyo.ioc_01.service等
    -->
    <context:component-scan base-package="com.mihoyo.ioc_01"/>
</beans>

情况2:指定排除组件

    <!-- 2.指定包,但是排除其中某些注解-->
    <context:component-scan base-package="com.mihoyo.ioc_01">
        <!-- 排除包下所有Repository注解 -->
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>

情况3:指定扫描组件

    <!-- 3.指定包,但只扫描其中所包含的某些注解-->
    <context:component-scan base-package="com.mihoyo.ioc_01" use-default-filters="false">
        <!-- 只扫描包下的Service注解 -->
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
    </context:component-scan>

注意:

base-package 表示指定包下的所有注解都生效,所以再指定其中某些生效,就重复(冲突)了。

必须要通过 use-default-filters="false" 关闭默认扫描规则后,再追加其中某些注解生效。

d. 测试 

public class SpringIoCTest {
    @Test
    public void testIoc_01() {
        //1.创建ioc容器
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-01.xml");
        //2.获取组件
        XxxDao xxxDao = applicationContext.getBean(XxxDao.class);
        System.out.println("xxxDao:" + xxxDao);

        //添加ioc注解,默认的组件名(id)为 首字母小写的类名
        Object xxxService = applicationContext.getBean("xxxService");
        System.out.println("xxxService:"+xxxService);

        //3.关闭容器
        applicationContext.close();
    }
}

注意:

在我们使用 XML 方式管理 bean 的时候,每个 bean 都有一个唯一标识——id 属性的值,便于在其他地方引用。

现在使用注解后,每个组件仍然应该有一个唯一标识。

默认情况:首字母小写的类名 就是 bean 的 id。(如 UserDao --> userDao)

此外,我们也可以使用 value 属性指定:

@Controller(value = "udao")
public class UserDao {
}

当注解中只设置一个属性时,value属性的属性名可以省略:

@Controller("udao")
public class UserDao {
}

(2)实验二:组件(Bean)作用域和周期方法注解

Ⅰ. 组件周期方法配置

周期方法声明:

@Component
public class JavaBean{
    //周期方法命名随意,但是要求方法必须是 public void 无形参
    @PostConstruct  //注解指定初始化方法
    public void init() {
        // 初始化逻辑
        System.out.println("JavaBean.init");
    }

    @PreDestroy //注解指定销毁方法
    public void clear() {
        // 释放资源逻辑
        System.out.println("JavaBean.clear");
    }
}
Ⅱ. 组件作用域配置
//@Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON) //单例,默认值
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE) //多例
@Component
public class JavaBean{
    ...
}

测试:

    @Test
    public void testIoc_02() {
        //1.创建ioc容器
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-02.xml");
        //2.获取组件
        JavaBean bean1 = applicationContext.getBean(JavaBean.class);
        JavaBean bean2 = applicationContext.getBean(JavaBean.class);
        System.out.println(bean1 == bean2);

        //3.关闭容器
        applicationContext.close();
    }

运行结果:

注意:多例时,ioc容器是不会管理 destory 周期方法的。

(3)实验三: Bean属性赋值:引用类型自动装配 (DI)

需求:UserController 需要 UserService

UserService接口:

public interface UserService {
    public void show();
}

UserService实现类: 

@Service
public class UserServiceImpl implements UserService{
    @Override
    public void show() {
        System.out.println("show");
    }
}

UserController:

@Controller
public class UserController {
    private UserService userService;
    public void show(){
        //调用业务层的show方法
        userService.show();
    }
}

Test:如何给 UserController 组件中的属性(引用类型 userService)赋值呢?


********* 自动装配注解(DI)********* 

a. 前提:

        参与自动装配的组件(需要装配、被装配)全部都必须在IoC容器中。

        注意:不区分IoC的方式!XML和注解都可以!

b. @Autowired注解

        在成员变量上直接标记@Autowired注解即可,不需要提供setXxx()方法

细节 :

① 标记位置:成员变量上,构造方法上,Setter方法上都可以

工作流程:ioc 容器中查找符合类型的组件对象,然后设置给当前属性(di)

 首先根据所需要的组件类型到 IOC 容器中查找

        1. 能够找到唯一的 bean:直接执行装配

        2. 如果完全找不到匹配这个类型的 bean:装配失败

        3. 和所需类型匹配的 bean 不止一个

                3-1. 没有 @Qualifier 注解:根据 @Autowired 标记位置成员变量的变量名作为 bean 的 id 进行匹配

                        3-1-1. 能够找到:执行装配

                        3-1-2. 找不到:装配失败

                3-2. 使用 @Qualifier 注解:根据 @Qualifier 注解中指定的名称作为 bean 的id进行匹配

                        3-2-1. 能够找到:执行装配

                        3-2-2. 找不到:装配失败


③ 正常情况下,找不到,就直接报错。此时,可以使用

佛系装配:给 @Autowired 注解设置 required = false 属性表示:能装就装,装不上就不装。

但是不推荐使用该方法,因为装配的数据后续都会由调用,可能会出现空指针异常!

 ④ 优化点:JSR-250注解@Resource

JSR(Java Specification Requests)是Java平台标准化进程中的一种技术规范,而JSR注解是其中一部分重要的内容。

JSR是Java提供的技术规范,也就是说,他只是规定了注解和注解的含义,JSR并不是直接提供特定的实现,而是提供标准和指导方针,由第三方框架(Spring)和库来实现和提供对应的功能

注意:@Resource注解属于JDK扩展包,所以不在JDK当中,需要额外引入以下依赖:(高于JDK11或低于JDK8需要引入以下依赖)

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

 @Autowired + @Qualifier(value = "userServiceImpl") = @Resource(name = "userServiceImpl")

 (4)实验四: Bean属性赋值:基本类型属性赋值 (DI)

 方案一:直接赋值

@Component
public class User {
    private String name = "张三";
}

方案二:使用 @value 注解赋值

@Component
public class User {
    @value("张三")
    private String name;
}

细节:@Value 通常用于注入外部化配置文件属性

a.声明外部配置(jdbc.properties)

jdbc.username=root
jdbc.password=123456

b. xml引入外部配置

<!-- 引入外部配置文件-->
<context:property-placeholder location="jdbc.properties" />

c. @Value注解读取配置

@Component
public class User {
    //${key} 取外部配置key对应的值!
    @value("${jdbc.username}")
    private String name;

    @value("${jdbc.password}")
    private String password;
}

注意:如果外部化配置中不存在值,也可以设置默认值

@Component
public class User {
    //${key:defaultValue} 没有key,可以给与默认值(admin)
    @value("${jdbc.username:admin}")
    private String name;

    @value("${jdbc.password}")
    private String password;
}

(5)实验五: 基于注解+XML方式整合三层架构组件

需求:搭建一个三层架构案例,模拟查询全部学生(学生表)信息,持久层使用JdbcTemplate和Druid技术,使用XML+注解方式进行组件管理。

数据库,项目依赖导入,实体类准备,和上述相同,这里不再重复介绍 。

三层架构搭建和实现:

a. 持久层

//接口
public interface StudentDao {

    /**
     * 查询全部学生数据
     * @return
     */
    List<Student> queryAll();
}

//实现类
@Repository
public class StudentDaoImpl implements StudentDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public List<Student> queryAll() {
        //jdbcTemplate进行数据库的查询(需要先在ioc容器中进行装配jdbcTemplate,不要自己去实例化)
        String sql = "select id,name,gender,age,class as classes from students";
        List<Student> students = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Student.class));
        System.out.println("studentDao:" + students);
        return students;
    }
}

b. 业务层

//接口
public interface StudentService {

    /**
     * 查询全部学员业务
     * @return
     */
    List<Student> findAll();

}

//实现类
@Service
public class StudentServiceImpl implements StudentService {
    @Autowired
    private StudentDao studentDao;

    @Override
    public List<Student> findAll() {
        List<Student> studentList = studentDao.queryAll();
        System.out.println("studentService:" + studentList);
        return studentList;
    }
}

c. 控制层

@Controller
public class StudentController {
    @Autowired
    private StudentService studentService;


    public void findAll(){
        List<Student> all = studentService.findAll();
        System.out.println("最终学员数据:"+all);
    }
}

三层架构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"
       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 https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 自己定义的类进行注解扫描 -->
    <context:component-scan base-package="com.mihoyo"></context:component-scan>

    <!-- 导入外部属性文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!-- 第三方的类仍然使用xml方式配置-->
    <!-- 配置数据源 -->
    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${mihoyo.url}"/>
        <property name="driverClassName" value="${mihoyo.driver}"/>
        <property name="username" value="${mihoyo.username}"/>
        <property name="password" value="${mihoyo.password}"/>
    </bean>

    <!-- 配置 JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 装配数据源 -->
        <property name="dataSource" ref="druidDataSource"/>
    </bean>
</beans>

 注意:

注解只能加载自定义的类上,对于官方已经写好的第三方类文件,都是只读的,仍需使用 xml 方式配置。

注解+XML IoC方式问题总结 :

① 自定义类可以使用注解方式,但是第三方依赖的类依然使用XML方式!

② XML格式解析效率低!

4.基于 配置类 方式管理 Bean 

完全注解开发:Spring 完全注解配置是指通过 Java配置类 代码来配置 Spring 应用程序,使用注解来替代原本在 XML 配置文件中的配置

(1)实验一:配置类和扫描注解 

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 https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 配置自动扫描的包 -->
    <context:component-scan base-package="com.mihoyo"/>

    <!-- 引入外部配置文件-->
    <context:property-placeholder location="jdbc.properties" />
</beans>

改为:配置类 + 注解方式(完全注解方式)

/*
    java的配置类,替代xml配置文件
        1.包扫描注解配置
        2.引用外部的配置文件
        3.声明第三方依赖的bean组件
* */

/*
* 步骤:
*   1.添加@Configurable 注解,代表这是配置类
*   2.实现上面的三个功能
* */

@ComponentScan("com.mihoyo")
@PropertySource(value="classpath:jdbc.,properties")
@Configurable
public class JavaConfiguration {

}

 注意:

① 这里只实现了前两个功能,第三个功能(声明第三方依赖的bean组件)下面实验二介绍。

② 在包扫描注解配置中,如果有多个包,写法如下:@ComponentScan({"com.xxx","com.yyy"})

测试创建IoC容器:



// ioc容器创建和读取组件的测试类
public class SpringIoCTest {
    //如何创建ioc容器,并且读取配置文件
    public void createIoC() {
        //创建容器,选择合适的容器实现即可
        /*
         * 接口
         *   BeanFactory
         *   ApplicationContext
         * 实现类
         *   ClassPathXmlApplicationContext      读取类路径下的xml配置方式
         *   FileSystemXmlApplicationContext     读取指定文件下的xml配置方式
         *   AnnotationConfigApplicationContext  读取配置类方式的ioc容器
         *   WebApplicationContext               web项目专属的配置的ioc容器
         * */
        
        //1.创建ioc容器
        // 方式一:直接创建容器并指定配置类
        ApplicationContext iocContainerAnnotation1 = new AnnotationConfigApplicationContext(JavaConfiguration.class);

        //方式二:先创建ioc容器对象,再指定配置类,最后刷新配置!(源码的配置过程)
       ApplicationContext iocContainerAnnotation2 = new AnnotationConfigApplicationContext();
        iocContainerAnnotation2.register(JavaConfiguration.class);//外部设置配置类
        iocContainerAnnotation2.refresh();//触发刷新配置,调用ioc和di的流程

        //2.获取bean
        StudentController bean = iocContainerAnnotation2.getBean(StudentController.class);
        
    }
}

 (2)实验二:@Bean定义组件

需求:将Druid连接池对象存储到IoC容器

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 https://www.springframework.org/schema/context/spring-context.xsd">


    <!-- 引入外部属性文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!-- 实验六 [重要]给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>

</beans>

改为:配置类方式

@ComponentScan("com.mihoyo")
@PropertySource(value = "classpath:jdbc.,properties")
@Configurable
public class JavaConfiguration {
    @Value("${jdbc.user}")
    String username;
    @Value("${jdbc.password}")
    String password;
    @Value("${jdbc.url}")
    String url;
    @Value("${jdbc.driver}")
    String driverClassName;

    /*
    * 一个<bean -> 一个方法
    *   方法的返回只类型 = bean 组件的类型 或者 它的接口和父类
    *   方法的名字 = beanId
    * 方法体可以自定义实现过程即可
    * 加上 @Bean 才会真正让配置类的方法创建的组件存储到ioc容器! */
    @Bean
    public DruidDataSource dataSource() {
        //具体的实例化过程
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setUrl(url);
        dataSource.setDriverClassName(driverClassName);
        //返回结果即可
        return dataSource;
    }
}

注意:

① 方法的返回值类型和名字都是有作用的,不能随意!

② 必须要加上 @Bean 注解

(3)实验三:高级特性:@Bean注解细节

Ⅰ. @Bean生成BeanId问题

@Bean注解源码:

public @interface Bean {
    //前两个注解可以指定Bean的标识
    @AliasFor("name")
    String[] value() default {};
    @AliasFor("value")
    String[] name() default {};
  
    //autowireCandidate 属性来指示该 Bean 是否候选用于自动装配。
    //autowireCandidate 属性默认值为 true,表示该 Bean 是一个默认的装配目标,
    //可被候选用于自动装配。如果将 autowireCandidate 属性设置为 false,则说明该 Bean 不是默认的装配目标,不会被候选用于自动装配。
    boolean autowireCandidate() default true;

    //指定初始化方法
    String initMethod() default "";
    //指定销毁方法
    String destroyMethod() default "(inferred)";
}

指定@Bean的名称:

    @Bean(name = "ds")
    public DruidDataSource dataSource() {
        //具体的实例化过程
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setUrl(url);
        dataSource.setDriverClassName(driverClassName);
        //返回结果即可
        return dataSource;
    }

默认情况,beanId 默认是方法名;

但可以通过 name / value 属性起名字,覆盖方法名进行指定。

Ⅱ. @Bean 周期方法指定

方案一:原有注解方案(@PostConstruct + @PreDestory)

public class JavaBean{
   @PostConstruct
   public void init() {
     // initialization logic
   }

   @PreDestory
   public void cleanup() {
     // destruction logic
   }
}

@Configuration
public class AppConfig {
   @Bean
   public JavaBean bean() {
     return new JavaBean();
   }
}

方案二:@Bean 属性指定(initMethod 和 destroyMethod 指定)

public class JavaBean{
   public void init() {
     // initialization logic
   }

   public void cleanup() {
     // destruction logic
   }
}

@Configuration
public class AppConfig {
   @Bean(initMethod = "init",destroyMethod = "cleanup")
   public JavaBean bean() {
     return new JavaBean();
   }
}
Ⅲ. @Bean Scope作用域

还是和以前一样,使用 @Scope 注解,默认是单例

    @Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON)
    @Bean
    public DruidDataSource dataSource() {
        //具体的实例化过程
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setUrl(url);
        dataSource.setDriverClassName(driverClassName);
        //返回结果即可
        return dataSource;
    }
Ⅳ. @Bean 引用其他组件

方案一:如果其他组件也是 @Bean 方法,可以直接调用该方法(本质上就是从容器获取组件)

    @Bean
    public DruidDataSource dataSource() {
        //具体的实例化过程
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setUrl(url);
        dataSource.setDriverClassName(driverClassName);
        //返回结果即可
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate() {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        //需要ioc容器的其他组件(dataSource)
        jdbcTemplate.setDataSource(dataSource());
        return jdbcTemplate;
    }

方案二:形参中声明想要的组件,可以是一个也可以是多个,ioc 容器会自动注入

    @Bean
    public DruidDataSource dataSource() {
        //具体的实例化过程
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setUrl(url);
        dataSource.setDriverClassName(driverClassName);
        //返回结果即可
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

注意:

形参变量注入,要求 ioc 容器中必须有对应类型的组件。

① 如果没有,会抛异常

② 如果一个,直接装配(形参变量名任意)

② 如果有多个,使用形参变量名 等同于 对应的 beanId 标识即可

    @Bean(name = "ds1")
    public DruidDataSource dataSource1() {
        //具体的实例化过程
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setUrl(url);
        dataSource.setDriverClassName(driverClassName);
        //返回结果即可
        return dataSource;
    }

    @Bean(name = "ds2")
    public DruidDataSource dataSource2() {
        //具体的实例化过程
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setUrl(url);
        dataSource.setDriverClassName(driverClassName);
        //返回结果即可
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource ds1) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(ds1);
        return jdbcTemplate;
    }

(4)实验四:高级特性:@Import扩展

@Configuration
public class ConfigA {

  @Bean
  public A a() {
    return new A();
  }
}

@Configuration
public class ConfigB {

  @Bean
  public B b() {
    return new B();
  }
}

如果有多个配置类,正常情况下,我们需要在 ioc 容器中一次性指定多个配置类:

ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigA.class,ConfigB.class);

但随着配置类数量增加,这么些会过于麻烦。

@Import 注释允许从另一个配置类加载 @Bean 定义,如以下示例所示:

@Configuration
public class ConfigA {

  @Bean
  public A a() {
    return new A();
  }
}

@Configuration
@Import(ConfigA.class)
public class ConfigB {

  @Bean
  public B b() {
    return new B();
  }
}

此时,在 ioc 容器中就不需要同时指定 ConfigA.class 和 ConfigB.class ,只需指定 ConfigB 即可

(5)实验五:基于注解+配置类方式整合三层架构组件

需求:搭建一个三层架构案例,模拟查询全部学生(学生表)信息,持久层使用JdbcTemplate和Druid技术,使用注解+配置类方式进行组件管理。

 数据库,项目依赖导入,实体类准备,三层架构组件和上述相同,这里不再重复介绍 。

三层架构IoC配置类

@Configurable
@ComponentScan(basePackages = "com.mihoyo")
@PropertySource(value = "classpath:jdbc.properties")
public class JavaConfig {
    @Value("${mihoyo.username}")
    String username;
    @Value("${mihoyo.password}")
    String password;
    @Value("${mihoyo.url}")
    String url;
    @Value("${mihoyo.driver}")
    String driverClassName;

    @Bean
    public DruidDataSource dataSource() {
        //具体的实例化过程
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setUrl(url);
        dataSource.setDriverClassName(driverClassName);
        //返回结果即可
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate() {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource());
        return jdbcTemplate;
    }
}

运行测试

public class SpringIoCTest {
    @Test
    public void testRun() {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(JavaConfig.class);
        StudentController studentController = applicationContext.getBean(StudentController.class);
        studentController.findAll();
    }
}

 注解+配置类 IoC方式总结:

① 完全摒弃了 XML 配置文件

② 自定义类使用 IoC 和 DI 注解标记 

③ 第三方类使用配置类声明方法 + @Bean 方式处理

④ 完全注解方式(配置类+注解)是现在主流配置方式

5.整合Spring5-Test5搭建测试环境

在之前,我们每次进行测试,都需要 new 一个 ioc 容器,过于麻烦。

整合测试环境作用:

① 不需要自己创建 IOC 容器对象了

② 任何需要的 bean 都可以在测试类中直接享受自动装配

a. 导入相关依赖

<!--junit5测试-->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.3.1</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>6.0.6</version>
    <scope>test</scope>
</dependency>

b. 整合测试注解使用 

//@SpringJUnitConfig(locations = {"classpath:spring-ioc.xml"})  //指定配置文件xml
@SpringJUnitConfig(value = {JavaConfig.class})  //指定配置类
public class Junit5IntegrationTest {
    
    @Autowired
    private User user;
    
    @Test
    public void testJunit5() {
        System.out.println(user);
    }
}

注意:

使用 @SpringJUnitConfig 注解,自动创建 ioc 容器。

如果是 xml 方式,需要使用 locations 属性指定 xml 配置文件。

如果是 配置类 方式,需要使用 value 属性指定 配置类。

此时,测试类中,可以自动装配任何所需要的 bean,不需要再通过 getBean 方法获取。

四、Spring Aop 面向切面编程

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

接口实现

/**
 * 实现计算接口,单纯添加 + - * / 实现! 掺杂其他功能!
 */
public class CalculatorPureImpl implements Calculator {
    
    @Override
    public int add(int i, int j) {
        int result = i + j;
        return result;
    }
    
    @Override
    public int sub(int i, int j) {
        int result = i - j;
        return result;
    }
    
    @Override
    public int mul(int i, int j) {
        int result = i * j;
        return result;
    }
    
    @Override
    public int div(int i, int j) {
        int result = i / j;
        return result;
    }
}

新需求: 需要在每个方法中,添加控制台输出,输出参数和输出计算后的返回值!

/**
 * 在每个方法中,输出传入的参数和计算后的返回结果!
 */
public class CalculatorLogImpl implements Calculator {
    
    @Override
    public int add(int i, int j) {
        System.out.println("参数是:" + i + "," + j);
        int result = i + j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
    
    @Override
    public int sub(int i, int j) {
        System.out.println("参数是:" + i + "," + j);
        int result = i - j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
    
    @Override
    public int mul(int i, int j) {
        System.out.println("参数是:" + i + "," + j);
        int result = i * j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
    
    @Override
    public int div(int i, int j) {
        System.out.println("参数是:" + i + "," + j);
        int result = i / j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
}

 代码问题分析:

① 代码缺陷

        对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力

        附加功能代码重复,分散在各个业务功能方法中!冗余,且不方便统一维护!

② 解决思路

        核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。

        将重复的代码统一提取,并且 动态插入 到每个业务方法!

 2.解决技术代理模式

代理模式:就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用

让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。

调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。

区别:

无代理场景:

有代理场景:

 代理在开发中实现的方式具体有两种:静态代理,动态代理技术(开发中常用)

(1)静态代理

主动创建代理类:

//代理类
public class StaticProxyCalculator implements Calculator {
     将被代理的目标对象声明为成员变量
    private Calculator calculator;

    //通过构造函数传入目标对象
    public StaticProxyCalculator(Calculator target) {
        this.calculator = target;
    }

    @Override
    public int add(int i, int j) {
        System.out.println("参数是:" + i + "," + j);
        //调用目标方法(核心代码)
        int result = calculator.add(i, j);
        System.out.println("result = " + result);
        return result;
    }

    @Override
    public int sub(int i, int j) {
        System.out.println("参数是:" + i + "," + j);
        int result = calculator.sub(i, j);
        System.out.println("result = " + result);
        return result;
    }

    @Override
    public int mul(int i, int j) {
        System.out.println("参数是:" + i + "," + j);
        int result = calculator.mul(i, j);
        System.out.println("result = " + result);
        return result;
    }

    @Override
    public int div(int i, int j) {
        System.out.println("参数是:" + i + "," + j);
        int result = calculator.div(i, j);
        System.out.println("result = " + result);
        return result;
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        //创建目标对象
        Calculator target = new CalculatorPureImpl();
        //创建代理对象
        StaticProxyCalculator proxy = new StaticProxyCalculator(target);//构造方法传入目标对象
        //调用
        proxy.add(1, 1);
    }
}

静态代理缺点:

①  实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。

② 仍然存在大量重复代码,造成冗余。

(2)动态代理

分类:

① JDK动态代理:JDK原生的实现方式,需要被代理的目标类必须实现接口。它会根据目标类的接口动态生成一个代理对象,而代理对象和目标对象有相同的接口。(兄弟)

② cglib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口!(认干爹)

JDK动态代理技术实现(了解):

代理工厂:基于jdk代理技术,生成代理对象 

//jdk的代理工厂
public class ProxyFactory {

    private Object target;

    //通过构造方法传入目标对象
    public ProxyFactory(Object target) {
        this.target = target;
    }

    public Object getProxy(){

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

        /* jdk生成代理对象:
            参数一:类加载器
            参数二:目标类实现的所有接口
            参数三:具体要进行的代理动作(非核心动作 + 调用目标方法)
         */
        return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
    }
}

测试:

public class Test {
    public static void main(String[] args) {
        //创建目标对象
        Calculator target = new CalculatorPureImpl();

        //jdk代理
        //new 代理工厂,生成代理对象
        ProxyFactory factory = new ProxyFactory(target);
        //返回的是代理对象,所以要用接口接值(代理对象和目标对象是兄弟关系)
        Calculator proxy = (Calculator) factory.getProxy();
        proxy.add(1,1);
    }
}

代理方式可以解决附加功能代码干扰核心代码和不方便统一维护的问题。

但是我们也发现,无论使用静态代理和动态代理 (jdk,cglib),代码的书写都比较繁琐

所以在实际开发中,不需要编写代理代码,我们可以使用 Spring AOP 框架,它会简化动态代理的实现!!!

3.面向切面编程思维(AOP) 

(1)概念

 AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善

OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。

不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系。在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。

所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

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

应用场景:解决非核心业务代码冗余问题!

(2)AOP术语名词介绍

① 横切关注点

从每个方法中抽取出来的同一类非核心业务。

在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。

AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。

横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务、异常等。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

② 通知(增强)

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

a. 前置通知:在被代理的目标方法前执行 

b. 返回通知:在被代理的目标方法成功结束后执行

c. 异常通知:在被代理的目标方法异常结束后执行

d. 后置通知:在被代理的目标方法最终结束后执行

e. 环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

③ 连接点 joinpoint 

指那些被拦截到的点。在 Spring 中,可以被动态代理拦截目标类的方法。

④ 切入点 pointcut

定位连接点的方式,或者可以理解成被选中的连接点。

⑤ 切面 aspect

切入点和通知的结合,是一个类。

⑥ 目标 target

被代理的目标对象

⑦ 代理 proxy

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

⑧ 织入 weave

指把通知应用到目标上,生成代理对象的过程。

可以在编译期织入,也可以在运行期织入,Spring采用后者。

  4.Spring AOP框架介绍和关系梳理

AOP一种区别于OOP的编程思维,用来完善和解决 OOP 的非核心代码冗余和不方便统一维护问题。

代理技术(动态代理 | 静态代理)是实现AOP思维编程的具体技术,但是自己使用动态代理实现代码比较繁琐。

而 Spring AOP 框架,是基于AOP编程思维,封装动态代理技术,简化动态代理技术实现的框架。

SpringAOP 内部帮助我们实现动态代理,我们只需写少量的配置,指定生效范围即可,即可完成面向切面思维编程的实现!

5.Spring AOP基于注解方式实现和细节

(1)Spring AOP底层技术组成

底层同时封装了 jdk动态代理 和 cglib,会根据具体情况(有无接口)选择合适的方式。

 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟模式)。

cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。

AspectJ:早期的AOP实现的框架,SpringAOP借用了 AspectJ 中的AOP注解。

(2)初步实现

需求:给计算的业务类,添加日志(横向插入增强代码)


a. 导入依赖

<dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>6.0.6</version>
</dependency>

<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>6.0.6</version>
</dependency>

jdk 动态代理只要有 java环境自动就有,而 cglib 已经加入到spring-core 包下,所以都不需要导入依赖。

spring-aop 的依赖也不需要导入,因为已经被 spring-context 进行依赖传递。

spring-aspects 也会进行依赖传递,自动导入 aspectJ 的依赖。

b. 正常编写核心业务,加入ioc容器

 接口:

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 CalculatorPureImpl implements Calculator {
    
    @Override
    public int add(int i, int j) {
    
        int result = i + j;
    
        return result;
    }
    
    @Override
    public int sub(int i, int j) {
    
        int result = i - j;
    
        return result;
    }
    
    @Override
    public int mul(int i, int j) {
    
        int result = i * j;
    
        return result;
    }
    
    @Override
    public int div(int i, int j) {
    
        int result = i / j;
    
        return result;
    }
}

 注意:

aop 功能只针对 ioc 容器的对象(为容器中的对象创建代理对象,将代理对象存储到 ioc 容器),所以一定要将目标对象存储到 ioc 容器中

c. 编写 ioc 的配置类

@Configurable
@ComponentScan(basePackages = "com.mihoyo.service")
public class JavaConfig {
}

d.测试环境

@SpringJUnitConfig(value = JavaConfig.class)
public class SpringAopTest {
    @Autowired
    private Calculator calculator;

    @Test
    public void test() {
        int result = calculator.add(1, 1);
        System.out.println("result = " + result);
    }
}

注意:

calculator 必须定义成接口类型,因为后续 ioc 容器生成的代理对象和目标对象有相同的接口(兄弟关系)。

ioc 容器中真实存储的是代理对象,目标对象并没有放到 ioc 容器中。

所以 @Autowired 自动装配的是生成的代理对象。

e. 定义增强类和增强方法(存储横切关注点的代码)

//增强类:内部存储增强代码
public class LogAdvice {
    //定义方法存储增强代码(具体定义几个方法,根据插入的位置决定)
    public void start(){
        System.out.println("方法开始了");
    }

    public void after(){
        System.out.println("方法结束了");
    }

    public void error(){
        System.out.println("方法报错了");
    }
}

f.增强类的配置(插入切点的位置,切点指定,切面配置等)

//增强类:内部存储增强代码
/* 步骤:
*       1.定义方法存储增强代码(具体定义几个方法,根据插入的位置决定)
*       2.使用注解配置,指定插入目标方法的位置
*           前置 @Before
*           后置 @AfterReturning
*           异常 @AfterThrowing
*           最后 @After
*           环绕 @Around
*           try{
*               前置
*               目标方法执行
*               后置
*           }catch(){
*               异常
*           }finally{
*               最后
*           }
*       3.配置切点表达式(选中被插入的方法[切点])
*       4.补全注解
*           ① 加入ioc容器 @Component(只有加入ioc容器,才能取到增强对象,再插入目标对象中)
*           ② 配置切面 @Aspect(切面 = 切点 + 增强)
*/
@Component
@Aspect
public class LogAdvice {
    @Before("execution(* com.mihoyo.service.impl.*.*(..))")
    public void start(){
        System.out.println("方法开始了");
    }

    @After("execution(* com.mihoyo.service.impl.*.*(..))")
    public void after(){
        System.out.println("方法结束了");
    }

    @AfterThrowing("execution(* com.mihoyo.service.impl.*.*(..))")
    public void error(){
        System.out.println("方法报错了");
    }
}

注意:

增强类必须也要加入到 ioc 容器中,因为只有加入ioc 容器,才能取到增强对象,再插入目标对象中。

g. 开启 aop 的配置(开启 aspect 注解的支持)

① xml 方式:

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

    <!-- 进行包扫描-->
    <context:component-scan base-package="com.mihoyo" />
    <!-- 开启aspectj框架注解支持-->
    <aop:aspectj-autoproxy />
</beans>

 ② 配置类方式:

@Configurable
@ComponentScan(basePackages = "com.mihoyo")
@EnableAspectJAutoProxy    //开启aspectj框架注解支持
public class JavaConfig {
}

注意:

为了让包扫描能够扫描到 增强类,basePackages 不能为原本的 com.mihoyo.service,而应修改为上级目录:com.mihoyo

(3)获取通知细节信息

Ⅰ. JointPoint接口

需要获取方法签名、传入的实参等信息时,可以在所有通知方法声明 JoinPoint 类型的形参。

    @Before("execution(* com.mihoyo.service.impl.*.*(..))")
    public void start(JoinPoint joinPoint) {
        //1.获取方法所属的类的信息
        String simpleName = joinPoint.getTarget().getClass().getSimpleName();
        System.out.println(simpleName);
        //2.获取方法名称
        String name = joinPoint.getSignature().getName();
        System.out.println(name);
        //3.获取方法的访问修饰符
        int modifiers = joinPoint.getSignature().getModifiers();
        String s = Modifier.toString(modifiers);//通过反射,将int类型转为字符串类型
        System.out.println(s);
        //3.获取参数列表
        Object[] args = joinPoint.getArgs();
        System.out.println(Arrays.toString(args));
    }

运行结果:

Ⅱ. 方法返回值 

返回通知中,

① 在 @AfterReturning 注解中通过 returning 属性指定一个名称

② 根据 returning 属性设置的名称,在通知方法中声明一个对应名的形参,用于接收返回结果

    @AfterReturning(value = "execution(* com.mihoyo.service.impl.*.*(..))", returning = "result")    //指定result接收
    public void afterReturning(Object result) {  //用result接收返回结果
        System.out.println(result);//2
    }
Ⅲ. 异常对象捕捉

异常通知中,

① 在 @AfterThrowing注解的 throwing 属性指定一个名称

② 根据 throwing 属性设置的名称,在通知方法中声明一个对应名的形参,用于接收目标方法抛出的异常对象

    @AfterThrowing(value = "execution(* com.mihoyo.service.impl.*.*(..))",throwing = "throwable")
    public void afterThrowing(Throwable throwable){
        System.out.println(throwable.getClass().getName());
    }

(4)切点表达式语法

AOP切点表达式:是一种用于指定切点的语言,它可以通过定义匹配规则,来选择需要被切入的目标对象。 

固定语法:execution(1 2 3.4.5(6))

1. 方法访问修饰符

        public / private / ...

2. 方法的返回值类型

        String / int / void

        如果不考虑访问修饰符和返回值类型,这两位可以整合成一起写 *

        但如果要是不考虑,必须两个都不考虑,不能只有一个不考虑(* String  ×) 

3. 包的位置

        具体包:com.mihoyo.service.impl

        单层模糊: com.mihoyo.service.*        ( * 表示单层模糊)

        多层模糊:com..impl        ( .. 表示任意层的模糊)

        细节: ..不能作为开头(com..impl  √         ..impl ×        *..impl √ )

4.类的名称

        具体:CalculatorPureImpl

        模糊:*

        部分模糊:*Impl(以 Impl 结尾)

5.方法名

         语法和类名一致

6.形参列表

        没有参数:()

        有具体参数:(String) /  (Sting,int)

        模糊参数:(..)        有没有参数都可以,多个也可以

        部分模糊:

                (String ..)  第一个参数是 String 类型,后面有没有无所谓

                (..int) 最后一个参数是 int 类型

                (String..int) 第一个是 String,最后一个是 int,中间无所谓

                (..int..) 包含 int 类型


Test:练习以下案例

① 查询某包某类下,访问修饰符是公有,返回值是 int 的全部方法

② 查询某包下类中第一个参数是 String 的方法

③ 查询全部包下,无参数的方法!

④ 查询 com 包下,以 int 参数类型结尾的方法

⑤ 查询指定包下,Service 开头类的私有返回值 int 的无参数方法

答案:

① execution(public int xx.yy.zz.*(..))

② execution(* xx.yy.zz.*(String..))

③ execution(* *..*.*())

④ execution(* com..*.*(..int))

⑤ execution(private int xx.yy.Service*.*())

(5)切点表达式的提取和复用

@Component
@Aspect
public class LogAdvice {
    @Before("execution(* com.mihoyo.service.impl.*.*(..))")
    public void start(){
        System.out.println("方法开始了");
    }

    @After("execution(* com.mihoyo.service.impl.*.*(..))")
    public void after(){
        System.out.println("方法结束了");
    }

    @AfterThrowing("execution(* com.mihoyo.service.impl.*.*(..))")
    public void error(){
        System.out.println("方法报错了");
    }
}

上述案例,我们在编写切点表达式时,发现所有增强方法的切点表达式都相同,造成了冗余

为了解决该问题,我们可以将切点提取,在增强上进行引用。

方案一:当前类中提取

 定义一个空方法,加上 @Pointcut 注解,再在增强注解中引用切点表达式的方法

@Component
@Aspect
public class LogAdvice {
    @Pointcut("execution(* com.mihoyo.service.impl.*.*(..))")
    public void pc(){}

    @Before("pc()")
    public void start(){
        System.out.println("方法开始了");
    }

    @After("pc()")
    public void after(){
        System.out.println("方法结束了");
    }

    @AfterThrowing("pc()")
    public void error(){
        System.out.println("方法报错了");
    }
}

方案二:不同类中引用(推荐)

定义一个存储切点的类,单独维护切点表达式。

其他不同类引用切点,只需添加 类的全限定符 +不同方法名

@Component
public class MyPointCut {
    @Pointcut("execution(* com.mihoyo.service.impl.*.*(..))")
    public void pc1(){}

    @Pointcut("execution(* com..impl.*.*())")
    public void pc2(){}
}
@Component
@Aspect
public class LogAdvice {
    @Before("com.mihoyo.piontcut.MyPointCut.pc1()")
    public void start(){
        System.out.println("方法开始了");
    }

    @After("com.mihoyo.piontcut.MyPointCut.pc1()")
    public void after(){
        System.out.println("方法结束了");
    }

    @AfterThrowing("com.mihoyo.piontcut.MyPointCut.pc1()")
    public void error(){
        System.out.println("方法报错了");
    }
}

注意:

存储切点的类也必须要加入到 ioc 容器中

② 虽然方案二中 类的全限定符 + 方法名 也很长,但它们都是引用关系。

     如果要修改切点,只需修改存储切点的类即可,增强类不用改动。

(6)环绕通知

环绕通知对应整个 try...catch...finally 结构,包括前面四种通知的所有功能。 

原有写法:

@Component
@Aspect
public class TxAdvice {
    @Before("com.mihoyo.piontcut.MyPointCut.pc()")
    public void begin(){
        System.out.println("事务开启");
    }

    @AfterReturning("com.mihoyo.piontcut.MyPointCut.pc()")
    public void commit(){
        System.out.println("事务提交");
    }

    @AfterThrowing("com.mihoyo.piontcut.MyPointCut.pc()")
    public void rollback(){
        System.out.println("事务回滚");
    }
    
    @After("com.mihoyo.piontcut.MyPointCut.pc()")
    public void end(){
        System.out.println("事务结束");
    }
}

环绕通知写法:

@Component
@Aspect
public class TxAroundAdvice {
    @Around("com.mihoyo.piontcut.MyPointCut.pc()")
    public Object transaction(ProceedingJoinPoint joinPoint) {
        //获取目标方法的参数
        Object[] args = joinPoint.getArgs();
        //定义目标方法的返回结果
        Object result = null;
        
        try {
            System.out.println("事务开启");
            //确保目标方法执行
            result = joinPoint.proceed(args);
            System.out.println("事务提交");
        } catch (Throwable e) {
            System.out.println("事务回滚");
            throw new RuntimeException(e);
        } finally {
            System.out.println("事务结束");
        }
        return result;
    }
}

细节:ProceedingJoinPoint  不仅可以获取目标方法的信息,还可以执行目标方法

(7)切面优先级设置

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

        优先级高的切面:外面

        优先级低的切面:里面

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

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

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

@Component
@Aspect
@Order(10)    //指定一个优先级的值,值越小,优先级越高(越高的前置先执行,后置后执行)
public class TxAdvice {
    @Before("com.mihoyo.piontcut.MyPointCut.pc()")
    public void begin(){
        System.out.println("事务开启");
    }

    @AfterReturning("com.mihoyo.piontcut.MyPointCut.pc()")
    public void commit(){
        System.out.println("事务提交");
    }

    @AfterThrowing("com.mihoyo.piontcut.MyPointCut.pc()")
    public void rollback(){
        System.out.println("事务回滚");
    }
    
    @After("com.mihoyo.piontcut.MyPointCut.pc()")
    public void end(){
        System.out.println("事务结束");
    }
}
@Component
@Aspect
@Order(20)
public class LogAdvice {
    @Before("com.mihoyo.piontcut.MyPointCut.pc()")
    public void start(){
        System.out.println("方法开始了");
    }

    @After("com.mihoyo.piontcut.MyPointCut.pc()")
    public void after(){
        System.out.println("方法结束了");
    }

    @AfterThrowing("com.mihoyo.piontcut.MyPointCut.pc()")
    public void error(){
        System.out.println("方法报错了");
    }
}

 运行结果:

 6.Spring AOP基于XML方式实现(了解)

注解方式:

@Component
@Aspect
@Order(10)
public class TxAdvice {
    @Before("com.mihoyo.piontcut.MyPointCut.pc()")
    public void begin(JointPoint jointPoint){
        System.out.println("事务开启");
    }

    @AfterReturning("com.mihoyo.piontcut.MyPointCut.pc()",returning="result")
    public void commit(Object result){
        System.out.println("事务提交");
    }

    @AfterThrowing("com.mihoyo.piontcut.MyPointCut.pc()",throwing="throwable")
    public void rollback(Throwable throwable){
        System.out.println("事务回滚");
    }
    
    @After("com.mihoyo.piontcut.MyPointCut.pc()")
    public void end(){
        System.out.println("事务结束");
    }
}

 xml 方式:

a. 准备增强类

@Component
public class TxAdvice {
    public void begin(){
        System.out.println("事务开启");
    }

    public void commit(){
        System.out.println("事务提交");
    }

    public void rollback(){
        System.out.println("事务回滚");
    }
    
    public void end(){
        System.out.println("事务结束");
    }
}

b.配置 Spring 配置文件

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

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

    <!-- 使用xml标签进行aop的配置:切面配置,声明切点,位置指定 -->
    <aop:config>
        <!-- 声明切点标签 -->
        <aop:pointcut id="pc" expression="execution(* com.mihoyo.service.impl.*.*(..))"/>

        <!-- 切面配置标签 -->
        <aop:aspect ref="txAdvice" order="10">
            <!-- begin -> @Before("pc()") -->
            <aop:before method="begin" pointcut-ref="pc"/>
            <!-- commit -> @After-returning(value="pc()",returning="result")-->
            <aop:after-returning method="commit" pointcut-ref="pc" returning="result"/>
            <!-- rollback -> @After-throwing(value="pc()",throwing="throwable")-->
            <aop:after-throwing method="rollback" pointcut-ref="pc" throwing="throwable"/>
            <!-- end -> @After("pc()")-->
            <aop:after method="end" pointcut-ref="pc"/>
        </aop:aspect>
    </aop:config>
</beans>

五、Spring 声明式事务

1.声明式事务概念

(1)编程式事务

编程式事务:指手动编写程序来管理事务,即通过编写代码的方式直接控制事务的提交和回滚。

在 Java 中,通常使用事务管理器 (如 Spring 中的 PlatformTransactionManager) 来实现编程式事务。

Connection conn = ...;
  
try {
    // 开启事务:关闭事务的自动提交
    conn.setAutoCommit(false);
    // 核心操作
    // 业务代码
    // 提交事务
    conn.commit();
  
}catch(Exception e){
  
    // 回滚事务
    conn.rollBack();
  
}finally{
  
    // 释放数据库连接
    conn.close();
  
}

优点:灵活性高,可以按照自己的需求来控制事务的粒度、模式

缺点:

        细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。

        代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。

(2)声明式事务

声明式事务:指使用注解或 XML 配置的方式来控制事务的提交和回滚。

开发者只需要添加配置即可, 具体事务的实现由第三方框架实现,避免我们直接进行事务操作。

优点:可以将事务的控制和业务逻辑分离开来,提高代码的可读性和可维护性。


 区别:

        编程式事务需要手动编写代码来管理事务;

        声明式事务可以通过配置文件或注解来控制事务。

 (3)Spring事务管理器

Spring声明式事务对应依赖:

        spring-tx: 包含声明式事务实现的基本规范(事务管理器规范接口和事务增强等等)

        spring-jdbc: 包含 DataSource 方式事务管理器实现类 DataSourceTransactionManager

        spring-orm: 包含其他持久层框架的事务管理器实现类例如:Hibernate / Jpa等

Spring声明式事务对应事务管理器接口 :

我们现在要使用的事务管理器是:

org.springframework.jdbc.datasource.DataSourceTransactionManager,将来整合 JDBC方式、JdbcTemplate方式、Mybatis方式的事务实现。

DataSourceTransactionManager类中的主要方法
doBegin()开启事务
doCommit()提交事务
doResume()恢复挂起的事务
doCommit()提交事务
doRollback()回滚事务

2.基于注解的声明式事务

(1)准备工作

a.准备项目

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

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

  <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>6.0.6</version>
      <scope>test</scope>
  </dependency>

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

  <!-- 数据库驱动 和 连接池-->
  <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.25</version>
  </dependency>

  <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.2.8</version>
  </dependency>

  <!-- spring-jdbc -->
  <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>6.0.6</version>
  </dependency>

  <!-- 声明式事务依赖-->
  <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>6.0.6</version>
  </dependency>


  <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aop</artifactId>
      <version>6.0.6</version>
  </dependency>

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

 b.外部配置文件(jdbc.properties)

mihoyo.url=jdbc:mysql://localhost:3306/jdbc
mihoyo.driver=com.mysql.cj.jdbc.Driver
mihoyo.username=root
mihoyo.password=123456

c.配置类

@Configurable
@ComponentScan(basePackages = "com.mihoyo")
@PropertySource(value = "classpath:jdbc.properties")
public class JavaConfig {
    @Value("${mihoyo.username}")
    String username;
    @Value("${mihoyo.password}")
    String password;
    @Value("${mihoyo.url}")
    String url;
    @Value("${mihoyo.driver}")
    String driverClassName;

    @Bean
    public DruidDataSource dataSource() {
        //具体的实例化过程
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setUrl(url);
        dataSource.setDriverClassName(driverClassName);
        //返回结果即可
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }
}

d.准备dao/service层

dao层:

@Repository
public class StudentDao {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    public void updateNameById(String name,Integer id){
        String sql = "update students set name = ? where id = ? ;";
        int rows = jdbcTemplate.update(sql, name, id);
    }

    public void updateAgeById(Integer age,Integer id){
        String sql = "update students set age = ? where id = ? ;";
        jdbcTemplate.update(sql,age,id);
    }
}

service层:

@Service
public class StudentService {
    
    @Autowired
    private StudentDao studentDao;
    
    public void changeInfo(){
        studentDao.updateAgeById(100,1);
        System.out.println("-----------");
        studentDao.updateNameById("test1",1);
    }
}

 e.测试环境搭建

@SpringJUnitConfig(JavaConfig.class)
public class TxTest {

    @Autowired
    private StudentService studentService;

    @Test
    public void  testTx(){
        studentService.changeInfo();
    }
}

(2)基本事务控制

a.配置事务管理器(根据框架选择对应的事务管理器加入到 ioc 容器

@Configurable
@ComponentScan(basePackages = "com.mihoyo")
@PropertySource(value = "classpath:jdbc.properties")
@EnableTransactionManagement    //开启事务注解的支持
public class JavaConfig {
    @Value("${mihoyo.username}")
    String username;
    @Value("${mihoyo.password}")
    String password;
    @Value("${mihoyo.url}")
    String url;
    @Value("${mihoyo.driver}")
    String driverClassName;

    @Bean
    public DruidDataSource dataSource() {
        //具体的实例化过程
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setUrl(url);
        dataSource.setDriverClassName(driverClassName);
        //返回结果即可
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    @Bean
    public TransactionManager transactionManager(DataSource dataSource) {
        //内部要进行事务的操作,是基于数据库连接池
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        //需要连接池对象
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }
}

b.使用声明事务注解 @Transactional

@Service
public class StudentService {
    
    @Autowired
    private StudentDao studentDao;

    @Transactional
    public void changeInfo(){
        studentDao.updateAgeById(100,1);
        System.out.println("-----------");
        studentDao.updateNameById("test1",1);
    }
}

细节:

添加事务:@Transactional(位置:方法 | 类上)

        方法上:当前方法添加事务

        类上:当前类下的所有方法添加事务

(3)事务属性

Ⅰ.只读

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

这样数据库就能够针对查询操作来进行优化,提升查询事务的效率。

所以,推荐事务中只有查询代码,使用只读模式。

// readOnly = true把当前事务设置为只读 默认是false!
@Transactional(readOnly = true)

细节: 

① 如果只读模式中有修改代码,会报异常:Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed

查询根本没有必要添加事务,何谈只读模式?

 一般情况下,都是通过在类上添加注解来添加事务,这样类下的所有方法都有事务。

而事务默认是非只读模式的,查询方法可以通过再次添加注解,设置为只读模式,提高效率。

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

事务在执行过程中,有可能因为遇到某些问题(死锁、网络延迟等),导致程序卡住,从而长时间占用数据库资源。

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

设置超时时间

@Service
public class StudentService {

    @Autowired
    private StudentDao studentDao;

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

细节:

① 默认的超时时间: -1(永远不超时) 

通过设置 timeout = 秒数,能够设置超时时间。一旦超过时间,就会触发事务回滚和释放异常。

② 如果类上设置了事务属性,方法也设置了事务注解,方法会不会生效?

不会生效:方法上的注解覆盖了类上的注解。 

Ⅲ. 事务异常

① 默认情况

默认只针对运行时异常 RuntimeException 回滚,编译时异常不回滚。

@Service
public class StudentService {

    @Autowired
    private StudentDao studentDao;

    @Transactional(readOnly = false)
    public void changeInfo() throws FileNotFoundException {
        studentDao.updateAgeById(100,1);
        //主动抛出一个检查异常,测试! 
        new FileInputStream("xxxx");
        studentDao.updateNameById("test1",1);
    }
}

结果:不会回滚,因为 FileNotFoundException 属于 IOException ,是编译时异常 

② 指定异常回滚:

rollbackFor属性:指定哪些异常类才会回滚,默认是 RuntimeException 和 Error 异常方可回滚。

/**
 * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
 */
@Transactional(readOnly = false,rollbackFor = Exception.class)
public void changeInfo() throws FileNotFoundException {
    studentDao.updateAgeById(100,1);
    //主动抛出一个检查异常,测试! 
    new FileInputStream("xxxx");
    studentDao.updateNameById("test1",1);
}

结果:回滚,因为 rollbackFor 指定的是 Exception,而 FileNotFoundException 属于 Exception。

③ 指定异常不回滚

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

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

@Service
public class StudentService {

    @Autowired
    private StudentDao studentDao;

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

结果: 不会回滚,因为 noRollbackFor 的指定了 FileNotFoundException 不回滚。

Ⅳ. 事务隔离级别 

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

读未提交(Read Uncommitted):事务可以读取未被提交的数据,容易产生脏读、不可重复读和幻读等问题。实现简单但不太安全,一般不用

② 读已提交(Read Committed):事务只能读取已经提交的数据,可以避免脏读问题,但可能引发不可重复读和幻读。

③ 可重复读(Repeatable Read):在一个事务中,相同的查询将返回相同的结果集,不管其他事务对数据做了什么修改。可以避免脏读和不可重复读,但仍有幻读的问题。 

④ 串行化(Serializable):最高的隔离级别,完全禁止了并发,只允许一个事务执行完毕之后才能执行另一个事务。可以避免以上所有问题,但效率较低不适用于高并发场景

@Transactional(isolation = Isolation.REPEATABLE_READ)

细节: 默认是第三个隔离级别,实际开发中推荐设置第二个级别

Ⅴ. 事务传播行为

@Transactional 注解通过 propagation 属性设置事务的传播行为。它的默认值是:

Propagation propagation() default Propagation.REQUIRED;

 propagation 属性的可选值由 org.springframework.transaction.annotation.Propagation 枚举类提供:

名称含义
REQUIRED 默认值如果父方法有事务,就加入;如果没有就新建,自己独立
REQUIRES_NEW不管父方法是否有事务,都新建事务,都是独立的

测试: 

a. 声明两个业务方法

@Service
public class StudentService {

    @Autowired
    private StudentDao studentDao;

    /**
     * 声明两个独立修改数据库的事务业务方法
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void changeAge(){
        studentDao.updateAgeById(99,1);
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void changeName(){
        studentDao.updateNameById("test2",1);
        int i = 1/0;
    }

}

b. 声明一个整合业务方法

@Service
public class TopService {

    @Autowired
    private StudentService studentService;

    @Transactional
    public void  topService(){
        studentService.changeAge();
        studentService.changeName();
    }
}

c. 传播行为测试

@SpringJUnitConfig(classes = JavapConfig.class)
public class TxTest {

    @Autowired
    private StudentService studentService;

    @Autowired
    private TopService topService;

    @Test
    public void  testTx() throws FileNotFoundException {
        topService.topService();
    }
}

结果:

由于父方法存在事务,子方法都是默认值 REQUIRED,所以会加入到父方法的事务中,成为一个事务。所以子方法 2 产生异常后,会进行回滚。即使子方法1没错,也会回滚。最后数据库没有发生任何改变。

如果子方法不是默认值,是 REQUIRES_NEW,那不管父方法有无事务,都会新建事务。此时子方法 2 发生异常回滚,不会影响到子方法 1。


注意:

① 在同一个类中,对于@Transactional注解的方法调用,事务传播行为不会生效。

这是因为Spring框架中使用代理模式实现了事务机制,在同一个类中的方法调用并不经过代理,而是通过对象的方法调用,因此@Transactional注解的设置不会被代理捕获,也就不会产生任何事务传播行为的效果。

② 其他传播行为值(了解):

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值