面向切面:单元测试、事务、资源操作

目录

一、单元测试

  • @SpringJUnitConfig注解是用于指定Spring的配置文件。在测试方法运行前,Spring会先根据该配置文件创建出需要的Bean,然后注入到测试类中。这样就可以在测试类中直接使用Spring的Bean进行测试了

  • 类似于:

    java 复制代码
    ApplicationContext context = new ClassPathXmlApplicationContext("xxx.xml");
    Xxxx xxx = context.getBean(Xxxx.class);

步骤一:引入依赖

xml 复制代码
<!--spring对junit的支持相关依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>6.0.2</version>
</dependency>

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

步骤二:配置文件

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

配置类扫描,用于自动装配Bean

步骤三:创建类

java 复制代码
@Component
public class User {

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

步骤四:演示

java 复制代码
@SpringJUnitConfig(locations = "classpath:beans.xml")
public class SpringJUnit5Test {

    @Autowired
    private User user;

    @Test
    public void testUser(){
        System.out.println(user);
    }
}

二、事务

2.1、概述

事务(Transaction)是指一个或多个操作序列组成的逻辑工作单元,这些操作要么全部成功,要么全部失败回滚。

特性:

  1. 原子性(Atomicity):事务是一个不可分割的工作单元,要么全部成功,要么全部失败,不允许出现部分成功部分失败的情况。
  2. 一致性(Consistency):事务执行前后,数据库的状态应该保持一致,如果一个事务执行失败,那么数据库应该恢复到执行前的状态。
  3. 隔离性(Isolation):多个事务之间应该互相隔离,事务之间不能互相干扰,避免脏读、不可重复读、幻读等问题。
  4. 持久性(Durability):事务完成后,对数据库的修改应该持久化保存,即使系统故障或崩溃,数据也不应该丢失。

2.1.1、编程式事务

编程式事务是通过在代码中编写事务管理代码来实现的,需要手动控制事务的开始、提交和回滚,需要在每个需要事务管理的方法中编写相关代码,这样代码耦合度高,且事务管理代码重复出现,不便于维护

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

2.1.2、声明式事务

声明式事务则是通过AOP的方式实现,将事务管理代码从业务逻辑中分离出来,通过在配置文件中声明事务管理,实现事务的自动管理,开发人员只需要在需要事务管理的方法上添加注解或者配置即可,大大简化了代码的编写和维护工作

java 复制代码
public interface UserService {
    void updateUser(User user);
}

@Service
@Transactional
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;

    @Override
    public void updateUser(User user) {
        userDao.update(user);
    }
}

说明:

  • 在这个例子中,@Transactional 注解被用于服务实现类的类级别上。这意味着,当调用 updateUser 方法时,Spring 将会创建一个事务,方法执行结束时,事务将被提交或回滚(如果出现异常)
  • 这样,在调用 updateUser 方法时,我们无需显式地开启和提交事务,Spring 框架将会自动处理事务的提交和回滚

2.2、JdbcTemplate

步骤一:加入依赖

xml 复制代码
<dependencies>
    <!--spring jdbc  Spring 持久化层支持jar包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>6.0.2</version>
    </dependency>
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.30</version>
    </dependency>
    <!-- 数据源 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.15</version>
    </dependency>
</dependencies>

步骤二:创建jdbc.properties

properties 复制代码
jdbc.user=root
jdbc.password=root
jdbc.url=jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false
jdbc.driver=com.mysql.cj.jdbc.Driver

步骤三:配置Spring的配置文件

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

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

    <!-- 配置数据源 -->
    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="username" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

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

</beans>

步骤四:准备数据库与测试表

sql 复制代码
CREATE DATABASE `spring`;

use `spring`;

CREATE TABLE `t_emp` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `sex` varchar(2) DEFAULT NULL COMMENT '性别',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

步骤五:创建测试类,整合JUnit,注入JdbcTemplate

java 复制代码
package com.atguigu.spring6;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

@SpringJUnitConfig(locations = "classpath:beans.xml")
public class JDBCTemplateTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;
    
}

步骤六:测试增删改功能

java 复制代码
@Test
//测试增删改功能
public void testUpdate(){
    //添加功能
	String sql = "insert into t_emp values(null,?,?,?)";
	int result = jdbcTemplate.update(sql, "张三", 23, "男");
    
    //修改功能
	String sql = "update t_emp set name=? where id=?";
    int result = jdbcTemplate.update(sql, "张三atguigu", 1);

    //删除功能
	String sql = "delete from t_emp where id=?";
	int result = jdbcTemplate.update(sql, 1);
}

