需求如下:
- 可在前端新增、修改java代码,并可实现服务不重启的前提下进行代码的部署运行。
- 相当于:可实现java代码的热部署。
代码如下:
- MyClassLoaderTest.java:测试类
- MyCompiler.java:编译类,可将java代码编译为字节码byte数组
- MyClassLoaderHelp.java:自定义classloader封装类
- MyClassLoader.java:自定义classloader(不要直接使用此类中的方法,应通过使用MyClassLoaderHelp来实现功能)
MyClassLoaderTest.java
import java.lang.reflect.Method;
public class MyClassLoaderTest {
/**
* 包路径
*/
private static final String pack = "com.code";
/**
* 类名称
*/
private static final String className = "TestCode";
private static String sourceCode = "package " + pack + ";" +
"public class " + className + " {" +
" public String invoke(String msg) throws Exception {" +
" return msg;" +
" }" +
"}";
/**
* cache :
* paramCode、paramLogic、time(时间戳)
* localMap :
* paramCode、time(时间戳)
*/
public static void main(String[] args) throws Exception {
/**
* 使用自定义classloader加载
*/
// 1. 加载class文件到自定义classloader
Class<?> clazz = MyClassLoaderHelp.findClass(pack + "." + className, 1L, sourceCode);
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("invoke", String.class);
method.setAccessible(true);
System.err.println(method.invoke(obj, "hello world!"));
/**
* 模拟类信息发生变化,则可以实现热部署并直接执行新代码
* 1. 当nonaTime变化时,会进行类的热部署,这里考虑使用时间是考虑到分布式服务部署的话,用更新标志位会产生问题,故使用代码的最新时间与已加载的类的时间来做比对
* 2. 修改代码以达到效果展示
*/
long nanoTime = 2L;
sourceCode = "package " + pack + ";" +
"public class " + className + " {" +
" public String invoke(String msg) throws Exception {" +
" return msg + \"代码改动,执行新的代码;\";" +
" }" +
"}";
for (int i = 0; i < 2; i++) {
// 当i=0,由于需要重新加载,则需要通过load加载
// 当i=1,由于已加载过,则可直接从cache加载
clazz = MyClassLoaderHelp.findClass(pack + "." + className, nanoTime, sourceCode);
obj = clazz.newInstance();
method = clazz.getDeclaredMethod("invoke", String.class);
method.setAccessible(true);
System.err.println(method.invoke(obj, "hello world!"));
}
}
}
MyCompiler.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 MyCompiler {
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)
) {
JavaFileObject javaFileObject = manager.makeStringSource(className, sourceCode);
JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, Arrays.asList(javaFileObject));
if (task.call()) {
return manager.getClassBytes();
}
}
return null;
}
/**
* 内存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(JavaFileManager.Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
if (kind == JavaFileObject.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('.', '/') + JavaFileObject.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);
}
};
}
}
}
}
MyClassLoaderHelp.java
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 自定义classloader包装类
*/
public class MyClassLoaderHelp {
/**
* 类信息缓存
*/
private static Map<String, Class> clazzs = new ConcurrentHashMap<>();
/**
* 用于记录当前加载的类的对应时间
* key : name
* value : 装载类的时间戳
*/
private static Map<String, Long> updateClazzs = new ConcurrentHashMap<>();
/**
* 重写findClass
*/
public static Class<?> findClass(String name, Long timestampCurrent, String sourceCode) throws Exception {
Long timeStampLastModify = updateClazzs.get(name);
// 1. 如果缓存中没有,则说明为新增操作,则重新装载class
if (timeStampLastModify == null) {
clazzs.remove(name);
}
// 2. 如果缓存中存在,且时间不一致,则说明有更新操作,则使用新的classloader并重新装载class文件
if (timeStampLastModify != null && timestampCurrent != timeStampLastModify) {
clazzs.remove(name);
MyClassLoader.createNewClassLoader();
}
// 1> 从缓存中获取
Class cache = clazzs.get(name);
if (null != cache) {
// 2> 若缓存存在,则直接返回
System.err.println(" 从缓存获取类信息! ");
return cache;
}
// 1. 若缓存中获取失败,则说明需要重新装载class文件,findClass之前先进行编译java数据为字节码数组
MyClassLoader.getInstance().compiler(name, sourceCode);
// 2. 通过字节码数组加载class
Class<?> aClass = MyClassLoader.getInstance().findClass(name);
// 3. 将数据放入缓存
clazzs.put(name, aClass);
// 4. 更新缓存时间
updateClazzs.put(name, timestampCurrent);
return aClass;
}
}
MyClassLoader.java
import java.util.HashMap;
import java.util.Map;
/**
* 自定义classloader
*/
public class MyClassLoader extends ClassLoader {
// 存放java文件对应的字节码,后期可以改存到redis,减少编译时间
private static Map<String, byte[]> CLASS_BYTES = new HashMap();
// MyClassLoader单例模式
private static volatile MyClassLoader instance;
private MyClassLoader() { }
public static MyClassLoader getInstance() {
if (null == instance) {
synchronized (MyClassLoader.class) {
if (null == instance) {
instance = new MyClassLoader();
}
}
}
return instance;
}
/**
* 重置classloader,返回新的classLoader,并从缓存移除此装载的类
*/
public synchronized static void createNewClassLoader() {
instance = new MyClassLoader();
}
/**
* 编译java文件并存入缓存中
*/
public void compiler(String name, String sourceCode) throws Exception {
CLASS_BYTES.put(name, MyCompiler.compiler(getClassName(name), sourceCode).get(name));
}
/**
* 重写findClass:此方法不要使用
*/
@Override
public Class<?> findClass(String name) {
System.err.println(" 通过findclass加载类信息! ");
byte[] b = CLASS_BYTES.get(name);
Class<?> aClass = defineClass(name, b, 0, b.length);
CLASS_BYTES.remove(name);
return aClass;
}
/**
* 获取类名称
* @param name : com.xx.ClassName
*/
private static String getClassName(String name) {
String[] packagePaths = name.split("\\.");
return packagePaths[packagePaths.length - 1];
}
}