Spring的事务管理难点剖析

1.DAO和事务管理的牵绊

事务管理的目的是保证数据操作的事务性(原子性、一致性、隔离性、持久性,即所谓的ACID),脱离了事务性,DAO照样可以顺利地进行数据操作

1.1.JDBC访问数据库

文档中用到的代码项目地址

public class UserJdbcWithoutTransManagerService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void addScore(String userName,int toAdd){
        String sql="update t_user u set u.score=u.score+? where user_name=?";
        jdbcTemplate.update(sql,toAdd,userName);
    }

    public static void main(String[] args) {
        ApplicationContext ctx=new ClassPathXmlApplicationContext("jdbcWithoutTx.xml");
        UserJdbcWithoutTransManagerService userJdbcWithoutTransManagerService= (UserJdbcWithoutTransManagerService) ctx.getBean("userService");
        JdbcTemplate jdbcTemplate= (JdbcTemplate) ctx.getBean("jdbcTemplate");
        BasicDataSource dataSource= (BasicDataSource) jdbcTemplate.getDataSource();
        //检查数据源autoCommit的设置
        System.out.println("autoCommit:"+dataSource.getDefaultAutoCommit());
        //插入一条记录,初始分数为10
        jdbcTemplate.execute("INSERT INTO t_user(user_name,password,score,last_logon_time) VALUES('tom','123456',10,"+System.currentTimeMillis()+")");
        //调用工作在无事务环境下的服务类方法,将分数添加到20分
        userJdbcWithoutTransManagerService.addScore("tom",20);
        //查看此时用户的分数
        int score=jdbcTemplate.queryForObject("select score from t_user where user_name='tom'",Integer.class);
        System.out.println("score:"+score);
        jdbcTemplate.execute("delete from t_user where user_name='tom'");
    }
}

 <context:component-scan base-package="com"/>
   <context:property-placeholder
            location="classpath:jdbc.properties" ignore-unresolvable="true"/>

    <!--apache.commons.dbcp.BasicDataSource-->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
          destroy-method="close"
          p:driverClassName="${jdbc.driverClassName}"
          p:url="${jdbc.url}"
          p:username="${jdbc.username}"
          p:password="${jdbc.password}"/>

    <bean id="jdbcTemplate"
          class="org.springframework.jdbc.core.JdbcTemplate"
          p:dataSource-ref="dataSource"/>

    <bean id="userService" class="com.servcie.UserJdbcWithoutTransManagerService"/>

在jdbcWithoutTx.xml中没有配置任何事务管理器,但是数据已经成功持久化到数据库中。在默认情况下,dataSource数据源的autoCommit被设置为ture,这也意味着所有通过JdbcTemplate执行的语句马上提交,没有事务。如果将dataSource的defaultAutoCommit设置为false,再次运行UserJdbcWithoutTransManagerService,将抛出错误,原因是新增及更改数据库的操作都没有提交到数据库,所以查询语句无法从数据库中查询匹配大记录而发生异常

对于强调读速度的应用,数据库本身可能就不支持事务,如使用MyISAM引擎的MySQL数据库。这时,无须在Spring应用中配置事务管理器,因为即使配置了,也没有用处

1.2.Hibernate访问数据库

对于Hibernate来说,情况就比较复杂了。因为Hibernate的事务管理拥有其自身的意义,它和Hibernate一级缓存存在密切的关系:在强调Session的save、update等方法时,Hibernate并不直接向数据库发送SQL语句,只在提交事务或flush一级缓存时才真正向数据库发送SQL。所以,即使底层数据库不支持事务,Hibernate的事务管理也是有一定好处的,不会对数据库操作的效率造成负面的影响。所以,如果使用Hiberbate数据访问技术,则没有理由不配置HibernateTransactionManager事务管理器

但是,如果不使用Hibernate事务管理器,Spring就会采取默认的事务管理器策略搜索(PROPAGATION_REQUIRED,readOnly)。如果有修改操作是不允许的,就会抛出异常

