自定义classloader并实现热部署-使用loadClass

需求如下:

  1. 可在前端新增、修改java代码,并可实现服务不重启的前提下进行代码的部署运行。
  2. 相当于:可实现java代码的热部署。

代码如下:

  1. ClassLoaderTest: 自定义classloader测试类
  2. ClazzCache: 自定义ClassLoader缓存类
  3. CustomCompiler: 自定义编译器
  4. CustomClassLoader: 自定义ClassLoader
  5. CustomClassLoaderParam: 自定义ClassLoader包装类(不要直接使用此类中的方法,应通过使用CustomClassLoader来实现功能)
  6. TestCode: 模拟前端写的java代码
  7. TestCodeUpdate: 模拟前端写的java代码

ClassLoaderTest.java

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@ActiveProfiles("dev")
@SpringBootTest(classes = ApiApplication.class)
public class ClassLoaderTest {

    @Test
    public void classLoaderTest() throws Exception {
        // 新增操作 - 第一次加载,应通过findClass获取
        ClazzCache testCode = CustomClassLoaderParam.findClass("TestCode", 1L);
        Object invoke = testCode.getMethod().invoke(testCode.getObj(), new EnterpriseMessage());
        System.err.println(invoke);
        // 第二次加载,应通过缓存获取
        testCode = CustomClassLoaderParam.findClass("TestCode", 1L);
        invoke = testCode.getMethod().invoke(testCode.getObj(), new EnterpriseMessage());
        System.err.println(invoke);
        // 新增操作 - 第一次加载,应通过findClass获取
        testCode = CustomClassLoaderParam.findClass("TestCodeUpdate", 1L);
        invoke = testCode.getMethod().invoke(testCode.getObj(), new EnterpriseMessage());
        System.err.println(invoke);

        // 更新操作 - 会创建新的ClassLoader,新的ClassLoader并没有加载,则会重新加载,通过findClass获取
        ClazzCache testCodeUpdate = CustomClassLoaderParam.findClass("TestCode", 2L);
        Object invokeUpdate = testCodeUpdate.getMethod().invoke(testCodeUpdate.getObj(), new EnterpriseMessage());
        System.err.println(invokeUpdate);
        // 再次获取,由于创建了新的ClassLoader,本应通过findClass获取,但做了缓存处理,则应通过缓存获取
        testCode = CustomClassLoaderParam.findClass("TestCodeUpdate", 1L);
        invoke = testCode.getMethod().invoke(testCode.getObj(), new EnterpriseMessage());
        System.err.println(invoke);

    }

}

在这里插入图片描述

ClazzCache.java

import lombok.*;

import java.io.Serializable;
import java.lang.reflect.Method;

/**
 * 自定义classloader缓存类信息
 */
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ClazzCache implements Serializable {

    /**
     * 实例对象
     */
    private Object obj;
    /**
     * 方法
     */
    private Method method;
    /**
     * 最后一次使用时间
     */
    private Long lastUsedTime = System.currentTimeMillis();

}

CustomCompiler.java

import javax.tools.*;
import javax.tools.JavaFileObject.Kind;
import java.io.*;
import java.net.URI;
import java.nio.CharBuffer;
import java.util.*;

/**
 * 自定义编译器
 */
public class CustomCompiler {

    private static JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

