Spring 笔记

Spring

本文是本人对 Spring 学习的一个大致的总结,水平有限,总结的并不好,希望大家能够给出建议。
文中若有错误,也希望大家能够指出。

概述

什么是 Spring?

Spring 是一个开源的 Java 应用程序框架,用于构建企业级应用程序。
它提供了全面的基础设施支持,比如:依赖注入(Ioc)、依赖注入(DI)、AOP(面向切面编程) 等功能,让开发者可以更加专注于对业务逻辑的实现。

Spring 的优点

  1. 提供依赖注入(DI):

    使得 Spring 维护不同组件之间的依赖关系,降低组件之间的耦合度。

  2. 面向切面编程(AOP):

    开发者可以将不同功能抽象成切面,并可以将这些切面和不同的组件关联起来,从而提高代码重用率和可维护性

  3. 提供 Ioc 容器:

    Spring 提供的 Ioc 容器可以对不同组件进行管理,并支持对组件的 AOP 增强,从而实现程序的解耦和高度可配置性。

  4. 降低了对其他框架的整合难度。

  5. 支持声明式事务管理:

    开发者可以通过配置来管理程序中的事务,简化事务管理过程。

  6. 便于测试

    Spring 可以方便的进行单元测试和集成测试,提高代码的可测试性和可靠性。

Spring 八大模块

Spring Webflux模块是在 Spring 5 的时候添加的。

Spring 的核心模块是 Spring Core 和 Spring AOP。Spring AOP 又是基于 Spring Core 实现的,因此Spring Core 是整个 Spring 的基石。
Spring Core 是 Spring 中最基础的部分,它提供了 依赖注入特征 来实现容器对 Bean 的管理。核心容器的主要组件是 BeanFactory,BeanFactory是工厂模式的一个实现,是任何 Spring 应用的核心部分,它使用 IoC 将应用配置和依赖从实际的应用代码中分离出来。

入门项目的搭建

  1. 导入依赖

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


对于 Spring 6 需要使用 Java 17

<properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <java.version>17</java.version>
</properties>
  1. /src/main/resouces 下创建 Spring 的核心配置文件:spring-config.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">

</beans>
  1. 编写一个用于测试的实体类 User.java
package com.guyi.spring.model.entity;

public class User {
    /**
     * 用户名
     */
    private String username;

    /**
     * 用户编号
     */
    private Integer no;

    // todo 
    // get、set、toString 方法自行补充
}

  1. spring-config.xml 注册 User Bean
<bean id="userBean" class="com.guyi.spring.model.entity.User"/>

这里的 id 就是 Bean 的名称
id 不能重复

  1. 编写测试 demo
@Test
public void testHelloSpringTest() {
    // 加载 Spring 配置文件, 这种方式只适用于配置文件在类路径的情况
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config.xml");
    
    // 根据 beanId 从 Spring 容器中获取对象
    Object userBean = classPathXmlApplicationContext.getBean("userBean");
    User user = (User) userBean;
    user.setUsername("张三");
    user.setNo(1);
    System.out.println(userBean);
}

image.png

前瞻

  1. Spring 默认调用类的无参构造方法来实例化对象

只要类中有公共的构造器,那就可以使用 Spring 实例化

  1. 对象创建好后,会存储到一个 Map<String, Object> 集合中
  2. 可以指定多个 Spring 配置文件
  3. 如果 Bean 的 id 不存在,会出现异常:NoSuchBeanDefinitionException
  4. 使用 Spring 实例化 Date 对象时,需要对这个对象进行日期格式化并且对得到的对象进行类型强制转化,或者使用如下代码:
/*
	第一个参数传 bean 的 id
	第二个参数传要的期望得到类型的clss
*/
Date nowTime = applicationContext.getBean("nowTime", Date.class)
  1. 如果 Spring 配置文件不在类路径下,使用如下代码加载:
ApplicationContext applicationContext 
	= new FileSystemXmlApplicationContext("path");

Spring 启动 Log4j2 日志

  1. 导入依赖
<!-- log4j2日志依赖 -->
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-core</artifactId>
  <version>2.19.0</version>
</dependency>
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-slf4j2-impl</artifactId>
  <version>2.19.0</version>
</dependency>
  1. src/main/resouces下提供 log4j2.xml 配置文件及相关配置,注意:配置文件的名字和位置都是固定的!!!
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <Loggers>
        <root level="DEBUG">
            <appenderRef ref="console"/>
        </root>
    </Loggers>
    <appenders>
        <console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-3level %logger{1024} - %msg%n"/>
        </console>
    </appenders>
</configuration>
  1. 测试日志框架是否引入成功
@Test
public void testLog4j2() {
    // 创建日志记录器对象
    // 获取 HelloDemo 类的日志记录器对象, 也就是这个类中的代码执行记录日志的话, 就输出相应信息
    Logger logger = LoggerFactory.getLogger(HelloDemo.class);

    // 记录日志, 根据不同级别输出日志
    logger.info("我是一条消息");
    logger.debug("我是一条调试消息");
    logger.error("我是一条错误消息");
}

image.png

对于日志记录器对象一般设置为常量

Spring 对 Ioc 的实现

声明:之后的代码运行结果截图可能是不完整的,对于 Bean 创建的日志信息不会截下来,只截我自己设定的输出,类似下面的信息不会出现在运行结果截图中:

image.png

IoC 概述

  1. Ioc 是一种编程思想,是一种设计模式;
  2. 在程序中不采用硬编码方式实例化对象,也不采用硬编码的方式维护对象与对象之间的依赖关系,即反转对象的创建权和对象和对象之间的维护权。
  3. Ioc 降低了程序的耦合度,符合 OPC 和 DIP 原则。
  4. Ioc 通过 依赖注入(DI) 实现。
依赖注入

Spring 通过依赖注入的方式完成 Bean 管理,即完成对 Bean 对象的创建以及对象属性的赋值,也可以说是对 Bean 对象之间关系的维护。

实现依赖注入的方式
  1. set 方式注入:使用 set 方法来完成依赖的注入
  2. 构造注入:使用构造器来完成依赖注入
  3. 属性注入:在 xml 中使用 property标签的 value 属性进行注入或者在属性上使用注解时,依赖的注入方式就为属性注入

注入简单数据类型

简单数据类型如下:

  1. 基本数据类型的包装类
  2. 枚举类型
  3. 实现了 CharSequence 接口的类,比如:String
  4. 实现了 Number 接口的类,比如:Integer

只要是数字即可。

  1. Date 类型:java.util.Date
  2. Class 类型
  3. Temporal 类型:java8新特性,时间时区
  4. URI 类型
  5. URL 类型
  6. Locale 类型

User.java 为例,User 类中有如下的实例变量:

  • String username
  • Integer no

这些都是简单数据类型的实例变量,在注册 Bean 的时候,可以使用 prooerty 标签的 name 属性指定对应实例变量,value 属性指定要赋给队友实例变量的值,如下:

<bean id="userBean2" class="com.guyi.spring.model.entity.User">
    <property name="username" value="张三"/>
    <property name="no" value="20"/>
</bean>

测试一下

/**
 * 测试注入简单类型数据
 */
@Test
public void testSimpleData() {
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config.xml");

    Object userBean = classPathXmlApplicationContext.getBean("userBean2");
    // 注意这里并没有给对象赋值
    System.out.println(userBean);
}

image.png

对于 Date,我们一般不将其视为简单数据类型来注入,因为使用注入简单类型数据的方式注入 Date 对象,注入形式比较复杂

<bean id="" class="">
    <!-- 如果把 Date 当成简单类型, 那 value 属性值很复杂 一般不把Date当简单数据类型-->
    <property name="birth" value="Wed Oct 19 16:28:13 CST 2023"/>
</bean>

“Wed Oct 19 16:28:13 CST 2023” 这一串时间字符串不符合我们的使用习惯

简单数据类型的应用

注入一个数据源

  1. 自定义一个数据源
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

/**
 * 数据源
 *
 * 所有数据源都要实现 javax.sql.DataSource
 * 数据源能够提供 Connection 对象
 */
public class MyDataSource implements DataSource {  // 把数据源交给 Spring 管理
    /**
     * 驱动
     */
    private String driver;

    /**
     * 链接
     */
    private String url;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    @Override
    public Connection getConnection() throws SQLException {
        // 获取连接对象需要的信息:driver、url、username、password
        return null;
    }