文档中用到的代码项目地址

@Service("hiberService")
public class UserHibernateWithoutTransManagerService {
    
    private HibernateTemplate hibernateTemplate;

    @Autowired
    public void setHibernateTemplate(HibernateTemplate hibernateTemplate) {
        this.hibernateTemplate = hibernateTemplate;
    }

    public void addScore(String userName,int toAdd){
        User user = hibernateTemplate.get(User.class,userName);
        user.setScore(user.getScore()+toAdd);
        //以下语句取消注释后,由于默认事务管理器不支持数据更改将报异常
        //通过Hibernate操作数据库
        hibernateTemplate.update(user);
        
    }

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/withouttx/hiber/hiberWithoutTx.xml");
        UserHibernateWithoutTransManagerService service = (UserHibernateWithoutTransManagerService)ctx.getBean("hiberService");

        JdbcTemplate jdbcTemplate = (JdbcTemplate)ctx.getBean("jdbcTemplate");
        BasicDataSource basicDataSource = (BasicDataSource)jdbcTemplate.getDataSource();

        //①检查数据源autoCommit的设置
        System.out.println("autoCommit:"+ basicDataSource.getDefaultAutoCommit());

        //②插入一条记录,初始分数为10
        jdbcTemplate.execute("INSERT INTO t_user(user_name,password,score,last_logon_time) VALUES('tom','123456',10,"+System.currentTimeMillis()+")");

        //③调用工作在无事务环境下的服务类方法,将分数添加20分
        service.addScore("tom",20);
        
        //④查看此时用户的分数
        int score = jdbcTemplate.queryForObject("SELECT score FROM t_user WHERE user_name ='tom'", Integer.class);
        System.out.println("score:"+score);
        jdbcTemplate.execute("DELETE FROM t_user WHERE user_name='tom'");
    }
}

<context:component-scan base-package="com.smart.withouttx.hiber"/>
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close" 
        p:driverClassName="${jdbc.driverClassName}"
        p:url="${jdbc.url}" 
        p:username="${jdbc.username}"
        p:password="${jdbc.password}"/>

    <bean id="jdbcTemplate"
          class="org.springframework.jdbc.core.JdbcTemplate"
          p:dataSource-ref="dataSource"/>

    <bean id="sessionFactory"
          class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"
          p:dataSource-ref="dataSource">
        <property name="annotatedClasses">
            <list>
                <value>smart.User</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
                <prop key="hibernate.show_sql">true</prop>
            </props>
        </property>
    </bean>

    <bean id="hibernateTemplate"
          class="org.springframework.orm.hibernate4.HibernateTemplate"
          p:sessionFactory-ref="sessionFactory"/>
    <bean id="userService" class="smart.withouttx.jdbc.UserJdbcWithoutTransManagerService"/>
    <bean id="hiberService" class="smart.withouttx.hiber.UserHibernateWithoutTransManagerService"/>

2.应用分层的迷惑

任何项目不一定必须按照web,service和dao三层来进行分层开发,对于一个简单的项目来说,往往只有一些对数据库增、删、改、查的操作,此时,过分强调“面向接口编程”,除了会带来更多的类文件,并不会有什么好处。

3.事务方法嵌套调用的迷茫

3.1.Spring事务传播机制回顾

Spring事务的一个被讹传很广的说法是:一个事务方法不应该调用另一个事务方法,否则会产生两个事务。结果造成开发人员在设计事务方法时束手束脚

Spring对事务控制的支持统一在TransactionDefinition类中描述,该类有以下几个重要的接口方法

  • int getPropagationBehavior():事务的传播级别
  • int getIsolationLevel():事务的隔离级别
  • int getTimeout():事务的过期时间
  • boolean isReadOnly():事务的读/写特性

除了事务的传播行为,对于事务的其它特性,Spring是借助底层资源的功能来完成的,Spring无非是充当了一个代理的角色。但是事务的传播行为却是Spring凭借自身的框架提供的功能,是Spring提供给开发者最珍贵的礼物。

