Spring 声明式事务 万字详解(通俗易懂)

news2025/4/1 23:36:41

目录

Δ前言

一、声明式事务快速入门

        1.为什么需要声明式事务?

        2.定义:

        3.应用实例:

二、声明式事务的传播机制

        1.引出问题:

        2.传播机制分类:

        3.应用实例:

三、声明式事务的隔离机制

        1.四种隔离级别:

        2.应用实例:

        3.事务超时回滚:

Δ总结


Δ前言

  • 第七节内容,up主要和大家分享一下Spring 声明式事务相关的内容;包括声明式事务的快速入门,事务传播机制和隔离机制
  • 注意事项——①代码中的注释也很重要;不要眼高手低,自己跟着敲一遍才真正有收获;点击文章的侧边栏目录或者文章开头的目录可以进行跳转。
  • 良工不示人以朴,up所有文章都会适时补充完善。大家如果有问题都可以在评论区进行交流或者私信up。感谢阅读!

一、声明式事务快速入门

        1.为什么需要声明式事务?

        传统的编程式事务(Programmatic Transaction)需要开发者手动编写代码来控制事务的开启、提交、回滚。
        传统编程式事务,事务逻辑与业务代码耦合性高,且每个事务方法需重复编写 try-catch 和事务模板代码,造成代码冗余;修改事务逻辑需逐个方法调整,易出错,维护成本高

        2.定义:

        声明式事务(Declarative Transaction)是 Spring 框架提供的一种 通过配置(而非代码)管理事务 的机制。开发者无需在业务代码中手动编写事务开启、提交或回滚逻辑,而是通过注解或 XML 定义事务规则,由 Spring 自动处理事务的完整生命周期。
        与Spring 声明式事务相关的核心类,就在spring-tx-xxx.jar包下,如下图所示:

        声明式事务的底层通过 动态代理 + AOP 拦截 实现,结合事务管理器和线程资源绑定,将事务逻辑与业务代码解耦。其核心思想“约定优于配置”,开发者只需关注业务逻辑,事务管理由框架自动完成。

        3.应用实例:

                需求:要求完成用户购买商品的业务逻辑处理,并正确地处理用户余额、商品库存的更新。(以下案例仅用于演示声明式事务,并无实际意义)
                分析
                 既然要求正确处理用户余额和商品库存的更新,所以数据库中肯定要有 用户账目表 商品库存表,而要想处理用户余额,必须得知道用户所购买的商品的价格,所以还需要一张 商品表,那么一共需要三张表。
                假设以Commodity表示商品,我们需要在 CommodityDAOImpl 中组织SQL语句,CommodityDAOImpl中至少需要实现三种数据库操作——查询商品的价格、更新用户余额、更新商品库存。而在 CommodityServiceImpl 中我们要完成业务逻辑的处理,其实就是把DAO层定义的若干个方法进行灵活地组合,从而实现特定的业务逻辑(此处为购买商品的业务逻辑)。
                实现
                首先,来创建 `user_account`, `commodities` , `inventory_report`三张表,SQL代码如下:

# 创建用户账目表
CREATE TABLE `user_account` (
		`user_id` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
		`user_name` VARCHAR(64) NOT NULL DEFAULT '',
		`user_balance` DOUBLE NOT NULL DEFAULT 0.0
) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin ENGINE INNODB;

INSERT INTO `user_account` 
		VALUES(NULL, 'Cyan', 500),(NULL, 'Five', 2000);

# 创建商品表
CREATE TABLE `commodities` (
		`c_id` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
		`c_name` VARCHAR(64) NOT NULL DEFAULT '',
		`c_price` DOUBLE NOT NULL DEFAULT 0.0
) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin ENGINE INNODB;

INSERT INTO `commodities`(`c_name`, `c_price`)
		VALUES('mini washing machine', 300.0),('lamp', 35.0),('keyboard', 150.0);
		
