Java的动态加载及类加载器实践

在Java中,除了事先编写好代码完成执行代码,还可以从任何地方加载类。动态类加载允许程序在运行时动态编译和加载代码,打破了传统静态编译的限制。

主要的应用场景是插件系统、规则引擎、热部署等场景,本文将探讨其实现原理,并给出完整的最佳实践方案。

一、动态加载核心原理

在这里插入图片描述

二、动态加载类代码

动态编译

// 该内容是字符串形式的类代码
String sourceCode = "public class DynamicDemo { /*...*/ }";
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// DynamicDemo是字符串形式类代码的类名
JavaFileObject source = new StringJavaSource("DynamicDemo", sourceCode);

// 使用内存文件管理器
MemoryJavaFileManager fileManager = new MemoryJavaFileManager(
    compiler.getStandardFileManager(null, null, null)
);

// 执行编译任务
compiler.getTask(null, fileManager, null, null, null, 
    Collections.singletonList(source)).call();

字节码内存管理

class MemoryJavaFileObject extends SimpleJavaFileObject {
    private final ByteArrayOutputStream bos = new ByteArrayOutputStream();

    @Override
    public OutputStream openOutputStream() {
        return bos; // 字节码写入内存流
    }

    public byte[] getBytes() {
        return bos.toByteArray();
    }
}

类加载

public class MemoryClassLoader extends ClassLoader {
    private final Map<String, byte[]> classMap;

    @Override
    protected Class<?> findClass(String name) {
        byte[] bytes = classMap.get(name);
        return defineClass(name, bytes, 0, bytes.length);
    }
}

利用反射调用

Class<?> clazz = classLoader.loadClass("DynamicDemo");
Object instance = clazz.newInstance();
Method method = clazz.getMethod("execute");
method.invoke(instance);

三、远程加载JAR到JVM

实现步骤

在这里插入图片描述

远程JAR下载

public byte[] downloadRemoteJar(String jarUrl) throws IOException {
    try (InputStream in = new URL(jarUrl).openStream();
         ByteArrayOutputStream out = new ByteArrayOutputStream()) {
        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = in.read(buffer)) != -1) {
            out.write(buffer, 0, bytesRead);
        }
        return out.toByteArray();
    }
}

内存JAR解析

public class JarInMemory {
    private final Map<String, byte[]> classEntries = new HashMap<>();
    private final Map<String, byte[]> resourceEntries = new HashMap<>();

    public JarInMemory(byte[] jarBytes) throws IOException {
        try (JarInputStream jis = new JarInputStream(new ByteArrayInputStream(jarBytes))) {
            JarEntry entry;
            while ((entry = jis.getNextJarEntry()) != null) {
                if (entry.isDirectory()) continue;

                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = jis.read(buffer)) != -1) {
                    bos.write(buffer, 0, bytesRead);
                }

                byte[] data = bos.toByteArray();
                String name = entry.getName();
                
                if (name.endsWith(".class")) {
                    String className = name.replace(".class", "").replace('/', '.');
                    classEntries.put(className, data);
                } else {
                    resourceEntries.put(name, data);
                }
            }
        }
    }
}

自定义类加载器

public class RemoteJarClassLoader extends ClassLoader {
    private final Map<String, byte[]> classMap;
    private final Map<String, byte[]> resourceMap;

    public RemoteJarClassLoader(ClassLoader parent, JarInMemory jar) {
        super(parent);
        this.classMap = new HashMap<>(jar.getClassEntries());
        this.resourceMap = new HashMap<>(jar.getResourceEntries());
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classBytes = classMap.get(name);
        if (classBytes == null) {
            throw new ClassNotFoundException(name);
        }
        return defineClass(name, classBytes, 0, classBytes.length);
    }

    @Override
    public InputStream getResourceAsStream(String name) {
        byte[] data = resourceMap.get(name);
        return data != null ? new ByteArrayInputStream(data) : null;
    }
}

四、更简洁的加载远程JAR

利用内置的URLClassLoader类加载器直接加载远程JAR,JVM 会按需下载 JAR 中的类文件,首次访问类时才会触发下载。加载的类遵循父委托机制,优先由父类加载器加载。

// 远程 JAR 地址
String jarUrl = "http://xxxxx.cn/abc.jar";

// 创建 ClassLoader
URL[] urls = { new URL(jarUrl) };
try (URLClassLoader classLoader = new URLClassLoader(urls)) {
    // 加载类
    Class<?> clazz = classLoader.loadClass("cn.tworice.MyClass");

    // 实例化并调用方法
    Object instance = clazz.getDeclaredConstructor().newInstance();
    clazz.getMethod("myMethod").invoke(instance);
}

五、类加载器的注意事项

需要注意的是,不同的类加载器即使加载的是同一个类,它们之间也不能相互转换。类的唯一性由类加载器实例 + 类的全限定类名进行标识。

举个例子,我的项目中有一个业务逻辑是动态加载的类实现了BaseNode接口,但由于BaseNode和动态加载的TestNode实现类使用了不同的类加载器,则无法将动态加载的TestNode实现类转为BaseNode接口。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二饭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值