所谓事务传播行为,就是多个事务方法相互调用时,事务如何在这些方法间传播。Spring支持以下7种事务传播行为:

  • PROPAGATION_REQUIRED:如果当前没有事务,就新建一个事务;如果已经存在一个事务,就加入到这个事务中。这是最常见的选择
  • PROPAGATION_SUPPORTS:支持当前事务。如果没有事务,就以非事务方式执行
  • PROPAGATION_MANDATORY:使用当前事务。如果没有当前事务,就抛出异常
  • PROPAGATION_REQUIRES_NEW:新建事务。如果当前存在事务,就把当前事务挂起
  • PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作。如果当前存在事务,就把当前事务挂起
  • PROPAGATION_NEVER:以非事务方式执行。如果当前存在事务,就抛出异常
  • PROPAGATION_NESTED:如果当前存在事务,就在嵌套事务内执行;如果当前没有事务,就执行与PROPAGATION_REQUIRED类似的操作

spring默认的事务传播行为是PROPAGATION_REQUIRED,它适合绝大多数场景,如果多个Service.methodX均工作在事务环境下(均被spring事务增强),且程序中存在调用链Service1.method1()->Service2.method2()->Service3.method3(),那么这3个服务类的3个方法通过Spring的事务传播机制都工作在同一个事务中

3.2相互嵌套的服务方法

文档中用到的代码项目地址

@Service("userService")
public class UserService extends BaseService {

    private JdbcTemplate jdbcTemplate;

    private ScoreService scoreService;

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Autowired
    public void setScoreService(ScoreService scoreService) {
        this.scoreService = scoreService;
    }

    public void logon(String userName) {
        System.out.println("before userService.updateLastLogonTime...");
        updateLastLogonTime(userName);
        System.out.println("after userService.updateLastLogonTime...");
        
        System.out.println("before scoreService.addScore...");
        scoreService.addScore(userName, 20);
        System.out.println("after scoreService.addScore...");

    }

    public void updateLastLogonTime(String userName) {
        String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";
        jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
    }

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/nestcall/applicatonContext.xml");
        UserService service = (UserService) ctx.getBean("userService");

        JdbcTemplate jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
        BasicDataSource basicDataSource = (BasicDataSource) jdbcTemplate.getDataSource();
        //插入一条记录,初始分数为10
        jdbcTemplate.execute("INSERT INTO t_user(user_name,password,score,last_logon_time) VALUES('tom','123456',10," + System.currentTimeMillis() + ")");

        //调用工作在无事务环境下的服务类方法,将分数添加20分
        System.out.println("before userService.logon method...");
        service.logon("tom");
        System.out.println("after userService.logon method...");

        jdbcTemplate.execute("DELETE FROM t_user WHERE user_name='tom'");
    }
}


@Service("scoreUserService")
public class ScoreService extends BaseService{

    private JdbcTemplate jdbcTemplate;

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void addScore(String userName, int toAdd) {
        String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";
        jdbcTemplate.update(sql, toAdd, userName);
    }
}


4.多线程的困惑

4.1.Spring通过单实例化Bean简化多线程问题

由于Spring的事务管理器是通过线程相关的ThreadLocal来保存数据访问基础设施(Connection实例)的,再结合Ioc和AOP实现高级声明式事务的功能,所以Spring的事务天然地和线程有着千丝万缕的联系

一个类能够以单实例的方式运行的前提是“无状态”,即一个类不能拥有状态化的成员变量。在传统的编程中,DAO必须持有一个Connection,而Connection就是状态化的对象.所以传统的DAO不能做成单实例的,每次要用时都必须创建一个新的实例。传统的Service由于内部包含了若干有状态的DAO成员变量,所以其本身也是有状态的

在Spring中,DAO和Service都以单实例的方式存在。Spring通过ThreadLocal将有状态的变量(如Connection等)本地线程化,达到另一个层面上的“线程无关”,从而实现线程安全。Spring不遗余力的将有状态的对象无状态化,就是要达到单实例化Bean的目的