    /**
     * 将java代码编译为class字节码并返回byte数组
     */
    public static Map<String, byte[]> compiler(String className, String sourceCode) throws Exception {
        try (
                StandardJavaFileManager stdManager = compiler.getStandardFileManager(null, null, null);
                MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager);
                Writer writer = new StringWriter()
        ) {
            JavaFileObject javaFileObject = manager.makeStringSource(className, sourceCode);
            JavaCompiler.CompilationTask task = compiler.getTask(writer, manager, null, null, null, Arrays.asList(javaFileObject));
            if (task.call()) {
                return manager.getClassBytes();
            } else {
                throw new Exception("编译错误:" + writer.toString());
            }
        }
    }

    /**
     * 内存Java文件管理器
     */
    static class MemoryJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
        final Map<String, byte[]> classBytes = new HashMap<String, byte[]>();
        final Map<String, List<JavaFileObject>> classObjectPackageMap = new HashMap<>();
        MemoryJavaFileManager(JavaFileManager fileManager) {
            super(fileManager);
        }
        public Map<String, byte[]> getClassBytes() {
            return new HashMap(this.classBytes);
        }
        @Override
        public void flush() {

        }
        @Override
        public void close() {
            classBytes.clear();
        }
        @Override
        public Iterable<JavaFileObject> list(Location location, String packageName, Set<Kind> kinds, boolean recurse) throws IOException {
            Iterable<JavaFileObject> it = super.list(location, packageName, kinds, recurse);
            if (kinds.contains(Kind.CLASS)) {
                final List<JavaFileObject> javaFileObjectList = classObjectPackageMap.get(packageName);
                if (javaFileObjectList != null) {
                    if (it != null) {
                        for (JavaFileObject javaFileObject : it) {
                            javaFileObjectList.add(javaFileObject);
                        }
                    }
                    return javaFileObjectList;
                } else {
                    return it;
                }
            } else {
                return it;
            }
        }

        @Override
        public String inferBinaryName(Location location, JavaFileObject file) {
            if (file instanceof MemoryInputJavaClassObject) {
                return ((MemoryInputJavaClassObject) file).inferBinaryName();
            }
            return super.inferBinaryName(location, file);
        }

        @Override
        public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling) throws IOException {
            if (kind == Kind.CLASS) {
                return new MemoryOutputJavaClassObject(className);
            } else {
                return super.getJavaFileForOutput(location, className, kind, sibling);
            }
        }

        JavaFileObject makeStringSource(String className, final String code) {
            String classPath = className.replace('.', '/') + Kind.SOURCE.extension;
            return new SimpleJavaFileObject(URI.create("string:///" + classPath), Kind.SOURCE) {
                @Override
                public CharBuffer getCharContent(boolean ignoreEncodingErrors) {
                    return CharBuffer.wrap(code);
                }
            };
        }

        void makeBinaryClass(String className, final byte[] bs) {
            JavaFileObject javaFileObject = new MemoryInputJavaClassObject(className, bs);
            String packageName = "";
            int pos = className.lastIndexOf('.');
            if (pos > 0) {
                packageName = className.substring(0, pos);
            }
            List<JavaFileObject> javaFileObjectList = classObjectPackageMap.get(packageName);
            if (javaFileObjectList == null) {
                javaFileObjectList = new LinkedList<>();
                javaFileObjectList.add(javaFileObject);
                classObjectPackageMap.put(packageName, javaFileObjectList);
            } else {
                javaFileObjectList.add(javaFileObject);
            }
        }

        class MemoryInputJavaClassObject extends SimpleJavaFileObject {
            final String className;
            final byte[] bs;

            MemoryInputJavaClassObject(String className, byte[] bs) {
                super(URI.create("string:///" + className.replace('.', '/') + Kind.CLASS.extension), Kind.CLASS);
                this.className = className;
                this.bs = bs;
            }

            @Override
            public InputStream openInputStream() {
                return new ByteArrayInputStream(bs);
            }

            public String inferBinaryName() {
                return className;
            }
        }

        class MemoryOutputJavaClassObject extends SimpleJavaFileObject {
            final String className;

            MemoryOutputJavaClassObject(String className) {
                super(URI.create("string:///" + className.replace('.', '/') + Kind.CLASS.extension), Kind.CLASS);
                this.className = className;
            }

            @Override
            public OutputStream openOutputStream() {
                return new FilterOutputStream(new ByteArrayOutputStream()) {
                    @Override
                    public void close() throws IOException {
                        out.close();
                        ByteArrayOutputStream bos = (ByteArrayOutputStream) out;
                        byte[] bs = bos.toByteArray();
                        classBytes.put(className, bs);
                        makeBinaryClass(className, bs);
                    }
                };
            }
        }
    }

}

CustomClassLoader.java

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

/**
 * 自定义classloader
 */
@Slf4j
public class CustomClassLoader extends ClassLoader {
    /**
     * 存放java文件对应的字节码,findClass之前存入,defineClass之后移除
     */
    public static Map<String, byte[]> CLASS_BYTES_TEMP = new ConcurrentHashMap();

    /**
     * MyClassLoader单例模式:DCL方式
     */
    private static volatile CustomClassLoader instance;
    private CustomClassLoader() { }
    public static CustomClassLoader getInstance() {
        if (null == instance) {
            synchronized (CustomClassLoader.class) {
                if (null == instance) {
                    instance = new CustomClassLoader();
                }
            }
        }
        return instance;
    }