# 创建商品库存表
CREATE TABLE`inventory_report` (
		`ir_id` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
		`ir_amount` INT UNSIGNED DEFAULT 0
) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin ENGINE INNODB;

INSERT INTO `inventory_report`
		VALUES(NULL, 3), (NULL, 11), (NULL, 9);
		

                创建后的三张表,如下图所示:(注意看清楚当前商品的库存用户的余额

                接着,我们来编写CommodityDAOImpl类 和 CommodityServiceImpl类,前者负责实现对上面三张表的操作,后者负责将DAO中的方法灵活组合,以实现购买商品的业务逻辑处理。
                CommodityDAOImpl类代码如下:

package com.cyan.spring.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

/**
 * @author : Cyan_RA9
 * @version : 22.0
 */
@Repository(value = "commodityDAO")
public class CommodityDAOImpl {
    //使用 @Autowired 进行自动装配
    @Autowired
    @Qualifier(value = "jdbcTemplate01")
    private JdbcTemplate jdbcTemplate;

    //根据 商品id 查询 商品价格
    public Float queryPriceById(Integer c_id) {
        String sql = "SELECT `c_price` FROM `commodities` WHERE `c_id` = ?";
        Float c_price = jdbcTemplate.queryForObject(sql, Float.class, c_id);
        return c_price;
    }

    //用户购买商品后,需要减少用户账目中的余额
    public void decreaseBalance(Integer user_id, Float c_price) {
        String sql = "UPDATE `user_account` " + "\n" +
                        "SET `user_balance` = `user_balance` - ? " + "\n" +
                        "Where `user_id` = ?";
        jdbcTemplate.update(sql, c_price, user_id);
    }

    //用户购买商品后,需要减少对应的商品库存量
    public void decreaseAmount(Integer c_id, int amount){
        String sql = "UPDATEX `inventory_report` " + "\n" +
                        "SET `ir_amount` = `ir_amount` - ? " + "\n" +
                        "Where `ir_id` = ?";
        jdbcTemplate.update(sql, amount , c_id);
    }
}

                CommodityServiceImpl类,代码如下:

package com.cyan.spring.service;

import com.cyan.spring.dao.CommodityDAOImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author : Cyan_RA9
 * @version : 22.0
 */
@Service
public class CommodityServiceImpl {
    @Autowired
    private CommodityDAOImpl commodityDAO;

    /**
     * 该方法用于实现购买商品。
     * @param user_id:用户id
     * @param c_id:商品id
     * @param amount:购买数量
     */
    public void purchase(int user_id, int c_id, int amount) {
        //1.查询到商品价格
        Float c_price = commodityDAO.queryPriceById(c_id);

        //2.购买商品后,更新对应用户的账户余额
        commodityDAO.decreaseBalance(user_id, c_price * amount);

        //3.购买商品后,更新对应的商品库存
        commodityDAO.decreaseAmount(c_id, amount);
    }
}

                别忘了最关键的一步,我们还没有配置xml!up以beans_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:contex="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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 配置扫面注解的包 -->
    <contex:component-scan base-package="com.cyan.spring.dao"/>
    <contex:component-scan base-package="com.cyan.spring.service"/>

    <!-- 配置启动基于注解的声明式事务 -->
    <!-- 注意一定要选择 springframework.org/schema/tx 下的tx命名空间 -->
    <tx:annotation-driven transaction-manager="dataSourceTransactionManager01"/>

    <!-- 配置properties文件 -->
    <contex:property-placeholder location="classpath:druid.properties"/>
    <!-- 配置数据源对象 -->
    <bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource01">
        <property name="driverClassName" value="${driverClassName}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <!-- 配置JdbcTemplate对象 -->
    <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate01">
        <property name="dataSource" ref="dataSource01"/>
    </bean>

    <!-- 配置事务管理器对象 -->
    <!--
        1.DataSourceTransactionManager实例,用于进行事务管理。
        2.一定要配置数据源属性,并且配置的数据源属性要和使用的jdbcTemplate配置的数据源一致。
    -->
    <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
            id="dataSourceTransactionManager01">
        <constructor-arg name="dataSource" ref="dataSource01"/>
    </bean>
</beans>

                最后,up以TxTest类为测试类,令用户Five尝试购买两台迷你洗衣机,代码如下:

package com.cyan.spring.test;

import com.cyan.spring.service.CommodityServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * @author : Cyan_RA9
 * @version : 22.0
 */
public class TxTest {
    @Test
    public void testPurchase() {
        //1.获得容器对象
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans_tx.xml");
        
        //2.根据容器对象获得CommodityServiceImpl对象
        CommodityServiceImpl commodityServiceImpl = (CommodityServiceImpl) ioc.getBean("commodityServiceImpl");

        //3.根据获得的CommodityServiceImpl 调用业务层方法
            /*
                id = 2的用户(即Five用户,初始余额是2000块),购买了 2件 id = 1的商品(即迷你洗衣机)
             */
        commodityServiceImpl.purchase(2, 1, 2);
    }
}

                运行结果如下:

                可能这时候就要有p小将(personable小将,指风度翩翩的人!)要说理了——啥玩意儿啊?写牛魔一大堆结果跑都跑不起来?👴要看的声明式事务搁哪儿呢?

                p小将你先别急,注意看报错信息,是“bad SQL grammar”,意思就是我们的SQL语句语法错误,很快我们找到,是CommodityDAOImpl类中的 decreaseAmount方法 出错了,如下图所示:

                其实up这里是 故意写错 的,那为什么要故意写错呢?——就是为了看看没有事务支持的后果。请注意,我们这时候是没有开启“声明式事务”的(因为压根就没有用声明式事务的注解),那么我们来看一下数据库中 商品库存表 和 用户账目表 的数据变化情况,如下图所示:(左边是用户帐目表,右边是商品库存表)

                可以看到,在没有 事务 的支持下,这种错误造成的后果就是——商品发不出去(库存没有减少),而用户的余额被白白扣掉了!

                下面我们引入声明式事务!其实只要在 purchase(int, int, int) 方法上面加上一个 @Transactional 即可,如下图所示:

                这时候如果我们再次运行TxTest测试类,会发现数据库中的数据没有变化,如下GIF图演示:

                可以看到,有了声明式事务的加持后,即便用于更新商品库存的SQL语句出错,但是由于事务本身的 原子性,事务中用于操作数据库的一组DML(Data Manipulation Language) 要么全部成功,要么全部失败(全部失败指的是无效),也就不会造成数据更新不完整的后果。

                好的,up现在将之前 故意写错 的SQL语句更改过来,如下图所示:

                然后我们重新进行测试(现在可是已经配置了声明式事务噢!),测试方法代码如下:

    @Test
    public void testPurchase() {
        //1.获得容器对象
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans_tx.xml");

        //2.根据容器对象获得CommodityServiceImpl对象
        CommodityServiceImpl commodityServiceImpl = (CommodityServiceImpl) ioc.getBean("commodityServiceImpl");

        //3.根据获得的CommodityServiceImpl 调用业务层方法
            /*
                user_id = 1的用户(即Cyan用户,初始余额是500块),购买了 2件 c_id = 2的商品(即lamp,台灯)
             */
        commodityServiceImpl.purchase(1, 2, 2);
    }

                运行结果:

                可以看到,在事务支持下,正确的数据库操作成功执行!


二、声明式事务的传播机制

        1.引出问题:

        如果一个事务中包含了其他的事务(例如,一个启动了声明式事务的方法中调用了其他方法,而被调用的方法本身也配置了声明式事务),那么该如何控制事务呢?

        2.传播机制分类:

                如下表所示:(尤其注意前两种方式)

传播属性描述
REQUIRED [默认]如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在它自己的事务内运行
REQUIRES_NEW当前的方法必须启动新事务,并在它自己的事务内运行。 如果有事务正在运行,应该将它挂起
SUPPORTS 如果有事务在运行,当前的方法就在这个事务内运行。 否则它可以不运行在事务中
NOT_SUPPORTED 当前的方法不应该运行在事务中。 如果有运行的事务,将它挂起
MANDATORY 当前的方法必须运行在事务内部,如果没有正在运行的事务,就抛出异常
NEVER 当前的方法不应该运行在事务中。 如果有运行的事务,就抛出异常
NESTED如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行。 否则,就启动一个新的事务,并在它自己的事务内运行

                @Transactional 中的propagation元素,默认传播机制是REQUIRED,如下图所示:

                我们下面来看一个演示案例。

        3.应用实例:

                需求:不同用户购买商品,要调用不同的方法。例如Cyan用户购买商品要调用f1()方法,Five用户购买商品要调用f2()方法。

                在上文“声明式事务快速入门”中,我们在 CommodityDAOImpl类 定义了三个方法——queryPriceById, decreaseBalance 和 decreaseAmount,现在我们在这个类新增一个方法,叫做decreaseAmountWrongVersion(见名知意,这个方法无法正确执行),代码如下:

    //一个故意写错的 用于更新商品库存量 的方法
    public void decreaseAmountWrongVersion(Integer c_id, int amount){
        String sql = "UPDATEXXXXX `inventory_report` " + "\n" +
                "SET `ir_amount` = `ir_amount` - ? " + "\n" +
                "Where `ir_id` = ?";
        jdbcTemplate.update(sql, amount , c_id);
    }

                同样地,我们在 CommodityServiceImpl类 中,也新增一个 “故意写错” 的方法,代码如下:

    /**
     * 这个方法是错误的,无法购买成功。
     */
    @Transactional
    public void purchaseWrongVersion(int user_id, int c_id, int amount) {
        Float c_price = commodityDAO.queryPriceById(c_id);

        commodityDAO.decreaseBalance(user_id, c_price * amount);

        //PS: decreaseAmountWrongVersion 这个方法是错误滴!!!
        commodityDAO.decreaseAmountWrongVersion(c_id, amount);
    }

                这样在CommodityServiceImpl类中,就有两个用于购买的方法了,且这两个方法都用了 @Transactional 修饰,如下图所示:

                好的,接下来我们还需要一个再来建 TxTestServiceImpl类,在这个类中我们要编写一个同样使用 @Transactional 修饰的方法,并在该方法中调用上图中的两个方法。TxTestServiceImpl类代码如下:

package com.cyan.spring.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author : Cyan_RA9
 * @version : 22.0
 */
@Service
public class TxTestServiceImpl {
    @Autowired
    @Qualifier(value = "commodityServiceImpl")
    CommodityServiceImpl commodityService;

    /**
     * 注意,外层方法 doublePurchase 本身,也使用了 @Transactional 修饰。
     * 即调用者 doublePurchase 本身,亦是一个事务。
     */
    @Transactional
    public void doublePurchase() {
        // user_id = 1 的用户(Cyan)购买 1件 c_id = 2(lamp)的商品
        commodityService.purchase(1, 2, 1);

        // user_id = 2 的用户(Five)购买 2件 c_id = 3(keyboard)的商品
        commodityService.purchaseWrongVersion(2, 3, 2);
    }
}

                注意,由于 @Transactional 启用的声明式事务,默认传播机制是REQUIRED,因此虽然 doublePurchase()方法 中调用的两个内层方法也都开启了声明式事务,但是都要算在外层方法的事务中,也就是仅仅有一个事务在进行,只要其中有一条DML失效,那么整个事务的操作全部作废。So,现在的这个 doublePurchase() 方法是一定执行失败的!

                好嘞,这时候为了验证事务传播机制,最后就需要一个测试方法啦~。代码如下:

    @Test
    public void testAffairPropagation() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans_tx.xml");

        TxTestServiceImpl txTestService = ioc.getBean(TxTestServiceImpl.class);
        
        txTestService.doublePurchase();
    }

                先不急着运行,我们先来瞄一眼数据库中商品库存表 用户帐目表的情况,如下图所示:(左边是商品库存表,右边是用户帐目表)

                运行测试方式,并查看数据库中商品库存表 用户帐目表的数据变化情况,如下GIF图所示:

                可以看到,商品库存和用户余额都没有减少!这就是默认传播属性REQUIRED的机制。

                还没完!下面我们把内层两个事务的 propagation属性 全部换成REQUIRED_NEW,如下图所示:

                那根据上面表格中对REQUIRED_NEW的解释,此时内外层的事务是相互独立的呀!所以不难猜到,成功的事务将正确提交且无法被回滚,而失败的事务仍然会立即回滚。下面我们重新运行测试方法,如下图所示:

                这个时候我们再去查看数据库中的商品库存表 用户帐目表的情况,如下图所示:(左边是商品库存表,右边是用户帐目表)

                由此可知——
                内层成功的事务在执行完毕后,会立即提交其独立事务,且已提交的事务无法回滚,即使外层事务后续失败
                内层失败的事务在抛出异常后,其独立事务会立即回滚且异常会向上传播到外层事务,导致外层事务也回滚(除非外层捕获并处理异常)
                ③ 外层事务回滚仅影响其自身的操作(如果有),不影响已提交的 内层的成功执行的 事务,即外层事务的回滚不会撤销已提交的内层的 REQUIRES_NEW 事务,因为它们是相互独立的


