需求如下:
- 可在前端新增、修改java代码,并可实现服务不重启的前提下进行代码的部署运行。
- 相当于:可实现java代码的热部署。
代码如下:
- ClassLoaderTest: 自定义classloader测试类
- ClazzCache: 自定义ClassLoader缓存类
- CustomCompiler: 自定义编译器
- CustomClassLoader: 自定义ClassLoader
- CustomClassLoaderParam: 自定义ClassLoader包装类(不要直接使用此类中的方法,应通过使用CustomClassLoader来实现功能)
- TestCode: 模拟前端写的java代码
- 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;
}
}
}