基于SPI机制和DataX插件热加载破坏双亲委派的思考

在开始阅读之前请先思考以下两个问题,并希望您能再接下来的文章中找到答案

1. 如果我自己实现了一个新的java.lang.String类,并通过UrlClassLoader加载使用该类,能否覆盖JDK中的  java.lang.String ?

2. 如果问题1的回答是不能,那用什么方式能做到覆盖JDK中的java.lang.String么?

一、双亲委派

                                         基于SPI机制和DataX插件热加载破坏双亲委派的思考_第1张图片

熟悉java类加载机制的一定都知道双亲委派,双亲委派模式的工作原理的是;如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。至于双亲委派的实现原理网上有很多文章可以参考,不是本文的重点,因此不再赘述。

二、破坏双亲委派

WHY AND HOW

1.父类加载器需委托子类加载器加载class文件

受到加载范围的限制,父类加载器无法加载到需要的文件,以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MYSQL CONNECTOR,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能加载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要破坏了双亲委派, 启动类加载器来委托子类加载器来加载Driver实现。这就是著名的SPI(SERVICE PROVIDER INTERFACE)机制,其基本概念和运用可参考以JDBC为例谈双亲委派模型的破坏

Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。其基本特点就是,父类提供接口,子类负责实现,父类接口通过配置文件中的实现类的全限定名确定具体的实现,并通过线程上下文加载器接在实现类,最终执行子类的方法实现。概括其逻辑为:加载类=》委托父类加载器加载=》父类加载器通过配置文件找到实现类并获取线程上下文加载器=》加载实现类=》返回实现类实例List

2.实现插件热插拔

DataX是一款热门的数据同步框架,可以将不同数据源的同步抽象为从源头数据源读取数据的Reader插件,以及向目标端写入数据的Writer插件,理论上DataX框架可以支持任意数据源类型的数据同步工作。为了支持对插件化的热插拔,DataX继承UrlClassLoader实现了类加载器JarLoader, 并实现了ClassLoaderSwapper进行类加载器的切换。

同样是破坏双亲委培机制,与SPI机制不同的是,它不通过双亲委派委托父类加载器加载,而是直接通过UrlClassLoader (JarLoader只是将路径下的所有jar加入classpath)去加载指定的插件类。概括其逻辑为: 加载类=》通过配置文件获取插件类名和路径=》实例化该插件UrlClassLoader=>将线程上下文加载器切换为UrlClassLoader并保存原来的线程上下文加载器=》加载插件实现类=》完成基于实现类的操作=》恢复原来的线程上下文加载器。下面是DATAX加载插件实现的部分源码

 

private Reader.Job initJobReader(JobPluginCollector jobPluginCollector) { 
        ClassLoaderSwapper classLoaderSwapper = ClassLoaderSwapper
            .newCurrentThreadClassLoaderSwapper(); 
    
        classLoaderSwapper.setCurrentThreadClassLoader(LoadUtil.getJarLoader(
                PluginType.READER, this.readerPluginName));

        Reader.Job jobReader = (Reader.Job) LoadUtil.loadJobPlugin(
                PluginType.READER, this.readerPluginName);

        classLoaderSwapper.restoreCurrentThreadClassLoader();
        return jobReader;
}

/*
* 为避免jar冲突,比如hbase可能有多个版本的读写依赖jar包,JobContainer和TaskGroupContainer
 * 就需要脱离当前classLoader去加载这些jar包,执行完成后,又退回到原来classLoader上继续执行接下来的代码
 */
public final class ClassLoaderSwapper {
    private ClassLoader storeClassLoader = null;

    private ClassLoaderSwapper() {
    }

    public static ClassLoaderSwapper newCurrentThreadClassLoaderSwapper() {
        return new ClassLoaderSwapper();
    }

    /**
     * 保存当前classLoader,并将当前线程的classLoader设置为所给classLoader
     */
    public ClassLoader setCurrentThreadClassLoader(ClassLoader classLoader) {
        this.storeClassLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(classLoader);
        return this.storeClassLoader;
    }

    /**
     * 将当前线程的类加载器设置为保存的类加载
     */
    public ClassLoader restoreCurrentThreadClassLoader() {
        ClassLoader classLoader = Thread.currentThread()
                .getContextClassLoader();
        Thread.currentThread().setContextClassLoader(this.storeClassLoader);
        return classLoader;
    }
}

 

本文的目的是梳理和理解目前主流的两种破坏双亲委派的方式,更好的理解双亲委派机制以及破坏双亲委派的目的和方式,同时对自己实现一些基于策略模式和插件化的项目有很好的参考意义

你可能感兴趣的:(JAVA开发)