[JAVA][国产化奇奇怪怪的问题]事后谈-数据库驱动混乱/未按预期连接服务端

451 阅读3分钟

背景:

  • AndDB和GoldenDB, 原G库提供的是Mysql版本数据库,A库为PG版本数据库
  • G方存在严重可靠性问题, 扯皮大半年, 现在提供一个PG版本和新驱动
  • 系统使用Kettle执行多库ETL及规整业务操作

问题:

引入新驱动后, 原AntDB方法异常

经简单排查, 定位为连接串中?grammar=oracle未生效, 导致nvl无法执行

解决:

  1. 首先怀疑是Driver类存在路径冲突, 易排查得知A方为org.postgresql.Driver, G方为com.goldendb.Driver.
  • 但是当时死马当活马医, 还是
    • (1)重写了一份自定义类加载器用于加载各自的驱动类, 重写Kettle源码中Class.forName加载驱动的部分代码
    • (2)然后验证是否可以执行连接A库的Kettle脚本正常使用Oracle语法
    • Result: 结论是还是不行
    • Result: 专业的人要敢于下判断, 有可信经验却还明知不太可能还去试, 这不是第一次了! 该罚
image.png
  • 加载器放在最后贴
  1. DEBUG kettle源码中实际获取到的驱动的地方, 确定获取到的Connection为com.goldendb.PgConnection, 不符合预期org.postgresql.PgConnnection

image.png

  1. Kettle使用DriverManager.getConnection方法获取连接, 检查源码逻辑:
  • 其逻辑为:遍历注册的全部驱动, try创建链接, 不为null就算成功直接返回
  • 经DEBUG, G库驱动排在前面, 居然还连接上A库服务端了, 离谱
  • 划重点!!!!Class.forName()只是保证驱动类初始化被加载/静态方法注册什么什么的, 实际获取连接就是遍历的 image.png
  1. 参考JDBC驱动程序注册 JDBC简介(二)-腾讯云开发者社区-腾讯云 (tencent.com)

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. 最终解决方案

土就是好, 好就是土

(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. 收工!

总结:

  1. 数据库选型应早期调研驱动兼容情况, 并要求驱动仅握手成功本厂商的服务端, 以早暴露问题
  2. 有可置信逻辑分析与案例证明的情况下, 没必要死马当活马医