    public void setDriver(String driver) {
        this.driver = driver;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "MyDataSource{" +
                "driver='" + driver + '\'' +
                ", url='" + url + '\'' +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return null;
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return null;
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {

    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {

    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return 0;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return null;
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return null;
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return false;
    }
}
  1. spring-config.xml 注册数据源对于的 Bean:
<!-- Spring 管理数据源 -->
<bean id="myDataSourceBean" class="com.guyi.spring.datasource.MyDataSource">
    <property name="driver" value="com.mysql.cj.jdbc.driver"/>
    <property name="url" value="jdbc:mysql://127.0.0.1:3306:/spring6"/>
    <property name="username" value="root"/>
    <property name="password" value="root"/>
</bean>

这里的自定义数据源没有具体的实现细节,无法测试。
以后开发中可以放入 Druid 数据库连接池时,可以参考这里的做法。

注入 Date
  1. 配置日期格式,在 spring-config.xml 中添加如下内容:
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="org.springframework.format.datetime.DateFormatter">
                <property name="pattern" value="yyyy-MM-dd"/>
            </bean>
        </set>
    </property>
</bean>
  1. 注入 Date 类型数据
<bean id="myDate" class="java.util.Date">
    <constructor-arg value="2022-01-01"/>
</bean>
<bean id="myBean" class="com.example.MyBean">
    <property name="date" ref="myDate"/>
</bean>

注入非简单数据类型

注入非简单数据类型,需要使用 property 标签的 ref 来注入,

  1. 创建一个 MyKey 类,表示现实生活中的钥匙,用于打开锁
/**
 * 模拟现实中的钥匙
 */
public class MyKey {
    // 暂时什么也没有
}
  1. 创建一个 MyLock 类,表示现实生活中的锁,MyLock 有一个 key 属性对应打开它的钥匙:
/**
 * 模拟现实中的锁
 */
public class MyLock {
    /**
     * 能够打开锁的钥匙
     */
    private MyKey key;

    // todo
    // set、get、toString 方法
}
  1. spring-config.xml 中添加如下 Bean
<bean id="myKeyBean" class="com.guyi.spring.model.entity.MyKey"/>
<bean id="myLockBean" class="com.guyi.spring.model.entity.MyLock">
    <property name="key" ref="myKeyBean"/>
</bean>

注意 <property name="key" ref="myKeyBean"/> 用的是 ref 属性

  1. 测试

image.png

set 注入

上边对 简单数据类型 和 非简单数据类型 的注入演示,使用的都是 set 注入。
使用 set 注入,必须提供无参构造set 方法!因为 set 注入是先调用无参构造进行实例化对象,在调用对应的 set 方法来注入依赖的。

外部注入

外部 Bean 的特点:要注入的 Bean 定义在外面,在 property 标签中使用 ref 属性进行注入,是常用的注入方式。

  1. 创建一个 UserDao.java
public class UserDao {
}
  1. 创建一个 OrderDao.java
public class OrderDao {
    /**
     * 假设 OrderDao 依赖于 userDao
     */
    private UserDao userDao;

	public void addOrder() {
        System.out.println("正在为用户-" + userDao + "创建订单...");
        System.out.println("新的订单创建成功...");
    }
    
    // todo
    // set、get、toString 方法
}
  1. spring-config.xml 中注册两个 Bean:
<bean id="userDaoBean" class="com.guyi.spring.dao.UserDao" />
<bean id="orderDaoBean" class="com.guyi.spring.dao.OrderDao">
    <property name="userDao" ref="userDaoBean" />
</bean>
  1. property 是用来给 Bean 的实例变量赋值的。name 属性用来指定实例变量,值为对应的实例变量名
  2. 对于 orderDaoBean 来说,userDaoBean 是一个外部 Bean,向orderDaoBean 注入 userDaoBean,称为外部注入
  1. 测试
/**
 * 测试 set 注入,使用外部注入的方式实现
 */
@Test
public void testExterior() {
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config.xml");
    Object orderDaoBean = classPathXmlApplicationContext.getBean("orderDaoBean");
    OrderDao orderDao = (OrderDao) orderDaoBean;
    orderDao.addOrder();
}

image.png

外部注入
  1. 还是用 UserDao.javaOrderDao.java 进行演示,在 spring-config.xml 中新注册一个 Bean
<bean id="orderDaoBean2" class="com.guyi.spring.dao.OrderDao">
    <property name="userDao">
        <bean class="com.guyi.spring.dao.UserDao" />
    </property>
</bean>

注意:此时的 UserDao 对应的 Bean 注册在 orderDaoBean2 的内部,并且可以不设置 id 属性(设置了之后 IDE 也会提示是多余的)。
此时,这个内部 Bean 是没办法复用的!!!

  1. 测试

image.png

小结
  1. set 注入,是通过 set 方法完成依赖注入的,使用时必须确保类中有 set 方法。
  2. 外部注入可以实现对被依赖的 Bean 的复用,外部注入无法实现对被依赖的 Bean 的复用,推荐使用外部注入。
  3. 简单分析 set 注入的原理:
  • 首先 Spring 通过反射机制实例化一个 Bean 对象(此时这个 Bean 是未初始化的,即实例变量没有注入对应的值),并获取其 Class 对象
  • Spring 根据得到的 Class 对象,获取到 Bean 对象所有的 set 方法
  • 对于每个 set 方法,Spring 会解析方法名,提取方法名中 “set” 后面的部分作为属性名,将首字母转为小写
  • 接下来,Spring 会判断 Bean 各个属性的类型,并根据类型来确定如何注入属性值:
    • 基本类型(简单数据类型)注入:通过解析属性的 setter 方法参数类型,将配置文件中的值转换成对应的基本类型,并调用 setter 方法注入属性值。
    • 引用类型注入:如果 setter 方法参数是其他 Bean 对象的类型,则 Spring 会通过容器中的 BeanFactory 获取对应的 Bean 对象,并调用 setter 方法注入引用。这样就实现了 Bean 之间的依赖注入
  • 最后,Spring 会将属性值注入到 Bean 对象中,通过调用 setter 方法完成属性的设置

构造器注入

根据参数的位置注入
  1. /src/main/resouces 下新创建一个 Spring 的配置文件:spring-config-constructor.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">

</beans>
  1. User.java 中中添加一个全参数构造器
// username 的位置为 0;no 的位置为 1
public User(String username, Integer no) {
    this.username = username;
    this.no = no;
}

注意:除了提供一个全参数构造器之外,还要提供一个无参构造器,否则再去运行 set 注入的例子,会报错的。

  1. spring-config-constructor.xml 中注册一个 Bean:
<!-- 构造器注入:根据参数位置注入 -->
<bean id="userConstructorIndexBean" class="com.guyi.spring.model.entity.User">
    <constructor-arg index="0" value="张三"/>
    <constructor-arg index="1" value="1"/>
</bean>

使用构造器注入,通过标签 constructor-arg 实现。可以通过 index 属性指明需要注入依赖的参数的下标,value/ref 属性来指定注入的值。

  1. 测试
/**
 * 构造器注入:根据参数位置注入
 */
@Test
public void testIndexInject() {
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config-constructor.xml");

    Object userConstructorBean = classPathXmlApplicationContext.getBean("userConstructorIndexBean");
    System.out.println(userConstructorBean);
}

image.png

根据参数的名称注入
  1. spring-config-constructor.xml 中注册一个 Bean:
<!-- 构造器注入:根据参数名称注入 -->
<bean id="userConstructorNameBean" class="com.guyi.spring.model.entity.User">
    <constructor-arg name="username" value="李四"/>
    <constructor-arg name="no" value="2"/>
</bean>

通过标签 constructor-arg标签的 name 属性指明需要注入依赖的参数的参数名,value/ref 属性来指定注入的值。

  1. 测试
/**
 * 构造器注入:根据参数名称注入
 */
@Test
public void testNameInject() {
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config-constructor.xml");

    Object userConstructorBean = classPathXmlApplicationContext.getBean("userConstructorNameBean");
    System.out.println(userConstructorBean);
}

image.png

根据类型注入

这种方式不指定 index 属性,也不指定 name 属性,框架根据类型自动注入。

  1. MyLock.java 中添加实例变量
/**
 * 锁的名称
 */
private String name;

/**
 * 锁的价格
 */
private Integer price;

对应的 setter、getter、toString、无参构造器、全参数构造器请自行添加

  1. spring-config-constructor.xml 中注册如下 Bean:
<!-- 构造器注入:根据参数类型注入 -->
<bean id="myKeyConstructorTypeBean" class="com.guyi.spring.model.entity.MyKey" />
<bean id="myLockConstructorTypeBean" class="com.guyi.spring.model.entity.MyLock">
    <constructor-arg ref="myKeyConstructorTypeBean"/>
    <constructor-arg>
        <bean class="java.lang.Integer">
            <constructor-arg value="10" />
        </bean>
    </constructor-arg>
    <constructor-arg>
        <bean class="java.lang.String">
            <constructor-arg value="王五" />
        </bean>
    </constructor-arg>
</bean>
  • 这种方式是将每个参数的类型都当成了非简单数据类型;
  • 在参数类型都不同时,可以不需要注意声明注入的依赖的顺序
  • 当有重复类型的参数时,要注意相同类型的依赖的声明顺序
  • 写法复杂,不推荐使用
<!-- 构造器注入:根据参数类型注入 -->
<bean id="myKeyConstructorTypeBean" class="com.guyi.spring.model.entity.MyKey" />
<bean id="myLockConstructorTypeBean" class="com.guyi.spring.model.entity.MyLock">
    <constructor-arg ref="myKeyConstructorTypeBean"/>
    <!-- 注意顺序 -->
    <constructor-arg value="王五"/>
    <constructor-arg value="10"/>
</bean>
  • 这种方式只有在参数类型不是简单类型时才使用 ref 或者创建内部 Bean 的方式来注入依赖
  • 在参数类型(简单数据类型视为同一类)都不同时,可以不需要注意声明注入的依赖的顺序
  • 推荐按照参数列表的顺序来声明要注入的依赖
  • 写法简洁,推荐使用
  1. 测试
/**
 * 构造器注入:根据参数类型匹配注入
 */
@Test
public void testTypeInject() {
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config-constructor.xml");

    Object userConstructorBean = classPathXmlApplicationContext.getBean("myLockConstructorTypeBean");
    System.out.println(userConstructorBean);
}

image.png

命名空间

P 命名空间
  1. 目的:简化 set 注入配置
  2. 使用前提:
  • 在 XML 头部信息中添加 p 命名空间的配置信息:xmlns:p=“http.springframework.org/schema/p”
  • p 命名空间注入是基于 setter 方法的,所以对应属性需要提供 setter 方法和无参构造器
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  
       xmlns:p="http://www.springframework.org/schema/p"
  
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean id="" class="" p:属性名="" p:属性名-ref="BeanId" />
</beans>
  • p:属性名=“值” 是用来注入简单类型数据的
  • p:属性名-ref=“BeanId” 是用来注入非简单类型数据的,比如大多数的自定义类型

实例

  1. spring-config.xml 引入 p 命名空间
xmlns:p="http://www.springframework.org/schema/p"
  1. spring-config.xml 中注册如下 Bean:
<bean id="myKeyBean2" class="com.guyi.spring.model.entity.MyKey"/>
<bean id="myLockBean2" class="com.guyi.spring.model.entity.MyLock"
      p:key-ref="myKeyBean2"
      p:name="锁2"
      p:price="10"
/>

不使用 P 命名空间的 set 注入:

<bean id="myLockBean2" class="com.guyi.spring.model.entity.MyLock">
  <property name="key" ref="myKeyBean2"/>
  <property name="name" value="锁2"/>
  <property name="price" value="10" />
</bean>

相对于使用 P 密码空间,这种写法更复杂

  1. 测试
/**
 * 测试 set 注入:使用 p 密码空间
 */
@Test
public void testP() {
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config.xml");
    Object lock = classPathXmlApplicationContext.getBean("myLockBean2");
    System.out.println(lock);
}

image.png

c 命名空间
  1. 目的:简化构造器注入配置
  2. 使用前提:
  • 需要 xml 文件头部添加信息:xmlns:c=“http://www.springframework.org/schema/c”
  • 提供对应的构造方法
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  
       xmlns:c="http://www.springframework.org/schema/c"
  
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <!-- 根据下标 -->
    <bean id="" class="" p:_index="下标" p:_index-ref="对应bean的id" />
  
    <!-- 根据属性名 -->
    <bean id="" class="" p:属性名="" p:属性名-ref="对应bean的id" />
</beans>

实例

  1. spring-config-constructor.xml 引入 c 命名空间
xmlns:c="http://www.springframework.org/schema/c"
  1. spring-config-constructor.xml 注册如下 Bean:
<!-- p 命名空间:使用下标 -->
<bean id="myKeyConstructorTypeBean2" class="com.guyi.spring.model.entity.MyKey" />
<bean id="myLockConstructorTypeBean2" class="com.guyi.spring.model.entity.MyLock" 
      c:_0-ref="myKeyConstructorTypeBean2"
      c:_1="锁3"
      c:_2="10"
/>
<!-- p 命名空间:使用参数名称 -->
<bean id="myLockConstructorTypeBean3" class="com.guyi.spring.model.entity.MyLock"
      c:key-ref="myKeyConstructorTypeBean2"
      c:name="锁4"
      c:price="15"
/>
  1. 测试
/**
 * 构造器注入:c 命名空间
 */
@Test
public void testP() {
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config-constructor.xml");

    Object userConstructorBean2 = classPathXmlApplicationContext.getBean("myLockConstructorTypeBean2");
    System.out.println(userConstructorBean2);

    Object userConstructorBean3 = classPathXmlApplicationContext.getBean("myLockConstructorTypeBean3");
    System.out.println(userConstructorBean3);
}

image.png

util 命名空间
  1. 目的:提供配置加载(达到配置复用的效果)、集合进行操作等功能。
  2. 使用时需要引入: xmlns:util="[http://www.springframework.org/schema/util"](http://www.springframework.org/schema/util")
<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">
    
</beans>

常用操作:

  1. 对集合
  • <util:list>:用于定义一个 List 集合。
  • <util:set>:用于定义一个 Set 集合。
  • <util:map>:用于定义一个 Map 集合。
  • <util:properties>:用于定义一个 Properties 对象。
    • <prop key="keyName">Value</prop>
  1. 资源加载
  • <util:properties>:用于加载属性文件,并将其转为 Properties 对象。
    • 使用 location 属性指定文件路径
  • <util:constant>:用于定义常量值,将某个类中使用 public static 修饰的属性注册为 Bean。
    • <util:constant static-field="常量全限定名称" id="" />
  1. 属性配置
  • <util:property-path>:用于引用某个对象的属性路径,将其注册为 Bean。
集合操作
  1. /src/main/resouces 下新创建 Spring 的配置文件:spring-config-util.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">
    

</beans>
  1. 引入 util 命名空间
xmlns:util="http://www.springframework.org/schema/util"

image.png

  1. xsi:schemaLocation 中添加如下内容
http://www.springframework.org/schema/util 
http://www.springframework.org/schema/util/spring-util.xsd

image.png

  1. 创建一个学生类:Student.java
/**
 * 学生类
 */
public class Student {

    /**
     * 学生学号
     */
    private Integer no;
    
    /**
     * 学生姓名
     */
    private String studentName;

    // todo
    // setter、getter、toString、对应构造方法自行添加
}
  1. 创建一个班级类 Clazz.java
/**
 * 班级类
 */
public class Clazz {
    /**
     * 班级名称
     */
    private String clazzName;

    /**
     * 班级里的学生
     */
    private List<Student> studentList;

    // todo
    // setter、getter、toString、对应构造方法自行添加
}
  1. spring-config-util.xml 注册如下 Bean:
<!-- 注册三个学生 Bean -->
<bean id="student1" class="com.guyi.spring.model.entity.Student">
    <constructor-arg index="0" value="1"/>
    <constructor-arg index="1" value="张三"/>
</bean>
<bean id="student2" class="com.guyi.spring.model.entity.Student">
    <constructor-arg index="0" value="2"/>
    <constructor-arg index="1" value="李四"/>
</bean>
<bean id="student3" class="com.guyi.spring.model.entity.Student">
    <constructor-arg index="0" value="3"/>
    <constructor-arg index="1" value="王五"/>
</bean>

<!-- 创建 List 集合, 并将三个学生 Bean 添加到集合中 -->
<util:list id="studentList"
           list-class="java.util.ArrayList"
           scope="singleton"
           value-type="com.guyi.spring.model.entity.Student">
    <ref bean="student1" />
    <ref bean="student2" />
    <ref bean="student3" />
</util:list>

<!-- 注册班级 Bean -->
<bean id="clazzBean" class="com.guyi.spring.model.entity.Clazz">
    <property name="clazzName" value="1班" />
    <property name="studentList" ref="studentList" />
</bean>

对 util 中出现的属性进行说明:

  1. scope:指定 Bean 创建的模式,在其他的 bean 标签中也有这个属性

    1. singleton:单例模式
    2. prototype:原型模式
  2. list-class:指定使用 List 接口的哪一个实现类

  3. value-type:指定泛型类型

  4. 测试

/**
 * 测试操作 List 集合
 */
@Test
public void testList() {
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config-util.xml");

    Object clazzBean = classPathXmlApplicationContext.getBean("clazzBean");
    System.out.println(clazzBean);
}

image.png

资源加载
  1. src/main/resources 下创建一个 jdbc.properties 属性配置文件:
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai
username=root
password=xxxx
  1. spring-config-util.xml 添加如下内容:

方式一:

<!-- 资源加载 -->
<util:properties id="jdbcConfig" location="classpath:jdbc.properties" />
<bean id="myDataSourceBean2" class="com.guyi.spring.datasource.MyDataSource">
    <property name="driver" value="#{jdbcConfig.driver}" />
    <property name="url" value="#{jdbcConfig.url}" />
    <property name="username" value="#{jdbcConfig.username}" />
    <property name="password" value="#{jdbcConfig.password}" />
</bean>

使用 #{} 来取 jdbcConfig 的属性。
使用这种方式不要在配置文件中个属性添加额外的前缀,不如:jdbc.driver

方式二:

<!-- 这种方式使用了 context 命名空间 -->
<context:property-placeholder location="jdbc.properties" />
<bean id="myDataSourceBean3" class="com.guyi.spring.datasource.MyDataSource">
    <property name="driver" value="\${jdbc.driver}"/>
    <property name="url" value="\${jdbc.url}"/>
    <property name="username" value="\${jdbc.username}"/>
    <property name="password" value="\${jdbc.password}"/>
</bean>

这种方式使用了 context 命名空间,不展开讲。
注意: 对于 , S p r i n g 会优先取系统的环境变量的值 , 比如: {}, Spring 会优先取系统的环境变量的值,比如: ,Spring会优先取系统的环境变量的值,比如:{username}: 得到的是 Windows 系统的用户名,因此属性配置文件的的 key 最好加个前缀

  1. 测试
/**
 * 测试资源加载
 */
@Test
public void testProperties() {
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config-util.xml");

    Object myDataSource = classPathXmlApplicationContext.getBean("myDataSourceBean2");
    System.out.println(myDataSource);
}

image.png

属性配置
  1. spring-config-util.xml 添加如下内容:
<util:property-path path="myDataSourceBean2.url" id="sourceUrl" />

myDataSourceBean2 是之前定义的一个 Bean 的名称(id)

  1. 测试
/**
 * 测试属性操作
 */
@Test
public void testPropertyPath() {
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config-util.xml");

    Object sourceUrl = classPathXmlApplicationContext.getBean("sourceUrl");
    System.out.println(sourceUrl);
}

image.png

自动装配

Spring 还可以完成自动化的注入,自动化注入又称自动装配,可以根据名字进行自动装配,也可以根据类型自动装配。
可以通过 bean 标签的 autowire 属性来声明采用什么机制进行自动装配。

根据名称装配

指定 autowire 属性的值为 byName
需要注意的是,被注入的 Bean 的 id,要和目标 Bean 中的属性名称一致。

  1. 创建 spring-config-autowire.xml,并注册如下 Bean
<!-- 这个 Bean 的 id 必须对于 MyLock 类的 key 变量名,否则无法根据名称完成自动装配 -->
<bean id="key" class="com.guyi.spring.model.entity.MyKey" />
<bean id="myLockBeanByName" class="com.guyi.spring.model.entity.MyLock" autowire="byName">
  <property name="name" value="" />
  <property name="price" value="10" />
</bean>

image.png

  1. 测试
/**
 * 测试根据名称自动注入
 */
@Test
public void testByName() {
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config-autowire.xml");

    Object myLockBeanByName = classPathXmlApplicationContext.getBean("myLockBeanByName");
    System.out.println(myLockBeanByName);
}
根据类型装配

指定 autowire 属性的值为 byType
需要提供 setter 方法和无参构造器。

  1. spring-config-autowire.xml,并注册如下 Bean
<bean id="key2" class="com.guyi.spring.model.entity.MyKey" />
<bean id="myLockBeanByName2" class="com.guyi.spring.model.entity.MyLock" autowire="byType">
    <property name="name" value="" />
    <property name="price" value="10" />
</bean>

注意点:

  • 使用类型装配,被依赖类型的 Bean 实例只能有一个。比如这里使用了类型装配,注册了一个类型为 MyKey 的 Bean,id 为 key2,而上一个例子中,也注册一个类型为 MyKey 的 Bean,id 为 key;这两个 Bean 必须去掉一个,否则程序执行会报错,这里先将 id 为 key 的 Bean 注释掉。
  • 使用类型装配时,被依赖 Bean 的 id 可以不写。
  1. 测试
/**
 * 测试根据名称自动注入
 */
@Test
public void testByType() {
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config-autowire.xml");

    Object myLockBeanByName = classPathXmlApplicationContext.getBean("myLockBeanByName2");
    System.out.println(myLockBeanByName);
}

image.png

Spring Bean 的作用域

  1. Singleton(默认):在整个应用程序的上下文中,只创建一个 Bean 实例。无论何时都返回同一个实例对象。
  2. Prototype:每次请求 Bean 时都会创建一个新的 Bean 实例。

以下作用域是在 Web 项目中才可以使用

  1. Request:每个 HTTP 请求都会创建一个新的Bean实例。在单次请求的处理过程中,所有的 Bean 引用都指向同一个 Bean 实例。
  2. Session:每个用户会话都会创建一个新的 Bean 实例。在一个用户的整个会话期间,所有的 Bean 引用都指向同一个 Bean 实例。
  3. Global Session:类似于 Session 作用域,但仅在基于 Portlet 的 web 应用程序中才有效。
  4. Application:在 Web 应用程序的整个生命周期内,只创建一个 Bean 实例。无论何时都返回同一个实例对象。
  5. WebSocket:在 WebSocket 会话期间创建的 Bean 实例。每个 WebSocket 连接都会创建一个新的Bean实例。

这些作用域可以通过在 Spring 的 XML 配置文件中使用 <bean> 元素的 scope 属性来指定。例如:

<bean id="myBean" class="com.example.MyBean" scope="prototype" />

BeanFactory 和 FactoryBean

在 Spring 中,Bean 可以分为两类:

  • 普通 Bean
  • 工厂 Bean

BeanFactroy

使用场景

  1. 获取 Bean
  2. 检索 Bean 是否存在于 容器中
  3. 判断 Bean 是否是单例的

什么是 BeanFactory?

  • BeanFactory 是 Spring框架的核心接口之一,负责创建和管理Bean实例。
  • 它是一个被用于构建和配置 Spring 容器的工厂接口,提供了从容器中获取 Bean 的方法。
  • BeanFactory 管理者 Spring 容器中的所有 Bean。
  • BeanFactory 是 Spring 的底层基础设施,提供了对Bean的创建、获取、注入和销毁等功能。
  • 它可以根据配置文件或注解配置,通过反射机制动态的创建 Bean,并将其装配到容器中。
  • 常见的 BeanFactory 实现包括 XmlBeanFactory 和 ApplicationContext

常用方法:

  • getBean(String name):根据 BeanId 获取 Bean 实例
  • getBean(String name, @NullableClass requiredType ):据 BeanID 和 Class 类型来得到Bean 实例,增加了类型安全验证机制。
  • containsBean(String name):根据 BeanId 进行检索,查看 Bean 是否在容器中
  • isSingleton(String name):根据 BeanId 判断 Bean 是否是单例的
  • isPrototype(String name):根据 BeanId 判断 Bean 是否是原型的
  • getType(String name):获取 Bean 的 Class 类型
  • getAliases(String name):获取 Bean 的别名,如果传入的是别名,那么原名也会返回

FactoryBean

什么是 FactoryBean?

  • FactoryBean 是 Spring 提供的一个特殊接口,允许自定义 Bean 的创建逻辑。
  • 实现 FactoryBean 接口的类可以充当一个工厂(也是一个 Bean),负责创建和管理其他 Bean 实例。
  • FactoryBean 在 Spring 容器中被视为一个普通的 Bean,并且可以像其他 Bean 一样进行依赖注入和配置。
  • 如果一个 Bean 实现了 FactoryBean 接口并处理了 FactoryBean 的生命周期方法,那么在从容器获取该 Bean 时,实际上获取到的是该 Bean 的getObject() 方法所返回的对象。

FactoryBean 接口的三个方法:

  • getObject():用于返回 FactoryBean 实例化好的 Bean。如果处于单例模式下,该实例 Bean 会被存在 Spring 容器的单例缓存池中。
  • getObjectType():返回 FactoryBean 创建的实例的类型。
  • isSingleton():默认单例单例模式, 返回值改为 false 变为原型模式。

为什么需要 FactroyBean?

  1. 便于创建实例化过程复杂的 Bean。在一些场景下可能需要在 Bean 的实例化过程中执行一些复杂的逻辑,这些逻辑可能很难通过配置来定义。FactoryBean 允许开发者在 getObject() 方法中自定义实例化 Bean 的过程,弥补了通过配置来实例化 Bean 的不足。
  2. 实现 Bean 定制化。FactoryBean 允许将 Bean 的实例化过程与容器的配置进行分离,通过实现这个接口,用户可以将 Bean 的实例化和配置逻辑封装在一起,在必要时执行定制化处理。
  3. 动态创建 Bean。有时候,开发者可能希望根据外部条件或参数来动态地创建 Bean 实例。FactoryBean 提供了创建和配置 Bean 的灵活性,使用户能够根据需要动态地创建和返回不同的 Bean 实例。
  4. 实现对象的缓存和复用。FactoryBean 可以管理自己创建的对象,使其能够在单例模式下进行缓存和复用。这样可以提高应用程序的性能和效率。

FactoryBean 接口为 Spring 框架提供了一种扩展机制,允许用户自定义 Bean 的创建逻辑,并提供了更好的灵活性和定制化能力。

Spring 中对 Bean 的实例化方式

这里主要讨论 Bean 的实例化方式

通过构造方法实例化

相当于使用构造注入的方式。对于 set 注入,也是先调用了类的无参构造进行实例化,后调用 setter 方法进行依赖注入的。
获取 Bean的方法是:通过 AplicationContext 对象的 getBean() 方法来获取指定的Bean。

通过简单工程模式实例化

  1. 创建一个 Star.java
public class Star {
    public Star() {
        System.out.println("Star 的构造方法执行了");
    }
}
  1. 创建一个 StarBeanFactory.java
/**
 * 充当简单工厂类模式中的工厂类角色
 */
public class StarBeanFactory {
    // 简单工厂模式中的工厂类中有一个静态方法
    public static Star get() {
        return new Star();
    }
}
  1. src/main/resources 中新建一个 spring-config-create-bean.xml,添加如下内容:
<bean id="starBeanFactory" class="com.guyi.spring.factory.StarBeanFactory" factory-method="get"/>

factory-method 属性指定的是工厂类中的静态方法,告诉框架, 调用哪个类的哪个方法获取 bean 对象。

  1. 测试
/**
 * 通过简单工厂模式实例化 Bean
 */
@Test
public void testSimpleFactory() {
    ApplicationContext applicationContext
            = new ClassPathXmlApplicationContext("spring-config-create-bean.xml");

    Star starBean = applicationContext.getBean("starBeanFactory", Star.class);
    System.out.println(starBean);
}

这里可以看到,调用 ApplactionContext 对象的 getBean() 时,可以传入一个 Class 对象,明确要获取的 Bean 的类型,如果没有传入这个参数,getBean() 返回的是一个 Object 类型的 Bean,使用时还要手动类型转换;传递这个参数后,返回的就是对应类型的 Bean,免去了手动类型转换的工作,也明确了需求,可读性更好,建议传递。

结果:
image.png

通过 factory-bean 实例化

  1. 新建一个 StarFactoryBean.java
public class StartFactoryBean {
    // 实例方法
    public Star get() {
        return new Star();
    }
}
  1. spring-config-create-bean.xml 添加如下内容:
<!-- 通过 factory-bean 属性和 factory-method 属性共同完成 -->
<bean id="starFactoryBean" class="com.guyi.spring.factory.StartFactoryBean"/>
<bean id="starBean2" factory-bean="starFactoryBean" factory-method="get"/>

先创建一个 工厂 Bean,利用这个工厂 Bean 实例化目标 Bean

  1. 测试
/**
 * 通过工厂模式实例化 Bean
 */
@Test
public void testFactoryBean() {
    ApplicationContext applicationContext
            = new ClassPathXmlApplicationContext("spring-config-create-bean.xml");

    Star starBean = applicationContext.getBean("starBean2", Star.class);
    System.out.println(starBean);
}

image.png

通过 FactoryBean 接口实例化

当类实现了 FactoryBean 接口后,就不需要指定 factory-bean 属性和 factory-method 属性了,简化了第三种方式。

示例:

  1. 创建一个 StarFactroyByFactoryBean 类,实现 FactoryBean 接口
public class StartFactoryByFactoryBean<T> implements FactoryBean<Star> {
    /**
     * 这里可以对实例化的 Bean 进行一些处理
     *
     * @return
     * @throws Exception
     */
    @Override
    public Star getObject() throws Exception {
        System.out.println("通过实现 FactoryBean 接口来实现示例化 Bean");
        return new Star();
    }

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

    /**
     * 默认单例单例模式, 返回值改为 false 变为原型模式
     *
     * @return
     */
    @Override
    public boolean isSingleton() {
        return FactoryBean.super.isSingleton();
    }
}
  1. 在 spring-config-create-bean.xml 中添加如下内容
<!-- 通过实现了 FactoryBean 的 Bean 来创建 StartBean -->
<bean id="startObj"  class="com.guyi.spring.factory.StartFactoryByFactoryBean" />

注意这里的 class 指向的是:StartFactoryByFactoryBean

  1. 测试
/**
 * 通过实现 FactoryBean 接口实现实例化 Bean
 */
@Test
public void testFactoryBeanInterface() {
    ApplicationContext applicationContext
            = new ClassPathXmlApplicationContext("spring-config-create-bean.xml");
    applicationContext.getBean("startObj", Star.class);
}

image.png

Spring Bean 的生命周期

Bean 生命周期的管理,可以参考 Spring 源码:AbstractAutowireCapableBeanFactory 类的 doCreateBean() 方法。

“Bean 后处理器” 是指实现了 BeanPostProcessor 接口的类

Bean 生命周期–五步

实例化 Bean --> Bean 属性的赋值 --> 初始化 Bean --> 使用 Bean --> 销毁 Bean

  1. 新建一个 cycle 包,用于编写演示 Bean 生命周期的代码。

  2. cycle 包下新建一个 BeanFiveCycle.java:

/**
 * 演示 Bean 生命周期--五步
 *
 * 第一步:实例化 Bean
 * 第二步:给 Bean 属性赋值
 * 第三步:初始化 Bean, 会调用 Bean 的 inti() 方法, 注意:这个 init() 要自己写, 自己配
 * 第四步:使用 Bean
 * 第五步:调用 Bean 的 destroy() 方法, 这个方法要自己写, 自己配
 */
public class BeanFiveCycle {
    private String name;

    public BeanFiveCycle() {
        System.out.println("第一步: 实例化 Bean");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        System.out.println("第二步: 给 Bean 属性赋值");
        this.name = name;
    }

    public void init() {
        System.out.println("第三步: 初始化 Bean");
    }

    public void destroy() {
        System.out.println("第五: 销毁 Bean");
    }
}
  1. 新建一个 spring-config-bean-cycle.xml,并添加如下内容:
<!-- 需要手动指定初始化方法和销毁方法 -->
<bean id="beanFiveCycle" class="com.guyi.spring.cycle.BeanFiveCycle" init-method="init" destroy-method="destroy">
    <property name="name" value="张三"/>
</bean>

使用 init-method 属性指定使用那个方法来初始化 Bean
使用 destroy 属性指定使用那个方法来销毁 Bean

  1. 测试
@Test
public void testBeanFiveCycle() {
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config-bean-cycle.xml");
    BeanFiveCycle beanFiveCycle = classPathXmlApplicationContext.getBean("beanFiveCycle", BeanFiveCycle.class);

    System.out.println("==========================");
    // 第四步:使用 Bean
    System.out.println("第四步: 使用 Bean");
    String name = beanFiveCycle.getName();
    System.out.println(name);
    System.out.println(beanFiveCycle);
    System.out.println("==========================");

    // 必须关闭 Spring 容器才会销毁 Bean
    classPathXmlApplicationContext.close();
}

image.png

Bean 生命周期–七步

实例化 Bean --> Bean 属性的赋值 --> Bean 后处理器 postProcessBeforeInitialization()
–> 初始化 Bean --> 使用 Bean
–> Bean 后处理器 postProcessAfterInitialization() --> 销毁 Bean

  1. cycle 包下新建一个 BeanFiveCycle.java
/**
 * 演示 Bean 的生命周期--七步
 */
public class BeanSevenCycle {
    private String name;

    public BeanSevenCycle() {
        System.out.println("第一步: 实例化 Bean");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        System.out.println("第二步: 给 Bean 属性赋值");
        this.name = name;
    }

    public void init() {
        System.out.println("第四步: 初始化 Bean");
    }

    public void destroy() {
        System.out.println("第七步: 销毁 Bean");
    }
}
  1. cycle 包下新建一个日志 Bean 后处理器BeanFiveCycle.java
/**
 * 日志 Bean后处理器
 */
public class logBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("第三步: 执行Bean后处理器的 postProcessBeforeInitialization 方法");
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    /**
     *
     * @param bean 刚创建的 Bean对象
     * @param beanName Bean对象的名字
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("执行 Bean 后处理器的 postProcessAfterInitialization 方法");
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
}
  1. spring-config-bean-cycle.xml,并添加如下内容:
<!-- 配置 Bean 后处理器, 将作用与该配置文件中的所有 Bean -->
<bean class="com.guyi.spring.cycle.logBeanPostProcessor"/>
<bean id="beanSevenCycle" class="com.guyi.spring.cycle.BeanSevenCycle" init-method="init" destroy-method="destroy">
    <property name="name" value="李四"/>
</bean>

由于 Bean 后处理器会作用与其所在配置文件内的所有 Bean,所以将上一个例子注册的 Bean 注释掉,以免多输出内容,对最后的观察产生干扰。

  1. 测试
@Test
public void testBeanSevenCycle() {
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config-bean-cycle.xml");
    BeanSevenCycle beanSevenCycle = classPathXmlApplicationContext.getBean("beanSevenCycle", BeanSevenCycle.class);

    System.out.println("==========================");
    // 第四步:使用 Bean
    System.out.println("第六步: 使用 Bean");
    String name = beanSevenCycle.getName();
    System.out.println(name);
    System.out.println(beanSevenCycle);
    System.out.println("==========================");

    // 必须关闭 Spring 容器才会销毁 Bean
    classPathXmlApplicationContext.close();
}

image.png

Bean 生命周期–十步

实例化 Bean --> Bean 属性的赋值 --> 检查 Bean 是否实现 Aware 接口,并进行设置
–> Bean 后处理器 postProcessBeforeInitialization()
–> 检查 Bean 是否实现了 InitializingBean 接口,并调用接口方法
–> 初始化 Bean
–> Bean 后处理器 postProcessAfterInitialization()
–> 使用 Bean
–> 检查 Bean 是否是否实现了 DisposableBean 接口,并调用接口方法
–> 销毁 Bean

  1. cycle 包下新建一个 BeanTenCycle.java
/**
 * 演示 Bean 的生命周期--十步
 */
public class BeanTenCycle
        implements BeanNameAware,
        BeanClassLoaderAware,
        BeanFactoryAware,
        InitializingBean,
        DisposableBean {

    private String name;

    public BeanTenCycle() {
        System.out.println("第一步: 实例化 Bean");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        System.out.println("第二步: 给 Bean 属性赋值");
        this.name = name;
    }

    public void init() {
        System.out.println("第六步: 初始化 Bean");
    }

    // 不能使用 destroy 命名了, 接口 DisposableBean 有一个 destroy() 方法
    public void destroyBean() {
        System.out.println("第十步: 销毁接口");
    }

    @Override
    public void setBeanName(String name) {
        System.out.println("第三步: 来自 BeanNameAware 接口, 传递 BeanName(id)");
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        System.out.println("第三步: 来自 BeanClassLoaderAware 接口, 传递 Bean 的类加载器");
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("第三步: 来自 BeanFactoryAware 接口, 传递 Bean 所在的 BeanFactory");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("第五步: 来自 InitializingBean 接口, 执行 afterPropertiesSet()");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("第九步: 来自 DisposableBean 接口, 执行 destroy()");
    }
}
  1. 重新写一个 Bean 后处理器 TenBeanPostProcessor
public class TenBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("第四步: 执行Bean后处理器的 postProcessBeforeInitialization 方法");
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    /**
     * @param bean     刚创建的 Bean对象
     * @param beanName Bean对象的名字
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("第七步: 执行 Bean 后处理器的 postProcessAfterInitialization 方法");
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
}
  1. spring-config-bean-cycle.xml,并添加如下内容:
<!-- 配置Bean后处理器, 将作用与该配置文件中的所有 Bean -->
<bean class="com.guyi.spring.cycle.TenBeanPostProcessor"/>
<bean id="beanTenCycle" class="com.guyi.spring.cycle.BeanTenCycle" init-method="init" destroy-method="destroyBean">
    <property name="name" value="王五"/>
</bean>

把其他内容注释掉

  1. 测试
@Test
public void testBeanTenCycle() {
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config-bean-cycle.xml");
    BeanTenCycle beanTenCycle = classPathXmlApplicationContext.getBean("beanTenCycle", BeanTenCycle.class);

    System.out.println("==========================");
    System.out.println("第八步: 使用 Bean");
    String name = beanTenCycle.getName();
    System.out.println(name);
    System.out.println(beanTenCycle);
    System.out.println("==========================");

    // 必须关闭 Spring 容器才会销毁 Bean
    classPathXmlApplicationContext.close();
}

image.png


Spring 对 Bean 的管理方式

Spring 容器只对 singleton 的 Bean(单例模式下的 Bean) 进行完整的生命周期管理。
如果是 prototype 作用域的 Bean,Spring 容器只负责将该 Bean 初始化完毕(具体可以完成前八步)。等客户端程序一旦获取到该 Bean 之后,Spring 将不再管理该对象的生命周期了。

将手动创建的 Bean 注册到 Spring 容器

使用 DefaultListtableBeanFactory 对象的 registerSingleton() 方法可以将手动创建的 Bean 交给 Spring 管理。

@Test
public void testRegisterBean() {
    User user = new User("赵六", 4);
    System.out.println("user: " + user);

    DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
    factory.registerSingleton("userBean", user);
    User userBean = factory.getBean("userBean", User.class);
    System.out.println("userBean: " + userBean);
}

image.png

循环依赖

参考链接:深度解读 Spring 循环依赖

建议阅读参考链接的文章

什么是循环依赖

两个或多个 Bean 之间互相依赖,形成循环。比如有两个 Bean:A、B,A 依赖于 B,B 又依赖于 A;或者自己依赖自己。

循环依赖问题能不能被解决

能够被解决的循环依赖必须同时满足以下条件:

  1. 循环依赖当中的 Bean 必须是单例的。
  2. Bean 的注入方式不能全是构造器注入。
  3. 先创建 Bean 的依赖注入方式不能是构造器注入。
验证

建一个包: cycledepend。在 cycledepend 下创建两个存在互相依赖关系的类。
image.png

然后创建一个 spring-config-cycle-depend.xml

  1. 验证当 A 和 B 都使用 set 注入时,会不会有问题:

spring-config-cycle-depend.xml 添加:

<bean id="aBean" class="com.guyi.spring.cycledepend.A">
    <property name="b" ref="bBean"/>
</bean>
<bean id="bBean" class="com.guyi.spring.cycledepend.B">
    <property name="a" ref="aBean"/>
</bean>

测试

@Test
public void testAllSetter() {
    ClassPathXmlApplicationContext classPathXmlApplicationContext
            = new ClassPathXmlApplicationContext("spring-config-cycle-depend.xml");

    Object aBean = classPathXmlApplicationContext.getBean("aBean");
    Object bBean = classPathXmlApplicationContext.getBean("bBean");
    System.out.println(aBean);
    System.out.println(bBean);
}

image.png

可以看到程序没有出错,在全是 set 注入的情况下,不会因为循环依赖而产生问题。

  1. 验证当 A 和 B 都使用构造器注入时,会不会有问题

spring-config-cycle-depend.xml 添加:

<bean id="aBean" class="com.guyi.spring.cycledepend.A">
    <constructor-arg ref="bBean" />
</bean>
<bean id="bBean" class="com.guyi.spring.cycledepend.B">
    <constructor-arg ref="aBean" />
</bean>

将之前的内容注释掉

测试:还是用之前的测试程序。执行后发现程序报错,给出如下信息:
image.png
image.png

  1. 测试 A 使用 set 注入,B 使用构造器注入,会不会有问题

spring-config-cycle-depend.xml 添加:

<bean id="aBean" class="com.guyi.spring.cycledepend.A">
    <constructor-arg ref="bBean"/>
</bean>
<bean id="bBean" class="com.guyi.spring.cycledepend.B">
    <property name="a" ref="aBean"/>
</bean>

将之前的内容注释掉

测试:还是用之前的测试程序。程序能够正常执行:
image.png

可以看到程序没有出错,在 A 是 set 注入,B是构造注入的情况下,不会因为循环依赖而产生问题。

  1. 测试 A 使用构造注器入,B 使用 set 注入,会不会出问题

spring-config-cycle-depend.xml 添加:

<bean id="aBean" class="com.guyi.spring.cycledepend.A">
    <constructor-arg ref="bBean"/>
</bean>
<bean id="bBean" class="com.guyi.spring.cycledepend.B">
    <property name="a" ref="aBean"/>
</bean>

将之前的内容注释掉

测试:还是用之前的测试程序。执行程序后发现,出现和测试-2一致的问题。

小结
依赖情况依赖注入方式是否存在循环依赖问题
A、B互相依赖,A 注册在前都使用 set 注入不存在
A、B互相依赖,A 注册在前都是以构造注入存在
A、B互相依赖,A 注册在前A 使用 set 注入;B 使用构造器注入不存在
A、B互相依赖,A 注册在前A 使用构造器注入; B 使用 set 注入存在

由最后两点可以看出:Spring 在创建 Bean 时默认会根据自然排序进行创建,所以 A 会先于 B 进行创建。

Spring 如何解决循环依赖

为了解决循环依赖的问题,Spring 使用了两个阶段的依赖注入过程:

  1. 创建 Bean 实例:首先,容器会创建 Bean 的空实例,即只有对象骨架但尚未初始化的 Bean。只有在创建 Bean 实例之后,Spring 才能将其放入缓存中以便其他 Bean 引用。
  2. 注入依赖:在创建了所有 Bean 的实例之后,Spring 容器会根据各个 Bean 之间的依赖关系,完成依赖注入。

Spring 解决循环依赖的原理是使用了 Bean 的 “提前暴露” 机制(early references),当容器在创建 Bean 实例时,会保留一个早期引用,用于解决循环依赖。在依赖注入时,Spring 会先将早期引用暂存,待所有 Bean 都完成初始化后,再进行属性赋值。

Spring Ioc 注解

Spring 提倡全注解式开发

声明 Bean 相关注解

注解说明
@Component用于标识一个类作为Spring容器中的组件(Bean),会被自动扫描和注册到容器中。
@Contorller用于标识一个类作为Spring MVC中的**控制器(Controller),**处理请求和返回视图。
@Service用于标识一个类作为业务逻辑层(service)的组件,通常被注入到其他类中使用。
@Repository用于标识一个类作为数据访问层(Dao)的组件,提供对数据库的访问操作。

其他注解都是 @Component 的别名,本质是一样的。设置这么多个作用相同的注解是为了增强程序可读性。

Spring 注解的使用

  1. 引入 AOP 依赖

这个依赖存在于 spring-context 中,不需要单独引入

  1. 在配置文件中添加 context 命名空间

image.png

xmlns:context="http://www.springframework.org/schema/context"

http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
  1. 在配置文件中指定扫描的包
<!-- 指定要扫描的包 -->
<context:component-scan base-package="com.guyi.spring.bean"/>

<!-- 多个包 -->
<!-- 方式一:使用逗号隔开 -->
<context:component-scan base-package="com.guyi.spring.bean, com.guyi.spring.dao"/>

<!-- 方式二:指定共同的父包, 这种方式效率低 -->
<context:component-scan base-package="com.guyi.spring"/>
  1. 在 Bean 类上使用注解

选择性实例化 Bean

<context:component-scan base-package="com.guyi.spring.bean" use-default-filters="false">
	<context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
</context:component-scan>

use-default-filters=“false” 让在 com.guyi.spring.bean 包下的所有定义 Bean 的注解失效。
context:include-filter 标签指定让某一类定义 Bean 的注解生效。

如果不想让指定包下的定义 Bean 的注解失效,但又需要然某一类定义 Bean 的注解失效,将 use-default-filters=“false” 去掉:

<context:component-scan base-package="com.guyi.spring.bean">
  <!-- 让 @Repository 失效 -->
	<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
</context:component-scan>

依赖注入的 Bean

注解作用
@Value注入简单类型依赖
@Autowired默认根据类型注入依赖
@Qualifier@Autowired 配合使用,可以根据名称注入依赖
@Resource先根据名称注入依赖,根据名称没找到依赖,就会根据类型注入依赖
Value 注解

可以标注属性、setter 方法、形参上。
如果标注在属性上,必须提供无参构造器。

  1. 创建一个 annotation 包,在这个包下定义一个 Dog 类
/**
 * use Value Annotation
 */
@Component("dog")
public class Dog {

    @Value("小黑")
    private String bogName;

    @Value("2")
    private Integer bogAge;

    // todo
    // toString 方法
}

使用这种方式不需要提供 setter 方法

  1. 创建一个配置文件 spring-config-annotation.xml,引入context 命名空间,并指定要扫描的包
<!-- 指定扫描的包 -->
<context:component-scan base-package="com.guyi.spring.annotation"/>
  1. 测试
@Test
public void testValue() {
    ApplicationContext applicationContext
            = new ClassPathXmlApplicationContext("spring-config-annotation.xml");

    Dog dog = applicationContext.getBean("dog", Dog.class);
    System.out.println(dog);
}

image.png

Autowired 注解

可以用来标注构造方法、普通方法、形参、注解,required 属性用来声明依赖的 Bean 是否必须存在。
Autowired 注解源码

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

	/**
	 * 声明带注解的依赖项是否是必需的。
	 */
	boolean required() default true;

}

Autowired 注解一般会和 Qualifier 注解一起使用:

@Autowired
@Qualifier("被注入 Bean 的名字")
private OrderDao oreder;

优先根据类型进行装配,如果没有找到对于类型Bean,就根据 Qualifier 指定的名称寻找 Bean 进行注入。

需要注意的是:如果只是根据类型装配,那装配的接口下只能有一个实现类,否则就会报错,不能使用多态,很糟糕。

@Autowired
private OrderDao oreder;
Resource 注解
  1. Resource 注解是 JDK 拓展包中提供的,是 JDK 的一部分,是标准注解,更具有通用性。
  2. Resource 注解默认根据名称装配,没有指定 name 时,使用属性名作为 name, 通过 name 找不到要注入的 Bean 对象,会开启通过类型的装配。
  3. Resource 可以标注属性,setter 方法、组件类上。
  4. Resource 在 JDK 拓展包中,需要额外引入以下依赖(JDK8 不需要额外引入,高于 JDK11或低于JDK8需要引入):
<!-- spring5 及以下版本引入依赖 -->
<dependency>
  <groupId>javax.annotation</groupId>
  <artifactId>javax.annotation-api</artifactId>
  <version>版本</version>
</dependency>
<!-- 使用@Resource spring6+ 版本引入依赖 -->
<dependency>
  <groupId>jakarta.annotation</groupId>
  <artifactId>jakarta.annotation-api</artifactId>
  <version>版本</version>
</dependency>

AOP

AOP 能够捕捉系统常用功能,将其转化为组件。
AOP(Aspect Oriented Programming):面向切片编程、面向切面编程。
AOP 是对 OOP 的补充延展。
AOP 底层通过动态代理实现:JDK 动态代理 + CGLIB 动态代理技术。Spring 在这两种动态代理之间灵活切换。如果代理的是接口,会默认使用 JDK 动态代理;如果要代理某个类,这个类又没有实现接口,就会使CGLIB。当然,可以通过配置让 Spring 只使用 CGLIB。

使用 Spring AOP应当导入 AOP 依赖

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

应用场景

一个系统当中会有一些系统服务:日志、事务管理、安全验证等。这些系统服务被称为:交叉业务
如果这些 交叉业务 都掺杂到业务代码中,会存在一些问题:

  • 交叉业务的代码反复出现,代码没有得到复用
  • 如果要修改这些交叉业务代码,必须修改多处
  • 开发人员无法专注于核心业务代码的编写,在编写核心业务代码是可能还需要处理这些交叉业务

这时使用 AOP 可以轻松的解决以上问题。

AOP 的优点

AOP 可以将与业务无关的代码独立出来,形成独立的组件,然后以横向交叉的方式应用到业务流程中。

  • 代码复用性增强
  • 代码易维护
  • 使开发者可以更好的关注业务逻辑

AOP 七大概念

  • 连接点 Joinpoint
    • 在程序的整个执行流程中,可以织入切面的位置。方法的执行前后,异常抛出后的位置等。
    • 连接点描述的是位置
  • 切点 Pointcut
    • 在程序执行流程中,真正织入切面的方法。一个切点可以对应多个连接点。
    • 切点本质是方法,真正织入切面的那个方法叫做切点。
  • 通知 Advice
    • 又叫增强,就是具体织入的代码,事务代码、日志代码等。
    • 通知包括:
      • 前置通知
      • 后置通知
      • 环绕通知
      • 异常通知
      • 最终通知
  • 切面 Aspect
    • 切点 + 通知就是切面
  • 织入 Weaving
    • 把通知应用到目标对象的过程
  • 代理对象 Proxy
    • 一个目标对象被织入通知后产生的新对象
  • 目标对象 Target
    • 被织入通知的对象

切面实现方式

实现方式:

  1. Spring 框架结合 AsprctJ 框架实现的 AOP,基于注解开发
  2. Spring 框架结合 AsprctJ 框架实现的 AOP,基于 XML 开发
  3. Spring 框架自己实现的 AOP,基于 XML 配置方式
定义切面

使用 @Aspect 标注切面类

/**
 * 则会使一个切面类
 */
@Aspect
@Component
public class myAspect {
}

可以使用 @Order 定义切面加载的优先级,值越小越早加载。

@Aspect
@Order(5)
@Component
public class myAspect {
}

如果没有指定 @Order,切面的加载顺序就按照类的名称排序进行加载

AOP 通知相关注解

使用时需要配合切面表达式一起使用

通知类型标签说明
前置通知@Before
后置通知@AfterReturning
环绕通知@Around
异常通知@AfterThrowing
最终通知@After 相当于是写在 finally 语句块中的
通知的执行顺序
  1. 不发生异常时:

  1. 发生异常时:

切面表达式

切面表达式类型有多种,这里只记录 exection 类型的切面表达式
对于切面表达式,没必要记,忘了就看笔记或者上网查

execution([控制访问服] 返回值类型 [全限定名称] 方法名(形参列表) [异常])
  • 访问权限控制修饰符
    • 可选
    • 不写,就是四个修饰符都有
  • 返回值类型
    • 必填项
    • “*” 表示返回值类型任意
  • 全限定类名
    • 可选
    • “…” 表示当前包以及子包下的所有类
    • 省略时表示所有的类
  • 方法名
    • 必填项
    • “*” 表示所有的方法
    • “set*” 表示所有的 set 方法
  • 形参列表
    • 必填项
    • “()” 表示没有参数的方法
    • “(…)” 参数类型和个数随意的方法
    • “(*)” 只有一个参数的方法
    • “(*, String)” 第一个参数类型随意,第二个参数是String
  • 异常
    • 可选项
    • 不填表示任意类型的异常
示例
  1. 给 service 包下的所有类中以 delete 开始的所有方法添加切面
execution(public * com.guyi.spring.service.*.delete*(..))

具体含义:service 包下任意类中使用 public 修饰的、返回值任意、只有一个形参的 delete 方法

