【问题标题】:Java 7 try-with-resources using spring transactions results in connection closed when committing使用spring事务的Java 7 try-with-resources导致提交时连接关闭
【发布时间】:2014-12-24 08:44:39
【问题描述】:

我正在尝试将一些现有的 Java 6 代码升级到 Java 7,在处理由 Spring 事务管理的 JDBC 连接时,我遇到了新的 try-with-resources 语法问题。数据库连接由 Oracle UCP 管理,并使用 Spring 提供的 DataSourceUtils 类进行检索。但是,为了为这个问题创建一个简单的示例,我使用的是 HSQLDB 2.3.2 和 Apache commons-dbcp 2.0.1。两种设置都会出现同样的问题。 SpringTransactionInterceptor提交事务时发生异常:

org.springframework.transaction.TransactionSystemException: Could not commit JDBC transaction; nested exception is java.sql.SQLException: Connection is null.
    at org.springframework.jdbc.datasource.DataSourceTransactionManager.doCommit(DataSourceTransactionManager.java:272)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:757)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:726)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:496)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:276)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:653)
    at com.creaseol.dao.SampleDao$$EnhancerBySpringCGLIB$$45ec8efa.putWithTryWithResources(<generated>)
    at com.creaseol.TestTryCatch.testWithResources(TestTryCatch.java:69)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:72)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:81)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:216)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:82)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:60)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:67)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:162)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
Caused by: java.sql.SQLException: Connection is null.
    at org.apache.commons.dbcp2.DelegatingConnection.checkOpen(DelegatingConnection.java:608)
    at org.apache.commons.dbcp2.DelegatingConnection.commit(DelegatingConnection.java:362)
    at org.springframework.jdbc.datasource.DataSourceTransactionManager.doCommit(DataSourceTransactionManager.java:269)
    ... 40 more

我创建了一个简单的 Maven 项目来显示该问题。所需的四个文件如下。我有一个 DAO,它有以下两个用 @Transactional 注释的方法:

protected Connection getConnection() {
    return DataSourceUtils.getConnection(m_ds);
}

@Transactional
public void putWithNormalTryCatch(int id) {
    Connection conn = null;
    CallableStatement statement = null;
    try {
        conn = getConnection();
        statement = conn.prepareCall(String.format("INSERT INTO test VALUES (%s)", id));
        statement.execute();
    }
    catch (SQLException sql) {
        throw new RuntimeException("failed during insert",sql);
    }
    finally {
        if (statement != null){
            try {
                statement.close();
            }
            catch (SQLException sqle) {
                System.err.println("Failed to close statement after execution. "+ sqle.getMessage());
            }
        }
        if (conn != null) {
            DataSourceUtils.releaseConnection(conn, m_ds);
        }
    }
}

@Transactional
public void putWithTryWithResources(int id) {
    try (Connection conn = getConnection();
         CallableStatement statement = conn.prepareCall(String.format("INSERT INTO test VALUES (%s)", id)))
    {
        statement.execute();
    }
    catch (SQLException sql) {
        throw new RuntimeException("failed during insert",sql);
    }
}

第一种方法putWithNormalTryCatch 使用旧式try catch 块,putWithTryWithResources 使用Java 7 try-with-resources。请注意,使用DataSourceUtils.getConnection 检索连接并使用DataSourceUtils.releaseConnection 释放它。

我认为问题在于 try-with-resources 实际上是在连接上调用关闭,而不是将其释放到池中。你真正想要的是在连接上调用close() 调用DataSourceUtils.releaseConnection

我可以简单地将 DataSource 包装在 TransactionAwareDataSourceProxy 中,但根据文档,这不是必需的。有更好的方法吗?


完整代码

DAO

package com.creaseol.dao;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.transaction.annotation.Transactional;

public class SampleDao {

    private final DataSource m_ds;

    public SampleDao(DataSource ds) {
        m_ds = ds;
    }

    protected Connection getConnection() {
        return DataSourceUtils.getConnection(m_ds);
    }

    @Transactional
    public void putWithNormalTryCatch(int id) {
        Connection conn = null;
        CallableStatement statement = null;
        try {
            conn = getConnection();
            statement = conn.prepareCall(String.format("INSERT INTO test VALUES (%s)", id));
            statement.execute();
        }
        catch (SQLException sql) {
            throw new RuntimeException("failed during insert",sql);
        }
        finally {
            if (statement != null){
                try {
                    statement.close();
                }
                catch (SQLException sqle) {
                    System.err.println("Failed to close statement after execution. "+ sqle.getMessage());
                }
            }
            if (conn != null) {
                DataSourceUtils.releaseConnection(conn, m_ds);
            }
        }
    }

    @Transactional
    public void putWithTryWithResources(int id) {
        try (Connection conn = getConnection();
             CallableStatement statement = conn.prepareCall(String.format("INSERT INTO test VALUES (%s)", id)))
        {
            statement.execute();
        }
        catch (SQLException sql) {
            throw new RuntimeException("failed during insert",sql);
        }
    }

