项目中如何配置事务

项目中使用事务有好几种方式,本文章的项目都是使用的Spring,如果你使用的是JDBC编程,那么请看这个

事务管理是应用系统开发中必不可少的一部分。Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为 编程式声明式 的两种方式。

  • 编程式:指的是通过编码方式实现事务,看这个

  • 声明式:基于 AOP, 将具体业务逻辑与事务处理解耦

    声明式事务管理使业务代码逻辑不受污染, 因此在实际使用中声明式事务用的比较多(所以本篇文章只讲声明式)。

声明式事务有两种方式配置:

  • 基于XML配置(本篇文章不讲,请自行百度
  • 基于 @Transactional 注解(本篇文章使用此方式

一、SSM

配置:

spring-mybatis.xml

<!-- 配置事务管理器 -->
<bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!-- 注入数据库连接池 -->
    <property name="dataSource" ref="dataSource"/>
</bean>

<!-- 使用基于注解方式配置事务 -->
<tx:annotation-driven transaction-manager="transactionManager"/>

使用:

20191104111456.png

只需在 Service 的类或方法上面写 @Transactional 注解即可, @Transactional 中的属性,下面会详细讲解。

二、SpringBoot

开启事务:

Application 类上面打个 @EnableTransactionManagement 注解即可。如下:

20191104111747.png

@EnableTransactionManagement有两个属性可以看看:

  • proxyTargetClass 默认false(标准的 JDK 基于接口的代理)

    该属性用于控制代理是基于接口的还是基于类被创建 设置为 true 表示使用基于子类实现的代理(CGLIB),设置为 false 表示使用基于接口实现的代理

  • mode 默认PROXY

    该属性表示是使用哪种事务切面,有 PROXYASPECTJ,想要更深入了解,可以看看源码

使用也是在 Service 上面加 @Transactional 注解即可。

三、我们在使用 @Transactional 注解的时候,需要注意的一些问题:

配置好了,先别着急去用,先来看看有哪些需要我们注意的。

1、默认配置下 Spring 只会回滚运行时、未检查异常(继承自 RuntimeException 的异常)或者 Error,但是我们可以配置 rollbackFor 来指定我们要处理的异常(下面会讲)

2、 @Transactional 注解只能应用到 public 可见度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错, 但是这个被注解的方法将不会展示已配置的事务设置。

3、 @Transactional 注解可以被应用于接口定义和接口方法、类定义和类的 public 方法上,但是Spring团队建议在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上(因为 Spring 的声明式事务是默认基于 SpringAOP 实现,而 SpringAOP 默认是使用 java 动态代理实现(基于接口实现),如果此时将代理改为cglib(基于子类实现),在接口上面加注解就没用了,所以为了保持兼容注解最好都写到实现类方法上)

4、如果 @Transactional 注解被写在 Service 的类上面,则表示类中所有的方法都被事务管理,但是有些查询方法是不需要事务管理,那么可以这样做(为什么这样,看完下面就明白了):

/**
 * 获取用户列表
 * <br/>
 * 查询不需要事务
 */
@Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
public PageInfo<User> listUsers(Page<User> page) {
    PageHelper.startPage(page.getPageNum(), page.getPageSize());
    List<User> users = userMapper.listUsers();
    return new PageInfo<>(users);
}

5、有些时候我们需要手动回滚事务,那么,有两种方法:

  1. service层处理事务,那么service中的方法中不做异常捕获,或者在catch语句中最后增加throw new RuntimeExcetpion()语句,以便让Aop捕获异常再去回滚,并且在service上层(webservice客户端,view层Controller)要继续捕获这个异常并处理

  2. 在service层方法的catch语句中增加:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();语句,手动回滚,这样上层就无需去处理异常(现在项目的做法)

四、@Transactional 详解

@Transactional 中的属性:

1、value、transactionManager

这两个作用一样,当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器

2、propagation

事务的传播行为,默认值为 Propagation.REQUIRED

可选的值有:

  • Propagation.REQUIRED

    如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。

  • Propagation.SUPPORTS

    如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。

  • Propagation.MANDATORY

    如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。

  • Propagation.REQUIRES_NEW

    重新创建一个新的事务,如果当前存在事务,暂停当前的事务。

  • Propagation.NOT_SUPPORTED

    以非事务的方式运行,如果当前存在事务,暂停当前的事务。

  • Propagation.NEVER

    以非事务的方式运行,如果当前存在事务,则抛出异常。

  • Propagation.NESTED

    Propagation.REQUIRED 效果一样。

3、isolation

事务的隔离级别,默认值为 Isolation.DEFAULT

可选的值有:

  • Isolation.DEFAULT

    使用底层数据库默认的隔离级别。(MySQl 默认:Repeatable read)

  • Isolation.READ_UNCOMMITTED

    (读未提交):最低级别,任何情况都无法保证。

  • Isolation.READ_COMMITTED

    (读已提交):可避免脏读的发生。

  • Isolation.REPEATABLE_READ

    (可重复读):可避免脏读、不可重复读的发生

  • Isolation.SERIALIZABLE

    (串行化):可避免脏读、不可重复读、幻读的发生。

4、timeout

事务的超时时间,默认值为-1,表示事务超时将依赖于底层事务系统,

如果超过该时间限制但事务还没有完成,则自动回滚事务。

5、readOnly

指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。

6、rollbackFor

需要触发回滚的异常定义,可定义多个,默认任何RuntimeException都将导致事务回滚,而任何Checked Exception将不导致事务回滚

7、noRollbackFor

抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。

下面重点讲讲 Propagation.REQUIRES_NEW 这种传播行为:

注:

下面有几个使用案例,如果你也想打印详细日志,你需要下面这样配置:

需要控制台打印MyBatis执行SQL日志的这样配置:

# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ MyBatis 配置 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ #
mybatis:
    configuration:
        # spring boot集成mybatis的方式打印sql:https://blog.csdn.net/qq_22194659/article/details/81120712
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

需要看到事务的创建详细日志的这样配置:

# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 日志 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ #
# 默认情况下,spring boot从控制台打印出来的日志级别只有ERROR, WARN 还有INFO,如果你想要打印debug级别的日志,可以通过配置debug=true
debug: true
logging:
    # 配置logging.level.*来具体输出哪些包的日志级别
    level:
        root: info
        org.springframework.web: debug
        # 打开 jdbc 事务执行日志
        org.springframework.jdbc.datasource: debug

先来看一段代码:

package com.blog.www.service;

import com.blog.www.domain.entity.User;
import com.blog.www.domain.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 事务测试服务
 */
@Transactional
@Service
public class TransactionTestService {

    @Autowired
    private UserMapper userMapper;

    public void test1() {
        test2();

        User user = new User();
        user.setAge((short)1);
        user.setUserName("lei");
        user.setPassword("212121");
        user.setDeleted(false);
        userMapper.insert(user);

        throw new RuntimeException("测试事务回滚");
    }


    public void test2() {
        User user = new User();
        user.setAge((short)2);
        user.setUserName("tom");
        user.setPassword("2121DSASDAS");
        user.setDeleted(false);
        userMapper.insert(user);
    }

}

这个Service里面有两个方法,使用test1去调用test2,在test2和test1都执行完之后,抛一个异常,结果是两条数据都回滚了,因为我们在类上面加了 @Transactional 注解,此注解传播行为默认是:Propagation.REQUIRED,所以此时test1和test2事务是同一个。

可以看看执行日志:

20191104214631.png
20191104214701.png

从日志可以看出,只创建了一个事务,事务也正常回滚了。

那么,如果现在有这样的一个需求:test1抛异常仅仅只回滚test1,test2不受影响,如何实现?

有人可能会想到,在test2上面加个 @Transactional 注解,设置传播行为为:Propagation.REQUIRES_NEW,这样test2就会创建新的事务,真的是这样么?我们试试

修改上面的代码为:

package com.blog.www.service;

import com.blog.www.domain.entity.User;
import com.blog.www.domain.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * 事务测试服务
 */
@Transactional
@Service
public class TransactionTestService {

    @Autowired
    private UserMapper userMapper;

    public void test1() {
        test2();

        User user = new User();
        user.setAge((short)1);
        user.setUserName("lei");
        user.setPassword("212121");
        user.setDeleted(false);
        userMapper.insert(user);

        throw new RuntimeException("测试事务回滚");
    }


    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void test2() {
        User user = new User();
        user.setAge((short)2);
        user.setUserName("tom");
        user.setPassword("2121DSASDAS");
        user.setDeleted(false);
        userMapper.insert(user);
    }

}

查看执行日志:

20191104220114.png

从日志可以看出,两个方法还是处于同一个事务中,两条数据都回滚了。

为什么会出现这种情况?

这就得看看 Spring 官方文档

20191104220457.png

In proxy mode (which is the default), only external method calls coming in through the proxy are intercepted. This means that self-invocation, in effect, a method within the target object calling another method of the target object, will not lead to an actual transaction at runtime even if the invoked method is marked with @Transactional.

大概意思:

在默认的代理模式下,只有目标方法由外部调用,才能被 Spring 的事务拦截器拦截。在同一个类中的两个方法直接调用,是不会被 Spring 的事务拦截器拦截。

就像上面的 test1 方法直接调用了同一个类中的 test2 方法,test2 方法不会被 Spring 的事务拦截器拦截。可以使用 AspectJ 取代 Spring AOP 代理来解决这个问题,但是这里暂不讨论。

为了解决这个问题,我们可以新建一个类,将 test2 放入新建的类,如下:

test1修改后:

package com.blog.www.service;

import com.blog.www.domain.entity.User;
import com.blog.www.domain.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 事务测试服务
 */
@Transactional
@Service
public class TransactionTest1Service {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private TransactionTest2Service transactionTest2Service;

    public void test1() {
        transactionTest2Service.test2();

        User user = new User();
        user.setAge((short) 1);
        user.setUserName("lei");
        user.setPassword("212121");
        user.setDeleted(false);
        userMapper.insert(user);

        throw new RuntimeException("测试事务回滚");
    }

}

test2修改后:

package com.blog.www.service;

import com.blog.www.domain.entity.User;
import com.blog.www.domain.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * 事务测试服务
 */
@Transactional
@Service
public class TransactionTest2Service {

    @Autowired
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void test2() {
        User user = new User();
        user.setAge((short)2);
        user.setUserName("tom");
        user.setPassword("2121DSASDAS");
        user.setDeleted(false);
        userMapper.insert(user);
    }

}

查看执行日志:

20191104221726.png
20191104221357.png

从日志可以清晰的看出,在执行test2时,test1的事务被挂起,test2重新创建了一个新的事务,此时,再去看看数据库:

20191104221943.png

只有test1回滚了,test2数据正常插入。

还有一个比较常见的场景,就是在循环体内使用事务,比如,现在需要循环插入1000条数据,需要保证中途有一条插入失败,只回滚失败的那条,前面已经执行的不受影响。

这个和上面差不多,只需要把循环执行的方法单独放入一个类中,将事务的传播行为设置成 Propagation.REQUIRES_NEW 即可。

测试代码如下:

package com.blog.www.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 事务测试服务
 */
@Transactional
@Service
@Slf4j
public class TransactionTest1Service {

    @Autowired
    private TransactionTest2Service transactionTest2Service;

    public void test1() {
        for (int i = 0; i < 10; i++) {
            transactionTest2Service.test2(i);
            if (i == 5) {
                // 模拟抛异常让 AOP 处理事务回滚,这里抛异常回滚,只能保证已经执行了的不会回滚,程序会停止执行
                throw new RuntimeException("测试循环体内事务");
                // 推荐使用手动回滚事务,手动回滚不影响程序继续执行
                // TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            }
        }
    }
}
package com.blog.www.service;

import com.blog.www.domain.entity.User;
import com.blog.www.domain.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * 事务测试服务
 */
@Transactional
@Service
public class TransactionTest2Service {

    @Autowired
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void test2(int i) {
        User user = new User();
        user.setAge((short)2);
        user.setUserName("tom" + i);
        user.setPassword("2121DSASDAS");
        user.setDeleted(false);
        userMapper.insert(user);
    }
}

执行日志如下:

2019-11-04 22:48:09.996 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [com.blog.www.service.TransactionTest1Service.test1]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
2019-11-04 22:48:09.996 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@29abba49] for JDBC transaction
2019-11-04 22:48:09.996 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@29abba49] to manual commit
2019-11-04 22:48:09.997 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Suspending current transaction, creating new transaction with name [com.blog.www.service.TransactionTest2Service.test2]
2019-11-04 22:48:09.997 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] for JDBC transaction
2019-11-04 22:48:09.998 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] to manual commit
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6a5de853]
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] will be managed by Spring
==>  Preparing: insert into user (deleted, age, user_name, password, create_time, update_time) values (?, ?, ?,?, ?, ?) 
==> Parameters: false(Boolean), 2(Short), tom0(String), 2121DSASDAS(String), null, null
<==    Updates: 1
==>  Preparing: SELECT LAST_INSERT_ID() 
==> Parameters: 
<==    Columns: LAST_INSERT_ID()
<==        Row: 52
<==      Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6a5de853]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6a5de853]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6a5de853]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6a5de853]
2019-11-04 22:48:10.005 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2019-11-04 22:48:10.005 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc]
2019-11-04 22:48:10.007 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] after transaction
2019-11-04 22:48:10.008 DEBUG 576 --- [nio-8085-exec-2] o.s.jdbc.datasource.DataSourceUtils      : Returning JDBC Connection to DataSource
2019-11-04 22:48:10.008 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Resuming suspended transaction after completion of inner transaction
2019-11-04 22:48:10.008 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Suspending current transaction, creating new transaction with name [com.blog.www.service.TransactionTest2Service.test2]
2019-11-04 22:48:10.008 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] for JDBC transaction
2019-11-04 22:48:10.008 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] to manual commit
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@af833aa]
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] will be managed by Spring
==>  Preparing: insert into user (deleted, age, user_name, password, create_time, update_time) values (?, ?, ?,?, ?, ?) 
==> Parameters: false(Boolean), 2(Short), tom1(String), 2121DSASDAS(String), null, null
<==    Updates: 1
==>  Preparing: SELECT LAST_INSERT_ID() 
==> Parameters: 
<==    Columns: LAST_INSERT_ID()
<==        Row: 53
<==      Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@af833aa]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@af833aa]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@af833aa]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@af833aa]
2019-11-04 22:48:10.016 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2019-11-04 22:48:10.016 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc]
2019-11-04 22:48:10.019 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] after transaction
2019-11-04 22:48:10.019 DEBUG 576 --- [nio-8085-exec-2] o.s.jdbc.datasource.DataSourceUtils      : Returning JDBC Connection to DataSource
2019-11-04 22:48:10.019 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Resuming suspended transaction after completion of inner transaction
2019-11-04 22:48:10.020 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Suspending current transaction, creating new transaction with name [com.blog.www.service.TransactionTest2Service.test2]
2019-11-04 22:48:10.020 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] for JDBC transaction
2019-11-04 22:48:10.020 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] to manual commit
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25ca9b0f]
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] will be managed by Spring
==>  Preparing: insert into user (deleted, age, user_name, password, create_time, update_time) values (?, ?, ?,?, ?, ?) 
==> Parameters: false(Boolean), 2(Short), tom2(String), 2121DSASDAS(String), null, null
<==    Updates: 1
==>  Preparing: SELECT LAST_INSERT_ID() 
==> Parameters: 
<==    Columns: LAST_INSERT_ID()
<==        Row: 54
<==      Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25ca9b0f]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25ca9b0f]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25ca9b0f]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25ca9b0f]
2019-11-04 22:48:10.028 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2019-11-04 22:48:10.029 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc]
2019-11-04 22:48:10.031 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] after transaction
2019-11-04 22:48:10.031 DEBUG 576 --- [nio-8085-exec-2] o.s.jdbc.datasource.DataSourceUtils      : Returning JDBC Connection to DataSource
2019-11-04 22:48:10.031 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Resuming suspended transaction after completion of inner transaction
2019-11-04 22:48:10.032 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Suspending current transaction, creating new transaction with name [com.blog.www.service.TransactionTest2Service.test2]
2019-11-04 22:48:10.032 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] for JDBC transaction
2019-11-04 22:48:10.032 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] to manual commit
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66ddaba0]
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] will be managed by Spring
==>  Preparing: insert into user (deleted, age, user_name, password, create_time, update_time) values (?, ?, ?,?, ?, ?) 
==> Parameters: false(Boolean), 2(Short), tom3(String), 2121DSASDAS(String), null, null
<==    Updates: 1
==>  Preparing: SELECT LAST_INSERT_ID() 
==> Parameters: 
<==    Columns: LAST_INSERT_ID()
<==        Row: 55
<==      Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66ddaba0]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66ddaba0]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66ddaba0]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66ddaba0]
2019-11-04 22:48:10.037 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2019-11-04 22:48:10.037 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc]
2019-11-04 22:48:10.039 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] after transaction
2019-11-04 22:48:10.039 DEBUG 576 --- [nio-8085-exec-2] o.s.jdbc.datasource.DataSourceUtils      : Returning JDBC Connection to DataSource
2019-11-04 22:48:10.040 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Resuming suspended transaction after completion of inner transaction
2019-11-04 22:48:10.040 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Suspending current transaction, creating new transaction with name [com.blog.www.service.TransactionTest2Service.test2]
2019-11-04 22:48:10.040 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] for JDBC transaction
2019-11-04 22:48:10.040 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] to manual commit
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@535d39c7]
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] will be managed by Spring
==>  Preparing: insert into user (deleted, age, user_name, password, create_time, update_time) values (?, ?, ?,?, ?, ?) 
==> Parameters: false(Boolean), 2(Short), tom4(String), 2121DSASDAS(String), null, null
<==    Updates: 1
==>  Preparing: SELECT LAST_INSERT_ID() 
==> Parameters: 
<==    Columns: LAST_INSERT_ID()
<==        Row: 56
<==      Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@535d39c7]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@535d39c7]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@535d39c7]
2019-11-04 22:48:10.046 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Initiating transaction rollback
2019-11-04 22:48:10.046 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Rolling back JDBC transaction on Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc]
2019-11-04 22:48:10.048 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@185e0cdc] after transaction
2019-11-04 22:48:10.048 DEBUG 576 --- [nio-8085-exec-2] o.s.jdbc.datasource.DataSourceUtils      : Returning JDBC Connection to DataSource
2019-11-04 22:48:10.049 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Resuming suspended transaction after completion of inner transaction
2019-11-04 22:48:10.049 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Initiating transaction rollback
2019-11-04 22:48:10.049 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Rolling back JDBC transaction on Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@29abba49]
2019-11-04 22:48:10.050 DEBUG 576 --- [nio-8085-exec-2] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@29abba49] after transaction
2019-11-04 22:48:10.050 DEBUG 576 --- [nio-8085-exec-2] o.s.jdbc.datasource.DataSourceUtils      : Returning JDBC Connection to DataSource
2019-11-04 22:48:10.051 DEBUG 576 --- [nio-8085-exec-2] .m.m.a.ExceptionHandlerExceptionResolver : Resolving exception from handler [public void com.blog.www.controller.TransactionTestController.testRollBack()]: java.lang.RuntimeException: 测试循环体内事务
2019-11-04 22:48:10.052 DEBUG 576 --- [nio-8085-exec-2] .m.m.a.ExceptionHandlerExceptionResolver : Invoking @ExceptionHandler method: public com.blog.www.bean.common.Response com.blog.www.web.GlobalExceptionHandNew.handleException(java.lang.Exception)
2019-11-04 22:48:10.055 ERROR 576 --- [nio-8085-exec-2] com.blog.www.web.GlobalExceptionHandNew  : 服务内部异常!测试循环体内事务