    /**
     * 重置classloader,返回新的classLoader
     */
    public synchronized static void createNewClassLoader() {
        instance = new CustomClassLoader();
    }

    /**
     * 编译java文件为字节码数据,并存入缓存中,供后续使用
     */
    public void compiler(String name, byte[] byteClazz) {
        CLASS_BYTES_TEMP.put(name, byteClazz);
    }

    /**
     * 重写findClass,自定义ClassLoader
     */
    @Override
    public Class<?> findClass(String packageName) {
        try {
            log.info(" loadClass [{}] from findClass !", packageName);
            byte[] byteClazz;
            // 从本地缓存获取byte字节码信息
            byteClazz = CLASS_BYTES_TEMP.get(packageName);
            if (byteClazz != null && byteClazz.length > 10) {
                return defineClass(packageName, byteClazz, 0, byteClazz.length);
            }
            return null;
        } finally {
            // 从本地缓存中移除字节码信息
            CLASS_BYTES_TEMP.remove(packageName);
        }
    }

}

CustomClassLoaderParam.java

import com.fintell.dp3.biz.entity.EnterpriseMessage;
import com.fintell.dp3.common.SpringContextHolder;
import com.fintell.dp3.common.redis.RedisClient;
import com.fintell.dp3.common.redis.RedisContact;
import com.fintell.dp3.extend.VariableBuiltInExtend;
import com.fintell.tools.report.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.io.File;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 自定义classloader包装类
 * 1. 通过类名进行加锁处理,保证类仅加载一次
 * 2. 判断本地缓存中,类的时间戳缓存是否存在,若不存在则表名为新增操作
 * 3. 判断本地缓存中,类的时间戳 与 要获取类的时间戳是否一致,若不一致则说明为更新操作,需要创建新的ClassLoader并重新加载类信息
 * 4. 判断本地缓存中,是否已存在类信息,若存在,则直接返回,若不存在,则使用ClassLoader.loadClass()进行加载,并存入缓存中
 */
@Slf4j
public class CustomClassLoaderParam {
    private static RedisClient redisClient = SpringContextHolder.getBean(RedisClient.class);
    /**
     * 扩展类的包路径,使用VariableBuiltInExtend所在的包路径
     */
    public static final String basePackage = VariableBuiltInExtend.class.getPackage().getName() + ".";
    /**
     * 类信息缓存
     */
    private static Map<String, ClazzCache> cacheClazz = new ConcurrentHashMap<>();
    /**
     * 用于记录当前加载的类的对应时间
     * key : name
     * value : 装载类的时间戳
     */
    private static Map<String, Long> updateClazzs = new ConcurrentHashMap<>();

    /**
     * 重写findClass,做缓存处理
     *
     * @param className        : 类名 -> 变量名
     * @param timestampCurrent : 变量最新的时间戳
     */
    public static ClazzCache findClass(String className, Long timestampCurrent) throws Exception {
        // 通过类名加锁
        synchronized (className) {
            Long timeStampLastModify = updateClazzs.get(className);
            // 1. 如果缓存中没有,则说明为新增操作,则重新装载class
            if (timeStampLastModify == null) {
                cacheClazz.remove(className);
            }
            // 2. 如果缓存中存在,且时间不一致,则说明为更新操作,则使用新的classloader并重新装载class文件
            if (timeStampLastModify != null && timestampCurrent != timeStampLastModify) {
                cacheClazz.remove(className);
                CustomClassLoader.createNewClassLoader();
            }
            // 1> 从缓存中获取
            ClazzCache cache = cacheClazz.get(className);
            if (null != cache) {
                // 2> 若缓存存在,则更新最后使用时间,并直接返回
                cache.setLastUsedTime(System.currentTimeMillis());
                log.info(" loadClass [{}] from cache !", className);
                return cache;
            }
            // 1. 若缓存中获取失败,则说明需要重新装载class文件,findClass之前先进行编译java数据为字节码数组
            CustomClassLoader.getInstance().compiler(basePackage + className, getClazzCode(className));
            // 2. 使用新的ClassLoader重新加载class
            Class aClass = CustomClassLoader.getInstance().loadClass(basePackage + className);
            // 3. 将数据放入本地缓存
            Method method = aClass.getDeclaredMethod("invoke", EnterpriseMessage.class);
            method.setAccessible(true);
            ClazzCache clazzCache = ClazzCache.builder().obj(aClass.newInstance()).method(method).build();
            cacheClazz.put(className, clazzCache);
            // 4. 更新缓存时间
            updateClazzs.put(className, timestampCurrent);
            // 5. 返回缓存类信息
            return clazzCache;
        }
    }