三、声明式事务的隔离机制

        1.四种隔离级别:

                up之前在 “MySQL——事务机制与隔离机制”一文中,已经讲过了MySQL的四种隔离级别脏读、幻读、不可重复读的定义,此处不再赘述,但up还是用表格来带大家简单回顾一下四种隔离级别,如下表所示:

MySQL隔离级别(4种)脏读幻读不可重复读加锁读
Read Uncommitted:读未提交No
Read committed:读已提交×No
Repeatable Read:可重复读(默认)×××No
Serializable:串行化(隔离级别最高)×××Yes

        2.应用实例:

                需求:连续两次查询同一商品的价格,但在查询第一次商品后,尝试更改事务的隔离机制,并测试两次查询的结果是否一致。

                实现:我们在 CommodityServiceImpl类 中新增一个方法用于实现连续两次查询,queryTwice方法代码如下:(设置断点)

    @Transactional
    public void queryTwice(int c_id) {
        Float priceV1 = commodityDAO.queryPriceById(c_id);
        System.out.println("第一次查询得到的价格为:" + priceV1);

        Float priceV2 = commodityDAO.queryPriceById(c_id);
        System.out.println("第二次查询得到的价格为:" + priceV2);
    }

                并且,我们要在第二次的查询语句那一行,设置一个断点,因为我们要通过Debug来进行测试。如下图所示:

                注意,此时我们并没有为 @Transactional 设置隔离级别,那么它就是默认的Repeatable Read,在这种隔离级别下,事务可以读到启动事务时刻数据库中的数据,并且不会被其他事务所进行的DML操作所影响

                好嘞,最后还需要一个测试方法来验证我们的结论,代码如下:

    @Test
    public void testIsolationMechanisms() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans_tx.xml");

        CommodityServiceImpl commodityService = ioc.getBean(CommodityServiceImpl.class);

        commodityService.queryTwice(3);
    }

                我们对这个方法进行Debug,如下图所示:

                可以看到,第一次查询的价格是150,并且程序现在卡在了断点处。
                趁它卡住,我们悄咪咪去Navicat中修改id为3的商品的价格,如下图所示:

                注意,由于up在修改语句时并没有开启新的事务,所以默认是直接提交的。接下来我们让程序直接跳转到下一个断点,如下图所示:

                嗯?明明keyboard的价格已经被修改成了15,但是第二次查询却还是原来修改之前的150,这就是默认隔离机制——Repeatable Read

                好,接下来up将事务的隔离机制改为Read Committed,如下图所示:

                然后进行同样的操作,先Debug,令程序卡在第一个断点,如下图所示:

                然后去Navicat中悄咪咪修改价格,如下图所示:

                让程序运行到下一个断点,如下图所示:

                喏,还真不一样了,这就是 Read Commmitted 啦!

        3.事务超时回滚:

                事务超时回滚,是指如果一个事务执行的时间超过了某个时间限制,就让该事务回滚;我们可以通过 @Transactionaltimeout元素来设置超时时间。下面来看一个演示案例:

                需求:测试事务超时回滚。
                实现:在 CommodityServiceImpl类 中新定义一个 purchaseEX方法代码如下:

    @Transactional(timeout = 3)
    /*
        timeout = 3 表示如果事务的执行时间超过了3秒,就进行回滚。
        若没有设置timeout元素,默认就是-1,表示使用数据库默认的事务超时时间,或者不支持超时回滚。
     */
    public void purchaseEX(int user_id, int c_id, int amount) {
        //1.查询到商品价格
        Float c_price = commodityDAO.queryPriceById(c_id);

        //2.购买商品后,更新对应用户的账户余额
        commodityDAO.decreaseBalance(user_id, c_price * amount);

        System.out.println("================ 4s > 3s =================");
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("================ 超时了,回滚! =================");

        //3.购买商品后,更新对应的商品库存
        commodityDAO.decreaseAmount(c_id, amount);
        System.out.println("👴购买成功🌶!");
    }

                然后还是在测试类新建一个测试方法,代码如下:

    @Test
    public void testTimeout() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans_tx.xml");

        CommodityServiceImpl commodityService = ioc.getBean(CommodityServiceImpl.class);

        /*
            id为2的用户(Five) 购买1件 id为1的商品(mini washing machine)
         */
        commodityService.purchaseEX(2, 1, 1);
    }

                在运行测试方法之前呢,我们先来瞄一眼数据库中商品库存表 用户帐目表的情况,如下图所示:(左边是商品库存表,右边是用户帐目表)

                运行结果如下:

                数据库有变化吗?如下图所示:

                压根儿没变!看来事务确实超时回滚了!
                为了确认事务超时回滚机制,我们把用于线程休眠的代码注释掉,然后重新运行测试方法,并查看数据库中商品库存表 和 用户帐目表的数据变化情况,如下GIF图所示:

                可以看到,在事务未超时的情况下,数据都被正确地更新!