4.2.启动独立线程调用事务方法

文档中用到的代码项目地址

@Service("userService")
public class UserService extends BaseService {
    private JdbcTemplate jdbcTemplate;
    private ScoreService scoreService;

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Autowired
    public void setScoreService(ScoreService scoreService) {
        this.scoreService = scoreService;
    }

    public void logon(String userName) {
        System.out.println("before userService.updateLastLogonTime method...");
        updateLastLogonTime(userName);
        System.out.println("after userService.updateLastLogonTime method...");

//      scoreService.addScore(userName, 20);
        Thread myThread = new MyThread(this.scoreService, userName, 20);//使用一个新线程运行
        myThread.start();
    }

    public void updateLastLogonTime(String userName) {
        String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";
        jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
    }

    private class MyThread extends Thread {
        private ScoreService scoreService;
        private String userName;
        private int toAdd;
        private MyThread(ScoreService scoreService, String userName, int toAdd) {
            this.scoreService = scoreService;
            this.userName = userName;
            this.toAdd = toAdd;
        }

        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("before scoreService.addScor method...");
            scoreService.addScore(userName, toAdd);
            System.out.println("after scoreService.addScor method...");
        }
    }

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/multithread/applicatonContext.xml");
        UserService service = (UserService) ctx.getBean("userService");

        JdbcTemplate jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
        //插入一条记录,初始分数为10
        jdbcTemplate.execute("INSERT INTO t_user(user_name,password,score,last_logon_time) VALUES('tom','123456',10," + System.currentTimeMillis() + ")");


        //调用工作在无事务环境下的服务类方法,将分数添加20分
        System.out.println("before userService.logon method...");
        service.logon("tom");
        System.out.println("after userService.logon method...");
        jdbcTemplate.execute("DELETE FROM t_user WHERE user_name='tom'");

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}



在相同线程中进行相互嵌套使用的事务方法工作在相同的事务中,如果这些相互嵌套调用的方法工作在不同的线程中,则不同线程下的事务方法工作在独立的事务中。

5.联合军中作战的混乱

5.1.Spring事务管理器的应对

Spring抽象的DAO体系兼容多种数据访问技术,比如Hibernate是一个非常优秀的ORM实现方案,但对底层SQL的控制不太方便,而MyBatis则通过模板化技术让用户方便地控制SQL,但没有Hibernate那样高的开发效率;自由度最高的就是直接使用Spring JDBC了,
但是代码是比较繁复的

Spring提供的事务管理能力能够很好应对ORM框架和JDBC联合,由于ORM框架的会话是对后者连接的封装,Spring会“足够智能地”在同一事务线程中让前者的会话封装后者的连接。因此,只要采用前者的事务管理器就行了。

序号 混合数据访问技术框架 事务管理器
1 Hibernate+Spring JDBC或MyBatis org.springframework.orm.hibernateX.HibernateTransactionManager
2 JPA+Spring JDBC或MyBatis org.springframework.orm.jpa.JpaTransactionManager
3 JDO+Spring JDBC或MyBatis org.springframework.orm.jdo.JdoTransactionManager
5.2.Hibernate+Spring JDBC混合框架的事务管理

下面展示ORM框架+JDBC框架的情况
文档中用到的代码项目地址

@Service("userService")
public class UserService extends BaseService {

    private HibernateTemplate hibernateTemplate;
    private ScoreService scoreService;

    @Autowired
    public void setHibernateTemplate(HibernateTemplate hibernateTemplate) {
        this.hibernateTemplate = hibernateTemplate;
    }

    @Autowired
    public void setScoreService(ScoreService scoreService) {
        this.scoreService = scoreService;
    }

    public void logon(String userName) {
        //1、通过Hibernate技术访问数据
        System.out.println("before userService.updateLastLogonTime()..");
        updateLastLogonTime(userName);
        System.out.println("end userService.updateLastLogonTime()..");
        //2、通过JDBC技术访问数据
        System.out.println("before scoreService.addScore()..");
        scoreService.addScore(userName, 20);
        System.out.println("end scoreService.addScore()..");
    }