java.lang.RuntimeException: 测试循环体内事务
	at com.blog.www.service.TransactionTest2Service.test2(TransactionTest2Service.java:30) ~[classes/:na]
	at com.blog.www.service.TransactionTest2Service$$FastClassBySpringCGLIB$$ae552b84.invoke(<generated>) ~[classes/:na]
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:747) ~[spring-aop-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:294) ~[spring-tx-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98) ~[spring-tx-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) ~[spring-aop-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689) ~[spring-aop-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at com.blog.www.service.TransactionTest2Service$$EnhancerBySpringCGLIB$$d21401df.test2(<generated>) ~[classes/:na]
	at com.blog.www.service.TransactionTest1Service.test1(TransactionTest1Service.java:21) ~[classes/:na]
	at com.blog.www.service.TransactionTest1Service$$FastClassBySpringCGLIB$$4673fea5.invoke(<generated>) ~[classes/:na]
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:747) ~[spring-aop-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:294) ~[spring-tx-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98) ~[spring-tx-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) ~[spring-aop-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689) ~[spring-aop-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at com.blog.www.service.TransactionTest1Service$$EnhancerBySpringCGLIB$$55d8f7e2.test1(<generated>) ~[classes/:na]
	at com.blog.www.controller.TransactionTestController.testRollBack(TransactionTestController.java:23) ~[classes/:na]
	at sun.reflect.GeneratedMethodAccessor188.invoke(Unknown Source) ~[na:na]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:45005) ~[na:1.8.0_151]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_151]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:209) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:136) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:783) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:635) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) [tomcat-embed-websocket-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:109) [spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at com.blog.www.web.filter.xss.XssFilter.doFilter(XssFilter.java:31) [classes/:na]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at com.blog.www.web.filter.i18n.I18nFilter.doFilter(I18nFilter.java:77) [classes/:na]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:123) [druid-1.1.10.jar:1.1.10]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter.doFilterInternal(HttpTraceFilter.java:90) [spring-boot-actuator-2.0.4.RELEASE.jar:2.0.4.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.filterAndRecordMetrics(WebMvcMetricsFilter.java:155) [spring-boot-actuator-2.0.4.RELEASE.jar:2.0.4.RELEASE]
	at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.filterAndRecordMetrics(WebMvcMetricsFilter.java:123) [spring-boot-actuator-2.0.4.RELEASE.jar:2.0.4.RELEASE]
	at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:108) [spring-boot-actuator-2.0.4.RELEASE.jar:2.0.4.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200) [spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.StandardContextValve.__invoke(StandardContextValve.java:96) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:41002) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:493) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:800) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:800) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1471) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_151]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_151]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at java.lang.Thread.run(Thread.java:748) [na:1.8.0_151]