    /**
     * 获取字节码信息
     */
    private static byte[] getClazzCode(String className) throws Exception{
        byte[] byteClazz;
        // 1. 从redis获取字节码
        byteClazz = (byte[]) redisClient.getObj(RedisContact.getJavaCodeKey(className));
        // 1.1 若从redis获取到字节码信息,则加载class
        if (byteClazz != null && byteClazz.length > 10) {
            return byteClazz;
        }
        // 2. 从数据库获取源码并进行编译
        String sourceCode = getSourceCode(className);
        if (sourceCode == null || StringUtils.isEmpty(sourceCode)) {
            throw new Exception("自定义变量不存在或变量逻辑不存在!");
        }
        // 3. 对源码进行编译
        Map<String, byte[]> compiler = CustomCompiler.compiler(className, sourceCode);
        byteClazz = compiler.get(basePackage + className);
        // 4. 放入缓存
        redisClient.set(RedisContact.getJavaCodeKey(className), byteClazz);
        return byteClazz;
    }

    /**
     * 模拟从数据库获取源码
     */
    private static String getSourceCode(String className) throws Exception {
        return FileUtil.readFile(new File("D:\\cls\\" + className + ".java"), "utf-8");
    }

}

TestCode.java

package com.fintell.dp3.extend;

import com.fintell.dp3.biz.entity.EnterpriseMessage;

public class TestCode {

    public Object invoke(EnterpriseMessage enterpriseMessage) throws Exception{
        Object defaultVal = 0;
        try {
            System.err.println(" add  ");
            return defaultVal;
        } catch (NullPointerException ne) {
            return defaultVal;
        } catch (Exception e) {
            throw e;
        }
    }

}

TestCodeUpdate.java

package com.fintell.dp3.extend;

import com.fintell.dp3.biz.entity.EnterpriseMessage;

public class TestCodeUpdate {

    public Object invoke(EnterpriseMessage enterpriseMessage) throws Exception{
        Object defaultVal = 0;
        try {
            System.err.println(" update  ");
            return defaultVal;
        } catch (NullPointerException ne) {
            return defaultVal;
        } catch (Exception e) {
            throw e;
        }
    }

}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
ClassLoaderJava中用于加载类文件的机制,可以通过在运行时动态加载新的类文件,实现热部署的功能。以下是一个简单的ClassLoader实现热部署的示例: ```java public class HotSwapClassLoader extends ClassLoader { public HotSwapClassLoader() { super(HotSwapClassLoader.class.getClassLoader()); } public Class loadClass(String className, boolean resolve) throws ClassNotFoundException { Class clazz = findLoadedClass(className); if (clazz == null) { try { clazz = findClass(className); } catch (ClassNotFoundException e) { clazz = super.loadClass(className, resolve); } } if (resolve) { resolveClass(clazz); } return clazz; } public Class loadByte(byte[] classBytes) { return defineClass(null, classBytes, 0, classBytes.length); } } ``` 这个HotSwapClassLoader类继承了JavaClassLoader类,重写了loadClass方法来实现热部署的功能。在loadClass方法中,先检查是否已经加载了该类,如果没有则自己尝试加载,如果还是找不到则交给父ClassLoader来加载。在loadByte方法中,通过字节流加载新的类文件。 使用这个HotSwapClassLoader类,可以在运行时动态地加载新的类文件,实现热部署的功能。例如,可以创建一个测试类: ```java public class Test { public void hello() { System.out.println("Hello World!"); } } ``` 然后在另外一个类中,使用HotSwapClassLoader来加载这个类,并调用它的hello方法: ```java public class Main { public static void main(String[] args) throws Exception { HotSwapClassLoader loader = new HotSwapClassLoader(); Class clazz = loader.loadClass("Test", false); Object obj = clazz.newInstance(); Method method = clazz.getDeclaredMethod("hello", null); method.invoke(obj, null); } } ``` 这个程序会输出"Hello World!",然后可以修改Test类的代码,重新编译,再运行Main类,就可以看到新的代码被加载并执行了,实现热部署的功能。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小安灬

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

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

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

打赏作者

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

抵扣说明:

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

余额充值