`
gelongmei
  • 浏览: 209996 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
文章分类
社区版块
存档分类
最新评论

Java 自定义 ClassLoader 实现隔离运行不同版本jar包的方式

 
阅读更多
Java 自定义 ClassLoader 实现隔离运行不同版本jar包的方式

1. 应用场景
有时候我们需要在一个 Project 中运行多个不同版本的 jar 包,以应对不同集群的版本或其它的问题。如果这个时候选择在同一个项目中实现这样的功能,那么通常只能选择更低版本的 jar 包,因为它们通常是向下兼容的,但是这样也往往会失去新版本的一些特性或功能,所以我们需要以扩展的方式引入这些 jar 包,并通过隔离执行,来实现版本的强制对应。

2. 实现
在 Java 中,所有的类默认通过 ClassLoader 加载,而 Java 默认提供了三层的 ClassLoader,并通过双亲委托模型的原则进行加载,其基本模型与加载位置如下(更多ClassLoader相关原理请自行搜索):

ClassLoader

Java 中默认的 ClassLoader 都规定了其指定的加载目录,一般也不会通过 JVM 参数来使其加载自定义的目录,所以我们需要自定义一个 ClassLoader 来加载装有不同版本的 jar 包的扩展目录,同时为了使运行扩展的 jar 包时,与启动项目实现绝对的隔离,我们需要保证他们所加载的类不会有相同的 ClassLoader,根据双亲委托模型的原理可知,我们必须使自定义的 ClassLoader 的 parent 为 null,这样不管是 JRE 自带的 jar 包或一些基础的 Class 都不会委托给 App ClassLoader(当然仅仅是将 Parent 设置为 null 是不够的,后面会说明)。与此同时这些实现了不同版本的 jar 包,是经过二次开发后的可以独立运行的项目。

2.1 实例
现在假定有这样一个需求,实现针对集群(比如 Hadoop 集群)版本为 V1 与 V2 的对应的执行程序,那么假定有如下项目:

Executor-Parent: 提供基础的 Maven 引用,可利用 Maven 一键打包所有的子模块/项目
Executor-Common: 提供基础的接口,已经有公有的实现等
Executor-Proxy: 执行不同版本程序的代理程序
Executor-V1: 版本为V1的执行程序
Executor-V2: 版本为V2的执行程序

这里为了更凸显 ClassLoader 的实现,不做 Executor-Parent 的实现,同时为了简便,也没有设置包名。

1) Executor-Common
在 Executor-Common 中提供一个接口,声明执行的具体方法:

public interface Executor {
    void execute(final String name);
}

这里的方法使用了基础类型 String,实际中可能会使用自定义的类型,那么在 Porxy 的实现中则需要使用自定义的 ClassLoader 来加载参数,并使用反射来获取方法(后面会有一个简单的示例)。回到之前的示例,这里同时提供一个抽象的实现类:

public class AbstractExecutor implements Executor {

    @Override
    public void execute(final String name) {
        this.handle(new Handler() {
            @Override
            public void handle() {
                System.out.println("V:" + name);
            }
        });
    }

    protected void handle(Handler handler) {
        handler.call();
    }

    protected abstract class Handler {
        public void call() {
            ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
            // 临时更改 ClassLoader
            Thread.currentThread().setContextClassLoader(AbstractExecutor.class.getClassLoader());

            handle();

            // 还原为之前的 ClassLoader
            Thread.currentThread().setContextClassLoader(oldClassLoader);
        }

        public abstract void handle();
    }
}

这里需要临时更改当前线程的 ContextClassLoader, 以应对扩展程序中可能出现的如下代码:

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

classLoader.loadClass(...);

因为它们会获取当前线程的 ClassLoader 来加载 class,而当前线程的ClassLoader极可能是App ClassLoader而非自定义的ClassLoader, 也许是为了安全起见,但是这会导致它可能加载到启动项目中的class(如果有),或者发生其它的异常,所以我们在执行时需要临时的将当前线程的ClassLoader设置为自定义的ClassLoader,以实现绝对的隔离执行。

2) Executor-V1 & Executor-V2
Executor-V1 和 Executor-V2 依赖了 Executor-Common.jar,并实现了 Executor 接口的方法:

public class ExecutorV1 extends AbstractExecutor {

    @Override
    public void execute(final String name) {
        this.handle(new Handler() {
            @Override
            public void handle() {
                System.out.println("V1:" + name);
            }
        });
    }

}

public class ExecutorV2 extends AbstractExecutor {

    @Override
    public void execute(final String name) {
        this.handle(new Handler() {
            @Override
            public void handle() {
                System.out.println("V2:" + name);
            }
        });
    }

}

这里仅仅是打印了它们的版本信息,实际中,它们可能需要引入不同的版本的 Jar 包,然后根据这些 Jar 包完成相应的操作。

3) Executor-Proxy
Executor-Proxy 利用自定义的 ClassLoader 和反射来实现加载与运行 ExecutorV1 和 ExecutorV2 中 Executor 接口的实现,而 ExecutorV1 和 ExecutorV2 将以 jar 包的形式被分别放置在 ${Executor-Proxy_HOME}\ext\v1 和 ${Executor-Proxy_HOME}\ext\v2 目录下,其中自定义的 ClassLoader 实现如下:

public class StandardExecutorClassLoader extends URLClassLoader {
    private final static String baseDir = System.getProperty("user.dir") + File.separator + "ext" + File.separator;

    public StandardExecutorClassLoader(String version) {
        super(new URL[] {}, null); // 将 Parent 设置为 null

        loadResource(version);
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 测试时可打印看一下
        System.out.println("Class loader: " + name);

        return super.loadClass(name);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            return super.findClass(name);
        } catch(ClassNotFoundException e) {
            return StandardExecutorClassLoader.class.getClassLoader().loadClass(name);
        }
    }

    private void loadResource(String version) {
        String jarPath = baseDir + version;

        // 加载对应版本目录下的 Jar 包
        tryLoadJarInDir(jarPath);
        // 加载对应版本目录下的 lib 目录下的 Jar 包
        tryLoadJarInDir(jarPath + File.separator + "lib");
    }

    private void tryLoadJarInDir(String dirPath) {
        File dir = new File(dirPath);
        // 自动加载目录下的jar包
        if (dir.exists() && dir.isDirectory()) {
            for (File file : dir.listFiles()) {
                if (file.isFile() && file.getName().endsWith(".jar")) {
                    this.addURL(file);
                    continue;
                }
            }
        }
    }

    private void addURL(File file) {
        try {
            super.addURL(new URL("file", null, file.getCanonicalPath()));
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

StandardExecutorClassLoader 在实例化时,会自动加载扩展目录下与其lib目录下的 jar 包,这里之所以要加载 lib 目录下的 jar,是为了加载扩展的依赖包。

有了StandardExecutorClassLoader,我们还需要一个调用各版本程序的代理类ExecutorPorxy,其实现如下:

import java.lang.reflect.Method;

public class ExecutorProxy implements Executor {
    private String version;
    private StandardExecutorClassLoader classLoader;

    public ExecutorProxy(String version) {
        this.version = version;
        classLoader = new StandardExecutorClassLoader(version);
    }

    @Override
    public void execute(String name) {
        try {
            // Load ExecutorProxy class
            Class<?> executorClazz = classLoader.loadClass("Executor" + version.toUpperCase());

            Object executorInstance = executorClazz.newInstance();
            Method method = executorClazz.getMethod("execute", String.class);

            method.invoke(executorInstance, name);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

这里是一个比较简单的实现,因为通过反射调用的方法的参数是基本类型,在实际中,更多的可能是自定义的参数,那么这时候则需要先通过自定义的 ClassLoader 加载其 Class,然后才能去获取对应的方法,下面是一个省去上下文的一个例子(不能直接运行):

public void call() throws IOException {
    try {
        // Load HBaseApi class
        Class<?> hbaseApiClazz = loadHBaseApiClass();
        Object hbaseApiInstance = hbaseApiClazz.newInstance();

        // Load parameter class
        Class<?> paramClazz = classLoader.loadClass(VO_PACKAGE_PATH + "." + sourceParame.getClass().getSimpleName());

        // Transition parameter to targeParameter from sourceParameter
        Object targetParam = BeanUtils.transfrom(paramClazz, sourceParame);

        // Get function
        Method method = hbaseApiClazz.getMethod(methodName, paramClazz);
        // Invoke function by targetParam
        method.invoke(hbaseApiInstance, targetParam);

    } catch(ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

3. 运行
将ExecutorV1 和 ExecutorV2分别打包,并将其打包后的 jar包与其依赖(lib目录下)放入 Executor-Proxy 项目的 ext\v1 和 ext\v2 目录下,在 Executor-Proxy 项目中则可以使用 Junit 进行测试:

public class ExecutorTest {

    @Test
    public void testExecuteV1() {

        Executor executor = new ExecutorProxy("v1");

        executor.execute("TOM");
    }

    @Test
    public void testExecuteV2() {

        Executor executor = new ExecutorProxy("v2");

        executor.execute("TOM");
    }

}

打印结果最终分别如下:

execute testExecuteV1():

V1:TOM

execute testExecuteV2():

V2:TOM

4. 总结
总的来说,实现隔离允许指定 jar 包,主要需要做到以下几点:

自定义 ClassLoader,使其 Parent = null,避免其使用系统自带的 ClassLoader 加载 Class。
在调用相应版本的方法前,更改当前线程的 ContextClassLoader,避免扩展包的依赖包通过Thread.currentThread().getContextClassLoader()获取到非自定义的 ClassLoader 进行类加载
通过反射获取 Method 时,如果参数为自定义的类型,一定要使用自定义的 ClassLoader 加载参数获取 Class,然后在获取 Method,同时参数也必须转化为使用自定义的 ClassLoade 加载的类型(不同 ClassLoader 加载的同一个类不相等)
实际运用中,往往容易做到第一点或第三点,而忽略第二点,比如使用 HBase 相关包时。

当然,这只是一种解决的方式,我们仍然可以使用微服务来达到同样甚至更棒的效果,

以上。

版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/t894690230/article/details/73252331
分享到:
评论

相关推荐

    自定义classloader的使用

    自定义Classloader允许开发者根据特定需求定制类的加载逻辑,例如加密类文件、隔离不同版本的库或者动态加载代码。本文将深入探讨自定义Classloader的使用。 一、Classloader的工作原理 Java的类加载机制遵循双亲...

    jar包隔离代码.zip

    1. ClassLoader隔离:通过自定义ClassLoader,为每个模块加载特定版本的jar包,确保各模块间的类加载互不影响。 2. OSGi(Open Service Gateway Initiative)框架:OSGi提供了模块化系统,允许在同一JVM中动态加载和...

    ClassLoader运行机制 自己写的

    这里我们将详细讨论ClassLoader的运行机制,特别是自定义ClassLoader的设计与实现。 ClassLoader的基本职责是根据类名动态加载对应的类文件。在Java中,类加载过程遵循双亲委派模型(Parent Delegation Model)。这...

    理解Java ClassLoader机制

    Java允许用户自定义ClassLoader,这在某些场景下非常有用,比如动态加载插件或者实现热更新。自定义ClassLoader需要继承`java.lang.ClassLoader`类,并重写`findClass()`或`loadClass()`方法。通过这两个方法,你...

    使用自定义ClassLoader解决反序列化serialVesionUID不一致问题 _ 回忆飘如雪1

    - **取消双亲委派模型**:改为当前`ClassLoader`优先加载,这样可以避免不一致的JAR被父`ClassLoader`加载,从而实现隔离。 - **便捷地共享依赖**:允许共享那些没有`serialVersionUID`冲突的类库,提高效率。 - **...

    java ClassLoader机制及其在OSGi中的应用

    用户还可以自定义ClassLoader,插入这个树结构中,以满足特定的加载需求。 三、ClassLoader的装载策略 从Java 1.2版本开始,引入了双亲委派模型(Delegation Model)。在该模型下,当一个ClassLoader收到加载类的...

    JAVA动态加载JAR zip包

    总结来说,Java动态加载JAR或ZIP包是通过自定义类加载器实现的,它可以让我们在运行时按需加载外部库,提高系统的可扩展性和灵活性。这个过程涉及到类加载器的创建、文件的读取、类的解析和实例化等多个步骤,是一项...

    探究java的ClassLoader及类变量初始化顺序

    自定义ClassLoader是Java灵活性的一个体现,开发者可以通过继承ClassLoader类并重写findClass()方法来自定义类的加载方式,例如从网络、数据库或者特定目录加载类。 接下来,我们讨论类变量初始化的顺序。Java中,...

    了解Java ClassLoader

    了解Java ClassLoader不仅有助于理解JVM的运作机制,还能帮助开发者解决一些特定场景下的问题,比如实现模块化的类加载、动态加载代码、隔离不同版本的库等。因此,它是Java程序员必备的知识点之一。

    java classloader讲义-淘宝网

    通过对Java ClassLoader的深入了解,我们可以更好地理解Java类的加载机制以及如何通过自定义ClassLoader来满足特定的应用需求。淘宝网的成功实践为我们提供了宝贵的参考案例,展示了ClassLoaders在实际项目中的重要...

    动态加载Apk、Jar

    本文将深入探讨如何通过自定义ClassLoader实现动态加载Apk和Jar包的功能。 首先,我们要理解ClassLoader的基本概念。在Java中,ClassLoader是负责加载类到JVM(Java虚拟机)的核心组件。它按照类名查找并加载相应的...

    加载同一类型但是版本不同JDBC驱动

    通常,我们可以创建一个子类继承`java.lang.ClassLoader`,并重写`loadClass()`方法来实现特定版本JDBC驱动的加载。 2. **加载驱动**: 在自定义类加载器中,使用`Class.forName()`方法加载特定版本的JDBC驱动。例如...

    ClassLoader类加载机制和原理详解

    但有时我们可能需要打破这种模型,比如实现类的版本控制或插件系统,这时可以通过自定义ClassLoader来实现。 5. 类加载器的关系图 Java中的ClassLoader形成了一个树状结构,Bootstrap ClassLoader位于顶端,其他类...

    自定义Java类加载器

    2. **Extension ClassLoader**:扩展类加载器,负责加载`&lt;JAVA_HOME&gt;\lib\ext`目录下的JAR包,或者被`-Djava.ext.dirs`指定的路径中的类。 3. **System ClassLoader**:也称为应用类加载器,负责加载`CLASSPATH`...

    ClassLoader的 一些测试

    4. 类隔离:通过自定义ClassLoader实现不同模块之间的类隔离,避免类冲突。 总的来说,深入理解ClassLoader的工作原理对于优化程序性能、构建灵活的插件系统和解决类冲突问题具有重要意义。通过测试和实践,我们...

    ava的ClassLoader介绍.doc

    编写自定义ClassLoader的主要原因是扩展加载类的方式,以满足特殊需求。例如,你可以: 1. **从网络加载类**:这使得Java applets可以从Web服务器动态下载并执行代码。 2. **验证数字签名**:在加载类之前进行安全...

    classloader源码

    自定义`ClassLoader`能够帮助我们实现特定的加载策略,比如隔离不同版本的库或动态加载类。 `ClassLoader`的工作原理主要是通过读取`.class`文件并将其转换为`Class`对象。默认情况下,Java使用`Bootstrap ...

    java应用程序类加载器,ClassLoader for java Application

    例如,可以创建自定义类加载器来实现按需加载、隔离不同版本的库,或者实现动态加载插件机制。通过重写`loadClass()`方法,开发者可以控制类的加载过程,实现特定的加载策略。 **多平台选择性配置**: Java的一个...

    ClassLoader总结

    总之,ClassLoader是Java运行时环境中的关键组件,理解其工作原理和如何自定义ClassLoader对于提高开发者的技术深度和解决问题的能力具有重要意义。通过阅读源码和实践,我们可以更深入地掌握这一领域,从而在实际...

Global site tag (gtag.js) - Google Analytics