  1. spring 包下的所有的方法
ececution(* com.guyi.spring..*..(..))
  1. 所有类的所有方法
execution(* *(..))

模拟转帐和取款业务流程

  1. service 包下新建一个 AccountService.java,模拟用户的转账、取款行为
/**
 * 目标对象
 */
@Service
public class AccountService {
    /**
     * 转账的业务方法
     */
    public void transfer() {
        System.out.println("银行户正在转账...");
    }

    /**
     * 取款的业务方法
     */
    public void withdraw() {
        System.out.println("银行账户正在取款...");
    }
}
  1. service 包下新建 OrderService.java,模拟生成订单业务
/**
 * 目标对象
 */
@Service
public class OrderService {
    /**
     * 生成订单的业务方法
     */
    public void generate() {
        System.out.println("正在生成订单...");
    }

    /**
     * 取消订单的业务方法
     */
    public void cancel() {
        // 故意出异常
        String s = null;
        s.toString();

        System.out.println("订单已取消...");
    }
}
  1. aspect 包下创建 TransactionAspect.java,处理事务的切面
/**
 * 处理事务的切面
 */
@Component
@Aspect
public class TransactionAspect {
    /**
     * 环绕通知
     * @param joinPoint 连接点
     */
    @Around("execution(* com.guyi.spring6.service..*(..))")
    public void aroundAdvice(ProceedingJoinPoint joinPoint) {

        try {
            // 前环绕
            System.out.println("开启事务");

            // 执行目标
            joinPoint.proceed();

            // 后环绕
            System.out.println("提交事务");

        } catch (Throwable e) {
            System.out.println("回滚事务");
        }
    }
}

不要忘记了导依赖

