通过Java字节码技术,实现对代码的动态修改,不需要重启服务或者热替换,即可实现业务功能的逻辑修改!
将字节数组转换为类class的实例,根据指定的字节数据创建指定名称的Class对象
/**
* 自定义类加载器
*
* @author huxiang
*/
public class BizClassLoader extends ClassLoader {
/**
* 重写类加载器方法
*
* @param fullName 全限定类名
* @param javaClassObject class对象
* @return
*/
public Class loadClass(String fullName, BizJavaClassFileObject javaClassObject) {
byte[] classData = javaClassObject.getBytes();
return this.defineClass(fullName, classData, 0, classData.length);
}
}
主要是为了重写openOutStream()方法,不输出字节码文件到文件,而是直接保存在一个字节输出流中
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
/**
* 自定义字节码文件类,重写openOutStream()方法,不输出字节码文件到文件,而是直接保存在一个输出流中
*
* @author huxiang
*/
public class BizJavaClassFileObject extends SimpleJavaFileObject {
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
public BizJavaClassFileObject(String name, JavaFileObject.Kind kind) {
super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind);
}
public byte[] getBytes() {
return outputStream.toByteArray();
}
//编译时候会调用openOutputStream获取输出流,并输出数据
@Override
public OutputStream openOutputStream() throws IOException {
return outputStream;
}
}
用于操作Java源码文件和类文件的抽象文件工具
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
/**
* 自定义文件管理器,重写getJavaFileOutput()方法,输出我们自定义的Java字节码对象。
*
* @author huxiang
*/
public class BizFileManager extends ForwardingJavaFileManager {
private BizJavaClassFileObject javaClassObject;
protected BizFileManager(StandardJavaFileManager standardJavaFileManager) {
super(standardJavaFileManager);
}
public BizJavaClassFileObject getJavaClassObject() {
return javaClassObject;
}
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className,
JavaFileObject.Kind kind, FileObject sibling) {
//生命周期内的Java编译对象,尽量复用,不要直接new
if (this.javaClassObject == null) {
this.javaClassObject = new BizJavaClassFileObject(className, kind);
}
return javaClassObject;
}
}
实现SimpleJavaFileObject 方法,SimpleJavaFileObject 为JavaFileObject中的大多数方法提供简单的实现,虽然JavaFileObject实现的源码文件基类很多,但是一般情况下使用SimpleJavaFileObject 足以处理90%以上业务的功能
import javax.tools.SimpleJavaFileObject;
import java.io.IOException;
import java.net.URI;
/**
* 自定义源码文件类,输出我们自己写的Java字节码文件类
*
* @author huxiang
*/
public class BizSimpleJavaFileObject extends SimpleJavaFileObject {
/**
* 源码文件内容
*/
private String contents = null;
/**
* 全限定类名
*/
private String className;
/**
* Kind.SOURCE表示要创建到内存中的源码文件为.java
*
* @param className
* @param contents
*/
public BizSimpleJavaFileObject(String className, String contents) {
super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.className = className;
this.contents = contents;
}
public CharSequence getCharContent(boolean ignoredEncodingErrors) throws IOException {
return contents;
}
public String getClassName() {
return className;
}
}
对外入口,一般建议加载到Spring容器中,实现方法的invoke
import com.paratera.console.common.BizException;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.tools.*;
import java.io.*;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 自定义动态编译器,invoke为入口,包括解析代码字符串,将代码字符串转换为Class,Object可执行对象,然后反射调用目标方法
*
* @author huxiang
*/
@Component
@Order(value = 2)
public class DynamicCompiler {
/**
* 编译出Class对象并加载到JVM内存
*
* @param fullClassName 全路径的类名
* @param javaCode java代码
* @return 目标类
*/
private Class<?> compileToClass(String fullClassName, String javaCode) throws Exception {
//获取环境Java编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//编译文件仓库对象
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
//编码设置
List<String> options = new ArrayList<>();
options.add("-encoding");
options.add("UTF-8");
//文件管理器对象,将文件仓库管理起来
BizFileManager fileManager = new BizFileManager(compiler.getStandardFileManager(diagnostics, null, null));
//需要编译成.java的文件对象,如果有多个,需要多次添加,我们这边就一个,并且限定了全路径类名和Java代码字符串
List<JavaFileObject> jfiles = new ArrayList<>();
jfiles.add(new BizSimpleJavaFileObject(fullClassName, javaCode));
//编译任务开启,正常这一步是JVM虚拟机调度线程自动执行
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, jfiles);
//编译任务执行
boolean success = task.call();
if (success) {
//编译成功,加载class对象至内存(而不是生成实实在在的.class文件到磁盘)
BizJavaClassFileObject javaClassObject = fileManager.getJavaClassObject();
BizClassLoader dynamicClassLoader = new BizClassLoader();
return dynamicClassLoader.loadClass(fullClassName, javaClassObject);
} else {
//编译出错(比如字符串非标准Java格式时异常)
for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
String error = compileError(diagnostic);
throw new RuntimeException(error);
}
throw new RuntimeException("编译失败!");
}
}
/**
* 编译错误信息异常报告
*
* @param diagnostic
* @return
*/
private String compileError(Diagnostic diagnostic) {
StringBuilder res = new StringBuilder();
res.append("LineNumber:[").append(diagnostic.getLineNumber()).append("]\n");
res.append("ColumnNumber:[").append(diagnostic.getColumnNumber()).append("]\n");
res.append("Message:[").append(diagnostic.getMessage(null)).append("]\n");
return res.toString();
}
/**
* 目标方法反射执行
*
* @param methodName
* @param args
* @return
*/
public Object invoke(String methodName, String... args) throws BizException {
//获取字符串代码内容
StringBuilder file = getFileInfo();
Class<?> clazz = null;
Object obj = null;
try {
//字节码编译处理,得到Class对象和执行对象
clazz = this.compileToClass("com.paratera.console.biz.handler.compiler.Compiler", file.toString());
obj = clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new BizException("反射获取对象实例异常:" + e.getMessage());
}
//反射调用目标方法
Method[] test = clazz.getDeclaredMethods();
List<Method> methods = Arrays.stream(test).filter(app ->
StringUtils.equals(app.getName(), methodName)).toList();
try {
return methods.get(0).invoke(obj, args);
} catch (Exception e) {
throw new BizException("没有该动态编译运行方法或则参数不匹配");
}
}
/**
* 读取文件内容
*
* @return
*/
@NotNull
private StringBuilder getFileInfo() throws BizException {
StringBuilder file = new StringBuilder();
InputStream is = this.getClass().getResourceAsStream("/compiler.txt");
InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
String line;
while (true) {
try {
if (!((line = br.readLine()) != null)) {
break;
}
} catch (IOException e) {
throw new BizException("IO流异常-1:" + e.getMessage());
}
file.append(line).append("\n");
}
try {
br.close();
isr.close();
is.close();
} catch (IOException e) {
throw new BizException("IO流异常-2:" + e.getMessage());
}
return file;
}
}
将可执行程序内容复制到resources/compiler.txt文件中,注意一定要是Java标准格式,比如”//“注释不要出现
import lombok.extern.slf4j.Slf4j;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
public class Compiler {
/**
* 解析1
*
* @param message
* @return
*/
public List
dynamicCompiler.invoke传入需要动态执行的方法名以及参数即可,参数可多个,根据目标方法来即可
import com.paratera.console.biz.Application;
import com.paratera.console.biz.handler.compiler.DynamicCompiler;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(
classes = Application.class,
args = {"--spring.config.import=configserver:http://localhost:20010", "--spring.profiles.active=local"},
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
public class TestCompiler {
@Autowired
private DynamicCompiler dynamicCompiler;
@Test
public void getQuota1() throws Exception {
Object obj = dynamicCompiler.invoke("getQuota1", """
Disk quotas for user pp824 (uid 6555):
Filesystem used quota limit grace files quota limit grace
/PARA/pp824 101500000 102400000 204800000 - 61739953 0 0 -
""");
System.out.println(obj);
}
@Test
public void getQuota2() throws Exception {
Object obj = dynamicCompiler.invoke("getQuota2", """
Block Limits | File Limits
Filesystem type KB quota limit in_doubt grace | files quota limit in_doubt grace Remarks
ssd USR 0 1048576000 2097152000 0 none | 1 9000000 10000000 0 none njugpfs_ssd.ssd02
""");
System.out.println(obj);
}
@Test
public void getQuota3() throws Exception {
Object obj = dynamicCompiler.invoke("getQuota3", """
Block Limits | File Limits
Filesystem Fileset type KB quota limit in_doubt grace | files quota limit in_doubt grace Remarks
dssg root USR 435054592 1048576000 1048576000 0 none | 1729156 0 0 0 none\s
""");
System.out.println(obj);
}
@Test
public void getQuota4() throws Exception {
Object obj = dynamicCompiler.invoke("getQuota4", """
Home DiskUsage : [150G] (Limit:300G)
Work1 DiskUsage : [377G] (Limit:6.0T)
""");
System.out.println(obj);
}
}