    public void updateLastLogonTime(String userName) {
        User user = hibernateTemplate.get(User.class,userName);
        user.setLastLogonTime(System.currentTimeMillis());
        hibernateTemplate.update(user);
        //将hibernate一级缓存中的内容刷新到数据
        hibernateTemplate.flush();//③请看下文的分析
    }

     public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/mixdao/applicationContext.xml");
        UserService service = (UserService) ctx.getBean("userService");
        JdbcTemplate jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
        //插入一条记录,初始分数为10
        jdbcTemplate.execute("INSERT INTO t_user(user_name,password,score,last_logon_time) VALUES('tom','123456',10,"+System.currentTimeMillis()+")");

        //调用工作在无事务环境下的服务类方法,将分数添加20分
        System.out.println("before userService.logon()..");
        service.logon("tom");
        System.out.println("after userService.logon()..");
         
        int score = jdbcTemplate.queryForObject("SELECT score FROM t_user WHERE user_name ='tom'", Integer.class);
        System.out.println("score:"+score);
        jdbcTemplate.execute("DELETE FROM t_user WHERE user_name='tom'");
    }
}
 <context:component-scan base-package="com.smart.mixdao"/>
    <context:property-placeholder location="classpath:jdbc.properties"/>
    
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close" 
        p:driverClassName="${jdbc.driverClassName}"
        p:url="${jdbc.url}" 
        p:username="${jdbc.username}"
        p:password="${jdbc.password}"/>

    <bean id="jdbcTemplate"
          class="org.springframework.jdbc.core.JdbcTemplate"
          p:dataSource-ref="dataSource"/>

    <bean id="sessionFactory"
          class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"
          p:dataSource-ref="dataSource">
        <property name="annotatedClasses">
            <list>
                <value>com.smart.User</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.Oracle10gDialect</prop>
                <prop key="hibernate.show_sql">true</prop>
            </props>
        </property>
    </bean>

    <bean id="hibernateTemplate"
          class="org.springframework.orm.hibernate4.HibernateTemplate"
          p:sessionFactory-ref="sessionFactory"/>

<!--使用Hibernate事务管理器-->
    <bean id="hiberManager"
          class="org.springframework.orm.hibernate4.HibernateTransactionManager"
          p:sessionFactory-ref="sessionFactory"/>

<!--使用UserServie及ScoreService共用方法都拥有事务-->
    <aop:config proxy-target-class="true">
        <aop:pointcut id="serviceJdbcMethod"
                      expression="within(com.smart.mixdao.BaseService+)"/>
        <aop:advisor pointcut-ref="serviceJdbcMethod"
                     advice-ref="hiberAdvice"/>
    </aop:config>
    <tx:advice id="hiberAdvice" transaction-manager="hiberManager">
        <tx:attributes>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>
    

在1处使用Hibernate技术操作数据库,而在2处调用addScore()方法,该方法内部使用Spring JDBC技术操作数据库。3处显示调用了flush方法(),将Session中的缓存同步到数据库中(马上向数据库发送一条更新记录的SQL语句)。之所以要显示执行flush()方法,是因为在默认情况下,Hibernate对数据的更新只记录在一级缓存中,要等到事务提交后者显示调用flush()方法时才会将一级缓存中的数据同步到数据库中。

6.特殊方法成漏网之鱼

6.1.哪些方法补不能实施Spring AOP事务

由于Spring事务管理是基于接口代理或动态字节码技术,通过AOP实施事务增强的。对于接口动态代理的AOP事务增强来说,由于接口的方法都必须是public的,这就要求实现类的实现方法也必须是public的,同时不能使用static修饰符。基于CGLib字节码动态代理方案是通过扩展增强类,动态创建其子类的方式进行AOP增强织入的。由于使用final、static、private修饰符的方法都不能被子类覆盖,相应地这些方法将无法实施AOP增强。