  1. 编写一个配置类
@Configuration
@ComponentScan({"com.guyi.spring.service", "com.guyi.spring.aspect"})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class SpringConfig {
}
  1. 测试
@Test
public void testAOP () {
    // 注意:这里使用配置类替代了配置文件, 使用的是:AnnotationConfigApplicationContext
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);

    AccountService accountService = context.getBean("accountService", AccountService.class);
    accountService.transfer();  // 转账
    accountService.withdraw();  // 取款

    OrderService orderService = context.getBean("orderService", OrderService.class);
    orderService.generate();  // 生成订单
    orderService.cancel();  // 取消订单
}

image.png

Spring 事务支持

什么是事务

在一个业务流程中,可能会需要多条的 DML 语句共同完成,事务就是保证一个业务流程中的多条 DML 语句同时执行成功,或者同时执行失败,从而保证数据的一致性。

事务有如下特性:

  1. 原子性:事务是最小的工作单元,不可再分,所有操作要么同时提交,要么同时回滚。
  2. 一致性:多条 DML 语句必须同时执行成功或者同时执行失败,执行失败后数据要回滚。
  3. 隔离性:不同事务之间不能相互影响。
  4. 持久性:事务一旦提交,其所做的修改必须永久保存到数据库中。即使系统方式故障或者宕机,数据也能够保持不变。