Δ总结

  • 🆗,以上就是Spring系列博文 第七小节 的全部内容了。
  • 总结一下,我们先是用一个详细的入门实例,了解了声明式事务的概念和使用,很明显地,我们可以感受到,声明式事务相比传统编程式事务最大的优点就是省事儿,不用写那么多重复的代码了,加两个注解统统搞定。之后,我们又一起学习了事务的传播机制和隔离机制,其实部分内容up在 MySQL系列 也分享过了,大家有兴趣可以去看看噢~。
  • 另外,之后up还会带大家深入分析Spring的底层源码,并且还会和大家一起 手动实现Spring底层机制,敬请期待~❀。
  • 下一节内容——暂时无了哈哈,Spring系列暂时告一段落,大家拜拜,感谢阅读!

        System.out.println("END---------------------------------------------------------");

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2325186.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL 当中的锁

MySQL 当中的锁 文章目录 MySQL 当中的锁MySQL 中有哪些主要类型的锁&#xff1f;请简要说明MySQL 的全局锁有什么用&#xff1f;MySQL 的表级锁有哪些&#xff1f;作用是什么&#xff1f;元数据锁&#xff08;MetaData Lock&#xff0c;MDL&#xff09;意向锁&#xff08;Inte…

[Linux]基础IO

基础IO C文件IO相关操作磁盘文件与内存文件inode&#xff08;index node&#xff09;硬链接与软连接硬链接软连接总结 动静态库静态库动态库总结 C文件IO相关操作 当前路径&#xff1a;进程运行的时候&#xff0c;所处的路径叫做当前路径 打开文件的时候&#xff0c;一定是进…