步骤七:

java 复制代码
public class Emp {

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

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

    @Override
    public String toString() {
        return "Emp{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                '}';
    }
}
java 复制代码
//查询:返回对象
@Test
public void testSelectObject() {
    //写法一
//        String sql = "select * from t_emp where id=?";
//        Emp empResult = jdbcTemplate.queryForObject(sql,
//                (rs, rowNum) -> {
//                    Emp emp = new Emp();
//                    emp.setId(rs.getInt("id"));
//                    emp.setName(rs.getString("name"));
//                    emp.setAge(rs.getInt("age"));
//                    emp.setSex(rs.getString("sex"));
//                    return emp;
//                }, 1);
//        System.out.println(empResult);

    //写法二
    String sql = "select * from t_emp where id=?";
    Emp emp = jdbcTemplate.queryForObject(sql,new BeanPropertyRowMapper<>(Emp.class),1);
    System.out.println(emp);
}
@Test
//查询数据返回list集合
public void testSelectList(){
    String sql = "select * from t_emp";
    List<Emp> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Emp.class));
    System.out.println(list);
}
@Test
//查询返回单个的值
public void selectCount(){
    String sql = "select count(id) from t_emp";
    Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
    System.out.println(count);
}

2.3、基于注解的声明式事务

笔记小结:

  1. 基本用例:
    步骤一:添加tx命名空间、添加事务管理器、开启事务的注解驱动
    步骤二:添加事务注解:@Transactional
  2. 事务属性:
    1. 只读:@Transactional(readOnly = true),告诉数据库,此操作只能读不能改写
    2. 超时:@Transactional(timeout = 3),当此操作超时时,做出提示
    3. 回滚策略:
      • rollbackFor属性:当事务方法抛出指定类型 的异常时,事务会回滚
      • rollbackForClassName属性:与rollbackFor属性相似,但是指定异常类型时使用字符串的方式
      • noRollbackFor属性:指定当事务方法抛出指定类型的异常时,事务不回滚
      • noRollbackForClassName属性:与noRollbackFor属性相似,但是指定异常类型使用字符串的方式
    4. 隔离级别:
      • READ_UNCOMMITTED:表示一个事务可以读取另一个未提交事务的数据。此级别会>出现脏读、不可重复读、幻读的问题,一般不建议使用。
      • READ_COMMITTED:表示一个事务只能读取另一个已经提交的事务的数据,可以避免>脏读问题,但不可重复读和幻读问题仍可能发生。
      • REPEATABLE_READ:表示一个事务在执行期间可以多次读取同一行数据,可以避免>脏读和不可重复读问题,但幻读问题仍可能发生。
      • SERIALIZABLE:表示一个事务在执行期间对所涉及的所有数据加锁,避免了脏读、不可重复读、幻读问题,但并发性能非常差。
    5. 传播行为:事务的传播行为是指在多个事务方法相互调用时,控制事务如何传播和影响彼此的行为的规则
    6. 全注解配置事务:@EnableTransactionManagement 开启注解式事务管理

2.3.1、基本用例-实现注解式的声明事务

准备工作:

创建BookController:

java 复制代码
package com.atguigu.spring6.controller;

@Controller
public class BookController {

    @Autowired
    private BookService bookService;

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

创建接口BookService:

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

创建实现类BookServiceImpl:

java 复制代码
package com.atguigu.spring6.service.impl;
@Service
public class BookServiceImpl implements BookService {

    @Autowired
    private BookDao bookDao;

    @Override
    public void buyBook(Integer bookId, Integer userId) {
        //查询图书的价格
        Integer price = bookDao.getPriceByBookId(bookId);
        //更新图书的库存
        bookDao.updateStock(bookId);
        //更新用户的余额
        bookDao.updateBalance(userId, price);
    }
}

创建接口BookDao:

java 复制代码
package com.atguigu.spring6.dao;
public interface BookDao {
    Integer getPriceByBookId(Integer bookId);

    void updateStock(Integer bookId);

    void updateBalance(Integer userId, Integer price);
}

创建实现类BookDaoImpl:

java 复制代码
package com.atguigu.spring6.dao.impl;
@Repository
public class BookDaoImpl implements BookDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public Integer getPriceByBookId(Integer bookId) {
        String sql = "select price from t_book where book_id = ?";
        return jdbcTemplate.queryForObject(sql, Integer.class, bookId);
    }

