背景:
- 选型其实没得选, 集团要求用磐维数据库
- 磐维为pg底层, 驱动类为 org.postgresql.Driver
问题:
建设末期, 要求接入监控统计功能, 接入现成产品xx指挥中心, 需额外引入AntDB数据源
AntDB和磐维都叫org.postgresql.Driver
救火
解决:
- 首先java机制决定了不允许2个类名一致的使用, 如果允许JVM会混乱无法确定希望使用的是哪一个
确认2个路线:
- (1)反编译,或者磐维提供新驱动,改驱动类路径
- (2)从常见的通过建路径一致的包与类, 改动其中一部分代码这种改法入手, 分析下 如何实现包名一致的并存
对于(1)来说,可行性是没问题的, 但还有现实因素, 明明是甲方不决策还既要又要, 延期拖到现在, 结果还喜欢叭叭叭怀疑厂商, 另一方从个人来说, 不想走这个方法.
对于(2)来说, 理论可行, 但要尝试去验证, 分析如下
- 类加载器机制load会先判断是否已加载过
- 两个名字一样的驱动类会被判定为已加载过, 最终只有一个会生效
- 数据库驱动的使用大学教的八股文: Class.forName, 然后才创建的实例
- 只要可以自定义加载, 让两种驱动在加载时, 都强制走读取自己的jar包里的class的逻辑即可
验证步骤:
- Class.forName支持指定类加载器
- Druid数据源是否支持个性化加载驱动类? Druid支持设置类加载器, 即使不支持也可以找到创建连接的地方集成重写掉
- 是否有办法读取手动读取jar包里class的byte码? 百度易得, 有
- 最终解决方案---先不考虑性能问题啦~应该也没事
土就是好, 好就是土
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);//暂时保存下来
}
}
}
}
总结:
- 沉淀通用类冲突解法
补充解法:
假设如果druid不是天生支持指定类加载器:
方向1: 就重写dataSource.getConnection,自定义加载器。
方向2: 继承套壳实际Driver,但实质上被继承的org.postgres.Driver还是绕不开加载哪个的问题,我们还是要保证使用自定义类加载器加载driver。
总归:
druid数据源getConnection底层是getDriver.connect,再底层是自己实现的加载Driver实例,没有用标准的DriverManager,我们如果要实现也是一个道路,自己加载,不用标准的DriverManager(因为本身遍历找Driver也挺抽象的)。
如果硬要用DriverManager,那就是重写Driver类中的调用DriverManager注册的逻辑,注册进去的实例我们也还是自定义类加载实现。但何苦呢,你还要解决遍历驱动的乱握手问题。