力扣刷题-热题100题-第27题(c++、python)

21. 合并两个有序链表 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/merge-two-sorted-lists/description/?envTypestudy-plan-v2&envIdtop-100-liked 常规法 创建一个新链表&#xff0c;遍历list1与list2&#xff0c;将新链表指向list1与list2…

Vue3 其它API Teleport 传送门

Vue3 其它API Teleport 传送门 在定义一个模态框时&#xff0c;父组件的filter属性会影响子组件的position属性&#xff0c;导致模态框定位错误使用Teleport解决这个问题把模态框代码传送到body标签下

windows下安装sublime

sublime4 alpha 4098 版本 下载 可以根据待破解的版本选择下载 https://www.sublimetext.com/dev crack alpha4098 的licence 在----- BEGIN LICENSE ----- TwitterInc 200 User License EA7E-890007 1D77F72E 390CDD93 4DCBA022 FAF60790 61AA12C0 A37081C5 D0316412 4584D…

Java高级JVM知识点记录,内存结构,垃圾回收,类文件结构,类加载器

JVM是Java高级部分&#xff0c;深入理解程序的运行及原理&#xff0c;面试中也问的比较多。 JVM是Java程序运行的虚拟机环境&#xff0c;实现了“一次编写&#xff0c;到处运行”。它负责将字节码解释或编译为机器码&#xff0c;管理内存和资源&#xff0c;并提供运行时环境&a…