    @Override
    public void updateStock(Integer bookId) {
        String sql = "update t_book set stock = stock - 1 where book_id = ?";
        jdbcTemplate.update(sql, bookId);
    }

    @Override
    public void updateBalance(Integer userId, Integer price) {
        String sql = "update t_user set balance = balance - ? where user_id = ?";
        jdbcTemplate.update(sql, price, userId);
    }
}

步骤一:添加配置文件中的tx命名空间以及配置

  • 添加tx命名空间
  • 添加事务管理器
  • 开启事务的注解驱动
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:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx
                           http://www.springframework.org/schema/tx/spring-tx.xsd">

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

    <!--
		开启事务的注解驱动。通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务-->
    <!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 -->
    <tx:annotation-driven transaction-manager="transactionManager" />
</beans>

步骤二:添加事务注解

  • Service层表示业务逻辑层,注解通常添加在业务层的功能上,可达到事务管理的目的
  • @Transactional注解标识在方法上,只会影响此方法被事务管理利器进行管理。
  • @Transactional注解标识在类上,则会影响该类中的所有方法被事务管理利器进行管理
java 复制代码
@Transactional
public void buyBook(Integer bookId, Integer userId) {
    //查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    //更新图书的库存
    bookDao.updateStock(bookId);
    //更新用户的余额
    bookDao.updateBalance(userId, price);
    //System.out.println(1/0);
}

2.3.2、事务属性:只读

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

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

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

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

2.3.3、事务属性:超时

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

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

说明:

此时,已为事务添加了事务超时的属性,因此进行查改操作超出单位时间则会抛出异常:

java 复制代码
org.springframework.transaction.TransactionTimedOutException: Transaction timed >out: deadline was Fri Jun 04 16:25:39 CST 2022

2.3.4、事务属性:回滚策略

​ 事务属性中的回滚策略指的是当事务出现异常时,应该如何处理事务的提交或回滚

属性分类:

  1. rollbackFor属性:当事务方法抛出指定类型的异常时,事务会回滚

    java 复制代码
    @Transactional(rollbackFor = {SQLException.class, IOException.class})
  2. rollbackForClassName属性:与rollbackFor属性相似,但是指定异常类型时使用字符串的方式

    java 复制代码
    @Transactional(rollbackForClassName = {"java.sql.SQLException", "java.io.IOException"})
  3. noRollbackFor属性:指定当事务方法抛出指定类型的异常时,事务不回滚。

    java 复制代码
    @Transactional(noRollbackFor = {NullPointerException.class, IllegalArgumentException.class})
  4. noRollbackForClassName属性:与noRollbackFor属性相似,但是指定异常类型时使用字符串的方式

    java 复制代码
    @Transactional(noRollbackForClassName = {"java.lang.NullPointerException", "java.lang.IllegalArgumentException"})

基本用例:

java 复制代码
@Transactional(noRollbackFor = ArithmeticException.class)
//@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
public void buyBook(Integer bookId, Integer userId) {
    //查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    //更新图书的库存
    bookDao.updateStock(bookId);
    //更新用户的余额
    bookDao.updateBalance(userId, price);
    System.out.println(1/0);
}

说明:

此时,@Transactional注解配置的属性为noRollbackFor,因此当出现ArithmeticException.class此类异常后,事务不会进行回滚

2.3.5、事务属性:隔离级别

​ 事务隔离级别是指在多个事务并发执行时,为了保证事务之间的数据一致性,数据库采用的一种隔离机制

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

说明:

  • READ_UNCOMMITTED:表示一个事务可以读取另一个未提交事务的数据。此级别会出现脏读、不可重复读、幻读的问题,一般不建议使用。
  • READ_COMMITTED:表示一个事务只能读取另一个已经提交的事务的数据,可以避免脏读问题,但不可重复读和幻读问题仍可能发生。
  • REPEATABLE_READ:表示一个事务在执行期间可以多次读取同一行数据,可以避免脏读和不可重复读问题,但幻读问题仍可能发生。
  • SERIALIZABLE:表示一个事务在执行期间对所涉及的所有数据加锁,避免了脏读、不可重复读、幻读问题,但并发性能非常差。
    基本用法
java 复制代码
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化

2.3.6、事务属性:传播行为

