[JAVA][国产化奇奇怪怪的问题]多数据库驱动类全路径冲突

145 阅读4分钟

背景:

  • 选型其实没得选, 集团要求用磐维数据库
  • 磐维为pg底层, 驱动类为 org.postgresql.Driver

问题:

建设末期, 要求接入监控统计功能, 接入现成产品xx指挥中心, 需额外引入AntDB数据源

AntDB和磐维都叫org.postgresql.Driver

救火

解决:

  1. 首先java机制决定了不允许2个类名一致的使用, 如果允许JVM会混乱无法确定希望使用的是哪一个

确认2个路线:

  • (1)反编译,或者磐维提供新驱动,改驱动类路径
  • (2)从常见的通过建路径一致的包与类, 改动其中一部分代码这种改法入手, 分析下 如何实现包名一致的并存

对于(1)来说,可行性是没问题的, 但还有现实因素, 明明是甲方不决策还既要又要, 延期拖到现在, 结果还喜欢叭叭叭怀疑厂商, 另一方从个人来说, 不想走这个方法.

对于(2)来说, 理论可行, 但要尝试去验证, 分析如下

  • 类加载器机制load会先判断是否已加载过
  • 两个名字一样的驱动类会被判定为已加载过, 最终只有一个会生效
  • 数据库驱动的使用大学教的八股文: Class.forName, 然后才创建的实例
  • 只要可以自定义加载, 让两种驱动在加载时, 都强制走读取自己的jar包里的class的逻辑即可

验证步骤:

  • Class.forName支持指定类加载器
  • Druid数据源是否支持个性化加载驱动类? Druid支持设置类加载器, 即使不支持也可以找到创建连接的地方集成重写掉
  • 是否有办法读取手动读取jar包里class的byte码? 百度易得, 有
  1. 最终解决方案---先不考虑性能问题啦~应该也没事

土就是好, 好就是土

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * Antdb jar 用类加载器
 * 只对antdb的包名开头 应用特殊加载
 */
@Slf4j
public class AntDbClassLoader extends ClassLoader {

    //todo 用于对一般类的双亲委派
    public AntDbClassLoader(ClassLoader parent) {
        super(parent);
    }

    //加载类的路径
    private String path = "";

    /**
     * 特殊处理加载类的逻辑
     *
     * @param name
     * @param resolve
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        //todo 非antdb的类, 走双亲委派
        if (!name.startsWith("org.postgresql")) {
            return super.loadClass(name, resolve);
        } else {
            //todo 对于antdb的类,特殊处理
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
//                Class<?> c = findLoadedClass(name);
                //todo 破坏: 总是认为没加载加载
                Class<?> c = null;
                if (c == null) {
                    long t0 = System.nanoTime();
                    //todo 破坏: 不尝试委派双亲
//                    try {
//                        if (parent != null) {
//                            c = parent.loadClass(name, false);
//                        } else {
//                            c = findBootstrapClassOrNull(name);
//                        }
//                    } catch (ClassNotFoundException e) {
//                        // ClassNotFoundException thrown if class not found
//                        // from the non-null parent class loader
//                    }

                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        c = findClass(name);

                        // this is the defining class loader; record the stats
                        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        sun.misc.PerfCounter.getFindClasses().increment();
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    }

    /**
     * 重写findClass,调用defineClass,将代表类的字节码数组转换为Class对象
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] dataByte = new byte[0];
        try {
            //todo Driver作为入口亲属 应该只要破坏这个加载了就行
            if (name.startsWith("org.postgresql")) {
                dataByte = ClassDataByByte$AntDbDriver(name);
                return this.defineClass(name, dataByte, 0, dataByte.length);
            } else {
                //todo 非antdb的一般类使用按super逻辑双亲委派加载
                return super.loadClass(name);
            }
        } catch (IOException e) {
            log.error(e.getMessage(), e);
            return null;
        }

    }

    //读取Class文件作为二进制流放入byte数组, findClass内部需要加载字节码文件的byte数组
    private byte[] ClassDataByByte$AntDbDriver(String realClassName) throws IOException {
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();

        String os = System.getProperty("os.name").toLowerCase();
        JarFile antDbJar;
        //累了, 就这样吧 8888888
        if (os.toLowerCase().contains("windows")) {
            //if windows local dev
            File sourceFile = new ClassPathResource("load" + File.separator + "antdb-5.0.jar").getFile();
            System.err.println(sourceFile.getAbsolutePath());
            antDbJar = new JarFile(sourceFile);
        } else {
            String tmpPath = this.getClass().getClassLoader().getResource("load").getFile();
            tmpPath = tmpPath.replaceAll("file:", "");
            tmpPath = tmpPath.substring(0, tmpPath.lastIndexOf("!"));
            tmpPath = tmpPath.substring(0, tmpPath.lastIndexOf("/"));
            String path = tmpPath + "/postgresql-42.2.10.jar";
            System.err.println(path);
            File sourceFile = new File(path);
            antDbJar = new JarFile(sourceFile);
        }


        Enumeration<JarEntry> entries = antDbJar.entries();
        JarEntry DriverClass = null;
        while (DriverClass == null && entries.hasMoreElements()) {
            JarEntry entry = entries.nextElement();
            System.err.println("load try " + entry.getName());
            if (entry.getName().equals(realClassName.replaceAll("\.", "/") + ".class")) {
                DriverClass = entry;
                break;
            }
        }
        if (DriverClass == null) {
            throw new IllegalArgumentException("not found " + realClassName + " at resources/load/antdb-5.0.jar");
        }

        is = antDbJar.getInputStream(DriverClass);
        int c = 0;
        while (-1 != (c = is.read())) { //读取class文件,并写入byte数组输出流
            arrayOutputStream.write(c);
        }
        data = arrayOutputStream.toByteArray(); //将输出流中的字节码转换为byte数组

        is.close();
        arrayOutputStream.close();

        return data;
    }


    private void readJAR(JarFile jar) throws IOException {
        Enumeration<JarEntry> en = jar.entries();
        while (en.hasMoreElements()) {
            JarEntry je = en.nextElement();
            String name = je.getName();
            if (name.endsWith(".class")) {
                String clss = name.replace(".class", "").replaceAll("/", ".");
                if (this.findLoadedClass(clss) != null) continue;

                InputStream input = jar.getInputStream(je);
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                int bufferSize = 4096;
                byte[] buffer = new byte[bufferSize];
                int bytesNumRead = 0;
                while ((bytesNumRead = input.read(buffer)) != -1) {
                    baos.write(buffer, 0, bytesNumRead);
                }
                byte[] cc = baos.toByteArray();
                input.close();
//                map.put(clss, cc);//暂时保存下来
            }
        }
    }
}

总结:

  1. 沉淀通用类冲突解法

补充解法:

假设如果druid不是天生支持指定类加载器:

方向1: 就重写dataSource.getConnection,自定义加载器。

方向2: 继承套壳实际Driver,但实质上被继承的org.postgres.Driver还是绕不开加载哪个的问题,我们还是要保证使用自定义类加载器加载driver。

总归:

druid数据源getConnection底层是getDriver.connect,再底层是自己实现的加载Driver实例,没有用标准的DriverManager,我们如果要实现也是一个道路,自己加载,不用标准的DriverManager(因为本身遍历找Driver也挺抽象的)。

如果硬要用DriverManager,那就是重写Driver类中的调用DriverManager注册的逻辑,注册进去的实例我们也还是自定义类加载实现。但何苦呢,你还要解决遍历驱动的乱握手问题。