【STL】queue

q u e u e queue queue 是一种容器适配器&#xff0c;设计为先进先出&#xff08; F i r s t I n F i r s t O u t , F I F O First\ In\ First\ Out,\ FIFO First In First Out, FIFO&#xff09;的数据结构&#xff0c;有两个出口&#xff0c;将元素推入队列的操作称为 p u …

20250330-傅里叶级数专题之离散时间傅里叶变换(4/6)

4. 傅里叶级数专题之离散时间傅里叶变换 20250328-傅里叶级数专题之数学基础(0/6)-CSDN博客20250330-傅里叶级数专题之傅里叶级数(1/6)-CSDN博客20250330-傅里叶级数专题之傅里叶变换(2/6)-CSDN博客20250330-傅里叶级数专题之离散傅里叶级数(3/6)-CSDN博客20250330-傅里叶级数专…

漏洞挖掘---迅饶科技X2Modbus网关-GetUser信息泄露漏洞

一、迅饶科技 X2Modbus 网关 迅饶科技 X2Modbus 网关是功能强大的协议转换利器。“X” 代表多种不同通信协议&#xff0c;能将近 200 种协议同时转为 Modbus RTU 和 TCP 服务器 。支持 PC、手机端等访问监控&#xff0c;可解决组态软件连接不常见控制设备难题&#xff0c;广泛…

