借鉴datax的一些代码,添加破坏双亲委派功能,实现在JVM中自定义加载器加载同包名、类名不同版本的类文件
1、创建类加载器切换类,使用Thread的ClassLoaderContext控制
/**
*
* 为避免jar冲突,比如hbase可能有多个版本的读写依赖jar包
* 就需要脱离当前classLoader去加载这些jar包,执行完成后,又退回到原来classLoader上继续执行接下来的代码
*/
public final class ClassLoaderSwapper {
private ClassLoader storeClassLoader = null;
private ClassLoaderSwapper() {
}
public static ClassLoaderSwapper newCurrentThreadClassLoaderSwapper() {
return new ClassLoaderSwapper();
}
/**
* 保存当前classLoader,并将当前线程的classLoader设置为所给classLoader
*
* @param
* @return
*/
public ClassLoader setCurrentThreadClassLoader(ClassLoader classLoader) {
this.storeClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(classLoader);
return this.storeClassLoader;
}
/**
* 将当前线程的类加载器设置为保存的类加载
* @return
*/
public ClassLoader restoreCurrentThreadClassLoader() {
ClassLoader classLoader = Thread.currentThread()
.getContextClassLoader();
Thread.currentThread().setContextClassLoader(this.storeClassLoader);
return classLoader;
}
}
2、自定义Jar包类加载器,重写loadClass方法,将双亲委派,改为逆向双亲委派
/**
* 提供Jar隔离的加载机制,会把传入的路径、及其子路径、以及路径中的jar文件加入到class path。
* 破坏双亲委派机制,改为逆向
* */
public class JarLoader extends URLClassLoader {
private static ThreadLocal threadLocal = new ThreadLocal<>();
private URL[] allUrl;
public JarLoader(String[] paths) {
this(paths, JarLoader.class.getClassLoader());
}
public JarLoader(String[] paths, ClassLoader parent) {
super(getURLs(paths), parent);
//暂时先这样
allUrl = threadLocal.get();
}
private static URL[] getURLs(String[] paths) {
if (null == paths || 0 == paths.length) {
throw new RuntimeException("jar包路径不能为空.");
}
List dirs = new ArrayList();
for (String path : paths) {
dirs.add(path);
JarLoader.collectDirs(path, dirs);
}
List urls = new ArrayList();
for (String path : dirs) {
urls.addAll(doGetURLs(path));
}
URL[] urls1 = urls.toArray(new URL[0]);
threadLocal.set(urls1);
return urls1;
}
private static void collectDirs(String path, List collector) {
if (null == path || "".equalsIgnoreCase(path)) {
return;
}
File current = new File(path);
if (!current.exists() || !current.isDirectory()) {
return;
}
for (File child : current.listFiles()) {
if (!child.isDirectory()) {
continue;
}
collector.add(child.getAbsolutePath());
collectDirs(child.getAbsolutePath(), collector);
}
}
private static List doGetURLs(final String path) {
if (null == path || "".equalsIgnoreCase(path)) {
throw new RuntimeException("jar包路径不能为空.");
}
File jarPath = new File(path);
if (!jarPath.exists() || !jarPath.isDirectory()) {
throw new RuntimeException("jar包路径必须存在且为目录.");
}
/* set filter */
FileFilter jarFilter = new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getName().endsWith(".jar");
}
};
/* iterate all jar */
File[] allJars = new File(path).listFiles(jarFilter);
List jarURLs = new ArrayList(allJars.length);
for (int i = 0; i < allJars.length; i++) {
try {
jarURLs.add(allJars[i].toURI().toURL());
} catch (Exception e) {
throw new RuntimeException("系统加载jar包出错", e);
}
}
return jarURLs;
}
//破坏双亲委派模型,采用逆向双亲委派
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
if (allUrl != null) {
String classPath = name.replace(".", "/");
classPath = classPath.concat(".class");
for (URL url : allUrl) {
byte[] data = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream is = null;
try {
File file = new File(url.toURI());
if (file != null && file.exists()) {
JarFile jarFile = new JarFile(file);
if (jarFile != null) {
JarEntry jarEntry = jarFile.getJarEntry(classPath);
if (jarEntry != null) {
is = jarFile.getInputStream(jarEntry);
int c = 0;
while (-1 != (c = is.read())) {
baos.write(c);
}
data = baos.toByteArray();
return this.defineClass(name, data, 0, data.length);
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return super.loadClass(name);
}
}
3、测试使用
@RunWith(JUnit4.class)
public class JarClassloaderTests {
String jar1_3 = "C:\\Users\\Administrator\\.gradle\\caches\\modules-2\\files-2.1\\com.iscas\\base\\1.3-RELEASE\\f068847d9148a666f6c4c74cd4c8cec6ee41fda5";
String jar1_4 = "C:\\Users\\Administrator\\.gradle\\caches\\modules-2\\files-2.1\\com.iscas\\base\\1.4-RELEASE\\9204c31cae18a2b4aef592e2bd7805d133da71a6";
@Test
public void test() throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, ClassNotFoundException {
System.out.println("=========================开始第一次测试,读取指定jar包1.3版本:=========================");
JarLoader jarLoader = new JarLoader(new String[]{jar1_3});
ClassLoaderSwapper classLoaderSwapper = ClassLoaderSwapper.newCurrentThreadClassLoaderSwapper();
classLoaderSwapper.setCurrentThreadClassLoader(jarLoader);
Class> aClass = Thread.currentThread().getContextClassLoader().loadClass("com.iscas.test.base.StringUtils");
classLoaderSwapper.restoreCurrentThreadClassLoader();
Object o = aClass.newInstance();
Method isEmptyMethod = aClass.getDeclaredMethod("isEmpty", String.class);
Object invoke = isEmptyMethod.invoke(o, "ewe");
System.out.println("=========================开始第二次测试,读取指定jar包1.4版本:=========================");
JarLoader jarLoader2 = new JarLoader(new String[]{jar1_4});
classLoaderSwapper.setCurrentThreadClassLoader(jarLoader2);
Class> aClass2 = Thread.currentThread().getContextClassLoader().loadClass("com.iscas.test.base.StringUtils");
classLoaderSwapper.restoreCurrentThreadClassLoader();
Object o2 = aClass2.newInstance();
Method isEmptyMethod2 = aClass2.getDeclaredMethod("isEmpty", String.class);
Object invoke2 = isEmptyMethod2.invoke(o2, "ewe");
}
}
4、得到结果,带版本号的输出是我随便写的StringUtils.isEmpty函数的System.out
=========================开始第一次测试,读取classpath下默认加载的StringUtils:=========================
这是1.4版本的测试
=========================开始第二次测试,读取指定jar包1.3版本:=========================
这是1.3版本的测试
=========================开始第二次测试,读取指定jar包1.4版本:=========================
这是1.4版本的测试
Process finished with exit code 0