spring6合集——spring概述以及OCP、DIP、IOC原则

spring6合集------Spring6核心知识点总结

  • 启示录
  • 一、SOLID原则
    • [1. 单一职责原则(SRP)](#1. 单一职责原则(SRP))
    • [2. 开闭原则(OCP)](#2. 开闭原则(OCP))
    • [3. 里氏替换原则(LSP)](#3. 里氏替换原则(LSP))
    • [4. 接口隔离原则(ISP)](#4. 接口隔离原则(ISP))
    • [5. 依赖倒置原则(DIP)](#5. 依赖倒置原则(DIP))
  • [二、控制反转IOC(Inversion of Control)](#二、控制反转IOC(Inversion of Control))
    • [1. 为什么会出现控制反转](#1. 为什么会出现控制反转)
    • [2. 依赖注入(DI)](#2. 依赖注入(DI))
      • [2.1 set注入](#2.1 set注入)
      • [2.2 构造方法注入](#2.2 构造方法注入)
    • [3. set注入专题](#3. set注入专题)
      • [3.1 外部注入Bean](#3.1 外部注入Bean)
      • [3.2 内部注入Bean](#3.2 内部注入Bean)
      • [3.3 注入简单类型的属性](#3.3 注入简单类型的属性)
      • [3.5 注入数组](#3.5 注入数组)
      • [3.6 set注入List集合](#3.6 set注入List集合)
      • [3.8 set注入Map集合](#3.8 set注入Map集合)
      • [3.9 set注入Properties](#3.9 set注入Properties)
      • [3.10 set注入null和空串](#3.10 set注入null和空串)
      • [3.11 set注入特殊字符串](#3.11 set注入特殊字符串)
    • [4. p命名空间注入](#4. p命名空间注入)
    • [5. c命名空间注入](#5. c命名空间注入)
    • [6. util命名空间](#6. util命名空间)
    • [7. 自动装配](#7. 自动装配)
      • [7.1 根据名称自动装配](#7.1 根据名称自动装配)
      • [7.2 根据类型自动装配](#7.2 根据类型自动装配)
    • [8. Spring引入外部属性配置文件](#8. Spring引入外部属性配置文件)
  • 三、Bean的作用域
    • [1. 单例模式(singleton)](#1. 单例模式(singleton))
    • [2. 原型作用域(property)](#2. 原型作用域(property))
    • [3. 其它作用域](#3. 其它作用域)
  • 四、GoF之工厂模式
    • [1. 工厂模式的三种形态](#1. 工厂模式的三种形态)
    • [2. 简单工厂模式](#2. 简单工厂模式)
    • [3. 工厂方法模式](#3. 工厂方法模式)
    • [5. 抽象工厂模式](#5. 抽象工厂模式)
    • [6. 三种工厂模式对比(重点:exclamation:)](#6. 三种工厂模式对比(重点:exclamation:))
  • 五、Bean的实例化方式
    • [1. 构造方法实例化](#1. 构造方法实例化)
    • 2、、通过简单工厂模式实例化
    • [3. 通过factory-bean实例化](#3. 通过factory-bean实例化)
    • [4. 通过FactoryBean接口实例化](#4. 通过FactoryBean接口实例化)
    • [5. BeanFactory和FactoryBean的区别](#5. BeanFactory和FactoryBean的区别)
    • [6. 为什么要学习Bean的不同创建方式](#6. 为什么要学习Bean的不同创建方式)
  • [六. Bean的生命周期](#六. Bean的生命周期)
    • [1. Bean生命周期之5步](#1. Bean生命周期之5步)
    • [2. Bean生命周期之7步](#2. Bean生命周期之7步)
    • [3. Bean生命周期之10步](#3. Bean生命周期之10步)
  • 七、Bean的循环依赖
    • [1. 什么是循环依赖](#1. 什么是循环依赖)
    • [2. singleton下的set注入](#2. singleton下的set注入)
    • [3. property下的set注入](#3. property下的set注入)
    • [4. singleton+构造注入](#4. singleton+构造注入)
    • [5. Spring解决循环依赖的原理和实现](#5. Spring解决循环依赖的原理和实现)
    • [6. 总结](#6. 总结)
  • 八、注解开发
    • [1. 核心组件注解](#1. 核心组件注解)
    • [2. 依赖注入相关注解](#2. 依赖注入相关注解)
  • 九、GOF之代理模式
    • [1. 静态代理](#1. 静态代理)
    • [2. 动态代理(Proxy类)](#2. 动态代理(Proxy类))
      • [2.1 JDK动态代理](#2.1 JDK动态代理)
      • [2.2 CgLib动态代理](#2.2 CgLib动态代理)
  • 十、Aop切面编程
    • [1. Aop介绍](#1. Aop介绍)
    • [2. Aop的七大术语](#2. Aop的七大术语)
      • [2.1 连接点( Joinpoint)](#2.1 连接点( Joinpoint))
      • [2.2 切点(Pointcut)](#2.2 切点(Pointcut))
      • [2.3 通知(Advice)](#2.3 通知(Advice))
      • [2.4 切面(Aspect)](#2.4 切面(Aspect))
      • [2.5 织入(Weaving)](#2.5 织入(Weaving))
      • [2.6 代理对象(Proxy)](#2.6 代理对象(Proxy))
      • [2.7 目标对象(Target)](#2.7 目标对象(Target))
    • [3. 切点表达式](#3. 切点表达式)
    • [4. 使用Spring的Aop编程](#4. 使用Spring的Aop编程)
      • [4.1 准备工作](#4.1 准备工作)
      • [4.2 核心步骤](#4.2 核心步骤)
      • [4.3 通知类型](#4.3 通知类型)
      • [4.4 切面的执行顺序](#4.4 切面的执行顺序)
      • [4.5 简化切点表达式](#4.5 简化切点表达式)
  • 十一、事务
  • 下期预告

启示录

同志们大家好,以及亲爱的作者你好,今天2025年6月19日开启spring6的新篇章,由于之前已经系统学习过spring6,但是碍于没有做笔记,只是跟着写了代码,对于很多的知识点又忘记啦。因此今天开始全面记录和学习spring6,加深记忆, gogogo出发喽!🎉🎉🎉

本文主要参考资料为动力节点老杜的spring6课程,B站可以直接搜到课程,非常的nice,同时也有笔记,我的思路是跟着笔记挑出重点进行总结和复习,出发点可能略有不同,以下是笔记的地址。

https://www.yuque.com/dujubin/ltckqu/kipzgd?singleDoc#p1WjS

一、SOLID原则

SOLID是面向对象设计(Object-Oriented Design, OOD)的五大基本原则的缩写,这些原则旨在提高软件的✨可维护性、✨可扩展性、✨可复用性和✨灵活性。

有点子抽象,一直再说OCP、IOC,是不是还不知道这是五大设计原则中的成员啊!

缩写 全称 中文含义
S Single Responsibility Principle 单一职责原则
O Open/Closed Principle 开闭原则
L Liskov Substitution Principle 里氏替换原则
I Interface Segregation Principle 接口隔离原则
D Dependency Inversion Principle 依赖倒置原则

1. 单一职责原则(SRP)

一个类只能有一个引起它变化的原因,或者说一个类只负责一项职责,只干一类工作

含义 🔍

  • 一个类或方法只干一类事情
  • 每个模块或组件只负责一类事情

示例 ✅

java 复制代码
public class UserController {

	public User getUserInfo();
	
	public List<User> getUserInfo();
	
}
java 复制代码
public class DeptController {

	public Dept getDeptInfo();
	
	public List<Dept> getDeptInfo();
	
}

错误示例 ❌

java 复制代码
public class UserController {

	public User getUserInfo();
	
	public Dept getDeptInfo();
	
}

优点 💡

  • 提高代码可读性
  • 降低耦合度
  • 易于维护和测试

上面的例子很清晰,针对于类来说,SRP原则比较容易实行,在正式的编码中对于不同的业务都有不同的Java类进行实现,当然也有为了方便,在一个类中存在多个类型的业务功能。我只能说,尽量尽量避免,一个优秀的程序员不能这样干

但是在方法中,这个例子就不太好举了,因为可能我们的业务需求就需要整合多项数据并分析得出结论,那SRP原则不就冲突了吗 ?我们根据它的设计初衷进行分析,就是为了降低代码耦合度啊和可读性这些,其实在方法中,我们谨记,如果有重复的代码,提出来成为一个方法,减少耦合度,同时对于有规律的代码块或者说目标明确的代码块,同时又比较臃肿,这类代码我们大多数情况下是为了得出一个结论或者一批数据,过程和其它代码没有关系,那这个方法就可以提出来,虽然它仅你调用,这样可读性就会很高,后面定位问题也非常好定位。

2. 开闭原则(OCP)

对扩展开放,对修改关闭

含义 🔍

  • 类、函数、模块应通过扩展来支持或增加新行为,而不是通过修改代码。

✅ 示例:

java 复制代码
interface Shape {
    double area();
}

class Rectangle implements Shape {
    public double area() { return width * height; }
}

class Circle implements Shape {
    public double area() { return Math.PI * radius * radius; }
}

新增图形只需添加新的子类,无需修改原有逻辑

💡 优点:

  • 提高系统可扩展性
  • 避免因修改导致的副作用

对于OCP原则来说核心就是:别改,你就新增

其实OCP原则才是我们在开发中最容易忽略的点,你一旦动了别人的代码,或者说你自己曾经写过的代码,这就意味了需要全面测试,不要说你有绝对的自信,你永远不会知道线上会因为什么报错,这都是经验之谈。所以在后面的代码中,能尽量将新的方法嵌入到之前的代码或者说直接新增加一个方法(前提是一个全新的功能,避免耦合度过高),如果情况所迫,其实我们还是得在原有基础上进行修改,对于你不知道或者没有完全把握的代码,不要去动,在下面写就行了。

3. 里氏替换原则(LSP)

子类型必须能够替换其基类型。

🔍 含义:

  • 所有引用基类的地方必须能够透明的使用其子类的对象。
  • 子类不能违反父类的行为契约。

📌 关键约束

  • 子类方法参数类型应比父类更宽松

  • 子类返回值类型应比父类更严格

  • 子类不应抛出父类未声明的异常

💡 优点:

  • 提高代码的健壮性和可重用性
  • 支持多态和接口编程

错误示例❌

java 复制代码
class Rectangle {
    protected int width, height;
    
    void setWidth(int w) { width = w; }
    void setHeight(int h) { height = h; }
}

class Square extends Rectangle {
    // 破坏父类行为约束
    void setWidth(int w) { width = height = w; }
}

正确示例✅

java 复制代码
interface Shape {
    int getArea();
}

class Rectangle implements Shape { /*...*/ }
class Square implements Shape { /*...*/ }

对于LSP原则我的理解是,在正常的程序开发中,我们尽量使用接口继承的方式,定义一个通用的接口,让业务组件都实现这个接口,从而达到LSP原则。但是这个原则我们其实用到的并不多,大多数体现在Java的源码中,或者第三方框架的源码中 ,就比如Map,HashMap,LinkedHashMap这种,父子类有严格的要求,HashMap的功能更加复杂,返回值更加详细,但又没有突破父级的限制,这就是典型的LSP原则。

4. 接口隔离原则(ISP)

客户端不应该被迫依赖于他们不使用的接口

🔍 含义:

  • 大而全的接口应该拆分为更小、更具体的接口。
  • 客户端只需知道它们实际使用的方法
  • 减少不必要的依赖和耦合

💡 优点:

  • 提高代码的可读性

对于ISP原则在最新的springCloud项目中非常的典型,我们使用的都是Controller、Service、ServiceImpl、Mapper、Dao等不同的业务包,一般客户端使用的都是Service中的接口,不需要关注内部实现,同时Service下面如果调用数据库的话,也只需要调用Mapper中的接口即可,不需要知道其内部实现,这样的拆分非常的清晰,在大公司中是分Java开发和数据库开发人员的,可能Java开发不需要知道数据库内部怎么实现,只需要调用对应的接口即可。

同时也需要注意,如果你的一个接口需要多个实现类继承,但是其中只有某些接口是共有的,这时候双方可能会有多余的方法继承,这时候可以使用抽象类,也就是public abstract interface ,可以选择性继承。

错误示例❌

java 复制代码
// 违反ISP
interface Worker {
    void work();
    void eat();
    void sleep();
}

class Robot implements Worker {
    void work() { /*...*/ }
    void eat() { /* 机器人不需要吃饭 */ }
    void sleep() { /* 机器人不需要睡觉 */ }
}

// 遵循ISP
interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

interface Sleepable {
    void sleep();
}

class Human implements Workable, Eatable, Sleepable {
    // 实现所有方法
}

class Robot implements Workable {
    // 只实现需要的方法
}

5. 依赖倒置原则(DIP)

高层模块不应该依赖于底层模块,两者都应该依赖于抽象,抽象不应该依赖于细节,细节应该依赖于抽象

上面听起来花里胡哨的,其实核心就一句话:面向接口编程

🔍 含义:

  • 通过抽象(接口或抽象类)进行解耦

  • 减少类之间的直接依赖

  • 提高代码的可测试性和灵活性

就是这张图,这就是DIP原则告诉我们应该做的。

嘿,到这里五个原则就讲完了,那这五个原则其实就是为了给IOC原则做铺垫,注意:IOC是spring中的核心原则,上面SOLID是软件设计需要遵循的五大原则,而spring做到了,实现的原理就是IOC控制反转

二、控制反转IOC(Inversion of Control)

1. 为什么会出现控制反转

Spring的核心就是IOC控制反转,那么为什么要出现这个控制反转,直接通过一个例子来说明。

java 复制代码
public class UserService {
	
	private userDao;

	public UserService () {
		// 一旦替换数据库,这里就要变化
		this.userDao= new UserDaoForMysql();
	} 

	... 具体的业务实现

}

上面的例子是一个简单的数据库查询,但是如果有一天甲方要求mysql数据库不安全,我要替换为其它数据库,如果按照上面那种旧版写法,那一个一个改去吧,很累的,因为耦合度太高了。因为我们没有办法直接new接口,只能new具体的实现类,而为了解耦合,Spring就出现了IOC控制反转,将userDao的创建和关系维护交给容器(Spring)去做,这样代码的改动就很小了,而且在编码中也非常的简洁。

2. 依赖注入(DI)

注意,上面说的IOC是Spring中非常重要的一种思想,而实现这种思想的手段是DI依赖注入💡。

依赖:指的是对象A和B之间的关系,也就是UserService和UserServiceImpl之间的关系。
注入:注入是一种手段,通过这种手段,可以让对象A和B对象产生关系

一般我们说的注入手段其实就是怎么给对象赋值,我们要使用userService就是new,那么注入实际上就是把new的这个过程交出去了,有两种常见的注入方式。

  • set注入:执行对象的set方法给属性赋值。
  • 构造方法注入:使用有参的构造方法给对象赋值。

2.1 set注入

📌 以下的例子来自老杜的笔记。

java 复制代码
package com.powernode.spring6.dao;

/**
 * @author 动力节点
 * @version 1.0
 * @className UserDao
 * @since 1.0
 **/
public class UserDao {

    public void insert(){
        System.out.println("正在保存用户数据。");
    }
}
java 复制代码
package com.powernode.spring6.service;

import com.powernode.spring6.dao.UserDao;

/**
 * @author 动力节点
 * @version 1.0
 * @className UserService
 * @since 1.0
 **/
public class UserService {

    private UserDao userDao;

    // 使用set方式注入,必须提供set方法。
    // 反射机制要调用这个方法给属性赋值的。
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void save(){
        userDao.insert();
    }
}
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="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>

    <bean id="userServiceBean" class="com.powernode.spring6.service.UserService">
        <property name="userDao" ref="userDaoBean"/>
    </bean>

</beans>

📌 实现原理:

  • 通过property标签获取到属性名
  • 通过属性名推断出set方法名
  • 通过反射机制调用set方法给属性赋值

❗️ 需要注意的是:

  • 对象的set方法一定要存在。
  • ref属性是要注入的bean对象的ID,而且不能重复

💡 核心

  • 通过反射机制调用bean的set方法,让两个对象之间产生关系

2.2 构造方法注入

java 复制代码
package com.powernode.spring6.dao;

/**
 * @author 动力节点
 * @version 1.0
 * @className OrderDao
 * @since 1.0
 **/
public class OrderDao {
    public void deleteById(){
        System.out.println("正在删除订单。。。");
    }
}
java 复制代码
package com.powernode.spring6.service;

import com.powernode.spring6.dao.OrderDao;

/**
 * @author 动力节点
 * @version 1.0
 * @className OrderService
 * @since 1.0
 **/
public class OrderService {
    private OrderDao orderDao;

    // 通过反射机制调用构造方法给属性赋值
    public OrderService(OrderDao orderDao) {
        this.orderDao = orderDao;
    }

    public void delete(){
        orderDao.deleteById();
    }
}
xml 复制代码
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService">
  <!--index="0"表示构造方法的第一个参数,将orderDaoBean对象传递给构造方法的第一个参数。-->
  <constructor-arg index="0" ref="orderDaoBean"/>
</bean>

不使用参数的下标,使用参数的名字也可以

xml 复制代码
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>

<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService">
  <!--这里使用了构造方法上参数的名字-->
  <constructor-arg name="orderDao" ref="orderDaoBean"/>
  <constructor-arg name="userDao" ref="userDaoBean"/>
</bean>

<bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>

不指定下标,不指定名字,也可以

xml 复制代码
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService">
  <!--没有指定下标,也没有指定参数名字-->
  <constructor-arg ref="orderDaoBean"/>
  <constructor-arg ref="userDaoBean"/>
</bean>

<bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>

当然,不指定顺序,也可以

xml 复制代码
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>

<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService">
  <!--顺序已经和构造方法的参数顺序不同了-->
  <constructor-arg ref="userDaoBean"/>
  <constructor-arg ref="orderDaoBean"/>
</bean>

<bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>

3. set注入专题

3.1 外部注入Bean

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="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>

    <bean id="userServiceBean" class="com.powernode.spring6.service.UserService">
        <property name="userDao" ref="userDaoBean"/>
    </bean>

</beans>

📌 特点

  • bean定义到外面,在property标签中使用ref属性进行注入,是最常用的方式

3.2 内部注入Bean

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="userServiceBean" class="com.powernode.spring6.service.UserService">
        <property name="userDao">
            <bean class="com.powernode.spring6.dao.UserDao"/>
        </property>
    </bean>

</beans>

3.3 注入简单类型的属性

java 复制代码
package com.powernode.spring6.beans;

/**
 * @author 动力节点
 * @version 1.0
 * @className User
 * @since 1.0
 **/
public class User {
    private int age;

    public void setAge(int age) {
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "User{" +
                "age=" + age +
                '}';
    }
}
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="userBean" class="com.powernode.spring6.beans.User">
        <!--如果像这种int类型的属性,我们称为简单类型,这种简单类型在注入的时候要使用value属性,不能使用ref-->
        <!--<property name="age" value="20"/>-->
        <property name="age">
            <value>20</value>
        </property>
    </bean>
</beans>

❗️ ❗️ ❗️

有人说这种注入是为了什么,有什么作用,其实这就是@Value注解的前身。我们经常将一些固定的配置放在yml或者properties文件中,如果在程序中想要使用,一般都是使用@Value注解,而上面的代码就是在@Value注解没有出现之前的替代方案。

java 复制代码
@Component
public class DatabaseConfig {
    @Value("${db.url}")
    private String url;
    
    @Value("${db.username}")
    private String username;
    
    @Value("${db.password}")
    private String password;
    
    @Value("${db.pool.size:10}")  // 默认值10
    private int poolSize;
    
    // getters...
}
xml 复制代码
db.url=jdbc:mysql://localhost:3306/mydb
db.username=admin
db.password=secret
db.pool.size=20

set注入简单数据类型如下

● 基本数据类型

● 基本数据类型对应的包装类

● String或其他的CharSequence子类

● Number子类

● Date子类

● Enum子类

● URI

● URL

● Temporal子类

● Locale

● Class

● 还包括以上简单值类型对应的数组类型。

3.5 注入数组

📌当注入数组类型是简单类型时

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="person" class="com.powernode.spring6.beans.Person">
        <property name="favariteFoods">
            <array>
                <value>鸡排</value>
                <value>汉堡</value>
                <value>鹅肝</value>
            </array>
        </property>
    </bean>
</beans>

📌 当数组中是非简单类型(对象)时使用ref注入

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="goods1" class="com.powernode.spring6.beans.Goods">
        <property name="name" value="西瓜"/>
    </bean>

    <bean id="goods2" class="com.powernode.spring6.beans.Goods">
        <property name="name" value="苹果"/>
    </bean>

    <bean id="order" class="com.powernode.spring6.beans.Order">
        <property name="goods">
            <array>
                <!--这里使用ref标签即可-->
                <ref bean="goods1"/>
                <ref bean="goods2"/>
            </array>
        </property>
    </bean>

</beans>

💡 要点

  • 如果数组中是简单类型,使用value标签。
  • 如果数组中是非简单类型,使用ref标签。

3.6 set注入List集合

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="peopleBean" class="com.powernode.spring6.beans.People">
        <property name="phones">
            <set>
                <!--非简单类型可以使用ref,简单类型使用value-->
                <value>110</value>
                <value>110</value>
                <value>120</value>
                <value>120</value>
                <value>119</value>
                <value>119</value>
            </set>
        </property>
    </bean>
</beans>

💡 要点:

  • 使用set标签
  • set集合中元素是简单类型的使用value标签,反之使用ref标签

3.8 set注入Map集合

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="peopleBean" class="com.powernode.spring6.beans.People">
        <property name="addrs">
            <map>
                <!--如果key不是简单类型,使用 key-ref 属性-->
                <!--如果value不是简单类型,使用 value-ref 属性-->
                <entry key="1" value="北京大兴区"/>
                <entry key="2" value="上海浦东区"/>
                <entry key="3" value="深圳宝安区"/>
            </map>
        </property>
    </bean>
</beans>

💡 要点

  • 使用map标签
  • 如果key是简单类型,使用 key 属性,反之使用 key-ref 属性。
  • 如果value是简单类型,使用 value 属性,反之使用 value-ref 属性。

3.9 set注入Properties

📌 Properties继承java.util.Hashtable,所以Properties也是一个Map集合

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="peopleBean" class="com.powernode.spring6.beans.People">
        <property name="properties">
            <props>
                <prop key="driver">com.mysql.cj.jdbc.Driver</prop>
                <prop key="url">jdbc:mysql://localhost:3306/spring</prop>
                <prop key="username">root</prop>
                <prop key="password">123456</prop>
            </props>
        </property>
    </bean>
</beans>

💡 要点

  • 使用标签嵌套标签完成。

3.10 set注入null和空串

📌 注入空串的两种方式

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="vipBean" class="com.powernode.spring6.beans.Vip">
        <!--空串的第一种方式-->
        <!--<property name="email" value=""/>-->
        <!--空串的第二种方式-->
        <property name="email">
            <value/>
        </property>
    </bean>

</beans>

📌 注入null的两种方式

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="vipBean" class="com.powernode.spring6.beans.Vip" />

</beans>

<?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="vipBean" class="com.powernode.spring6.beans.Vip">
        <property name="email">
            <null/>
        </property>
    </bean>

</beans>

💡 要点

  • 注入空字符串可以使用<value/>和value=''
  • 注入null可以使用<null/>标签或者不给属性赋值

3.11 set注入特殊字符串

📌 xml中的五个特殊字符串 <、>、'、"、&

💡第一种解决方案,使用特殊符号转义

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

💡 第二种解决方案,将含有特殊符号的字符串放到:<![CDATA[]]> 当中。因为放在CDATA区中的数据不会被XML文件解析器解析

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

    <bean id="mathBean" class="com.powernode.spring6.beans.Math">
        <property name="result">
            <!--只能使用value标签-->
            <value><![CDATA[2 < 3]]></value>
        </property>
    </bean>

</beans>

4. p命名空间注入

📌 目的:简化配置

📌 前提条件

  1. 添加p命名空间的配置信息:xmlns:p="http://www.springframework.org/schema/p"
  2. p命名空间是基于setter方法注入的,需要对应的setter方法
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:p="http://www.springframework.org/schema/p"
       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="customerBean" class="com.powernode.spring6.beans.Customer" p:name="zhangsan" p:age="20"/>

</beans>

5. c命名空间注入

📌 目的:简化配置,不过这里简化的是构造方法的注入。

📌 前提条件

  1. 添加p命名空间的配置信息:xmlns:c="http://www.springframework.org/schema/c"
  2. 需要提供构造方法
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:c="http://www.springframework.org/schema/c"
       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="myTimeBean" class="com.powernode.spring6.beans.MyTime" c:year="1970" c:month="1" c:day="1"/>-->

    <bean id="myTimeBean" class="com.powernode.spring6.beans.MyTime" c:_0="2008" c:_1="8" c:_2="8"/>

</beans>

6. util命名空间

📌 目的:配置复用,也就是定义的对象可以使用在多个Bean中。

📌 前提条件

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:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <util:properties id="prop">
        <prop key="driver">com.mysql.cj.jdbc.Driver</prop>
        <prop key="url">jdbc:mysql://localhost:3306/spring</prop>
        <prop key="username">root</prop>
        <prop key="password">123456</prop>
    </util:properties>

    <bean id="dataSource1" class="com.powernode.spring6.beans.MyDataSource1">
        <property name="properties" ref="prop"/>
    </bean>

    <bean id="dataSource2" class="com.powernode.spring6.beans.MyDataSource2">
        <property name="properties" ref="prop"/>
    </bean>
</beans>

7. 自动装配

7.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userService" class="com.powernode.spring6.service.UserService" autowire="byName"/>
    
    <bean id="aaa" class="com.powernode.spring6.dao.UserDao"/>

</beans>

📌 关键点:

  1. 添加属性:autowire="byName"
  2. UserSerivice类中有一个对象aaa,必须拥有aaa对象的set方法,同时名称和配置文件中的Bean的id相同。

7.2 根据类型自动装配

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="accountService" class="com.powernode.spring6.service.AccountService" autowire="byType"/>

    <bean id="x" class="com.powernode.spring6.dao.AccountDao"/>
    <bean id="y" class="com.powernode.spring6.dao.AccountDao"/>

</beans>

📌 要点:

  1. 添加属性:autowire="byType"
  2. 底层同样基于set方法注入,但是要注意,同一个配置文件中不能同时拥有两个相同类型的Bean

8. Spring引入外部属性配置文件

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="jdbc.properties"/>
    
    <bean id="dataSource" class="com.powernode.spring6.beans.MyDataSource">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
    </bean>
</beans>
xml 复制代码
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/spring
username=root
password=root123

📌 要点

  1. 引入依赖:xmlns:context="http://www.springframework.org/schema/context"
  2. 使用context标签引入对应的文件:<context:property-placeholder location="jdbc.properties"/>
  3. 使用value属性,用${}符号获取对应的值。

三、Bean的作用域

1. 单例模式(singleton)

默认情况下,Spring的Ioc容器创建的Bean对象是单例的,这个单例的含义是:每个Spring容器中,这个Bean对应一个唯一的实例,也就是内存地址相同。

为什么默认采用单例?

  • 性能考虑:减少对象创建和销毁的开销

  • 资源共享:适合无状态的Bean共享使用

  • 设计合理性:大多数情况下,服务类对象不需要多个实例

2. 原型作用域(property)

原型作用域与单例模式相反,每次获取Bean实例的时候都会创建新的对象,代码中实现如下:

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="sb" class="com.powernode.spring6.beans.SpringBean" scope="prototype" />

</beans>

❗️ ❗️ ❗️ 注意,如果要使用原型作用域,必须了解以下几点:

  1. Spring会在每次调用时创建新的对象,但是不会主动去销毁!
  2. 原型作用域适用的情况是需要线程安全的场景,或者说对象中的属性是隔离的,不同方法不能互相影响。
java 复制代码
// 假设有以下Bean类
public class SpringBean {

	// 这个属性是隔离的,不同方法不能互相影响
    private int counter;
    
    public void increment() {
        counter++;
    }
    
    public int getCount() {
        return counter;
    }
}

❓ 怎么手动销毁原型作用域

  1. 实现 DisposableBean 接口
java 复制代码
public class SpringBean implements DisposableBean {
    private int counter;
    
    public void increment() {
        counter++;
    }
    
    public int getCount() {
        return counter;
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("SpringBean 实例正在被销毁,执行清理工作...");
        // 在这里释放资源,如关闭文件、数据库连接等
    }
}
  1. xml中配置destroy-method
xml 复制代码
<bean id="sb" class="com.powernode.spring6.beans.SpringBean" scope="prototype" 
      destroy-method="customDestroy"/>

💡 真实使用场景

java 复制代码
public class PrototypeBeanTest {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = 
            new ClassPathXmlApplicationContext("spring.xml");
        
        // 获取 prototype bean
        SpringBean bean1 = context.getBean("sb", SpringBean.class);
        SpringBean bean2 = context.getBean("sb", SpringBean.class);
        
        // 使用bean...
        
        // 手动触发销毁(Spring不会自动调用)
        // 需要根据实际实现选择调用方式
        if (bean1 instanceof DisposableBean) {
            try {
                ((DisposableBean) bean1).destroy();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        
        // 或者如果使用destroy-method方式
        // 可以通过反射调用指定方法
        try {
            Method destroyMethod = bean2.getClass().getMethod("customDestroy");
            destroyMethod.invoke(bean2);
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        context.close();
    }
}

3. 其它作用域

● singleton:默认的,单例。

● prototype:原型。每调用一次getBean()方法则获取一个新的Bean对象。或每次注入的时候都是新对象。

● request:一个请求对应一个Bean。仅限于在WEB应用中使用

● session:一个会话对应一个Bean。仅限于在WEB应用中使用

● global session:portlet应用中专用的 。如果在Servlet的WEB应用中使用global session的话,和session一个效果。(portlet和servlet都是规范。servlet运行在servlet容器中,例如Tomcat。portlet运行在portlet容器中。)

● application:一个应用对应一个Bean。仅限于在WEB应用中使用

● websocket:一个websocket生命周期对应一个Bean。仅限于在WEB应用中使用

● 自定义scope:很少使用。

四、GoF之工厂模式

📌 设计模式:一种可以被重复利用的解决方案

📌 GoF(Gang of Four),中文名------四人组。

《Design Patterns: Elements of Reusable Object-Oriented Software》(即《设计模式》一书),1995年由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著。这几位作者常被称为"四人组(Gang of Four)"。

📌 该书中描述了23种设计模式。我们平常所说的设计模式就是指这23种设计模式。不过除了GoF23种设计模式之外,还有其它的设计模式,比如:JavaEE的设计模式(DAO模式、MVC模式等)。

📌 GoF23种设计模式可分为三大类:

  • 创建型(5个):解决对象创建问题
    单例模式
    工厂方法模式
    抽象工厂模式
    ■ 建造者模式
    原型模式
  • 结构型(7个):一些类或对象组合在一起的经典结构
    ■ 代理模式
    ■ 装饰模式
    ■ 适配器模式
    ■ 组合模式
    ■ 享元模式
    ■ 外观模式
    ■ 桥接模式
  • 行为型(11个):解决类或对象之间的交互问题
    ■ 策略模式
    ■ 模板方法模式
    ■ 责任链模式
    ■ 观察者模式
    ■ 迭代子模式
    ■ 命令模式
    ■ 备忘录模式
    ■ 状态模式
    ■ 访问者模式
    ■ 中介者模式
    ■ 解释器模式

1. 工厂模式的三种形态

  1. 简单工厂模式:不属于23中设计模式,又叫做静态工厂方法模式,实际就是工厂模式的特殊实现(简化版)。
  2. 工厂方法模式:23种设计模式之一。
  3. 抽象工厂模式:23种设计模式之一。

2. 简单工厂模式

📌 三个主要角色

  • 抽象产品:某一类产品的统一父类,比如美食。
  • 具体产品:某一类产品的具体代表,比如火鸡面。
  • 工厂类:根据类型可以生产处具体的美食。

📌 抽象产品角色

java 复制代码
package com.powernode.factory;

/**
 * 武器(抽象产品角色)
 * @author 动力节点
 * @version 1.0
 * @className Weapon
 * @since 1.0
 **/
public abstract class Weapon {
    /**
     * 所有的武器都有攻击行为
     */
    public abstract void attack();
}

📌 具体产品角色

java 复制代码
package com.powernode.factory;

/**
 * 坦克(具体产品角色)
 * @author 动力节点
 * @version 1.0
 * @className Tank
 * @since 1.0
 **/
public class Tank extends Weapon{
    @Override
    public void attack() {
        System.out.println("坦克开炮!");
    }
}

📌 工厂类角色

java 复制代码
package com.powernode.factory;

/**
 * 工厂类角色
 * @author 动力节点
 * @version 1.0
 * @className WeaponFactory
 * @since 1.0
 **/
public class WeaponFactory {
    /**
     * 根据不同的武器类型生产武器
     * @param weaponType 武器类型
     * @return 武器对象
     */
    public static Weapon get(String weaponType){
        if (weaponType == null || weaponType.trim().length() == 0) {
            return null;
        }
        Weapon weapon = null;
        if ("TANK".equals(weaponType)) {
            weapon = new Tank();
        } else if ("FIGHTER".equals(weaponType)) {
            weapon = new Fighter();
        } else if ("DAGGER".equals(weaponType)) {
            weapon = new Dagger();
        } else {
            throw new RuntimeException("不支持该武器!");
        }
        return weapon;
    }
}

📌 客户端角色

java 复制代码
package com.powernode.factory;

/**
 * @author 动力节点
 * @version 1.0
 * @className Client
 * @since 1.0
 **/
public class Client {
    public static void main(String[] args) {
        Weapon weapon1 = WeaponFactory.get("TANK");
        weapon1.attack();

        Weapon weapon2 = WeaponFactory.get("FIGHTER");
        weapon2.attack();

        Weapon weapon3 = WeaponFactory.get("DAGGER");
        weapon3.attack();
    }
}

📌 简单工厂模式的优点:

  • 客户端不需要关注细节,直接根据参数类型得到对应的对象,初步实现了责任的分离。客户端只负责"消费",工厂负责"生产",生产和消费分离

📌 简单工厂模式的缺点:

  • 工厂类集中了所有产品的生产逻辑,可以称之为上帝类,一旦出现问题则系统瘫痪
  • 不符合OCP开闭原则,进行系统扩展时需要修改工厂类,而不是新增。

3. 工厂方法模式

📌 角色:

  • 抽象工厂:申明工厂方法,返回抽象产品类型,就是工厂的父类或者父接口。
  • 具体工厂:重写抽象工厂方法,返回具体的产品实例。
  • 抽象产品
  • 具体产品

📌 抽象产品角色

java 复制代码
package com.powernode.factory;

/**
 * 武器类(抽象产品角色)
 * @author 动力节点
 * @version 1.0
 * @className Weapon
 * @since 1.0
 **/
public abstract class Weapon {
    /**
     * 所有武器都有攻击行为
     */
    public abstract void attack();
}

📌 具体产品角色

java 复制代码
package com.powernode.factory;

/**
 * 具体产品角色
 * @author 动力节点
 * @version 1.0
 * @className Gun
 * @since 1.0
 **/
public class Gun extends Weapon{
    @Override
    public void attack() {
        System.out.println("开枪射击!");
    }
}

📌 抽象工厂角色

java 复制代码
package com.powernode.factory;

/**
 * 武器工厂接口(抽象工厂角色)
 * @author 动力节点
 * @version 1.0
 * @className WeaponFactory
 * @since 1.0
 **/
public interface WeaponFactory {
    Weapon get();
}

📌 具体工厂角色

java 复制代码
package com.powernode.factory;

/**
 * 具体工厂角色
 * @author 动力节点
 * @version 1.0
 * @className GunFactory
 * @since 1.0
 **/
public class GunFactory implements WeaponFactory{
    @Override
    public Weapon get() {
        return new Gun();
    }
}

📌 客户端

java 复制代码
package com.powernode.factory;

/**
 * @author 动力节点
 * @version 1.0
 * @className Client
 * @since 1.0
 **/
public class Client {
    public static void main(String[] args) {
        WeaponFactory factory = new GunFactory();
        Weapon weapon = factory.get();
        weapon.attack();

        WeaponFactory factory1 = new FighterFactory();
        Weapon weapon1 = factory1.get();
        weapon1.attack();
    }
}

💡 工厂方法模式就是为了解决简单工厂的弊端:不符合OCP开闭原则的问题,在上面的例子中,如果后期要扩展的话,我们只需要增加对应的产品和抽象工厂以及具体工厂,没有修改原来的代码,在其基础上进行扩展。

💡 优点:

  • 客户端可以直接根据名称创建对象。
  • 扩展性高,只需要扩展工厂类即可。
  • 屏蔽产品的具体实现,只需要关心产品的接口。

❌ 缺点:

  • 类爆炸,需要不断的扩展工厂,增加了系统的复杂度。

5. 抽象工厂模式

📌 角色:

  • 抽象工厂:申明工厂方法,返回抽象产品类型,就是工厂的父类或者父接口。
  • 具体工厂:重写抽象工厂方法,返回具体的产品实例。
  • 抽象产品
  • 具体产品

武器产品族

java 复制代码
package com.powernode.product;

/**
 * 武器产品族
 * @author 动力节点
 * @version 1.0
 * @className Weapon
 * @since 1.0
 **/
public abstract class Weapon {
    public abstract void attack();
}
java 复制代码
package com.powernode.product;

/**
 * 武器产品族中的产品等级1
 * @author 动力节点
 * @version 1.0
 * @className Gun
 * @since 1.0
 **/
public class Gun extends Weapon{
    @Override
    public void attack() {
        System.out.println("开枪射击!");
    }
}
java 复制代码
package com.powernode.product;

/**
 * 武器产品族中的产品等级2
 * @author 动力节点
 * @version 1.0
 * @className Dagger
 * @since 1.0
 **/
public class Dagger extends Weapon{
    @Override
    public void attack() {
        System.out.println("砍丫的!");
    }
}

水果产品族

java 复制代码
package com.powernode.product;

/**
 * 水果产品族
 * @author 动力节点
 * @version 1.0
 * @className Fruit
 * @since 1.0
 **/
public abstract class Fruit {
    /**
     * 所有果实都有一个成熟周期。
     */
    public abstract void ripeCycle();
}
java 复制代码
package com.powernode.product;

/**
 * 水果产品族中的产品等级1
 * @author 动力节点
 * @version 1.0
 * @className Orange
 * @since 1.0
 **/
public class Orange extends Fruit{
    @Override
    public void ripeCycle() {
        System.out.println("橘子的成熟周期是10个月");
    }
}
java 复制代码
package com.powernode.product;

/**
 * 水果产品族中的产品等级1
 * @author 动力节点
 * @version 1.0
 * @className Orange
 * @since 1.0
 **/
public class Orange extends Fruit{
    @Override
    public void ripeCycle() {
        System.out.println("橘子的成熟周期是10个月");
    }
}
java 复制代码
package com.powernode.product;

/**
 * 水果产品族中的产品等级2
 * @author 动力节点
 * @version 1.0
 * @className Apple
 * @since 1.0
 **/
public class Apple extends Fruit{
    @Override
    public void ripeCycle() {
        System.out.println("苹果的成熟周期是8个月");
    }
}

抽象工厂类

java 复制代码
package com.powernode.factory;

import com.powernode.product.Fruit;
import com.powernode.product.Weapon;

/**
 * 抽象工厂
 * @author 动力节点
 * @version 1.0
 * @className AbstractFactory
 * @since 1.0
 **/
public abstract class AbstractFactory {
    public abstract Weapon getWeapon(String type);
    public abstract Fruit getFruit(String type);
}

具体工厂类

java 复制代码
package com.powernode.factory;

import com.powernode.product.Dagger;
import com.powernode.product.Fruit;
import com.powernode.product.Gun;
import com.powernode.product.Weapon;

/**
 * 武器族工厂
 * @author 动力节点
 * @version 1.0
 * @className WeaponFactory
 * @since 1.0
 **/
public class WeaponFactory extends AbstractFactory{

    public Weapon getWeapon(String type){
        if (type == null || type.trim().length() == 0) {
            return null;
        }
        if ("Gun".equals(type)) {
            return new Gun();
        } else if ("Dagger".equals(type)) {
            return new Dagger();
        } else {
            throw new RuntimeException("无法生产该武器");
        }
    }

    @Override
    public Fruit getFruit(String type) {
        return null;
    }
}
java 复制代码
package com.powernode.factory;

import com.powernode.product.*;

/**
 * 水果族工厂
 * @author 动力节点
 * @version 1.0
 * @className FruitFactory
 * @since 1.0
 **/
public class FruitFactory extends AbstractFactory{
    @Override
    public Weapon getWeapon(String type) {
        return null;
    }

    public Fruit getFruit(String type){
        if (type == null || type.trim().length() == 0) {
            return null;
        }
        if ("Orange".equals(type)) {
            return new Orange();
        } else if ("Apple".equals(type)) {
            return new Apple();
        } else {
            throw new RuntimeException("我家果园不产这种水果");
        }
    }
}
java 复制代码
package com.powernode.client;

import com.powernode.factory.AbstractFactory;
import com.powernode.factory.FruitFactory;
import com.powernode.factory.WeaponFactory;
import com.powernode.product.Fruit;
import com.powernode.product.Weapon;

/**
 * @author 动力节点
 * @version 1.0
 * @className Client
 * @since 1.0
 **/
public class Client {
    public static void main(String[] args) {
        // 客户端调用方法时只面向AbstractFactory调用方法。
        AbstractFactory factory = new WeaponFactory(); // 注意:这里的new WeaponFactory()可以采用 简单工厂模式 进行隐藏。
        Weapon gun = factory.getWeapon("Gun");
        Weapon dagger = factory.getWeapon("Dagger");

        gun.attack();
        dagger.attack();

        AbstractFactory factory1 = new FruitFactory(); // 注意:这里的new FruitFactory()可以采用 简单工厂模式 进行隐藏。
        Fruit orange = factory1.getFruit("Orange");
        Fruit apple = factory1.getFruit("Apple");

        orange.ripeCycle();
        apple.ripeCycle();
    }
}

✅ 优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象

❌ 缺点:产品族扩展非常困难,要增加一个系列的某一产品,既要在AbstractFactory里加代码,又要在具体的里面加代码。

6. 三种工厂模式对比(重点❗️)

模式 核心区别 适用场景
简单工厂 一个工厂类通过条件判断创建所有产品。 产品种类少,逻辑简单。
工厂方法 一个工厂类只生产一种产品,通过子类扩展。 需要灵活扩展单一产品类型。
抽象工厂 一个工厂类生产一个产品族(多个相关产品)。 需要保证一组产品的一致性(如跨平台)。

对比总结

  • 简单工厂:集中式管理,但违反开闭原则(新增产品需修改工厂类)。
  • 工厂方法:单一职责,支持扩展,但每个产品需对应一个工厂类。
  • 抽象工厂:解决产品族协同问题,但扩展新产品类型需修改接口。

注意了,如果在反复阅读之后还是不懂上面三种工厂模式的区别,不妨听我用大白话叙述一下,用我们生活中常见的例子进行说明。

  • 简单工厂:小商店
  • 工厂方法:超市
  • 抽象工厂:商超

拿简单工厂来说,因为其只有一个核心的工厂类(上帝类),所以并不太适合复杂的场景,因为其并不符合OCP开闭原则,一旦业务复杂,扩展将会很复杂,但是如果我们的业务比较简单,事实上可以使用简单工厂 ,这和小商店是一样的,里面有很多种类的东西,但其实类型并不是很复杂,一个售货员就可以管理和出售,如果规模大,可以多雇几个,但实际上内部的实现逻辑是比较简单的。

接上,如果小商店扩展扩展发现,管理逐渐费劲,而且效率不够高的时候,他就需要进行拆分,进化为超市,也就是工厂方法模式,对于不同种类的商品进行分类,并且招聘对应的售货员(工厂类),如果后期需要扩展,那就增加对应商品的分类,同时招收售货员。这样一来管理就变得方便了,在一定承受范围内可以继续扩展。

继续,在不断发展之下,这个老板又想继续扩展,而且想要增加不同类型和系列的产品比如娱乐、休息和衣服等模块,所以他成立了一个商超,也就是抽象工厂模式。到这一步普通的管理就显得微不足道了,如果不断的扩展是很麻烦的,所以他成立了一个管理部门(抽象工厂)由这个管理部门去吸纳不同模块的管理人才,然后不同模块的管理者在根据职责实现对应产品的创建、宣传、售卖 等等,但是如果要继续扩展则需要从管理层开始,不断的一层一层维护,直到最后的具体商品,同时也需要改变原有的管理层架构。这样做的好处就是不同的但又类型统一的模块都在一起,用户可以根据需求直接面向抽象工厂,由抽象工厂对应到具体的工厂,最后得到用户的产品。

在这种设计模式下,商超的魅力就在于解耦,用户不需要直面具体的产品,直接面向销售或者大厅管理者,由他们带领用户去到指定的模块,再由对应模块的负责人对接,然后拿到具体的产品。解耦解耦,无非就是将一个需求实现的过程拆分,每一步都有清晰的职责,虽然过程复杂,但是实现简单有效。这样的痛点就是怎样维护这个过程,而这个过程对于用户来说是隐藏的。

再到我们的互联网公司,越大的公司,每一个员工的职责越详细,板块越清晰,但需要一些特殊的人才来将其管理和汇总,我们可以理解为中枢。

其实到头来不过一句话,没有什么是加一层解决不了的,仔细想想这一步一步的过程不就是多加了一层吗?

五、Bean的实例化方式

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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userBean" class="com.powernode.spring6.bean.User"/>

</beans>

2、、通过简单工厂模式实例化

java 复制代码
package com.powernode.spring6.bean;

/**
 * @author 动力节点
 * @version 1.0
 * @className Vip
 * @since 1.0
 **/
public class Vip {
}
java 复制代码
package com.powernode.spring6.bean;

/**
 * @author 动力节点
 * @version 1.0
 * @className VipFactory
 * @since 1.0
 **/
public class VipFactory {
    public static Vip get(){
        return new Vip();
    }
}
xml 复制代码
<bean id="vipBean" class="com.powernode.spring6.bean.VipFactory" factory-method="get"/>
java 复制代码
@Test
public void testSimpleFactory(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    Vip vip = applicationContext.getBean("vipBean", Vip.class);
    System.out.println(vip);
}

3. 通过factory-bean实例化

java 复制代码
package com.powernode.spring6.bean;

/**
 * @author 动力节点
 * @version 1.0
 * @className Order
 * @since 1.0
 **/
public class Order {
}
java 复制代码
package com.powernode.spring6.bean;

/**
 * @author 动力节点
 * @version 1.0
 * @className OrderFactory
 * @since 1.0
 **/
public class OrderFactory {
    public Order get(){
        return new Order();
    }
}
xml 复制代码
<bean id="orderFactory" class="com.powernode.spring6.bean.OrderFactory"/>
<bean id="orderBean" factory-bean="orderFactory" factory-method="get"/>
java 复制代码
@Test
public void testSelfFactoryBean(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    Order orderBean = applicationContext.getBean("orderBean", Order.class);
    System.out.println(orderBean);
}

实质就是通过工厂方法模式创建

4. 通过FactoryBean接口实例化

java 复制代码
package com.powernode.spring6.bean;

/**
 * @author 动力节点
 * @version 1.0
 * @className Person
 * @since 1.0
 **/
public class Person {
}
java 复制代码
package com.powernode.spring6.bean;

import org.springframework.beans.factory.FactoryBean;

/**
 * @author 动力节点
 * @version 1.0
 * @className PersonFactoryBean
 * @since 1.0
 **/
public class PersonFactoryBean implements FactoryBean<Person> {

    @Override
    public Person getObject() throws Exception {
        return new Person();
    }

    @Override
    public Class<?> getObjectType() {
        return null;
    }

    @Override
    public boolean isSingleton() {
        // true表示单例
        // false表示原型
        return true;
    }
}
xml 复制代码
<bean id="personBean" class="com.powernode.spring6.bean.PersonFactoryBean"/>
java 复制代码
@Test
public void testFactoryBean(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    Person personBean = applicationContext.getBean("personBean", Person.class);
    System.out.println(personBean);

    Person personBean2 = applicationContext.getBean("personBean", Person.class);
    System.out.println(personBean2);
}

FactoryBean在Spring中是一个接口。被称为"工厂Bean"。"工厂Bean"是一种特殊的Bean。所有的"工厂Bean"都是用来协助Spring框架来创建其他Bean对象的

5. BeanFactory和FactoryBean的区别

  • BeanFactory是一个工厂,可以用来创建Bean对象。
  • FactoryBean是一种Bean,他是Spring中用来辅助创建其它Bean对象的一种特殊Bean。

6. 为什么要学习Bean的不同创建方式

📌 其主要目的是让我们更好的理解Spring的底层哲学,就是最重要的Ioc控制反转思想的实现,主要体现在:

  • 解耦
  • 扩展性

📌 为什么我们在看这一章的时候感觉很懵逼,没什么用,因为在正常开发中是用不到的,大多数使用在框架的开发和程序中特殊业务下需要统一提供的功能,这些都是架构师来做的。比如简单工厂模式,我们可以用在不同数据源的切换上。

xml 复制代码
<bean id="dataSource" class="com.example.DataSourceFactory" factory-method="createDataSource"/>
java 复制代码
public class DataSourceFactory {
    public static DataSource createDataSource() {
        return new HikariDataSource(); // 根据配置返回不同实现
    }
}

📌 或者说在mybatis和shiro中也是常用的,不能每一次数据库请求都需要创建一次连接吧,用户每次登陆的统一信息管理只存一份就行了吧,也好管理,类似与这种,有兴趣的伙伴可以看源码,看看他们的底层是怎么实现的。

六. Bean的生命周期

💡 Spring其实就是一个管理Bean对象的工厂,它负责对象的创建和销毁,Bean的生命周期实际上就是对象从创建到销毁的整个过程,比如:

  • 什么时候创建Bean?
  • 创建Bean前后会调用什么方法?
  • Bean对象什么时候销毁?
  • Bean对象销毁前后会调用什么方法?

💡 所以其本质可以总结为:我们需要知道在哪一步调用了什么方法,以便我们可以添加自己的逻辑!

1. Bean生命周期之5步

  1. 实例化Bean
  2. Bean属性赋值
  3. 初始化Bean
  4. 使用Bean
  5. 销毁Bean
java 复制代码
package com.powernode.spring6.bean;

/**
 * @author 动力节点
 * @version 1.0
 * @className User
 * @since 1.0
 **/
public class User {
    private String name;

    public User() {
        System.out.println("1.实例化Bean");
    }

    public void setName(String name) {
        this.name = name;
        System.out.println("2.Bean属性赋值");
    }

    public void initBean(){
        System.out.println("3.初始化Bean");
    }

    public void destroyBean(){
        System.out.println("5.销毁Bean");
    }

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

    <!--
    init-method属性指定初始化方法。
    destroy-method属性指定销毁方法。
    -->
    <bean id="userBean" class="com.powernode.spring6.bean.User" init-method="initBean" destroy-method="destroyBean">
        <property name="name" value="zhangsan"/>
    </bean>

</beans>
java 复制代码
package com.powernode.spring6.test;

import com.powernode.spring6.bean.User;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * @author 动力节点
 * @version 1.0
 * @className BeanLifecycleTest
 * @since 1.0
 **/
public class BeanLifecycleTest {
    @Test
    public void testLifecycle(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        User userBean = applicationContext.getBean("userBean", User.class);
        System.out.println("4.使用Bean");
        // 只有正常关闭spring容器才会执行销毁方法
        ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;
        context.close();
    }
}

❗️ 需要注意的是:

  • 只有正常关闭Spring容器的时候,bean的销毁方法才会被调用
  • ClassPathXmlApplicationContext类才有close()方法
  • 配置文件中的init-method指定初始化方法。destroy-method指定销毁方法

2. Bean生命周期之7步

💡 这里相比于上面多了两步,分别是Bean初始化前和初始化后

java 复制代码
package com.powernode.spring6.bean;

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

/**
 * @author 动力节点
 * @version 1.0
 * @className LogBeanPostProcessor
 * @since 1.0
 **/
public class LogBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("Bean后处理器的before方法执行,即将开始初始化");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("Bean后处理器的after方法执行,已完成初始化");
        return bean;
    }
}

💡 配置Bean后处理器

xml 复制代码
<!--配置Bean后处理器。这个后处理器将作用于当前配置文件中所有的bean。-->
<bean class="com.powernode.spring6.bean.LogBeanPostProcessor"/>

3. Bean生命周期之10步

Aware相关的接口包括:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware

  • 当Bean实现了BeanNameAware,Spring会将Bean的名字传递给Bean。
  • 当Bean实现了BeanClassLoaderAware,Spring会将加载该Bean的类加载器传递给Bean。
  • 当Bean实现了BeanFactoryAware,Spring会将Bean工厂对象传递给Bean。
java 复制代码
package com.powernode.spring6.bean;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;

/**
 * @author 动力节点
 * @version 1.0
 * @className User
 * @since 1.0
 **/
public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {
    private String name;

    public User() {
        System.out.println("1.实例化Bean");
    }

    public void setName(String name) {
        this.name = name;
        System.out.println("2.Bean属性赋值");
    }

    public void initBean(){
        System.out.println("6.初始化Bean");
    }

    public void destroyBean(){
        System.out.println("10.销毁Bean");
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        System.out.println("3.类加载器:" + classLoader);
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("3.Bean工厂:" + beanFactory);
    }

    @Override
    public void setBeanName(String name) {
        System.out.println("3.bean名字:" + name);
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("9.DisposableBean destroy");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("5.afterPropertiesSet执行");
    }
}

通过测试可以看出来:

  • InitializingBean的方法早于init-method的执行。
  • DisposableBean的方法早于destroy-method的执行。

七、Bean的循环依赖

1. 什么是循环依赖

A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我

准备两个对象,演示在spring中什么情况下会出现循环依赖问题

java 复制代码
package com.powernode.spring6.bean;

/**
 * @author 动力节点
 * @version 1.0
 * @className Husband
 * @since 1.0
 **/
public class Husband {
    private String name;
    private Wife wife;

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

    public String getName() {
        return name;
    }

    public void setWife(Wife wife) {
        this.wife = wife;
    }

    // toString()方法重写时需要注意:不能直接输出wife,输出wife.getName()。要不然会出现递归导致的栈内存溢出错误。
    @Override
    public String toString() {
        return "Husband{" +
                "name='" + name + '\'' +
                ", wife=" + wife.getName() +
                '}';
    }
}
java 复制代码
package com.powernode.spring6.bean;

/**
 * @author 动力节点
 * @version 1.0
 * @className Wife
 * @since 1.0
 **/
public class Wife {
    private String name;
    private Husband husband;

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

    public String getName() {
        return name;
    }

    public void setHusband(Husband husband) {
        this.husband = husband;
    }

    // toString()方法重写时需要注意:不能直接输出husband,输出husband.getName()。要不然会出现递归导致的栈内存溢出错误。
    @Override
    public String toString() {
        return "Wife{" +
                "name='" + name + '\'' +
                ", husband=" + husband.getName() +
                '}';
    }
}

2. singleton下的set注入

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="husbandBean" class="com.powernode.spring6.bean.Husband" scope="singleton">
        <property name="name" value="张三"/>
        <property name="wife" ref="wifeBean"/>
    </bean>
    <bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="singleton">
        <property name="name" value="小花"/>
        <property name="husband" ref="husbandBean"/>
    </bean>
</beans>
java 复制代码
package com.powernode.spring6.test;

import com.powernode.spring6.bean.Husband;
import com.powernode.spring6.bean.Wife;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * @author 动力节点
 * @version 1.0
 * @className CircularDependencyTest
 * @since 1.0
 **/
public class CircularDependencyTest {

    @Test
    public void testSingletonAndSet(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);
        Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);
        System.out.println(husbandBean);
        System.out.println(wifeBean);
    }
}

💡 在singleton + set注入的情况下,循环依赖是没有问题的。Spring可以解决这个问题

3. property下的set注入

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="husbandBean" class="com.powernode.spring6.bean.Husband" scope="prototype">
        <property name="name" value="张三"/>
        <property name="wife" ref="wifeBean"/>
    </bean>
    <bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="prototype">
        <property name="name" value="小花"/>
        <property name="husband" ref="husbandBean"/>
    </bean>
</beans>

执行测试程序:发生了异常,异常信息如下:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'husbandBean': Requested bean is currently in creation: Is there an unresolvable circular reference?

at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:265)

at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)

at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:325)

... 44 more
翻译为:创建名为"husbandBean"的bean时出错:请求的bean当前正在创建中:是否存在无法解析的循环引用

📌 经过测试,只有都为property的情况下才会出现,只要其中有一个是singleton就可以避免这个问题,主要原因如下:

4. singleton+构造注入

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="hBean" class="com.powernode.spring6.bean2.Husband" scope="singleton">
        <constructor-arg name="name" value="张三"/>
        <constructor-arg name="wife" ref="wBean"/>
    </bean>

    <bean id="wBean" class="com.powernode.spring6.bean2.Wife" scope="singleton">
        <constructor-arg name="name" value="小花"/>
        <constructor-arg name="husband" ref="hBean"/>
    </bean>
</beans>

📌 现象和原因

依旧会产生和上面一样的循环依赖问题,主要原因是因为通过构造方法注入导致的。因为构造方法注入会导致实例化对象的过程和对象属性赋值的过程没有分离开,必须在一起完成导致的。

5. Spring解决循环依赖的原理和实现

📌 前提条件

set + singleton模式

📌 根本原因

根本的原因在于:这种方式可以做到将"实例化Bean"和"给Bean属性赋值"这两个动作分开去完成。

  • 实例化Bean的时候:调用无参数构造方法来完成。此时可以先不给属性赋值,可以提前将该Bean对象"曝光"给外界
  • 给Bean属性赋值的时候:调用setter方法来完成。
  • 两个步骤是完全可以分离开去完成的,并且这两步不要求在同一个时间点上完成
  • 也就是说,Bean都是单例的,我们可以先把所有的单例Bean实例化出来,放到一个集合当中(我们可以称之为缓存),所有的单例Bean全部实例化完成之后,以后我们再慢慢的调用setter方法给属性赋值。这样就解决了循环依赖的问题。

📌 代码实现

  • Cache of singleton objects: bean name to bean instance. 单例对象的缓存:key存储bean名称,value存储Bean对象【一级缓存】
  • Cache of early singleton objects: bean name to bean instance. 早期单例对象的缓存:key存储bean名称,value存储早期的Bean对象【二级缓存】
  • Cache of singleton factories: bean name to ObjectFactory. 单例工厂缓存:key存储bean名称,value存储该Bean对应的ObjectFactory对象【三级缓存】

这三个缓存其实本质上是三个Map集合。

💡 提前曝光的核心:addSingletonFactory

💡 获取曝光对象的步骤

💡 从源码中可以看到,spring会先从一级缓存中获取Bean,如果获取不到,则从二级缓存中获取Bean,如果二级缓存还是获取不到,则从三级缓存中获取之前曝光的ObjectFactory对象,通过ObjectFactory对象获取Bean实例,这样就解决了循环依赖的问题。

6. 总结

💡 Spring只能解决setter方法注入的单例bean之间的循环依赖。ClassA依赖ClassB,ClassB又依赖ClassA,形成依赖闭环。Spring在创建ClassA对象后,不需要等给属性赋值,直接将其曝光到bean缓存当中。在解析ClassA的属性时,又发现依赖于ClassB,再次去获取ClassB,当解析ClassB的属性时,又发现需要ClassA的属性,但此时的ClassA已经被提前曝光加入了正在创建的bean的缓存中,则无需创建新的的ClassA的实例,直接从缓存中获取即可。从而解决循环依赖问题。

八、注解开发

上面都是基于配置文件进行Bean的装配的,当然我们后面都是基于注解开发的,这大大减少了我们的工作量,不过还是得需要了解其中的底层工作原理,下面介绍一下我们常用的一些Spring注解。

1. 核心组件注解

  1. @Component
  • 作用:通用的组件注解,标识一个类为Spring组件

  • 使用场景:当不确定一个类属于哪一层时使用

  1. @Controller
  • 作用:标识一个类为Spring MVC控制器

  • 特点:是@Component的特殊化,主要用于处理HTTP请求

  1. @Service
  • 作用:标识一个类为业务服务层组件

  • 特点:是@Component的特殊化,用于业务逻辑层

  1. @Repository
  • 作用:标识一个类为数据访问层组件

  • 特点:是@Component的特殊化,具有将数据库操作抛出的原生异常转换为Spring的持久化异常的功能

四个注解的关系:@Controller、@Service、@Repository都是@Component的特殊化,从功能上可以互相替换,但使用特定注解能更好地表达类的用途,并且某些特定注解会有额外的功能(如@Repository的异常转换)

2. 依赖注入相关注解

  1. @Autowired
  • 作用:自动装配依赖对象

  • 特点:

1.1 默认按类型匹配

1.2 可以用在构造器、方法、字段和参数上

1.3 是Spring提供的注解


  1. @Resource
  • 作用:自动装配依赖对象

  • 特点:

2.1 默认按名称匹配,名称可通过name属性指定

2.2 是JSR-250标准注解,不属于Spring

2.3 可以用在字段和方法上


  1. @Qualifier
  • 作用:当有多个相同类型的bean时,用于指定具体的bean

  • 常与@Autowired一起使用

  1. @Value
  • 作用:注入属性值,支持SpEL表达式

  • 使用场景:

4.1 注入简单值

4.2 注入配置文件中的属性

4.3 使用SpEL表达式


注解 来源 主要用途 特点
@Component Spring 通用组件声明 最基础的组件注解
@Controller Spring MVC控制器 处理HTTP请求,@Component的特殊化
@Service Spring 业务服务层 业务逻辑处理,@Component的特殊化
@Repository Spring 数据访问层 异常自动转换,@Component的特殊化
@Autowired Spring 依赖注入 默认按类型匹配,支持构造器/方法/字段/参数
@Resource JSR-250 依赖注入 默认按名称匹配,支持name属性指定
@Qualifier Spring 限定注入 配合@Autowired解决多个同类型bean的歧义
@Value Spring 属性注入 支持直接值注入、配置文件属性注入(${})和SpEL表达式注入(#{}))

九、GOF之代理模式

代理模式是GOF中23种设计模式之一 ,核心就是两个字"代理",简单点来说就是中间件(在生活中就是中介和销售的例子),代理模式属于结构型设计模式,主要特点如下:

  • 保护目标对象,不会直接接触到目标对象,使用代理类。
  • 简化代码,增加灵活性。
  • 代理类拥有和目标类一样的功能。

代理模式的角色:

  • 代理类(代理主题)
  • 目标类(真实主题)
  • 共用接口(代理类和目标类共同实现的接口)

💡 可以联想一下Shiro,可以判断当前的登陆状态,也可以获取到当前登录的用户信息。

1. 静态代理

java 复制代码
package com.powernode.mall.service.impl;

import com.powernode.mall.service.OrderService;

/**
 * @author 动力节点
 * @version 1.0
 * @className OrderServiceImpl
 * @since 1.0
 **/
public class OrderServiceImpl implements OrderService {
    @Override
    public void generate() {
        try {
            Thread.sleep(1234);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已生成");
    }

    @Override
    public void detail() {
        try {
            Thread.sleep(2541);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单信息如下:******");
    }

    @Override
    public void modify() {
        try {
            Thread.sleep(1010);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已修改");
    }
}

现在的场景是有一个类中有三个方法,需要统计这三个方法的用时,正常有三种解决办法。

📌 第一种:直接在源码上添加统计时间的代码,弊端是违反OCP原则,优势是实现简单。

java 复制代码
package com.powernode.mall.service.impl;

import com.powernode.mall.service.OrderService;

/**
 * @author 动力节点
 * @version 1.0
 * @className OrderServiceImpl
 * @since 1.0
 **/
public class OrderServiceImpl implements OrderService {
    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(1234);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已生成");
        long end = System.currentTimeMillis();
        System.out.println("耗费时长"+(end - begin)+"毫秒");
    }

    @Override
    public void detail() {
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(2541);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单信息如下:******");
        long end = System.currentTimeMillis();
        System.out.println("耗费时长"+(end - begin)+"毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(1010);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已修改");
        long end = System.currentTimeMillis();
        System.out.println("耗费时长"+(end - begin)+"毫秒");
    }
}

📌 第二种,编写一个子类继承上面的目标类,在子类中统计对应的方法耗时,优势是符合OCP原则,缺点是存在类爆炸和重复代码的问题,增加了代码的耦合度。

java 复制代码
package com.powernode.mall.service.impl;

/**
 * @author 动力节点
 * @version 1.0
 * @className OrderServiceImplSub
 * @since 1.0
 **/
public class OrderServiceImplSub extends OrderServiceImpl{
    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        super.generate();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void detail() {
        long begin = System.currentTimeMillis();
        super.detail();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        super.modify();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }
}

📌 第三种:使用静态代理

java 复制代码
package com.powernode.mall.service;

/**
 * @author 动力节点
 * @version 1.0
 * @className OrderServiceProxy
 * @since 1.0
 **/
public class OrderServiceProxy implements OrderService{ // 代理对象

    // 目标对象
    private OrderService orderService;

    // 通过构造方法将目标对象传递给代理对象
    public OrderServiceProxy(OrderService orderService) {
        this.orderService = orderService;
    }

    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.generate();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void detail() {
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.detail();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.modify();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }
}

❗️ 注意,这里还是创建了一个新的类,但是我们是实现了对应的Service接口,相当于最开始说的代理类和目标类实现同一个接口,代理类拥有和目标类相同的功能,但是在代理类中我们可以增加个性化功能,不影响目标类。

❗️ 弊端:无法解决类爆炸的问题

2. 动态代理(Proxy类)

动态代理主要是为了解决静态代理的类爆炸问题,原理就是在内存中动态的生成对应的字节码文件,用完之后销毁,代码中只需要一份逻辑代码。

2.1 JDK动态代理

只能代理接口

📌 创建一个统计时间的类

java 复制代码
package com.powernode.mall.service;

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

/**
 * @author 动力节点
 * @version 1.0
 * @className TimerInvocationHandler
 * @since 1.0
 **/
public class TimerInvocationHandler implements InvocationHandler {
    // 目标对象
    private Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 目标执行之前增强。
        long begin = System.currentTimeMillis();
        // 调用目标对象的目标方法
        Object retValue = method.invoke(target, args);
        // 目标执行之后增强。
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
        // 一定要记得返回哦。
        return retValue;
    }
}
java 复制代码
package com.powernode.mall;

import com.powernode.mall.service.OrderService;
import com.powernode.mall.service.TimerInvocationHandler;
import com.powernode.mall.service.impl.OrderServiceImpl;

import java.lang.reflect.Proxy;

/**
 * @author 动力节点
 * @version 1.0
 * @className Client
 * @since 1.0
 **/
public class Client {
    public static void main(String[] args) {
        // 创建目标对象
        OrderService target = new OrderServiceImpl();
        // 创建代理对象
        OrderService orderServiceProxy = (OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                                                                                target.getClass().getInterfaces(),
                                                                                new TimerInvocationHandler(target));
        // 调用代理对象的代理方法
        orderServiceProxy.detail();
        orderServiceProxy.modify();
        orderServiceProxy.generate();
    }
}

InvocationHandler接口中有一个方法invoke,这个invoke方法上有三个参数:

  • 第一个参数:Object proxy。代理对象。设计这个参数只是为了后期的方便,如果想在invoke方法中使用代理对象的话,尽管通过这个参数来使用。
  • 第二个参数:Method method。目标方法
  • 第三个参数:Object[] args。目标方法调用时要传的参数

可以看到JDK的动态代理完美的解决了类爆炸的问题,而且大幅度的简化了代码,只需要改动调用的地方即可,符合OCP原则(没有改动业务代码)。

2.2 CgLib动态代理

既可以代理接口,也可以代理类

📌 创建代理类

java 复制代码
package com.powernode.mall.service;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * @author 动力节点
 * @version 1.0
 * @className TimerMethodInterceptor
 * @since 1.0
 **/
public class TimerMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 前增强
        long begin = System.currentTimeMillis();
        // 调用目标
        Object retValue = methodProxy.invokeSuper(target, objects);
        // 后增强
        long end = System.currentTimeMillis();
        System.out.println("耗时" + (end - begin) + "毫秒");
        // 一定要返回
        return retValue;
    }
}

📌 客户端使用

java 复制代码
package com.powernode.mall;

import com.powernode.mall.service.TimerMethodInterceptor;
import com.powernode.mall.service.UserService;
import net.sf.cglib.proxy.Enhancer;

/**
 * @author 动力节点
 * @version 1.0
 * @className Client
 * @since 1.0
 **/
public class Client {
    public static void main(String[] args) {
        // 创建字节码增强器
        Enhancer enhancer = new Enhancer();
        // 告诉cglib要继承哪个类
        enhancer.setSuperclass(UserService.class);
        // 设置回调接口
        enhancer.setCallback(new TimerMethodInterceptor());
        // 生成源码,编译class,加载到JVM,并创建代理对象
        UserService userServiceProxy = (UserService)enhancer.create();

        userServiceProxy.login();
        userServiceProxy.logout();

    }
}

CgLib的动态代理实现起来更加的舒服和简洁,个人比较推荐CgLib,其中有字节码增强器,效率也会更快一些。

对于高版本的jdk需要增加两个参数。

● --add-opens java.base/java.lang=ALL-UNNAMED

● --add-opens java.base/java.lang=ALL-UNNAMED

十、Aop切面编程

📌 Aop是一种编程技术

📌 Aop底层是通过JDK和CjLib动态代理实现的,接口使用JDK,类使用CjLib,也可以强制使用CjLib

📌 Aop技术通常使用在日志和全局的某些通用功能处理上,比如上面的时间分析,又或者是行为日志

1. Aop介绍

Aop大部分的使用场景是交叉业务,所谓交叉业务指的是系统中的一些通用服务例如:安全、日志、事务管理等

📌 为什么会出现交叉业务?

  • 类似日志和安全这种问题在代码中大多数都是重复代码,提出来单独处理可以减少重复代码,并且方便管理和扩展。
  • 交叉业务出现可以让程序员更加专注业务代码,增加开发效率。

📌 为什么要学习Aop?

  • 这是一种重要的编程思想,能增加我们在开发中的灵活性。
  • 重点:普通程序猿转向高级程序猴的重要一步,操作系统的架构。

📌 总结:将与核心业务无关的代码抽出来形成一个单独的组件,然后以横向交叉的方式应用到业务中的过程被称作Aop编程。

📌 Aop的优点

  • 代码复用性高。
  • 灵活性强,好扩展
  • 让开发者更加关注业务代码

2. Aop的七大术语

2.1 连接点( Joinpoint)

📌 在程序的整个执行流程中,可以织入切面的位置。例如方法的执行前后,异常抛出之后的位置等。

2.2 切点(Pointcut)

📌 在程序执行过程织入切面的方法。(一个切点对应多个连接点

2.3 通知(Advice)

📌 通知又叫做增强,就是具体织入的代码,可分为以下几种:

  • 前置通知
  • 后置通知
  • 环绕通知
  • 异常通知
  • 最终通知

2.4 切面(Aspect)

📌 切点+通知组成切面。

2.5 织入(Weaving)

📌 把通知应用到目标对象上的过程。

2.6 代理对象(Proxy)

📌 一个目标对象被织入通知后产生的新对象。

2.7 目标对象(Target)

📌 被织入通知的对象。

3. 切点表达式

切点表达式用来定义通知(Advice)往哪些方法上切入

切入点表达式语法格式:

execution([访问控制权限修饰符 ] 返回值类型 [全限定类名 ]方法名(形式参数列表 ) [异常])

访问控制权限修饰符

● 可选项。

● 没写,就是4个权限都包括。

● 写public就表示只包括公开的方法。

返回值类型

● 必填项。

● * 表示返回值类型任意。

全限定类名

● 可选项。

● 两个点"..."代表当前包以及子包下的所有类。

● 省略时表示所有的类。

方法名

● 必填项。

表示所有方法。
● set
表示所有的set方法。

形式参数列表

● 必填项

● () 表示没有参数的方法

● (...) 参数类型和个数随意的方法

● () 只有一个参数的方法
● (
, String) 第一个参数类型随意,第二个参数是String的。

异常

● 可选项。

● 省略时表示任意异常类型。

java 复制代码
service包下所有的类中以delete开始的所有方法

execution(public * com.powernode.mall.service.*.delete*(..))
java 复制代码
mall包下所有的类的所有的方法

execution(* com.powernode.mall..*(..))
java 复制代码
所有类的所有方法

execution(* *(..))

4. 使用Spring的Aop编程

  • 第一种方式:Spring框架结合AspectJ框架实现的AOP,基于注解方式。(常用)
  • 第二种方式:Spring框架结合AspectJ框架实现的AOP,基于XML方式
  • 第三种方式:Spring框架自己实现的AOP,基于XML配置方式。

❓ 什么是AspectJ?(Eclipse组织的一个支持AOP的框架。AspectJ框架是独立于Spring框架之外的一个框架,Spring框架用了AspectJ)

AspectJ项目起源于帕洛阿尔托(Palo Alto)研究中心(缩写为PARC)。该中心由Xerox集团资助,Gregor Kiczales领导,从1997年开始致力于AspectJ的开发,1998年第一次发布给外部用户,2001年发布1.0 release。为了推动AspectJ技术和社团的发展,PARC在2003年3月正式将AspectJ项目移交给了Eclipse组织,因为AspectJ的发展和受关注程度大大超出了PARC的预期,他们已经无力继续维持它的发展。

4.1 准备工作

使用Spring+AspectJ的AOP需要引入的依赖如下

xml 复制代码
<!--spring context依赖-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>6.0.0-M2</version>
</dependency>
<!--spring aop依赖-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>6.0.0-M2</version>
</dependency>
<!--spring aspects依赖-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>6.0.0-M2</version>
</dependency>
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 http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

</beans>

4.2 核心步骤

📌 第一步:目标对象必须被Spring管理,也就是添加@Component注解

java 复制代码
package com.powernode.spring6.service;

// 目标类
@Component
public class OrderService {
    // 目标方法
    public void generate(){
        System.out.println("订单已生成!");
    }
}

📌 第二步:开启组件扫描自动代理

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 http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--开启组件扫描-->
    <context:component-scan base-package="com.powernode.spring6.service"/>
    <!--开启自动代理-->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

<aop:aspectj-autoproxy proxy-target-class="true"/> 开启自动代理之后,凡事带有@Aspect注解的bean都会生成代理对象。

proxy-target-class="true" 表示采用cglib动态代理。

proxy-target-class="false" 表示采用jdk动态代理。默认值是false。即使写成false,当没有接口的时候,也会自动选择cglib生成代理类。

📌 第三步:添加切面类和通知,并配置切点表达式

java 复制代码
package com.powernode.spring6.service;

import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.aspectj.lang.annotation.Aspect;

// 切面类
@Aspect
@Component
public class MyAspect {
    
    // 切点表达式
    @Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
    // 这就是需要增强的代码(通知)
    public void advice(){
        System.out.println("我是一个通知");
    }
}

到这里一个简单的例子就完成了,Aop编程在有些公司中非常的普遍,如果要更好的使用Aop编程,对于我们程序猴的要求也是比较高的,比如说严格遵循SOLID五大原则,尤其是ISP接口隔离SRP单一职责原则,这样可以更好的织入通知,嵌入交叉业务。更有甚者对于方法名也有严格的要求,现在知道为什么了吧,都是为了更好的实现Aop。

4.3 通知类型

  • 前置通知:@Before 目标方法执行之前的通知
  • 后置通知:@AfterReturning 目标方法执行之后的通知
  • 环绕通知:@Around 目标方法之前添加通知,同时目标方法执行之后添加通知。
  • 异常通知:@AfterThrowing 发生异常之后执行的通知
  • 最终通知:@After 放在finally语句块中的通知

💡 这个顺序就代表了在程序中通知的执行顺序!

java 复制代码
package com.powernode.spring6.service;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

// 切面类
@Component
@Aspect
public class MyAspect {

    @Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知开始");
        // 执行目标方法。
        proceedingJoinPoint.proceed();
        System.out.println("环绕通知结束");
    }

    @Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void beforeAdvice(){
        System.out.println("前置通知");
    }

    @AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void afterReturningAdvice(){
        System.out.println("后置通知");
    }

    @AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void afterThrowingAdvice(){
        System.out.println("异常通知");
    }

    @After("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void afterAdvice(){
        System.out.println("最终通知");
    }

}

❗️ 注意:

  1. 最终通知在发生异常后也是会执行的
  2. 发生异常后,环绕通知的结束部分不会执行,后置通知也不会执行

4.4 切面的执行顺序

🚀 我们知道,业务流程当中不一定只有一个切面,可能有的切面控制事务,有的记录日志,有的进行安全控制 ,如果多个切面的话,顺序如何控制:可以使用@Order注解来标识切面类,为@Order注解的value指定一个整数型的数字,数字越小,优先级越高

java 复制代码
package com.powernode.spring6.service;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Order(1) //设置优先级
public class YourAspect {

    @Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("YourAspect环绕通知开始");
        // 执行目标方法。
        proceedingJoinPoint.proceed();
        System.out.println("YourAspect环绕通知结束");
    }

    @Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void beforeAdvice(){
        System.out.println("YourAspect前置通知");
    }

    @AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void afterReturningAdvice(){
        System.out.println("YourAspect后置通知");
    }

    @AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void afterThrowingAdvice(){
        System.out.println("YourAspect异常通知");
    }

    @After("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void afterAdvice(){
        System.out.println("YourAspect最终通知");
    }
}

4.5 简化切点表达式

💡 使用@Pointcut注解定义切点表达式,方法名可以被其它注解作为切点表达式的key。

java 复制代码
package com.powernode.spring6.service;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

// 切面类
@Component
@Aspect
@Order(2)
public class MyAspect {
    
    @Pointcut("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void pointcut(){}

    @Around("pointcut()")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知开始");
        // 执行目标方法。
        proceedingJoinPoint.proceed();
        System.out.println("环绕通知结束");
    }

    @Before("pointcut()")
    public void beforeAdvice(){
        System.out.println("前置通知");
    }

    @AfterReturning("pointcut()")
    public void afterReturningAdvice(){
        System.out.println("后置通知");
    }

    @AfterThrowing("pointcut()")
    public void afterThrowingAdvice(){
        System.out.println("异常通知");
    }

    @After("pointcut()")
    public void afterAdvice(){
        System.out.println("最终通知");
    }

}

十一、事务

1. 事务概述

📌 在一个业务流程中,多条DML语句要么全部成功要么全部失败,这就叫做事务。

📌事务的处理过程

  • 第一步:开启事务
  • 第二步:执行业务代码
  • 第三步:提交事务(commit)
  • 第四步:回滚事务(rollback)(如果出现异常的情况下)

📌事务的四个特性

  • 原子性:事务是最小工作单元,不可分割
  • 一致性:要么同时成功,要么同时失败
  • 持久性:持久性是事务结束的标识
  • 隔离性:不同事务之间不能互相影响

2. Spring对事务的支持(@Transactional)

xml 复制代码
前提:xmlns:tx="http://www.springframework.org/schema/tx"

开启事务支持
<tx:annotation-driven transaction-manager="transactionManager"/>

📌 首先开启事务的支持,后面我们直接使用@Transactional注解即可,添加在类和方法上都可,底层还是使用Aop实现的,原理和我们手动提交是一样的,只不过使用Aop进行了简化。

java 复制代码
package com.powernode.bank.service.impl;

import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author 动力节点
 * @version 1.0
 * @className AccountServiceImpl
 * @since 1.0
 **/
@Service("accountService")
@Transactional
public class AccountServiceImpl implements AccountService {

    @Resource(name = "accountDao")
    private AccountDao accountDao;

    @Override
    public void transfer(String fromActno, String toActno, double money) {
        // 查询账户余额是否充足
        Account fromAct = accountDao.selectByActno(fromActno);
        if (fromAct.getBalance() < money) {
            throw new RuntimeException("账户余额不足");
        }
        // 余额充足,开始转账
        Account toAct = accountDao.selectByActno(toActno);
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);
        int count = accountDao.update(fromAct);

        // 模拟异常
        String s = null;
        s.toString();

        count += accountDao.update(toAct);
        if (count != 2) {
            throw new RuntimeException("转账失败,请联系银行");
        }
    }
}

3. 事务的属性

3.1 属性的分类

事务中的重点属性:

  • 事务传播行为
  • 事务隔离级别
  • 事务超时
  • 只读事务
  • 设置出现哪些异常回滚事务
  • 设置出现哪些异常不回滚事务

3.2 事务传播行为

📌 在Service中,方法a和方法b都有事务,那这两个事务是单独的还是会合并为一个呢?这就是事务的传播行为。

@Transactional(propagation = Propagation.REQUIRED)

📌 一共有七种传播行为:

  • REQUIRED:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】
  • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行【有就加入,没有就不管了】
  • MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常【有就加入,没有就抛异常】
  • REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】
  • NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务【不支持事务,存在就挂起】
  • NEVER:以非事务方式运行,如果有事务存在,抛出异常【不支持事务,存在就抛异常】
  • NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】
Spring事务传播行为选择指南
传播行为 当前有事务 当前无事务 适用场景
REQUIRED 加入当前事务 新建一个事务 大多数业务方法(默认行为)
REQUIRES_NEW 新建事务并挂起当前事务 新建一个事务 需要独立事务的操作(如日志记录)
SUPPORTS 加入当前事务 非事务方式执行 查询方法,支持但不要求事务
NOT_SUPPORTED 挂起当前事务,非事务执行 非事务方式执行 不需要事务支持的操作
MANDATORY 加入当前事务 抛出异常 必须被事务方法调用的操作
NEVER 抛出异常 非事务方式执行 禁止在事务中执行的操作
NESTED 创建嵌套事务(保存点) 新建一个事务 需要部分回滚的复杂业务
使用说明:
  1. REQUIRED:最常用,适用于大多数业务场景
  2. REQUIRES_NEW:用于需要独立提交/回滚的操作
  3. NESTED:提供部分回滚能力,但并非所有数据库都支持
  4. SUPPORTS/NOT_SUPPORTED:根据是否需要事务支持选择
  5. MANDATORY/NEVER:用于强制/禁止事务环境

3.3 事务的隔离级别

📌 三大读问题

  • 脏读:读取到没有提交到数据库中的数据。
  • 不可重复读:同一个事务中两次读写的数据不一致。
  • 幻读:读取的数据是假的。

📌 四个隔离级别

  • 读未提交 READ_UNCOMMITTED
  • 读提交 READ_COMMITTED
  • 可重复度 REPEATABLE_READ
  • 序列化 SERIALIZABLE
事务隔离级别详解
隔离级别 脏读(Dirty Read) 不可重复读(Non-repeatable Read) 幻读(Phantom Read) 典型实现方式 适用场景
读未提交 (Read Uncommitted) ✅ 可能 ✅ 可能 ✅ 可能 读不加锁,写加排他锁 对一致性要求极低,追求最高性能
读已提交 (Read Committed) ❌ 避免 ✅ 可能 ✅ 可能 读时加共享锁(立即释放),写加排他锁 多数数据库默认级别,平衡一致性与性能
可重复读 (Repeatable Read) ❌ 避免 ❌ 避免 ✅ 可能* 读时加共享锁(事务结束释放),写加排他锁 需要事务内多次读取一致的场景
串行化 (Serializable) ❌ 避免 ❌ 避免 ❌ 避免 范围锁,完全串行化执行 最高一致性要求,如金融交易

*注:MySQL的InnoDB引擎在"可重复读"级别通过MVCC机制避免了大部分幻读问题

隔离级别选择建议:
  1. 优先使用数据库默认级别(通常为Read Committed)
  2. 对数据一致性要求高的场景使用Repeatable Read
  3. 只有特别敏感的业务(如资金结算)才考虑Serializable
  4. 几乎不应该使用Read Uncommitted
在Spring中使用事务的隔离性
java 复制代码
@Transactional(isolation = Isolation.READ_COMMITTED)

3.4 事务超时

java 复制代码
@Transactional(timeout = 10)

📌 这里的超时时间指的是事务中最后一条DML语句之前的执行时间,超时了就回滚。

不计入超时时间

java 复制代码
@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {
    accountDao.insert(act);
    // 睡眠一会
    try {
        Thread.sleep(1000 * 15);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

计入超时时间

java 复制代码
@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {
    // 睡眠一会
    try {
        Thread.sleep(1000 * 15);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    accountDao.insert(act);
}

3.5 只读事务

java 复制代码
@Transactional(readOnly = true)

📌 作用:启用Spring的优化策略,提高查询语句的效率,其余增删改DML无法执行。

3.6 哪些异常回滚事务

java 复制代码
@Transactional(rollbackFor = RuntimeException.class)

3.7 哪些异常不会滚事务

java 复制代码
@Transactional(noRollbackFor = NullPointerException.class)

下期预告

老杜的笔记最后是有说明Spring的八大设计模式的,虽然在课程中详细的学习了单例、原型链、简单工厂、工厂模式、抽象工厂、代理模式、静态代理、动态代理等,如果在加上其它的例如装饰器模式,说实话还是有一点混,所以决定重开一章单独学习23种设计模式。

相关推荐
找不到、了1 小时前
Spring的Bean原型模式下的使用
java·spring·原型模式
超级小忍1 小时前
Spring AI ETL Pipeline使用指南
人工智能·spring
Boilermaker19924 小时前
【Java EE】SpringIoC
前端·数据库·spring
写不出来就跑路5 小时前
Spring Security架构与实战全解析
java·spring·架构
sleepcattt5 小时前
Spring中Bean的实例化(xml)
xml·java·spring
小七mod6 小时前
【Spring】Java SPI机制及Spring Boot使用实例
java·spring boot·spring·spi·双亲委派
ruan1145147 小时前
Java Lambda 类型推断详解:filter() 方法与 Predicate<? super T>
java·开发语言·spring·stream
paopaokaka_luck7 小时前
基于SpringBoot+Vue的非遗文化传承管理系统(websocket即时通讯、协同过滤算法、支付宝沙盒支付、可分享链接、功能量非常大)
java·数据库·vue.js·spring boot·后端·spring·小程序
邓不利东12 小时前
Spring中过滤器和拦截器的区别及具体实现
java·后端·spring
努力的小郑13 小时前
Spring三级缓存硬核解密:二级缓存行不行?一级缓存差在哪?
java·spring·面试