• Spring5入门到实战------15、事务操作---概念--场景---声明式事务管理---事务参数--注解方式---xml方式


    1、事务概念

    1.1、什么事务

    (1)事务是数据库操作最基本单元,逻辑上一组操作,要么都成功,如果有一个失败所有操作都失败
    (2)典型场景:银行转账

    • 张三 转账 100 元 给 李四
    • 张三 少 100,李四 多 100

    1.2、事务四个特性(ACID)

    这四个属性通常称为ACID特性

    (1)原子性
    (2)一致性
    (3)隔离性
    (4)持久性

    原子性(atomicity)。一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。

    一致性(consistency)。事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。

    隔离性(isolation)。一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

    持久性(durability)。持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

    2、事务场景搭建

    在这里插入图片描述

    2.1 创建数据表

    在这里插入图片描述

    2.2 创建 service,搭建 dao,完成对象创建和注入关系

    (1)service 注入 dao,在 dao 注入 JdbcTemplate

    /**
     * @author Lenovo
     * @version 1.0
     * @data 2022/10/25 21:22
     */
    @Service
    public class UserService {
        @Autowired
        private UserDao userDao;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    /**
     * @author Lenovo
     * @version 1.0
     * @data 2022/10/25 21:21
     */
    @Repository
    public class UserDaoImpl implements UserDao{
        @Autowired
        private JdbcTemplate jdbcTemplate;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2.3 在 dao 创建两个方法:多钱和少钱的方法,在 service 创建方法(转账的方法)

    /**
     * @author Lenovo
     * @version 1.0
     * @data 2022/10/25 21:21
     */
    @Repository
    public class UserDaoImpl implements UserDao{
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
    
        @Override
        public void addMoney() {
            String sql = "update t_account set money = money + ? where username = ?";
            jdbcTemplate.update(sql,100,"李四");
        }
    
        @Override
        public void reduceMoney() {
            String sql = "update t_account set money = money - ? where username = ?";
            jdbcTemplate.update(sql,100,"张三");
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    /**
     * @author Lenovo
     * @version 1.0
     * @data 2022/10/25 21:22
     */
    @Service
    public class UserService {
        @Autowired
        private UserDao userDao;
    
        public void accountMoney(){
            //张三少钱
            userDao.reduceMoney();
            //李四多钱
            userDao.addMoney();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2.4 测试1

        @Test
        public void testJdbcTemplate4() {
            ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
            UserService userService = context.getBean("userService", UserService.class);
            userService.accountMoney();
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.5 结果

    如果没有意外是正常的
    在这里插入图片描述

    2.6 测试2

    如果在执行转帐的时候、突然出现了意外。造成转账终端。一方钱转出去、自账户的钱也少了。但是另外一个人没收到钱款。这不就芭比Q啦。

    模拟转账失败:人为出现异常

    /**
     * @author Lenovo
     * @version 1.0
     * @data 2022/10/25 21:22
     */
    @Service
    public class UserService {
        @Autowired
        private UserDao userDao;
    
        public void accountMoney(){
            //张三少钱
            userDao.reduceMoney();
            int i = 1/0;
            //李四多钱
            userDao.addMoney();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2.7 测试结果

    在这里插入图片描述
    在这里插入图片描述

    3、使用事务解决

    3.1 基本思路

        public void accountMoney(){
    
            try{
                //1、开启事务
    
                //2、进行业务操作
                
                //张三少钱
                userDao.reduceMoney();
                
                //异常模拟
                int i = 1/0;
                //李四多钱
                userDao.addMoney();
                
                //3、没有发生异常,提交事务
                
            }catch(Exception e){
                //4、出现异常,事务回滚
            }
        
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    4、Spring事务管理介绍

    4.1 事务添加位置

    事务添加到 JavaEE 三层结构里面 Service 层(业务逻辑层)

    4.2 Spring中有两种方式实现事务管理操作

    • 编程式事务管理
    • 声明式事务管理(使用)

    声明式事务管理
    (1)基于注解方式(使用)
    (2)基于 xml 配置文件方式

    在 Spring 进行声明式事务管理,底层使用 AOP 原理

    4.3、Spring 事务管理 API

    提供一个接口,代表事务管理器,这个接口针对不同的框架提供不同的实现类
    在这里插入图片描述

    5、注解声明式事务管理

    这里可以忽略、直接看完全注解开发中的配置类。实现的效果一样

    5.1、在 spring 配置文件配置事务管理器

    <!--创建事务管理器-->
    <bean id="transactionManager" 
    class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
     <!--注入数据源-->
     <property name="dataSource" ref="dataSource"></property>
    </bean>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    5.2、在 spring 配置文件,开启事务注解

    1、在 spring 配置文件引入名称空间 tx

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           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/aop http://www.springframework.org/schema/aop/spring-aop.xsd
                                http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    
    
    </beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2、开启事务注解

    
    
    
    • 1
    • 2

    5.3 在类中添加事务注解

    在 service 类上面(或者 service 类里面方法上面)添加事务注解

    • (1)@Transactional,这个注解添加到类上面,也可以添加方法上面
    • (2)如果把这个注解添加类上面,这个类里面所有的方法都添加事务
    • (3)如果把这个注解添加方法上面,为这个方法添加事务
    /**
     * @author Lenovo
     * @version 1.0
     * @data 2022/10/25 21:22
     */
    @Service
    @Transactional //添加到类上
    public class UserService {
        @Autowired
        private UserDao userDao;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    5.4 开启事务后、再辞测试转账

    这里开启事务、有异常、事务会回滚。
    在这里插入图片描述
    在这里插入图片描述

    6、声明式事务管理参数配置(注解)

    1、在 service 类上面添加注解@Transactional,在这个注解里面可以配置事务相关参数

    在这里插入图片描述

    6.1 propagation:事务传播行为

    多事务方法直接进行调用,这个过程中事务 是如何进行管理的
    在这里插入图片描述
    在这里插入图片描述

    可以在开启事务的时候、配置

    @Service
    @Transactional(propagation = Propagation.REQUIRED)
    public class UserService {
        @Autowired
        private UserDao userDao;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    6.2 ioslation:事务隔离级别

    事务有特性成为隔离性,多事务操作之间不会产生影响。不考虑隔离性产生很多问题

    有三个读问题:

    • 1、脏读
    • 2、不可重复读
    • 3、虚(幻)读

    脏读:一个未提交事务读取到另一个未提交事务的数据

    好比数据库的数据本来是5000,A事务对其操作后的数据变为6000,此时B事务拿到A事务操作后的数据6000。但是B事务进行了事务回滚。数据又变成了5000。那A事务读取到的数据就是不正确的。
    在这里插入图片描述

    不可重复读:一个未提交事务读取到另一提交事务修改数据

    好比数据库原始数据是5000。A想拿到数据库本来的5000。但是B事务拿取到这个数据5000。B事务将拿到的数据修改为900,并且提交了数据。这个时候数据库的数据就变成了900。此时A事务要想再从数据库拿出这个数据,就不在是之前的5000了。
    在这里插入图片描述

    **虚读:**一个未提交事务读取到另一提交事务添加数据

    6.3 解决方法

    通过设置事务隔离级别,解决读问题

    在这里插入图片描述

    @Service
    @Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.REPEATABLE_READ)
    public class UserService {
        @Autowired
        private UserDao userDao;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    6.4 timeout:超时时间

    • (1)事务需要在一定时间内进行提交,如果不提交进行回滚
    • (2)默认值是 -1 ,设置时间以秒单位进行计算

    6.5、readOnly:是否只读

    • (1)读:查询操作,写:添加修改删除操作
    • (2)readOnly 默认值 false,表示可以查询,可以添加修改删除操作
    • (3)设置 readOnly 值是 true,设置成 true 之后,只能查询

    6.6、rollbackFor:回滚

    • (1)设置出现哪些异常进行事务回滚

    6.7、noRollbackFor:不回滚

    • (1)设置出现哪些异常不进行事务回滚

    7、XML 声明式事务管理

    很大一坨

    • 第一步 配置事务管理器
    • 第二步 配置通知
    • 第三步 配置切入点和切面

    测试的时候读取xml文件

    ApplicationContext context =
             new ClassPathXmlApplicationContext("bean.xml");
    
    • 1
    • 2
    
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           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/aop http://www.springframework.org/schema/aop/spring-aop.xsd
                                http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    
        
        <context:component-scan base-package="com.zyz">context:component-scan>
        
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
              destroy-method="close">
            <property name="url" value="jdbc:mysql://localhost:3306/user_db"/>
            <property name="username" value="root"/>
            <property name="password" value="root"/>
            <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        bean>
    
        
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            
            <property name="dataSource" ref="dataSource">property>
        bean>
    
        
        <bean id="TransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            
            <property name="dataSource" ref="dataSource">property>
        bean>
    
    
        
        <tx:advice id="txadvice">
            
            <tx:attributes>
                
                <tx:method name="accountMoney" propagation="REQUIRED"/>
                
            tx:attributes>
        tx:advice>
        
        
        <aop:config>
            
            <aop:pointcut id="pt" expression="execution(* com.zyz.spring5.service.UserService.*(..))"/>
            
            <aop:advisor advice-ref="txadvice" pointcut-ref="pt"/>
        aop:config>
    
    
    beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    8、完全注解形式

    将xml改写成配置类的形式

    测试的时候读取配置类

     ApplicationContext context = new 
            AnnotationConfigApplicationContext(MyConfig.class);
    
    • 1
    • 2

    /**
     * @author Lenovo
     * @version 1.0
     * @data 2022/10/26 20:29
     */
    @Configuration //配置类
    @ComponentScan(basePackages = "com.zyz") //组件扫描
    @EnableTransactionManagement //开启事务
    public class MyConfig {
        /**
         *     创建数据库连接池
         */
        @Bean
        public DruidDataSource getDruidDataSource() {
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setDriverClassName("com.mysql.jdbc.Driver");
            dataSource.setUrl("jdbc:mysql://localhost:3306/user_db");
            dataSource.setUsername("root");
            dataSource.setPassword("root");
            return dataSource;
        }
    
        /**
         * 创建 JdbcTemplate 对象
         */
        @Bean
        public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
            //到 ioc 容器中根据类型找到 dataSource
            JdbcTemplate jdbcTemplate = new JdbcTemplate();
            //注入 dataSource
            jdbcTemplate.setDataSource(dataSource);
            return jdbcTemplate;
        }
        /**
         *  创建事务管理器
         */
        @Bean
        public DataSourceTransactionManager
        getDataSourceTransactionManager(DataSource dataSource) {
            DataSourceTransactionManager transactionManager = new
                    DataSourceTransactionManager();
            transactionManager.setDataSource(dataSource);
            return transactionManager;
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    9、完整的项目代码(纯注解形式)

    9.1 目录结构

    在这里插入图片描述

    9.2 MyConfig.java

    package com.zyz.spring5.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;
    
    /**
     * @author Lenovo
     * @version 1.0
     * @data 2022/10/26 20:29
     */
    @Configuration //配置类
    @ComponentScan(basePackages = "com.zyz") //组件扫描
    @EnableTransactionManagement //开启事务
    public class MyConfig {
        /**
         *     创建数据库连接池
         */
        @Bean
        public DruidDataSource getDruidDataSource() {
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setDriverClassName("com.mysql.jdbc.Driver");
            dataSource.setUrl("jdbc:mysql://localhost:3306/user_db");
            dataSource.setUsername("root");
            dataSource.setPassword("root");
            return dataSource;
        }
    
        /**
         * 创建 JdbcTemplate 对象
         */
        @Bean
        public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
            //到 ioc 容器中根据类型找到 dataSource
            JdbcTemplate jdbcTemplate = new JdbcTemplate();
            //注入 dataSource
            jdbcTemplate.setDataSource(dataSource);
            return jdbcTemplate;
        }
        /**
         *  创建事务管理器
         */
        @Bean
        public DataSourceTransactionManager
        getDataSourceTransactionManager(DataSource dataSource) {
            DataSourceTransactionManager transactionManager = new
                    DataSourceTransactionManager();
            transactionManager.setDataSource(dataSource);
            return transactionManager;
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    9.3 UserDao.java

    package com.zyz.spring5.dao;
    
    /**
     * @author Lenovo
     * @version 1.0
     * @data 2022/10/25 21:21
     */
    public interface UserDao {
    
        /**
         * 增加
         */
        public void addMoney();
    
        /**
         * 减少
    
         */
        public void reduceMoney();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    9.4 UserDaoImpl.java

    package com.zyz.spring5.dao;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    
    /**
     * @author Lenovo
     * @version 1.0
     * @data 2022/10/25 21:21
     */
    @Repository
    public class UserDaoImpl implements UserDao{
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
    
        @Override
        public void addMoney() {
            String sql = "update t_account set money = money + ? where username = ?";
            jdbcTemplate.update(sql,100,"李四");
        }
    
        @Override
        public void reduceMoney() {
            String sql = "update t_account set money = money - ? where username = ?";
            jdbcTemplate.update(sql,100,"张三");
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    9.5 User.java

    这里可以使用lombok、简化 get\set

    package com.zyz.spring5.entity;
    
    /**
     * @author Lenovo
     * @version 1.0
     * @data 2022/10/25 21:20
     */
    public class User {
        private String id;
        private String username;
        private int money;
    
        public String getId() {
            return id;
        }
    
        public void setId(String id) {
            this.id = id;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public int getMoney() {
            return money;
        }
    
        public void setMoney(int money) {
            this.money = money;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    9.6 UserService.java

    package com.zyz.spring5.service;
    
    import com.zyz.spring5.dao.UserDao;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Isolation;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;
    
    /**
     * @author Lenovo
     * @version 1.0
     * @data 2022/10/25 21:22
     */
    @Service
    @Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.REPEATABLE_READ)
    public class UserService {
        @Autowired
        private UserDao userDao;
    
    
        public void accountMoney(){
                //张三少钱
                userDao.reduceMoney();
                //异常模拟
                int i = 1/0;
                //李四多钱
                userDao.addMoney();
    
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    9.7 TestDemo.java

    package com.zyz.spring5.test;
    
    import com.zyz.spring5.config.MyConfig;
    import com.zyz.spring5.service.UserService;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @author Lenovo
     * @version 1.0
     * @data 2022/10/23 16:26
     */
    public class TestDemo {
    
        @Test
        public void testJdbcTemplate4() {
            ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
            UserService userService = context.getBean("userService", UserService.class);
            userService.accountMoney();
    
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    10、后语

    一句话、事务很重要。银行的转账业务可不是一般的复杂
    学无止境。。。。

  • 相关阅读:
    初识数据结构
    MyBatis中的$和#,你知道他们的区别吗?
    ly-tab插件动态更改高度进行多端适配
    【MySQL】4、MySQL备份与恢复
    C语言--每日五道选择题--Day14
    Docker Compose使用教程
    解决vue项目导出当前页Table为Excel
    RIP,EIGRP,OSPF区别
    路由跟踪命令 tracert 命令详解
    web server apache tomcat11-08-JNDI Resources
  • 原文地址:https://blog.csdn.net/weixin_43304253/article/details/127538708