​ 事务的传播行为是指在多个事务方法相互调用时,控制事务如何传播和影响彼此的行为的规则。在Spring框架中,事务的传播行为由Propagation枚举类定义,常用的传播行为包括如下:

  1. REQUIRED(默认):如果当前存在一个事务,则加入该事务;如果当前没有事务,则创建一个新的事务。换句话说,没有就新建,有就加入
  2. SUPPORTS:如果当前存在一个事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。换句话说,有就加入,没有就不管了
  3. MANDATORY:如果当前存在一个事务,则加入该事务;如果当前没有事务,则抛出异常。换句话说,有就加入,没有就抛异常
  4. REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则将当前事务挂起。换句话说,不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起
  5. NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则将当前事务挂起。换句话说,不支持事务,存在就挂起
  6. NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。换句话说,不支持事务,存在就抛异常
  7. NESTED:如果当前存在一个事务,则在嵌套事务内执行;如果当前没有事务,则按照 PROPAGATION_REQUIRED 执行。换句话说,有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样
java 复制代码
@Transactional(propagation = Propagation.REQUIRED)
public void transactionalMethod() {
    // ...
}

2.3.7、全注解配置事务(重点)

步骤一:添加配置类

java 复制代码
package com.atguigu.spring6.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration // 表示该类为配置类
@ComponentScan("com.atguigu.spring6") // 开启组件扫描,扫描com.atguigu.spring6包下的组件
@EnableTransactionManagement // 开启注解式事务管理
public class SpringConfig {

    @Bean // 声明一个Bean对象
    public DataSource getDataSource(){
        DruidDataSource dataSource = new DruidDataSource(); // 创建Druid连接池
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); // 配置驱动类名
        dataSource.setUrl("jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false"); // 配置数据库URL
        dataSource.setUsername("root"); // 配置数据库用户名
        dataSource.setPassword("root"); // 配置数据库密码
        return dataSource; // 返回配置好的数据源对象
    }

    @Bean(name = "jdbcTemplate") // 声明一个Bean对象并命名为"jdbcTemplate"
    public JdbcTemplate getJdbcTemplate(DataSource dataSource){
        JdbcTemplate jdbcTemplate = new JdbcTemplate(); // 创建JdbcTemplate对象
        jdbcTemplate.setDataSource(dataSource); // 设置JdbcTemplate的数据源
        return jdbcTemplate; // 返回配置好的JdbcTemplate对象
    }

    @Bean // 声明一个Bean对象
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(); // 创建DataSourceTransactionManager对象
        dataSourceTransactionManager.setDataSource(dataSource); // 设置数据源
        return dataSourceTransactionManager; // 返回配置好的DataSourceTransactionManager对象
    }
}

说明:

当使用全注解配置事务时,需要声明一个事务管理器,并使用注解@EnableTransactionManagement开启事务管理器

步骤二:演示

java 复制代码
@Test
public void testTxAllAnnotation(){
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
    BookController accountService = applicationContext.getBean("bookController", BookController.class);
    accountService.buyBook(1, 1);
}

2.3.8、底层原理

​ 在 Java 中,使用 @Transactional 注解实现方法级别的事务管理是基于 Spring 框架的特性和 AOP(面向切面编程)的原理。

底层原理如下:

  1. Spring 通过 AOP 功能拦截带有 @Transactional 注解的方法的执行。
  2. 当方法被调用时,Spring 在运行时会为该方法创建一个代理对象。
  3. 代理对象在方法执行前后会插入事务相关的逻辑。
  4. 在方法开始时,事务管理器会开启一个新的数据库事务。
  5. 如果方法执行成功(无异常抛出),事务管理器会提交事务,将对数据库的修改持久化到数据库。
  6. 如果方法执行出现异常,事务管理器会回滚事务,撤销对数据库的修改,恢复到事务开始之前的状态。
  7. 方法执行完成后,事务管理器会关闭事务。

​ 通过 AOP 的方式,Spring 能够在方法执行前后对事务进行控制。它在方法执行前开启事务,在方法执行后提交或回滚事务,从而保证了数据的一致性和完整性。

​ 需要注意的是,@Transactional 注解的生效还依赖于事务管理器的配置和使用环境的支持。Spring 框架提供了多种事务管理器的实现,如基于 JDBC 的事务管理器和基于 JTA 的事务管理器,可以根据具体的需求选择合适的事务管理器。同时,Spring 需要在配置文件中启用事务管理器,并确保被 @Transactional 注解修饰的方法是通过 Spring 容器获取的代理对象来调用的。这样才能使事务注解生效并正确地应用事务管理的功能。

三、资源操作:Resources

3.1、Resources接口