网络安全之前端学习(css篇2)

那么今天我们继续来学习css&#xff0c;预计这一章跟完后&#xff0c;下一章就是终章。然后就会开始js的学习。那么话不多说&#xff0c;我们开始吧。 字体属性 之前讲到了css可以改变字体属性&#xff0c;那么这里来详细讲一讲。 1.1字体颜色 之前讲到了对于字体改变颜色食…

PS底纹教程

1.ctrlshiftU 去色 2.新建纯色层 颜色中性灰&#xff1b;转换为智能对象 3.纯色层打开滤镜&#xff08;滤镜库&#xff09;&#xff1b; 素描下找到半调图案&#xff0c;数值调成大小5对比1&#xff1b; 再新建一层&#xff0c;素描下找到撕边&#xff0c;对比拉到1&#x…

解决pyinstaller GUI打包时无法打包图片问题

当我们的python GuI在开发时。经常会用到图片作为背景&#xff0c;但是在打包后再启动GUI后却发现&#xff1a;原先调试时好端端的背景图片竟然不翼而飞或者直接报错。这说明图片没有被pyinstaller一起打包…… 要解决这个问题很简单&#xff0c;就是更改图片的存储方式。 tk…

蓝桥杯真题------R格式(高精度乘法,高精度加法)

对于高精度乘法和加法的同学可以学学这几个题 高精度乘法 高精度加法 文章目录 题意分析部分解全解 后言 题意 给出一个整数和一个浮点数&#xff0c;求2的整数次幂和这个浮点数相乘的结果最后四舍五入。、 分析 我们可以发现&#xff0c;n的范围是1000,2的1000次方非常大&am…