6.2.事务增强遗漏实例

文档中用到的代码项目地址

@Service("userService")
public class UserService implements UserServiceInterface{
   //1、private方法因访问权限的限制,无法被子类覆盖
    private void method1() {
        System.out.println("in method1");
    }
   //2、final方法无法被子类覆盖
    public final void method2() {
        System.out.println("in method2");
    }
   //3、static 是类级别的方法,无法被子类覆盖
    public static void method3() {
        System.out.println("in method3");
    }
   //4、public 方法可以被子类覆盖,因此可以被动态字节码增强
    public void method4() {
        System.out.println("in method4");
    }
   //5、final方法不能被子类覆盖
    public final void method5() {
        System.out.println("in method5");
    }
   //6、protected方法可以被子类覆盖,因此可以被动态字节码增强
    protected void method6(){
        System.out.println("in method6");
    }

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/special/applicationContext.xml");
        UserService service = (UserService) ctx.getBean("userService");

        System.out.println("before method1");
        service.method1();
        System.out.println("after method1");

        System.out.println("before method2");
        service.method2();
        System.out.println("after method2");

        System.out.println("before method3");
        service.method3();
        System.out.println("after method3");

        System.out.println("before method4");
        service.method4();
        System.out.println("after method4");

        System.out.println("before method5");
        service.method5();
        System.out.println("after method5");

        System.out.println("before method6");
        service.method6();
        System.out.println("after method6");

        //基于接口的动态代理
//        UserServiceInterface service = (UserServiceInterface) ctx.getBean("userService");
//        System.out.println("before method4");
//        service.method4();
//        System.out.println("after method4");
//
//        System.out.println("before method5");
//        service.method5();
//        System.out.println("after method5");
    }
}



这些不能被Spring事务增强的特殊方法并非就不工作在事务环境中,只要他们被外层的事务方法调用了,由于Spring事务管理的传播级别,内部方法也可以工作工作在外部方法所启动的事务上下文中。这些方法不能启动事务增强,是指这些方法不能启动事务,但是外层方法的事务上下文依旧可以顺利地传播到这些方法中。换句话说,这些无法启动事务的方法被无事务的上下文方法调用,则他们就工作在无事务的上下文中;反之,如果被有事务上下文的方法调用,则它们就工作在事务上下文中

7、数据连接泄漏

7.1、底层连接资源的访问问题

获取被Spring管控的数据连接,Spring提供了两种方法:其一是使用数据资源获取工具类;其二是对数据源(或衍生品,如Hibernate的SessionFactory)进行代理

7.2.Spring JDBC数据连接泄漏

文档中用到的代码项目地址

@Service("jdbcUserService")
public class JdbcUserService {
    private JdbcTemplate jdbcTemplate;

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Transactional
    public void logon(String userName) {
        try {
            Connection conn = jdbcTemplate.getDataSource().getConnection();
//            Connection conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());
            
            String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";
            jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
            Thread.sleep(1000);//②模拟程序代码的执行时间
        } catch (Exception e) {
            e.printStackTrace();
        }

    }


    public static void asynchrLogon(JdbcUserService userService, String userName) {
        UserServiceRunner runner = new UserServiceRunner(userService, userName);
        runner.start();
    }

    public static void sleep(long time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void reportConn(BasicDataSource basicDataSource) {
        System.out.println("连接数[active:idle]-[" +
                       basicDataSource.getNumActive()+":"+basicDataSource.getNumIdle()+"]");
    }

    private static class UserServiceRunner extends Thread {
        private JdbcUserService userService;
        private String userName;

        public UserServiceRunner(JdbcUserService userService, String userName) {
            this.userService = userService;
            this.userName = userName;
        }

        public void run() {
            userService.logon(userName);
        }
    }


    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/connleak/applicatonContext.xml");
        JdbcUserService userService = (JdbcUserService) ctx.getBean("jdbcUserService");

        BasicDataSource basicDataSource = (BasicDataSource) ctx.getBean("dataSource");
        JdbcUserService.reportConn(basicDataSource);
        
        JdbcUserService.asynchrLogon(userService, "tom");
        JdbcUserService.sleep(500);
        JdbcUserService.reportConn(basicDataSource);


        JdbcUserService.sleep(2000);
        JdbcUserService.reportConn(basicDataSource);


        JdbcUserService.asynchrLogon(userService, "john");
        JdbcUserService.sleep(500);
        JdbcUserService.reportConn(basicDataSource);


        JdbcUserService.sleep(2000);
        JdbcUserService.reportConn(basicDataSource);

    }
}
7.3.事务环境下通过DataSourceUtils获取数据连接

