前言:
学习源码好处多多。最近在学习datax的源码时,碰到一个骚操作,自定义实现URLClassLoader加载器,可以加载指定路径下的jar包。
这个很普通,没什么大不了。关键在于后面的操作,将这个自定义的URLClassLoader设置给创建的Thread中,这样就实现了不同的Thread在执行任务时,使用到了不同的URLClassLoader加载的不同的jar包。我们先一起来看下。
1.自定义URLClassLoader/**
* 提供Jar隔离的加载机制,会把传入的路径、及其子路径、以及路径中的jar文件加入到class path。
*/
public class JarLoader extends URLClassLoader {
public JarLoader(String[] paths) {
this(paths, JarLoader.class.getClassLoader());
}
public JarLoader(String[] paths, ClassLoader parent) {
super(getURLs(paths), parent);
}
// 直接通过getURLs()方法将指定path路径下的jar包全部扫描到到当前ClassLoader
private static URL[] getURLs(String[] paths) {
Validate.isTrue(null != paths && 0 != paths.length,
"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));
}
return urls.toArray(new URL[0]);
}
private static void collectDirs(String path, List collector) {
if (null == path || StringUtils.isBlank(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) {
Validate.isTrue(!StringUtils.isBlank(path), "jar包路径不能为空.");
File jarPath = new File(path);
Validate.isTrue(jarPath.exists() && jarPath.isDirectory(),
"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 DataXException.asDataXException(
FrameworkErrorCode.PLUGIN_INIT_ERROR,
"系统加载jar包出错", e);
}
}
return jarURLs;
}
}
代码不算复杂,就是直接继承URLClassLoader,然后扫描指定urls路径下的所有jar包,加载进来即可
2.设置线程ClassLoader /**
* TaskExecutor是一个完整task的执行器
* 其中包括1:1的reader和writer
*/
class TaskExecutor {
private Thread readerThread;
private Thread writerThread;
public TaskExecutor(Configuration taskConf, int attemptCount) {
...
// 生成writerThread
writerRunner = (WriterRunner) generateRunner(PluginType.WRITER);
this.writerThread = new Thread(writerRunner,
String.format("%d-%d-%d-writer",
jobId, taskGroupId, this.taskId));
//通过设置thread的contextClassLoader,即可实现同步和主程序不通的加载器
this.writerThread.setContextClassLoader(LoadUtil.getJarLoader(
PluginType.WRITER, this.taskConfig.getString(
CoreConstant.JOB_WRITER_NAME)));
// 生成readerThread
readerRunner = (ReaderRunner) generateRunner(PluginType.READER,transformerInfoExecs);
this.readerThread = new Thread(readerRunner,
String.format("%d-%d-%d-reader",
jobId, taskGroupId, this.taskId));
// 通过设置thread的contextClassLoader,即可实现同步和主程序不通的加载器
this.readerThread.setContextClassLoader(LoadUtil.getJarLoader(
PluginType.READER, this.taskConfig.getString(
CoreConstant.JOB_READER_NAME)));
}
}
正常来说,我们在创建Thread时,并不会设置其ClassLoader,直接使用主线程的ClassLoader即可。那么这里为什么要这样使用呢?好处在哪里。
2.1 主动设置Thread ClassLoader优势首先我们要明白datax所面临的一个现状,它作为一个同步数据库数据的工具,它所面临的源数据库和目标数据库有可能是不同的(源可以是Oracle、mysql...,目标也可以是这些),所以如果我们在主线程中直接加载所有的数据源jar包的话,那么很可能读线程或写线程根本用不到这些jar包,就浪费了相关资源。
所以最好的方式就是,当前线程使用到了哪些jar包就直接只加载这些jar包就可以了,所以才有了这个操作。
总结:这种线程加载方式对我们有哪些启示呢?
后续我们在做线程执行隔离时,可以考虑使用这种方式,只加载当前线程需要的jar包,这样便有效的实现jar包隔离。