Nginx — Nginx安装证书模块(配置HTTPS和TCPS)

一、安装和编译证书模块 [rootmaster nginx]# wget https://nginx.org/download/nginx-1.25.3.tar.gz [rootmaster nginx]# tar -zxvf nginx-1.25.3.tar.gz [rootmaster nginx]# cd nginx-1.25.3 [rootmaster nginx]# ./configure --prefix/usr/local/nginx --with-http_stub_…

回调后门基础

回调后门概述 回调后门&#xff08;Reverse Shell&#xff09;是一种常见的攻击方式&#xff0c;攻击者通过受害主机主动连接到远程服务器&#xff08;攻击者控制的机器&#xff09;&#xff0c;从而获得远程控制权限。 工作原理 受害者主机 运行一个恶意代码&#xff0c;尝…

深度学习 Deep Learning 第13章 线性因子模型

深度学习 Deep Learning 第13章 线性因子模型 内容概要 本章深入探讨了线性因子模型&#xff0c;这是一类基于潜在变量的概率模型&#xff0c;用于描述数据的生成过程。这些模型通过简单的线性解码器和噪声项捕捉数据的复杂结构&#xff0c;广泛应用于信号分离、特征提取和数…

【个人笔记】用户注册登录思路及实现 springboot+mybatis+redis

基本思路 获取验证码接口 验证码操作用了com.pig4cloud.plugin的captcha-core这个库。 AccountControl的"/checkCode"接口代码&#xff0c;通过ArithmeticCaptcha生成一张验证码图片&#xff0c;通过text()函数得到验证码的答案保存到变量code&#xff0c;然后把图…

聚类(Clustering)基础知识3

文章目录 一、聚类的性能评价1、聚类性能评价&#xff08;1&#xff09;聚类性能评价方法&#xff1a; 2、参考模型 (reference model)&#xff08;1&#xff09;数据集&#xff1a;&#xff08;2&#xff09;聚类结果&#xff1a;&#xff08;3&#xff09;参考模型&#xff1…

RK3588使用笔记:设置程序/服务开机自启

一、前言 一般将系统用作嵌入式设备时肯定要布置某些程序&#xff0c;这时候就需要对程序设置开机自己&#xff0c;否则每次都要人为启动&#xff0c;当有些嵌入式系统未连接显示屏或者无桌面环境去操作启动程序时&#xff0c;程序自启就是必须的了&#xff0c;本文介绍在纯li…

python实现股票数据可视化

最近在做一个涉及到股票数据清洗及预测的项目&#xff0c;项目中需要用到可视化股票数据这一功能&#xff0c;这里我与大家分享一下股票数据可视化的一些基本方法。 股票数据获取 目前&#xff0c;我已知的使用python来获取股票数据方式有以下三种: 爬虫获取&#xff0c;实现…