使用ClassLoader解决依赖冲突

https://www.bilibili.com/video/av68658611

假设我们要引用两个包,两个包里面有一个相同的类,但是版本不同,而这个类是被包里的其他对象依赖的。如果我们要同时使用这两个包里的对象,应该怎么做?

这是两个包的结构:

img

每个包中都有一个C,而C在A包中返回版本是1.0,在B中返回是2.0

img

img

img

img

如果我们直接调用的话:

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中,以便于之后通过类加载器加载类或者创建类的对象时使用

posted @ 2020-02-25 14:26  别再闹了  阅读(1484)  评论(0)    收藏  举报