如何避免Liferay重新部署模块时关闭外部JNDI HikariCP连接池?
问题场景
我们基于Liferay 7.1.3-ga4开发Service Builder模块,通过Tomcat JNDI配置的HikariCP连接池访问外部数据库。服务器首次启动时连接正常,但热部署模块后,Liferay会在卸载阶段调用HikariDataSource#close关闭连接池,导致后续数据库操作报错“HikariDataSource已关闭”,只能重启Tomcat才能恢复,严重影响开发效率和CI部署流程。
已按官方文档完成的配置:
- 将
hikaricp.jar和slf4j-api.jar放置到Tomcat的lib/ext目录 - 在
server.xml中配置HikariJNDIFactory的Resource - 在
context.xml中添加ResourceLink关联JNDI资源 - 模块通过
ext-spring.xml配置DataSource Bean,service.xml关联实体与该数据源
问题根源
热部署时,Liferay的com.liferay.portal.dao.jdbc.DataSourceFactoryImpl会检查数据源类型:如果是HikariDataSource,则直接调用其close方法关闭连接池;而Tomcat DBCP的数据源因类型不匹配,不会触发该逻辑。由于我们的HikariCP是Tomcat全局管理的JNDI资源,被Liferay模块意外关闭后,后续无法重新初始化。
解决方案
方案一:自定义数据源包装类(推荐)
通过包装类隐藏HikariDataSource的真实类型,让Liferay无法识别并触发关闭逻辑。
- 编写包装类,实现
javax.sql.DataSource接口,所有方法委托给内部的HikariDataSource实例:
package com.yourcompany.util; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class NonClosableHikariWrapper implements DataSource { private final DataSource delegate; public NonClosableHikariWrapper(DataSource delegate) { this.delegate = delegate; } @Override public Connection getConnection() throws SQLException { return delegate.getConnection(); } @Override public Connection getConnection(String username, String password) throws SQLException { return delegate.getConnection(username, password); } @Override public <T> T unwrap(Class<T> iface) throws SQLException { return delegate.unwrap(iface); } @Override public boolean isWrapperFor(Class<?> iface) throws SQLException { return delegate.isWrapperFor(iface); } // 重写close方法(若Liferay通过反射调用,空实现即可) public void close() {} }
- 修改模块的
ext-spring.xml,将JNDI获取的数据源包装成该类的实例:
<bean id="externalDataSource" class="com.yourcompany.util.NonClosableHikariWrapper"> <constructor-arg> <jee:jndi-lookup jndi-name="java:comp/env/jdbc/YourExternalDB" /> </constructor-arg> </bean>
方案二:覆盖Liferay核心DataSourceFactory类
通过OSGi fragment模块修改DataSourceFactoryImpl的逻辑,跳过对外部JNDI数据源的关闭操作。
- 自定义
DataSourceFactoryImpl,修改destroyDataSource方法:
package com.yourcompany.override; import com.liferay.portal.dao.jdbc.DataSourceFactoryImpl; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; public class CustomDataSourceFactoryImpl extends DataSourceFactoryImpl { @Override public void destroyDataSource(DataSource dataSource) { if (dataSource instanceof HikariDataSource) { // 在这里添加判断逻辑,识别是否为外部JNDI数据源 // 例如通过数据源名称、自定义属性等方式判断 boolean isExternalJndiDS = isExternalJndiDataSource(dataSource); if (!isExternalJndiDS) { super.destroyDataSource(dataSource); } } else { super.destroyDataSource(dataSource); } } private boolean isExternalJndiDataSource(DataSource dataSource) { // 实现自定义判断逻辑,比如获取数据源名称匹配外部DB标识 HikariDataSource hikariDS = (HikariDataSource) dataSource; return hikariDS.getPoolName().startsWith("ExternalDB-"); } }
- 编写
bnd.bnd文件,将该类作为fragment覆盖Liferay核心模块:
Bundle-SymbolicName: com.yourcompany.datasource.factory.override Bundle-Version: 1.0.0 Fragment-Host: com.liferay.portal.dao.jdbc;bundle-version="[7.1.3,7.2)" Liferay-Require-SchemaVersion: 1
方案三:使用Tomcat数据源代理
在Tomcat中配置DataSourceProxy包装HikariCP数据源,让Liferay识别到的是代理类而非HikariDataSource。
修改Tomcat的context.xml,添加代理后的资源链接:
<ResourceLink name="jdbc/ExternalDBProxy" global="jdbc/YourExternalDB" type="javax.sql.DataSource" />
然后在模块的ext-spring.xml中引用这个代理后的JNDI名称:
<jee:jndi-lookup id="externalDataSource" jndi-name="java:comp/env/jdbc/ExternalDBProxy" />
总结
推荐使用方案一,无需修改Liferay核心代码,兼容性强,后续升级Liferay版本时改动成本低。
内容的提问来源于stack exchange,提问作者shawmanz32na