Spring 提供了一个能从当前事务上下文中获取绑定的数据连接的工具类,即DataSourceUtils。Spring强调必须使用DataSourceUtils获取数据连接,Spring的JdbcTemplate内部也是通过DataSourceUtils来获取连接的。DataSourceUtils提供了若干获取和释放数据连接的静态方法,

  • static Connection doGetConnection(DataSource dataSource):首先尝试从事务上
    下文中获取连接,失败后再从数据源获取连接
  • static Connection getConnection(DataSource dataSource):和doGetConnection()
    方法的功能一样,实际上,其内部就是通过调用doGetConnection()方法获取连接的
  • static void doReleaseConnection(Connection conn,DataSource dataSource):释
    放连接,放回连接池中
  • static void releaseConnection(Connection conn,DataSource dataSource):和doR
    eleaseConnection()方法的功能一样,实际上,其内部就是通过调用doReleaseConnection()方法获取连接的
    文档中用到的代码项目地址
@Service("jdbcUserService")
public class JdbcUserService {
    private JdbcTemplate jdbcTemplate;

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Transactional
    public void logon(String userName) {
        try {
//            Connection conn = jdbcTemplate.getDataSource().getConnection();
            Connection conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());
            
            String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";
            jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
            Thread.sleep(1000);//②模拟程序代码的执行时间
        } catch (Exception e) {
            e.printStackTrace();
        }

    }


    public static void asynchrLogon(JdbcUserService userService, String userName) {
        UserServiceRunner runner = new UserServiceRunner(userService, userName);
        runner.start();
    }

    public static void sleep(long time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void reportConn(BasicDataSource basicDataSource) {
        System.out.println("连接数[active:idle]-[" +
                       basicDataSource.getNumActive()+":"+basicDataSource.getNumIdle()+"]");
    }

    private static class UserServiceRunner extends Thread {
        private JdbcUserService userService;
        private String userName;

        public UserServiceRunner(JdbcUserService userService, String userName) {
            this.userService = userService;
            this.userName = userName;
        }

        public void run() {
            userService.logon(userName);
        }
    }


    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/connleak/applicatonContext.xml");
        JdbcUserService userService = (JdbcUserService) ctx.getBean("jdbcUserService");

        BasicDataSource basicDataSource = (BasicDataSource) ctx.getBean("dataSource");
        JdbcUserService.reportConn(basicDataSource);
        
        JdbcUserService.asynchrLogon(userService, "tom");
        JdbcUserService.sleep(500);
        JdbcUserService.reportConn(basicDataSource);


        JdbcUserService.sleep(2000);
        JdbcUserService.reportConn(basicDataSource);


        JdbcUserService.asynchrLogon(userService, "john");
        JdbcUserService.sleep(500);
        JdbcUserService.reportConn(basicDataSource);


        JdbcUserService.sleep(2000);
        JdbcUserService.reportConn(basicDataSource);

    }
}
7.4.无事务环境下通过DataSourceUtils获取数据连接

如果DataSourceUtils在没有事务上下文的方法中使用getConnection()方法连接,那么依然会造成数据连接泄漏,需要按照下面所示显示释放掉连接
文档中用到的代码项目地址

 @Transactional
    public void logon(String userName) {
        Connection conn =null;
        try {
//            Connection conn = jdbcTemplate.getDataSource().getConnection();
            conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());
            
            String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";
            jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
            Thread.sleep(1000);//②模拟程序代码的执行时间
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            //显示使用DataSourceUtils释放连接
            DataSourceUtils.releaseConnection(conn,jdbcTemplate.getDataSource());
        }

    }
