自定义classloader实现热加载jar

122 阅读4分钟

✨这里是第七人格的博客✨小七,欢迎您的到来~✨

🍅系列专栏:【架构思想】🍅

✈️本篇内容: 自定义classloader实现热加载jar✈️

🍱本篇收录完整代码地址:gitee.com/diqirenge/s…🍱

楔子

小七最近收到一个需求,需要加载符合条件的jar到正在运行的系统中。因为对热部署那一套,小七以前有过简单的调研,所以首先想到了Osgi、Sofa-Ark等框架,但是仅仅只是想简单的热加载一个jar,引入这种重量级的框架,实属是杀鸡用牛刀,于是小七思考是不是可以写一个自己的类加载器来实现这一个功能。

第一步:添加maven依赖

<dependencies>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>30.1-jre</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.7</version>
    </dependency>
</dependencies>

第二步:创建jar包路径构造类

主要逻辑如下:

1、申明默认jar包路径

2、获取路径时,如果有指定路径那么使用指定的路径,如果没有指定路径,那么使用默认的路径

public final class JarPathBuilder {

    /**
     * 默认ext插件路径
     * 可以暴露出去,做到参数控制
     */
    private static final String DEFAULT_EXT_PLUGIN_PATH = "/ext-lib/";

    /**
     * 得到jar路径
     *
     * @param path 路径
     * @return {@link File}
     */
    public static File getJarPath(final String path) {
        if (StringUtils.isNotEmpty(path)) {
            System.out.println("开始加载【" + path + "】路径下的jar包");
            return new File(path);
        }
        System.out.println("开始加载【ext-lib】路径下的jar包");
        return buildJarPath();
    }

    /**
     * 构建jar路径
     *
     * @return {@link File}
     */
    private static File buildJarPath() {
        URL url = JarPathBuilder.class.getResource(DEFAULT_EXT_PLUGIN_PATH);
        return Optional.ofNullable(url).map(u -> new File(u.getFile())).orElse(new File(DEFAULT_EXT_PLUGIN_PATH));
    }

}

第三步:定义需要被加载的jar的目录结构

我们这里定义,需要加载的jar的结构和maven打包出来的jar一致。

我们编写一个测试jar如下: 在这里插入图片描述

完整代码地址:gitee.com/diqirenge/s…

执行package命令获取jar包:sheep-web-demo-custom-classloader-jar-1.0-SNAPSHOT.jar 在这里插入图片描述

第四步:创建自定义类加载器

1 继承ClassLoader并实现Closeable接口

public final class CustomLoader extends ClassLoader implements Closeable{}

2 标记该加载器支持并行类加载机制

static {
    registerAsParallelCapable();
}

注: 类加载器在类初始化时,通过调用 ClassLoader.registerAsParallelCapable 来标记该加载器支持并行类加载机制。

支持该机制的加载器称之为 可并行 的类加载器。需要注意的是,ClassLoader类是默认可并行加载的,但它的子类仍须通过注册接口调用来支持可并行机制,也就是说,可并行机制不可继承。

在委托结构设计不是很有层次性(如出现闭环委托)的情况下,这些类加载器需要实现并行机制,否则会出现死锁问题。具体可以参考loadClass的函数源码。

3 私有化构造方法,避免该类被new出来

private CustomLoader() {
    super(CustomLoader.class.getClassLoader());
}

4 添加一些属性

/**
 * 自定义加载程序
 */
private static volatile CustomLoader customLoader;

/**
 * 对象缓存池
 */
private final ConcurrentHashMap<String, Object> objectPool = new ConcurrentHashMap<>();

/**
 * 锁
 */
private final ReentrantLock lock = new ReentrantLock();

/**
 * jar包
 */
private final List<CustomJar> jars = Lists.newArrayList();

5 单例模式获取对象

/**
 * 双重检索,获得实例
 *
 * @return {@link CustomLoader}
 */