事务的处理过程:

  1. 开启事务
  2. 执行业务的核心代码
  3. 提交事务 / 回滚事务

Spring 实现事务的方式

  • 编程式事务:通过编写代码实现对业务的管理
  • 声明式事务
    • 基于注解
    • 基于 XML 配置
开启声明式事务

方式一:

  1. 在 Spring 配置文件中配置数据源,这里使用德鲁伊

德鲁伊依赖

<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version></version>
</dependency>

配置数据源

<!-- 配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
	<property name="driverClassName" value=""/>
    <property name="url" value=""/>
    <property name="username" value=""/>
    <property name="password" value=""/>
</bean>
  1. 配置事务管理器
<!-- 配置事务管理器 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="" ref="数据源"/>
</bean>
  1. 开启事务支持

需要引入 tx 和 aop 命名空间

xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"

http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd

需要导入以下依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.18</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.3.18</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.3.18</version>
</dependency>
  1. 声明式事务配置:

在需要应用事务的bean或方法上使用tx:advice和tx:attributes元素进行配置,如下:

<bean id="userService" class="com.example.UserService">
    <property name="userRepository" ref="userRepository"/>
</bean>

<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

<aop:config>
    <aop:pointcut id="userServicePointcut" expression="execution(* com.example.UserService.*(..))"/>
    <aop:advisor advice-ref="transactionAdvice" pointcut-ref="userServicePointcut"/>