7.5.JdbcTemplate如何做到对连接泄漏的免疫

通过分析JdbcTemplate的代码,可以清楚的看到它开放的每个数据库操作的方法:首先使用DataSourceUtils获取连接,然后在方法返回之前使用DataSourceUtils释放连接
文档中用到的代码项目地址

public <T> T execute(StatementCallback<T> action) throws DataAccessException {
        Assert.notNull(action, "Callback object must not be null");
        //获取连接
        Connection con = DataSourceUtils.getConnection(this.getDataSource());
        Statement stmt = null;

        Object var7;
        try {
            Connection conToUse = con;
            if (this.nativeJdbcExtractor != null && this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
                conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
            }

            stmt = conToUse.createStatement();
            this.applyStatementSettings(stmt);
            Statement stmtToUse = stmt;
            if (this.nativeJdbcExtractor != null) {
                stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
            }

            T result = action.doInStatement(stmtToUse);
            this.handleWarnings(stmt);
            var7 = result;
        } catch (SQLException var11) {
            JdbcUtils.closeStatement(stmt);
            stmt = null;
            DataSourceUtils.releaseConnection(con, this.getDataSource());
            con = null;
            throw this.getExceptionTranslator().translate("StatementCallback", getSql(action), var11);
        } finally {
            JdbcUtils.closeStatement(stmt);
            //显示的关闭Jdbc连接
            DataSourceUtils.releaseConnection(con, this.getDataSource());
        }

        return var7;
    }
7.6.使用TransactionAwareDataSourceProxy

如果不得以要显示获取数据连接,除了可以使用DataSourceUtils获取事务上下文绑定的连接,还可以通过TransactionAwareDataSourceProxy对数据源进行代理。数据源对象被代理后就具有事务上下文感知的能力,通过代理数据源的getConnection()方法获取连接和使用DataSourceUtils.getConnection()方法获取连接是一样的
文档中用到的代码项目地址

<context:component-scan base-package="smart.connleak"/>
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <bean id="originDataSource" class="org.apache.commons.dbcp.BasicDataSource"
          destroy-method="close"
          p:driverClassName="${jdbc.driverClassName}"
          p:url="${jdbc.url}"
          p:username="${jdbc.username}"
          p:password="${jdbc.password}"/>
    
    <!--<bean id="dataSource"
        class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy">
        <property name="targetDataSource">
            <bean class="org.apache.commons.dbcp.BasicDataSource"
                  destroy-method="close"
                  p:driverClassName="${jdbc.driverClassName}"
                  p:url="${jdbc.url}"
                  p:username="${jdbc.username}"
                  p:password="${jdbc.password}"/>
        </property>
    </bean> -->   
    
    <bean id="dataSource" class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy"
        p:targetDataSource-ref="originDataSource"/>


    <bean id="jdbcTemplate"
          class="org.springframework.jdbc.core.JdbcTemplate"
          p:dataSource-ref="dataSource"/>

    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
          p:dataSource-ref="dataSource"/>         
    <tx:annotation-driven/>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容

  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,801评论 6 342
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,649评论 18 139
  • 5.Spring的事务 通常情况下,J2EE有2种事务管理方式:全局事务和本地事务,2种事务都比较明显的缺陷。 全...
    FTOLsXD阅读 1,504评论 0 8
  • 很多人喜欢这篇文章,特此同步过来 由浅入深谈论spring事务 前言 这篇其实也要归纳到《常识》系列中,但这重点又...
    码农戏码阅读 4,729评论 2 59
  • 这是某公司2017前端实习招聘的一道编程测试题——用JS实现一个对话框,水平垂直居中,有半透明遮罩层效果。自己研究...
    剪影Boy阅读 1,809评论 1 6