Hibernate与Tomcat JDBC连接池:Connection已关闭异常排查与解决请求
运行在Tomcat 9上的Web应用,使用Hibernate、PostgreSQL及Tomcat JDBC连接池,应用处于autocommit=false、CURRENT_SESSION_CONTEXT_CLASS=thread模式。
Tomcat数据源配置
<Resource name="xxx" auth="Container" type="javax.sql.DataSource" driverClassName="org.postgresql.Driver" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" url="jdbc:postgresql://localhost:5432/db" username="xxx" password="xxx" maxActive="200" maxIdle="50" minIdle="30" defaultAutoCommit="false" jmxEnabled="false" jdbcInterceptors="org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport" removeAbandoned="true" removeAbandonedTimeout="30" logAbandoned="true" suspectTimeout="20" validationQuery="SELECT 1" testWhileIdle="true" timeBetweenEvictionRunsMillis="5000" minEvictableIdleTimeMillis="60000" testOnBorrow="true" validationInterval="10000" closeMethod="close" />
Hibernate SessionFactory配置
ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().applySettings(...) .applySetting( AvailableSettings.TRANSACTION_COORDINATOR_STRATEGY, "jdbc" ) .applySetting( AvailableSettings.CURRENT_SESSION_CONTEXT_CLASS, "thread" ) .applySetting( AvailableSettings.USE_NEW_ID_GENERATOR_MAPPINGS, false ) .applySetting( AvailableSettings.DIALECT, "org.hibernate.dialect.PostgreSQL10Dialect" ) .applySetting( AvailableSettings.DEFAULT_SCHEMA, "public" ) .applySetting( AvailableSettings.DRIVER, "org.postgresql.Driver" ) .applySetting( "hibernate.temp.use_jdbc_metadata_defaults", false ) .build(); Metadata metadata = new MetadataSources(serviceRegistry) .addAnnotatedClass(…).getMetadataBuilder().build(); sessionFactory = metadata.getSessionFactoryBuilder().build();
数据库操作代码模式
Session s = sessionFactory.getCurrentSession(); s.beginTransaction(); try { … … s.getTransaction().commit(); } catch (Exception x) { x.printStackTrace(); s.getTransaction().rollback(); }
当try-catch中操作超时或因代码bug导致事务泄漏(未执行commit()/rollback())时,Tomcat会将连接标记为“abandoned”并从池中移除,此时Hibernate会大量抛出以下异常:
org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() for servlet [dispatcher] in context with path [] threw exception [Request processing failed; nested exception is org.hibernate.TransactionException: JDBC begin transaction failed: ] with root cause java.sql.SQLException: Connection has already been closed. at org.apache.tomcat.jdbc.pool.ProxyConnection.invoke(ProxyConnection.java:121) at org.apache.tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.java:109) at org.apache.tomcat.jdbc.pool.interceptor.AbstractCreateStatementInterceptor.invoke(AbstractCreateStatementInterceptor.java:79) at org.apache.tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.java:109) at org.apache.tomcat.jdbc.pool.TrapException.invoke(TrapException.java:39) at org.apache.tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.java:109) at org.apache.tomcat.jdbc.pool.interceptor.AbstractCreateStatementInterceptor.invoke(AbstractCreateStatementInterceptor.java:79) at org.apache.tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.java:109) at org.apache.tomcat.jdbc.pool.DisposableConnectionFacade.invoke(DisposableConnectionFacade.java:81) at jdk.proxy2/jdk.proxy2.$Proxy92.setAutoCommit(Unknown Source) at org.hibernate.resource.jdbc.internal.AbstractLogicalConnectionImplementor.begin(AbstractLogicalConnectionImplementor.java:72) at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.begin(LogicalConnectionManagedImpl.java:285) at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.begin(JdbcResourceLocalTransactionCoordinatorImpl.java:246) at org.hibernate.engine.transaction.internal.TransactionImpl.begin(TransactionImpl.java:83) at org.hibernate.internal.AbstractSharedSessionContract.beginTransaction(AbstractSharedSessionContract.java:503) at jdk.internal.reflect.GeneratedMethodAccessor191.invoke(Unknown Source) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at org.hibernate.context.internal.ThreadLocalSessionContext$TransactionProtectionWrapper.invoke(ThreadLocalSessionContext.java:351) at jdk.proxy3/jdk.proxy3.$Proxy91.beginTransaction(Unknown Source)
异常抛出点在Session.beginTransaction()。
问题1:如何强制Hibernate在此场景下从Tomcat JDBC连接池获取新的可用连接,并输出告警日志?
解决方案:
自定义Hibernate连接检查逻辑
在调用beginTransaction()前,主动检查当前Session关联的连接有效性,若连接已关闭则销毁旧Session并重新获取,同时输出告警日志:Session s = sessionFactory.getCurrentSession(); try { // 验证连接有效性 if (!s.getJdbcConnectionAccess().obtainConnection().isValid(0)) { s.close(); s = sessionFactory.getCurrentSession(); LOGGER.warn("检测到废弃连接,已重新初始化Session和连接"); } s.beginTransaction(); // 业务逻辑执行 s.getTransaction().commit(); } catch (SQLException | HibernateException e) { if (s.getTransaction().isActive()) { s.getTransaction().rollback(); } LOGGER.error("事务执行失败", e); }注:
isValid(0)中的0表示使用驱动默认超时时间,可根据实际调整。扩展Tomcat连接池拦截器
继承org.apache.tomcat.jdbc.pool.JdbcInterceptor实现自定义拦截器,在invoke()方法中捕获Connection has already been closed异常,自动重新获取连接并记录告警:public class ReconnectInterceptor extends JdbcInterceptor { private static final Logger LOGGER = LoggerFactory.getLogger(ReconnectInterceptor.class); @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { return super.invoke(proxy, method, args); } catch (SQLException e) { if ("Connection has already been closed.".equals(e.getMessage())) { ConnectionPool pool = getPool(); Connection newConn = pool.borrowConnection(); LOGGER.warn("废弃连接已替换为新连接"); return method.invoke(newConn, args); } throw e; } } }然后在Tomcat数据源配置中添加该拦截器:
jdbcInterceptors="org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport;com.yourpackage.ReconnectInterceptor"启用Hibernate连接验证配置
在Hibernate配置中添加:hibernate.connection.test_on_borrow=true hibernate.connection.validation_query=SELECT 1让Hibernate在获取连接时自动验证有效性,失效则从连接池重新获取,配合Tomcat连接池的
testOnBorrow=true配置,形成双重验证。
问题2:采用哪些方法可以定位代码中未提交事务的bug?
定位方法:
利用Tomcat连接池的废弃连接日志
当前配置已开启logAbandoned="true",Tomcat会在标记连接为废弃时,打印该连接的完整调用栈。直接在日志中搜索AbandonedConnection关键词,即可定位到未正确提交/回滚事务的代码路径。添加事务监控拦截器
自定义拦截器(如SpringMethodInterceptor),在事务结束后检查状态,若发现未提交/回滚的事务则打印告警和调用栈:public Object invoke(MethodInvocation invocation) throws Throwable { Session s = sessionFactory.getCurrentSession(); Transaction tx = null; boolean txStarted = false; try { tx = s.beginTransaction(); txStarted = true; Object result = invocation.proceed(); tx.commit(); return result; } catch (Exception e) { if (txStarted && tx.isActive()) { tx.rollback(); } throw e; } finally { if (txStarted && tx.isActive()) { LOGGER.error("检测到未提交事务,调用栈:", new Exception()); } } }开启Hibernate统计功能
配置hibernate.generate_statistics=true,通过JMX或日志查看事务统计数据,对比事务启动数与提交/回滚数,找到异常差值对应的业务模块,再结合日志定位问题代码。静态代码分析
使用SonarQube等工具扫描代码,识别事务泄漏风险点:比如try块中启动事务,但catch块未处理回滚、分支路径跳过commit()/rollback()等情况。运行时动态追踪
使用Arthas等Java诊断工具,在生产环境中动态追踪Session.beginTransaction()、commit()、rollback()方法的调用,统计每个方法的调用次数,定位只调用beginTransaction()但无后续提交/回滚的线程和代码路径。
内容的提问来源于stack exchange,提问作者Jakub Królikowski