</aop:config>

或者开启事务管理器,再需要进行事务的管理的类或者方法上添加 @Transactional 注解即可。

<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

加在类上:这个类的的所有方法都应用事务
加在方法上:只对这个方法应用事务

方式二:

方式一比较繁琐,不建议使用

  1. 编写配置类
@Configuration
@EnableTransactionManagement  // 开启事务注解驱动管理器
@ComponentScan
public class SpringConfig2 {

    /**
     * 定义数据源
     *
     * @return 数据源对象
     */
    @Bean
    public DruidDataSource getDruidDataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();

        druidDataSource.setUsername("");
        druidDataSource.setPassword("");
        druidDataSource.setUrl("");  // 实际是给 jdbcUrl 赋值
        druidDataSource.setDriverClassName("");  // 实际是给 driverClass 赋值
        return druidDataSource;
    }

    /**
     * 定义事务管理器
     *
     * @param druidDataSource
     * @return
     */
    @Bean
    public PlatformTransactionManager transactionManager(DruidDataSource druidDataSource) {
        return new DataSourceTransactionManager(druidDataSource);
    }

}

这样就可以使用 @Transactional 来管理事务了

设置事务属性

事务传播行为(Propagation)

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

事务传播行为在 Spring 中被定义为枚举类型:

public enum Propagation() {
    REQUIRED(0),  // 默认
    SUPPORIS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    ENVER(5),
    ENSTED(6);
}
  • REQUIRED(0):支持当前事务,如果不存在就新建一个(没有就新建,有就加入。这个是默认的)
  • SUPPORIS(1):支持当前事务,如果当前没有事务,就以非事务方式执行(有就加入,没有就算了)
  • MANDATORY(2):必须允许在一个事务当中,如果当前没有事务发生,就抛异常(有就加入,没有就抛异常)
  • REQUIRES_NEW(3):开启一个新的事物,如果一个事务已经存在,就将这个事务挂起(不存在事务嵌套)
  • NOT_SUPPORTED(4):以非事务方式执行,如果事务存在,挂起当前事务(不支持事务,有就挂起)
  • ENVER(5):以非事务方式执行,如果存在事务,抛异常(不支持事务,存在抛异常)
  • ENSTED(6):如果当前事务正在进行,则该事务的对应方法应当执行在一个嵌套式事务中,被嵌套的事务可以独立于外层事务进行提交或回滚,如果外层事务存在,行为就像 REQUIRES_NEW 一样(有事务,就在这个事务里嵌套一个完全独立的事务,嵌套的事务可以独立的提交或回滚,没有事务就和 REQUIRES_NEW 一样)

在代码中设置事务的传播行为

@Transactional(propagation = Propagation.REQUIRED)
事务隔离级别
public enum Isolation {
    DEFAULT(-1),	// 默认级别; mysql的默认级别是4; 甲骨文的默认级别是2
    READ_UNCOMMITED(1),  // 读未提交
    READ_COMMITED(2),  // 读已提交
    REPEATABLE_READ(4),  // 可重复读
    SERIALIZABLE(8);  // 序列化
}

设置事务隔离级别

@Transactional(isolation = Isolation.READ_COMMITED)
事务超时
@Transactional(timeout = 10)
  • 表示设置事务的超时时间为10秒。如果超过 10 秒,该事务的 DML 语句还没有执行完,就选择回滚。
  • 默认值是 -1,表示没有时间限制
  • 事务超时时间是指哪段时间?
    • 在当前事务中,最后一条 DML 语句执行之前的时间。如果最后一条 DML 语句后面还有很多的业务逻辑,这些业务代码执行的时间不被计入超时时间。
只读事务
@Transactional(readOnly = true)

将当前事务设置为只读事务,也就是当前事务只能执行 select 语句,不能执行增、删、改。
作用是:启动 Spring 的优化策略,提高查询结果。
如果该事务中确实没有增、删、改操作,建议设置为只读事务。

异常回滚设置
@Transactional(rollbackFor = RuntimeException.class)

表示只有发生了 RuntimeException异常或其子类异常才会回滚

@Transactional(noRollbackFor = NullPointerException.class)

表示发生 NullPointerException 异常或其子类异常不回滚

Spring 常用注解汇总

组件相关

Bean 注解:https://zhuanlan.zhihu.com/p/99870991

注解说明
@Component用于标识一个类作为Spring容器中的组件(Bean),会被自动扫描和注册到容器中。
@Contorller用于标识一个类作为Spring MVC中的**控制器(Controller),**处理请求和返回视图。
@Service用于标识一个类作为业务逻辑层(service)的组件,通常被注入到其他类中使用。
@Repository用于标识一个类作为数据访问层(Dao)的组件,提供对数据库的访问操作。
@Bean方法的返回值(是一个对象)交给 Spring 管理

依赖注入相关

注解作用
@Value注入简单类型依赖
@Autowired默认根据类型注入依赖
@Qualifier@Autowired 配合使用,可以根据名称注入依赖
@Resource先根据名称注入依赖,根据名称没找到依赖,就会根据类型注入依赖

配置相关

注解作用
@Configuration用于标识一个类作为 Spring 配置类,替代传统的XML配置文件,用于定义、组合和装配Bean。
@EnableTransactionManagement用在配置类上,表示开启事务注解驱动,让程序支持使用注解开启事务
@Transactional用在类上、方法上,对于类,被标记类的所有方法都开启事务,对于方法,别标记的方法开启事务
@ComponentScan组件扫描器,可以将指定包下的被组件相关注解标注的类注册到日期中
@EnableAspectJAutoProxy用在配置类上,开启注解式 AOP,通过 AspectJ 自动代理完成

AOP 相关

注解作用
@Aspect声明一个类为切面类。
@Before在目标方法执行前执行切面逻辑。
@After在目标方法执行后(不论是否发生异常)执行切面逻辑。
@AfterReturning在目标方法执行后,如果没有发生异常,则执行切面逻辑。
@AfterThrowing在目标方法执行后,如果发生异常,则执行切面逻辑。
@Around在目标方法执行前和执行后执行切面逻辑。
@Pointcut声明一个切入点,用于定义切面逻辑的执行位置。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值