    public List<Integer> getValues() {
        try (Connection conn = getConnection();
             CallableStatement statement = conn.prepareCall("SELECT id FROM test"))
        {
            statement.execute();
            try (ResultSet rs = statement.getResultSet()) {
                List<Integer> values = new ArrayList<Integer>();
                while(rs.next()){
                    values.add(rs.getInt(1));
                }
                return values;
            }
        }
        catch (SQLException sql) {
            throw new RuntimeException("failed during insert",sql);
        }
    }
}

弹簧上下文

<?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: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/tx    
            http://www.springframework.org/schema/tx/spring-tx.xsd">

    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"
        destroy-method="close">
        <property name="driverClassName" value="org.hsqldb.jdbc.JDBCDriver" />
        <property name="url" value="jdbc:hsqldb:mem:testdb" />
    </bean>

    <bean id="sampleDao" class="com.creaseol.dao.SampleDao">
        <constructor-arg ref="dataSource"/>
    </bean> 

    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager" />
</beans>

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.creaseol</groupId>
    <artifactId>SpringDBJava7Test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <spring.version>4.1.1.RELEASE</spring.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-dbcp2</artifactId>
            <version>2.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!-- TEST DEPENDENCIES -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <!-- our in-memory database provider -->
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <version>2.3.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

测试类

package com.creaseol;

import static org.junit.Assert.assertEquals;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.creaseol.dao.SampleDao;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:test-context.xml")
public class TestTryCatch {

    @Autowired
    private DataSource dataSource;

    @Autowired
    protected SampleDao m_dao;

    protected Connection getConnection(){
        return DataSourceUtils.getConnection(dataSource);
    }

    @Before
    public void setup() throws SQLException{
        Connection conn = getConnection();
        CallableStatement statement = conn.prepareCall("CREATE TABLE test (id INTEGER)");
        statement.execute();
        statement.close();
        close(conn);
    }

    protected void close(Connection conn) {
        if (conn != null){
            DataSourceUtils.releaseConnection(conn, dataSource);
        }
    }

    @After
    public void tearDown() throws SQLException {
        Connection conn = getConnection();
        CallableStatement statement = conn.prepareCall("DROP TABLE test");
        statement.execute();
        statement.close();
        close(conn);
    }

    @Test
    public void testVanilla(){
        m_dao.putWithNormalTryCatch(42);
        assertEquals(1,m_dao.getValues().size());
        assertEquals(42,(int)m_dao.getValues().get(0));
    }

    @Test
    public void testWithResources(){
        m_dao.putWithTryWithResources(100);
        assertEquals(1,m_dao.getValues().size());
        assertEquals(100,(int)m_dao.getValues().get(0));
    }
}

【问题讨论】:

  • 如果你不想隐式调用close,你不应该使用try-with-resources。另见MCVE
  • 当然是在连接上调用close()。这就是它的用途。
  • 我知道它在每个资源上调用close(),但是对于 Spring 管理的事务,在提交事务(可能包括其他操作)之前,不应将连接返回到池中。 try-with-resources 完全符合我的预期,但我认为Spring 有办法很好地使用它(例如,连接对象被包装在调用DataSourceUtils.releaseConnection 的代理中)。我想知道的是内置机制来执行此操作,而不是他们建议不要使用的TransactionAwareDataSourceProxy

标签: java spring jdbc spring-jdbc spring-transactions


【解决方案1】:

如果您有一些资源要与 try-with-resources 语句一起使用,但它不是 AutoCloseable(例如,您希望在块末尾解锁的 java.util.concurrent.locks.ReentrantLock ),或者您不想关闭而是执行一些其他操作,例如您想要释放回池的数据库连接,您可以将其包装在一个包装器中,该包装器调用您想要的任何操作已经执行而不是close()

public class ReleasableConnection implements AutoCloseable {
    private final Connection connection;
    private final DataSource dataSource;
    private boolean released;

    public ReleaseableConnection(Connection connection, DataSource dataSource) {
        this.connection = connection;
        this.dataSource = dataSource;
        released = false;
    }

    public Connection getConnection() {
        return connection;
    }

    @Override
    public void close() {
        if (!released) {
            DataSourceUtils.releaseConnection(connection, dataSource);
            released = true; // make it idempotent
        }
    }
}

然后你可以在 try-with-resources 块中使用它,如下所示:

try (ReleasableConnection rc = new ReleasableConnection(getConnection(), m_ds)) {
    Connection conn = rc.getConnection();
    // do something with the connection
    // it will be released automatically
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-08-19
    • 2013-06-30
    • 2017-12-04
    • 1970-01-01
    • 2020-01-01
    • 2019-03-12
    • 2013-07-13
    相关资源
    最近更新 更多