一 背景
java代码动态编译的场景有很多,比如说
-
1、 开 发 分 布 式 应 用。 这 对 开 发 远 程 的 客 户 端 应 用 程 序 最 有 用, 客 户 端 仅 需 要 安 装 一 些 基 本 的 系 统 和 一 个 能 实 现 动 态 类 载 入 机 制 的 类, 需 要 本 地 系 统 不 存 在 的 功 能 时, 仅 需 要 从 网 络 动 态 载 入 并 执 行 相 应 类 即 可 获 得 特 定 功 能。 因 为 客 户 端 所 使 用 的 总 是 软 件 的 最 新 版 本, 所 以 不 再 存 在 软 件 的 升 级 和 维 护 问 题, 即 实 现 了 所 谓 的" 零 管 理" 模 式。 -
2、 对.class 文 件 加 密。 由 于Java 的 字 节 码(bytecode) 容 易 被 反 编 译, 大 部 分 开 发Java 应 用 程 序 的 公 司 均 担 心 自 己 的 成 果 被 别 人 不 劳 而 获。 其 实 可 以 将 类 文 件 进 行 适 当 的 加 密 处 理, 执 行 时 使 用 自 己 的 类 载 入 器 进 行 相 应 的 解 密, 就 可 以 解 决 这 个 问 题。 -
3、 使 第 三 方 开 发 者 易 于 扩 展 你 的 应 用。 从 前 面 可 知, 所 有 可 以 被 你 的 类 载 入 器 动 态 载 入 并 被 执 行 的 类, 必 须 继 承 你 定 义 的 类 或 实 现 你 定 义 的 接 口, 这 样, 你 可 以 制 订 一 些 规 则, 使 其 他 开 发 者 不 必 了 解 你 的 应 用 程 序 也 可 以 扩 充 功 能。
在我们实际工作中,难免有一些简单的查询需求,并且这些需求是经常变动的,如果每次都因为加几个字段上线一次,显然效率是很低的,这个时候我们就可以将这些简单的查询代码写到类似于disconf的配置文件中,如果要进行简单的改动,直接将代码加上发布就可以了。
二 实际操作
我们想进行远端代码动态加载,首先要解决两个问题。 1 代码动态编译,而且要编译的是一串java字符串代码,并不是文件。springboot下还要解决编译时的‘jar in jar’问题,要编译的代码对jar包进行引用,这些被引用的jar包都已经打包在sprngboot项目的jar包里面,如果不解决这个问题,就会出现 idea开发模式下能够正常运行,但是到了打完包运行的时候,会报 classnotfound 异常。 2 代码动态加载,这个就需要重写java的 classloader的 findClass 方法 ,判定如果是远端配置文件的代码,就要调用自定义类加载器的 defineClass。
三 关键代码
1 编译代码
private static Map<String, byte[]> compile(String className, String javaStr) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager stdManager = getStandardFileManager(null, null, null);
try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) {
JavaFileObject javaFileObject = manager.makeStringSource(className, javaStr);
JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, Arrays.asList(javaFileObject));
Boolean result = task.call();
if (result != null && result.booleanValue()) {
return manager.getClassBytes();
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
2 字节码加载入内存
public class MemoryClassLoader extends URLClassLoader {
private static final Map<String, byte[]> classBytes = new ConcurrentHashMap<>();
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] buf = classBytes.get(name);
if (buf == null) {
return super.findClass(name);
}
classBytes.remove(name);
return defineClass(name, buf, 0, buf.length);
}
}
3 解决jar in jar 问题
class MemoryJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
final Map<String, byte[]> classBytes = new HashMap<String, byte[]>();
final Map<String, List<JavaFileObject>> classObjectPackageMap = new HashMap<>();
private JavacFileManager javaFileManager;
public final static Map<String, List<JavaFileObject>> CLASS_OBJECT_PACKAGE_MAP = new HashMap<>();
private static final Object lock = new Object();
private boolean isInit = false;
public void init(){
try {
String jarBaseFile = MemoryClassLoader.getPath();
JarFile jarFile = new JarFile(new File(jarBaseFile));
List<JarEntry> entries = jarFile.stream().filter(jarEntry -> {
return jarEntry.getName().endsWith(".jar");
}).collect(Collectors.toList());
JarFile libTempJarFile = null;
List<JavaFileObject> onePackgeJavaFiles = null;
String packgeName = null;
for (JarEntry entry : entries) {
libTempJarFile = jarFile.getNestedJarFile(jarFile.getEntry(entry.getName()));
if(libTempJarFile.getName().contains("tools.jar")){
continue;
}
Enumeration<JarEntry> tempEntriesEnum = libTempJarFile.entries();
while (tempEntriesEnum.hasMoreElements()) {
JarEntry jarEntry = tempEntriesEnum.nextElement();
String classPath = jarEntry.getName().replace("/", ".");
if (!classPath.endsWith(".class") || jarEntry.getName().lastIndexOf("/") == -1) {
continue;
} else {
packgeName = classPath.substring(0, jarEntry.getName().lastIndexOf("/"));
onePackgeJavaFiles = CLASS_OBJECT_PACKAGE_MAP.containsKey(packgeName) ? CLASS_OBJECT_PACKAGE_MAP.get(packgeName) : new ArrayList<>();
onePackgeJavaFiles.add(new MemorySpringBootInfoJavaClassObject(jarEntry.getName().replace("/", ".").replace(".class", ""),
new URL(libTempJarFile.getUrl(), jarEntry.getName()), javaFileManager));
CLASS_OBJECT_PACKAGE_MAP.put(packgeName,onePackgeJavaFiles);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
isInit = true;
}
MemoryJavaFileManager(JavaFileManager fileManager) {
super(fileManager);
this.javaFileManager = (JavacFileManager)fileManager;
}
public Map<String, byte[]> getClassBytes() {
return new HashMap<String, byte[]>(this.classBytes);
}
@Override
public void flush() throws IOException {
}
@Override
public void close() throws IOException {
classBytes.clear();
}
public List<JavaFileObject> getLibJarsOptions(String packgeName) {
synchronized (lock){
if(!isInit){
init();
}
}
return CLASS_OBJECT_PACKAGE_MAP.get(packgeName);
}
@Override
public Iterable<JavaFileObject> list(Location location,
String packageName,
Set<Kind> kinds,
boolean recurse)
throws IOException {
if ("CLASS_PATH".equals(location.getName()) && MemoryClassLoader.isJar()) {
List<JavaFileObject> result = getLibJarsOptions(packageName);
if(result!=null){
return result;
}
}
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);
}
};
}
}
}
四 github示例
https://github.com/1315402725/compile-demo
|