public static CustomLoader getInstance() {
    if (null == customLoader) {
        synchronized (CustomLoader.class) {
            if (null == customLoader) {
                customLoader = new CustomLoader();
            }
        }
    }
    return customLoader;
}

6 创建静态内部内-自定义jar

/**
 * 自定义jar
 *
 * @author lizongyang
 * @date 2023/03/03
 */
private static class CustomJar {

    /**
     * jar文件
     */
    private final JarFile jarFile;

    /**
     * 源路径
     */
    private final File sourcePath;

    CustomJar(final JarFile jarFile, final File sourcePath) {
        this.jarFile = jarFile;
        this.sourcePath = sourcePath;
    }
}

7 编写加载扩展jar的核心方法

/**
 * 加载扩展jar
 *
 * @param path 路径
 * @return {@link List}<{@link Object}>
 * @throws IOException            io异常
 * @throws ClassNotFoundException 类没有发现异常
 * @throws InstantiationException 实例化异常
 * @throws IllegalAccessException 非法访问异常
 */
public List<Object> loadExtendJar(final String path) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
    File[] jarFiles = JarPathBuilder.getJarPath(path).listFiles(file -> file.getName().endsWith(".jar"));
    if (null == jarFiles) {
        return Collections.emptyList();
    }
    List<Object> results = new ArrayList<>();
    try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
        for (File each : Objects.requireNonNull(jarFiles)) {
            outputStream.reset();
            JarFile jar = new JarFile(each, true);
            jars.add(new CustomJar(jar, each));
            Enumeration<JarEntry> entries = jar.entries();
            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement();
                String entryName = jarEntry.getName();
                if (entryName.endsWith(".class") && !entryName.contains("$")) {
                    String className = entryName.substring(0, entryName.length() - 6).replaceAll("/", ".");
                    Object instance = getOrCreateInstance(className);
                    if (Objects.nonNull(instance)) {
                        results.add(instance);
                    }
                }
            }
        }
    }
    return results;
}
/**
 * 获取或创建实例
 *
 * @param className 类名
 * @return {@link T}
 * @throws ClassNotFoundException 类没有发现异常
 * @throws IllegalAccessException 非法访问异常
 * @throws InstantiationException 实例化异常
 */
@SuppressWarnings("unchecked")
private <T> T getOrCreateInstance(final String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
    if (objectPool.containsKey(className)) {
        System.out.println("从缓存中获取的className为【" + className + "】");
        return (T) objectPool.get(className);
    }
    lock.lock();
    try {
        System.out.println("开始创建className为【" + className + "】的实例");
        Object inst = objectPool.get(className);
        if (Objects.isNull(inst)) {
            Class<?> clazz = Class.forName(className, true, this);
            inst = clazz.newInstance();
            objectPool.put(className, inst);
        }
        System.out.println("创建className为【" + className + "】的实例结束");
        return (T) inst;
    } finally {
        lock.unlock();
    }
}

8 编写main方法

public class CustomLoaderAction {

    public static void main(String[] args) {
        System.out.println("=======>主线程启动<=======");
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("loader-pool-%d").build();
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, namedThreadFactory);
        executor.scheduleAtFixedRate(() -> {
            Date now = new Date();
            System.out.println();
            System.out.println(now + "=======>定时任务开始执行<=======");
            try {
                List<Object> objects = CustomLoader.getInstance().loadExtendJar("");
                Object o = objects.get(0);
                Method say = o.getClass().getMethod("say", String.class);
                say.invoke(o, " 第七人格");
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(now + "=======>定时任务结束<=======");
        }, 3, 30, TimeUnit.SECONDS);
        while (true) {
            // 保持主线程不断
        }
    }
}

9 启动main方法

因为当前指定目录下没有jar包,所以系统报错 在这里插入图片描述

10 将测试jar包放入指定目录

在这里插入图片描述

输出结果: 在这里插入图片描述

说明热加载jar成功

完整代码

待加载的jar

gitee.com/diqirenge/s…

自定义加载器

gitee.com/diqirenge/s…