3.1.1常用方法

  • boolean exists():检查资源是否存在。
  • boolean isReadable():检查资源是否可读。
  • boolean isOpen():检查资源是否打开。
  • URL getURL():获取资源的URL。
  • URI getURI():获取资源的URI。
  • File getFile():获取资源对应的文件。
  • long contentLength():获取资源的长度。
  • long lastModified():获取资源的最后修改时间。
  • Resource createRelative(String relativePath):创建相对于当前资源的相对资源。
  • String getFilename():获取资源的文件名。
  • String getDescription():获取资源的描述信息。
  • InputStream getInputStream():获取资源的输入流。

3.2、Resources的实现类

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

3.2.1、UrlResource访问网络资源

此类用于表示URL类型的资源,可以通过URL对象或String类型的URL路径进行实例化,用来访问网络资源。

  • http:------该前缀用于访问基于HTTP协议的网络资源。
  • ftp:------该前缀用于访问基于FTP协议的网络资源
  • file: ------该前缀用于从文件系统中读取资源
java 复制代码
public class UrlResourceDemo {

    public static void loadAndReadUrlResource(String path){
        // 创建一个 Resource 对象
        UrlResource url = null;
        try {
            url = new UrlResource(path);
            // 获取资源名
            System.out.println(url.getFilename());
            System.out.println(url.getURI());
            // 获取资源描述
            System.out.println(url.getDescription());
            //获取资源内容
            System.out.println(url.getInputStream().read());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        //1 访问网络资源
        //loadAndReadUrlResource("http://www.atguigu.com");

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

3.2.2、ClassPathResource 访问类路径下资源

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

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

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

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

3.2.3、FileSystemResource 访问文件系统资源

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

java 复制代码
public class FileSystemResourceDemo {

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

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

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

3.2.4、ServletContextResource、InputStreamResource、ByteArrayResource

  • ServletContextResource

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

  • InputStreamResource

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

  • ByteArrayResource

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

3.3、ResourceLoader接口

ResourceLoader接口是Spring框架中的一个核心接口,它定义了用于加载资源的统一访问方式。它提供了一种统一的方法,可以加载各种类型的资源,例如文件、类路径资源、URL资源等。

3.3.1、常用方法

  • Resource getResource(String location): 根据给定的资源位置(location)获取一个Resource对象。资源位置可以是文件路径、类路径、URL等。具体的资源加载策略由ResourceLoader的具体实现类决定。
  • ClassLoader getClassLoader(): 获取用于加载类的ClassLoader对象。这对于加载类路径下的资源非常有用。

3.3.2、实现类

Spring框架提供了多个实现ResourceLoader接口的类,包括:

  • DefaultResourceLoader: 默认的资源加载器,可用于加载类路径、文件系统和URL资源。
  • FileSystemResourceLoader: 用于加载文件系统中的资源。
  • ClassPathResourceLoader: 用于加载类路径下的资源。
  • ServletContextResourceLoader: 用于加载Web应用程序上下文中的资源。

3.3.3、基本用例

java 复制代码
public static void main(String[] args) {
    // 创建Spring应用上下文,从类路径中加载配置文件
    ApplicationContext ctx = new ClassPathXmlApplicationContext();
    //        通过ApplicationContext访问资源
    //        ApplicationContext实例获取Resource实例时,
    //        默认采用与ApplicationContext相同的资源访问策略
    Resource res = ctx.getResource("atguigu.txt");
    System.out.println(res.getFilename());
}

说明:

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

java 复制代码
public static void main(String[] args) {
    // 创建Spring应用上下文,指定文件系统路径加载配置文件
    ApplicationContext ctx = new FileSystemXmlApplicationContext();
    Resource res = ctx.getResource("atguigu.txt");
    System.out.println(res.getFilename());
}

说明:

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

java 复制代码
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.Resource;

public class ResourceLoaderExample {

    public static void main(String[] args) {
        // 创建一个ApplicationContext容器对象,该对象会读取classpath(类路径)下的名为applicationContext.xml的配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

        // 使用 classpath: 前缀指定使用 ClassPathResource 实现类
        Resource resource1 = context.getResource("classpath:config.properties");
        System.out.println("Resource 1: " + resource1.getClass().getSimpleName());

        // 使用 file: 前缀指定使用 FileSystemResource 实现类
        Resource resource2 = context.getResource("file:/path/to/file.txt");
        System.out.println("Resource 2: " + resource2.getClass().getSimpleName());

        // 使用 http: 前缀指定使用 UrlResource 实现类
        Resource resource3 = context.getResource("http://www.example.com");
        System.out.println("Resource 3: " + resource3.getClass().getSimpleName());
    }
}

说明:

使用 ApplicationContext 的 getResource() 方法获取了三个不同类型的资源,使用不同的前缀指定了使用不同的 Resource 实现类

3.4、ResourceLoaderAware接口

ResourceLoaderAware 是一个 Spring Bean 接口,用于将 ResourceLoader 实例注入到实现该接口的 Bean 中。它定义了一个 setResourceLoader(ResourceLoader resourceLoader) 方法,Spring 容器在启动时将 ResourceLoader 实例作为参数传递给该方法。

作用

​ 实现ResourceLoaderAware接口的类可以获取Spring容器的ResourceLoader实例,从而利用Spring容器提供的资源加载功能

基本用例

步骤一:创建Bean

java 复制代码
package com.atguigu.spring6.resouceloader;

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

public class TestBean implements ResourceLoaderAware {

    private ResourceLoader resourceLoader;

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

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

}

说明:

实现ResourceLoaderAware接口必须实现setResourceLoader方法,

步骤二:配置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="testBean" class="com.atguigu.spring6.resouceloader.TestBean"></bean>
</beans>

步骤三:演示

java 复制代码
public static void main(String[] args) {
    //Spring容器会将一个ResourceLoader对象作为该方法的参数传入
    ApplicationContext ctx = new ClassPathXmlApplicationContext("bean.xml");
    TestBean testBean = ctx.getBean("testBean",TestBean.class);
    //获取ResourceLoader对象
    ResourceLoader resourceLoader = testBean.getResourceLoader();
    System.out.println("Spring容器将自身注入到ResourceLoaderAware Bean 中 ? :" + (resourceLoader == ctx));
    //加载其他资源
    Resource resource = resourceLoader.getResource("atguigu.txt");
    System.out.println(resource.getFilename());
    System.out.println(resource.getDescription());
}

说明:

因为ApplicationContext接口的实现类ClassPathXmlApplicationContext实现了ResourceLoader接口,因此ClassPathXmlApplicationContext实例对象,也具备"ResourceLoader"的功能。因此,实现了ResourceLoaderAware此接口的对象,就可以利用Spring容器提供的资源加载功能

3.5、动态获取Resource资源

3.5.1、含义

​ Spring 框架不仅充分利用了策略模式来简化资源访问,而且还将策略模式和 IoC 进行充分地结合,最大程度地简化了 Spring 资源访问。当应用程序中的 Bean 实例需要访问资源时,Spring 有更好的解决方法:直接利用依赖注入

3.5.2、作用

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

3.5.3、基本用例

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

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

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

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

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

步骤三:演示

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

3.6、确定资源访问路径

3.6.1、概述

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

3.6.2、实现类指定访问路径

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

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

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

详细步骤,请查看Resources的实现类

3.6.3、前缀指定访问路径

  • classpath前缀

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

说明:

在使用ApplicationContext中,指定路径时,使用**classpath:**为前缀

  • classpath通配符
java 复制代码
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:bean.xml");

说明:

在使用ApplicationContext中,指定路径时,使用classpath*为前缀,表示Spring将会搜索类加载路径下所有满足该规则的配置文件。例如:bean.xml、beans.xml

  • 通配符其他使用
java 复制代码
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:bean*.xml");

说明:

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

java 复制代码
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:bean*.xml");

说明:

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

相关推荐
Miketutu2 小时前
Spring MVC消息转换器
java·spring
小小虫码3 小时前
项目中用的网关Gateway及SpringCloud
spring·spring cloud·gateway
带刺的坐椅8 小时前
无耳科技 Solon v3.0.7 发布(2025农历新年版)
java·spring·mvc·solon·aop
精通HelloWorld!11 小时前
使用HttpClient和HttpRequest发送HTTP请求
java·spring boot·网络协议·spring·http
LUCIAZZZ12 小时前
基于Docker以KRaft模式快速部署Kafka
java·运维·spring·docker·容器·kafka
拾忆,想起12 小时前
如何选择Spring AOP的动态代理?JDK与CGLIB的适用场景
spring boot·后端·spring·spring cloud·微服务
鱼骨不是鱼翅14 小时前
Spring Web MVC基础第一篇
前端·spring·mvc
hong_zc16 小时前
Spring MVC (三) —— 实战演练
java·spring·mvc
Future_yzx17 小时前
Spring AOP 入门教程:基础概念与实现
java·开发语言·spring
安清h17 小时前
【基于SprintBoot+Mybatis+Mysql】电脑商城项目之用户注册
数据库·后端·mysql·spring·mybatis