使用ClassLoader解决依赖冲突
https://www.bilibili.com/video/av68658611
假设我们要引用两个包,两个包里面有一个相同的类,但是版本不同,而这个类是被包里的其他对象依赖的。如果我们要同时使用这两个包里的对象,应该怎么做?
这是两个包的结构:
每个包中都有一个C,而C在A包中返回版本是1.0,在B中返回是2.0
如果我们直接调用的话:
package em;
import com.company.*;
/**
* Created by Frank on 2019/9/22.
*/
public class 一啥也不改报错 {
public static void main(String[] args){
new A().run();
new B().run();
}
}
A是会返回Ok的,但是b会error。原因是:main函数是被AppClassLoader加载进来的,而在main函数中new的对象也会使用该加载器进行加载。在加载完A包中的C后,再次加载B包中的C时,类加载器会发现已经加载了com.company.C,所以直接调用A包中的C。这是常见的依赖问题。
如果我们使用这样的方式呢?
package em;
import cl.MyCL;
import com.company.A;
import com.company.B;
/**
* Created by Frank on 2019/9/22.
*/
public class 二错误理解报错 {
public static void main(String[] args){
ClassLoader loader = new MyCL("C:\Users\Frank David\Desktop\cldemo\lib","A.jar");
Thread.currentThread().setContextClassLoader(loader);
new A().run();
loader = new MyCL("C:\Users\Frank David\Desktop\cldemo\lib","B.jar");
Thread.currentThread().setContextClassLoader(loader);
new B().run();
}
}
这也会得到和刚才一样的结果,因为Thread的ContextClassLoader只是一个属性,提供了一个set和get方法,并不代表这个线程真的就改用该类加载器了。
一个简单的解决方法就是new出两个类加载器分别加载:
package em;
import cl.MyCL;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
/**
* Created by Frank on 2019/9/22.
*/
public class 三简单粗暴的正确做法 {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
ClassLoader loader1 = new MyCL("C:\Users\Frank David\Desktop\cldemo\lib","A.jar");
ClassLoader loader2 = new MyCL("C:\Users\Frank David\Desktop\cldemo\lib","B.jar");
Class clazzA = loader1.loadClass("com.company.A");
Class clazzB = loader2.loadClass("com.company.B");
Object a = clazzA.newInstance();
Object b = clazzB.newInstance();
Method runA = a.getClass().getDeclaredMethod("run");
Method runB = b.getClass().getDeclaredMethod("run");
runA.invoke(a,null);
runB.invoke(b,null);
}
}
可以看到,上面完全没有平常的对象创建和方法调用的过程,而是全程都使用了反射的方式。可以完成任务,但是显得比较复杂。
下面是一种更加简洁的解决办法。说是简洁是因为调用简单,但是需要多个工具类的辅助
package em;
import cl.ReCL;
import com.company.A;
import com.company.B;
import util.ReRun;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Created by Frank on 2019/9/22.
*/
public class 四比较牛逼的方法 {
private static boolean flag = true;
public static void setFlag(){
flag = false;
}
public static void main(String[] args) throws Exception{
if(flag) ReRun.reRun("C:\Users\Frank David\Desktop\cldemo\target\classes","em.四比较牛逼的方法",args);
new A().run();
new B().run();
}
}
我们来看这里用到的ReRun是什么类:
package util;
import cl.ReCL;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Created by Frank on 2019/9/22.
*/
public class ReRun{
public static void reRun(String classpath,String mainClass,String[] args) throws Exception{
ReCL reCL = new ReCL(classpath);
Class clazz = reCL.loadClass(mainClass);
Method mainMethod = clazz.getMethod("main",String[].class);
Method setFlagMethod = clazz.getDeclaredMethod("setFlag");
setFlagMethod.invoke(null);
mainMethod.invoke(null, (Object) args);
System.exit(0);
}
}
ReCL:
package cl;
import java.io.*;
/**
* Created by Frank on 2019/9/22.
*/
public class ReCL extends ClassLoader{
private String classpath;
public ReCL(String classpath){
this.classpath = classpath;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if(name.startsWith("em")) {
try {
byte[] classDate = getDate(name);
if (classDate == null) {
} else {
//defineClass方法将字节码转化为类
return defineClass(name, classDate, 0, classDate.length);
}
} catch (IOException e) {
e.printStackTrace();
}
}else if(name.equals("com.company.A")){
ClassLoader loader = new MyCL("C:\Users\Frank David\Desktop\cldemo\lib","A.jar");
return loader.loadClass("com.company.A");
}
else if(name.equals("com.company.B")){
ClassLoader loader = new MyCL("C:\Users\Frank David\Desktop\cldemo\lib","B.jar");
return loader.loadClass("com.company.B");
}
return super.loadClass(name);
}
//返回类的字节码
private byte[] getDate(String className) throws IOException{
InputStream in = null;
ByteArrayOutputStream out = null;
String path=classpath + File.separatorChar +
className.replace('.',File.separatorChar)+".class";
try {
in=new FileInputStream(path);
out=new ByteArrayOutputStream();
byte[] buffer=new byte[2048];
int len=0;
while((len=in.read(buffer))!=-1){
out.write(buffer,0,len);
}
return out.toByteArray();
}
catch (FileNotFoundException e) {
e.printStackTrace();
}
finally{
in.close();
out.close();
}
return null;
}
}
MyCL:
package cl;
import java.io.*;
import java.util.HashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* Created by Frank on 2019/9/22.
*/
public class MyCL extends ClassLoader{
private HashMap<String,Class> classes = new HashMap();
private String classpath;
private String jarName;
public MyCL(String classpath,String jarName){
this.classpath = classpath;
this.jarName = jarName;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if(!name.startsWith("com.company")){
return super.loadClass(name);
}
if(classes.containsKey(name)){
return classes.get(name);
}
try {
byte [] classDate=getDate(name);
if(classDate==null){}
else{
//defineClass方法将字节码转化为类
// defineClass(name,classDate,0,classDate.length);
Class c = defineClass(name, classDate, 0, classDate.length, null);
classes.put(name,c);
return c;
}
} catch (IOException e) {
e.printStackTrace();
}
return super.loadClass(name);
}
//返回类的字节码
private byte[] getDate(String className) throws IOException{
String tmp = className.replaceAll("\.","/");
JarFile jar = new JarFile(classpath+"/"+jarName);
JarEntry entry = jar.getJarEntry(tmp + ".class");
InputStream is = jar.getInputStream(entry);
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int nextValue = is.read();
while (-1 != nextValue) {
byteStream.write(nextValue);
nextValue = is.read();
}
return byteStream.toByteArray();
}
}
我来大致说一下实现的过程。
执行main方法时,会调用ReRun中的reRun方法,该方法再创建了一个ReCL对象并通过该对象再一次获取main的主类和其方法。先通过设置flag避免再次调用reRun方法。此时的类加载器是ReCL而非是默认的AppClassLoader。我们在这个类加载器内部进行操作:
else if(name.equals("com.company.A")){
ClassLoader loader = new MyCL("C:\Users\Frank David\Desktop\cldemo\lib","A.jar");
return loader.loadClass("com.company.A");
}
else if(name.equals("com.company.B")){
ClassLoader loader = new MyCL("C:\Users\Frank David\Desktop\cldemo\lib","B.jar");
return loader.loadClass("com.company.B");
}
在创建A对象和B对象的时候,分别使用一个新的类加载器进行加载,这样就避免了依赖问题。
当然这种方法单独用是很麻烦的,封装为中间件就好多了
我们也可以从源代码中学到类加载器的实现:
//返回类的字节码
private byte[] getDate(String className) throws IOException{
String tmp = className.replaceAll("\.","/");
JarFile jar = new JarFile(classpath+"/"+jarName);
JarEntry entry = jar.getJarEntry(tmp + ".class");
InputStream is = jar.getInputStream(entry);
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int nextValue = is.read();
while (-1 != nextValue) {
byteStream.write(nextValue);
nextValue = is.read();
}
return byteStream.toByteArray();
}
然后:
//defineClass方法将字节码转化为类
// defineClass(name,classDate,0,classDate.length);
Class c = defineClass(name, classDate, 0, classDate.length, null);
classes.put(name,c);
return c;
classes是类加载器中的一个map:
private HashMap<String,Class> classes = new HashMap();
通过名称可以找到类。类加载器将加载了的所有的类放入这个map中,以便于之后通过类加载器加载类或者创建类的对象时使用