再看看数据库:

20191104225210.png

可以发现前面插入的不受影响。

五、@Transactional 事务实现机制

AOP 代理后的方法调用执行流程:

20191104222443.png

在应用系统调用声明了 @Transactional 的目标方法时,Spring Framework 默认使用 AOP 代理,在代码运行时生成一个代理对象,根据 @Transactional 的属性配置信息,这个代理对象决定该声明 @Transactional 的目标方法是否由拦截器 TransactionInterceptor 来使用拦截,在 TransactionInterceptor 拦截时,会在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑, 最后根据执行情况是否出现异常,利用抽象事务管理器 AbstractPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务。

Spring AOP 代理有 CglibAopProxy 和 JdkDynamicAopProxy 两种,以 CglibAopProxy 为例,对于 CglibAopProxy,需要调用其内部类的 DynamicAdvisedInterceptor 的 intercept 方法。对于 JdkDynamicAopProxy,需要调用其 invoke 方法。

20191104222225.png

事务管理的框架是由抽象事务管理器 AbstractPlatformTransactionManager 来提供的,而具体的底层事务处理实现,由 PlatformTransactionManager 的具体实现类来实现,如事务管理器 DataSourceTransactionManager。不同的事务管理器管理不同的数据资源 DataSource,比如 DataSourceTransactionManager 管理 JDBC 的 Connection。

20191104222314.png


作者:不敲代码的攻城狮
出处:https://www.cnblogs.com/leigq/
任何傻瓜都能写出计算机可以理解的代码。好的程序员能写出人能读懂的代码。

 
原文地址:https://www.cnblogs.com/leigq/p/13406509.html