背景:
- AndDB和GoldenDB, 原G库提供的是Mysql版本数据库,A库为PG版本数据库
- G方存在严重可靠性问题, 扯皮大半年, 现在提供一个PG版本和新驱动
- 系统使用Kettle执行多库ETL及规整业务操作
问题:
引入新驱动后, 原AntDB方法异常
经简单排查, 定位为连接串中?grammar=oracle未生效, 导致nvl无法执行
解决:
- 首先怀疑是Driver类存在路径冲突, 易排查得知A方为org.postgresql.Driver, G方为com.goldendb.Driver.
- 但是当时死马当活马医, 还是
- (1)重写了一份自定义类加载器用于加载各自的驱动类, 重写Kettle源码中Class.forName加载驱动的部分代码
- (2)然后验证是否可以执行连接A库的Kettle脚本正常使用Oracle语法
- Result: 结论是还是不行
- Result: 专业的人要敢于下判断, 有可信经验却还明知不太可能还去试, 这不是第一次了! 该罚
- 加载器放在最后贴
- DEBUG kettle源码中实际获取到的驱动的地方, 确定获取到的Connection为com.goldendb.PgConnection, 不符合预期org.postgresql.PgConnnection
- Kettle使用DriverManager.getConnection方法获取连接, 检查源码逻辑:
- 其逻辑为:遍历注册的全部驱动, try创建链接, 不为null就算成功直接返回
- 经DEBUG, G库驱动排在前面, 居然还连接上A库服务端了, 离谱
- 划重点!!!!Class.forName()只是保证驱动类初始化被加载/静态方法注册什么什么的, 实际获取连接就是遍历的
JDBC 4.0 的规范规定,所有 JDBC 4.0 的驱动 jar 文件必须包含一个 java.sql.Driver,它位于 jar 文件的 META-INF/services 目录下。这个文件里每一行便描述了一个对应的驱动类
在启动项目或是服务时,会判断当前classspath中的所的jar包,并检查他们META-INF目录下,是否包含services文件夹,如果包含,就会将里面的配置加载成相应的服务。
最终DEBUG结果为, G库和A库的驱动乱了套了, 注册顺序是 G排在A前面
正常情况下,如果数据库仅接收使用自己的驱动的连接,那么G即使排在第一个, 也会conn==null, 进而遍历到A的驱动创建连接并返回
但问题是这俩国产数据库, 都是套的PG, 居然就驱动和服务端驴唇不对马嘴的连上了, 但G并没有处理设置语法的那部分连接串?grammar=oracle, 导致A库服务端没有按照Oracle语法提供服务
- 最终解决方案
土就是好, 好就是土
(1) kettle指定使用自定义驱动, 其中重点为静态代码块中, 强制将自身置为第0个驱动
package org.goldendb;
import cn.hutool.core.util.ReflectUtil;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;
;
/**
* @author zhengxy.
* @date 2024/10/9 15:50
*/
public class ExpectAbleDriver extends org.goldendb.Driver {
public static final ThreadLocal<Boolean> expectGoldenDBDriver = new ThreadLocal<>();
static {
try {
List<Object> registeredDrivers = (List<Object>) ReflectUtil.getStaticFieldValue(ReflectUtil.getField(DriverManager.class, "registeredDrivers"));
//注册进去
java.sql.DriverManager.registerDriver(new ExpectAbleDriver());
//加到第1个位置
registeredDrivers.add(0, registeredDrivers.get(registeredDrivers.size() - 1));
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
public ExpectAbleDriver() throws SQLException {
super();
}
@Override
public Connection connect(String url, Properties info) throws SQLException {
Boolean expectMe = expectGoldenDBDriver.get();
if (Boolean.TRUE.equals(expectMe)) {
return super.connect(url, info);
} else {
return null;
}
}
}
(2)重写kettle源码, 根据类名判断是否期望使用G库驱动
如果不期望, 那么上面的自定义驱动直接返回null-->进而DriverManager中遍历continue
ExpectAbleDriver.expectGoldenDBDriver.set(classname.contains("goldendb"));
6. 收工!
总结:
- 数据库选型应早期调研驱动兼容情况, 并要求驱动仅握手成功本厂商的服务端, 以早暴露问题
- 有可置信逻辑分析与案例证明的情况下, 没必要死马当活马医