✨这里是第七人格的博客✨小七,欢迎您的到来~✨
🍅系列专栏:【架构思想】🍅
✈️本篇内容: 自定义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
自定义加载器