1、什么是动态编译
在 Java 中,动态编译是指在运行时动态地编译 Java 源代码,生成字节码,并加载到 JVM 中执行。动态编译可以用于实现动态代码生成、动态加载、插件化等功能。
动态编译,简单来说就是在Java程序运行时编译源代码。
从JDK1.6开始,引入了Java代码重写过的编译器接口,使得我们可以在运行时编译Java源代码,然后再通过类加载器将编译好的类加载进JVM,这种在运行时编译代码的操作就叫做动态编译。
静态编译:编译时就把所有用到的Java代码全都编译成字节码,是一次性编译。
动态编译:在Java程序运行时才把需要的Java代码的编译成字节码,是按需编译。
静态编译示例:
静态编译实际上就是在程序运行前将所有代码进行编译,我们在运行程序前用Javac命令或点击IDE的编译按钮进行编译都属于静态编译。
比如,我们编写了一个xxx.java
文件,里面是一个功能类,如果我们的程序想要使用这个类,就必须在程序启动前,先调用Javac编译器来生成字节码文件。
如果使用动态编译,则可以在程序运行过程中再对xxx.java
文件进行编译,之后再通过类加载器对编译好的类进行加载,同样能正常使用这个功能类。
1.1、动态编译的相关概念
-
JavaFileManager 对象:用于管理编译过程中的文件。
- JavaFileManager 是一个接口,提供了对 Java 文件的管理功能,包括创建、查找、读写等操作。JavaFileManager 有多种实现方式,例如 StandardJavaFileManager、ForwardingJavaFileManager 等。
-
DiagnosticListener 对象:用于收集编译时的诊断信息。
- DiagnosticListener 是一个接口,用于接收编译时的诊断信息,例如错误、警告等。
-
JavaFileObject 对象:表示要编译的 Java 源代码。
- JavaFileObject 是一个抽象类,用于表示 Java 源代码或字节码。JavaFileObject 有多种实现方式,例如 SimpleJavaFileObject、JavaFileObjectWrapper 等。
1.2、如何简单的实现动态编译
- 创建一个 JavaCompiler 对象,该对象用于编译 Java 源代码。
- 创建一个 DiagnosticCollector 对象,该对象用于收集编译时的诊断信息。
- 创建一个 JavaFileManager 对象,该对象用于管理编译过程中的文件。
- 创建一个 JavaFileObject 对象,该对象用于表示要编译的 Java 源代码。
- 调用 JavaCompiler 对象的 getTask 方法,传入 JavaFileManager 对象和 DiagnosticCollector 对象,获取一个 CompilationTask 对象。
- 调用 CompilationTask 对象的 call 方法,编译 Java 源代码。
- 获取 DiagnosticCollector 对象的诊断信息,并处理编译结果。
下面是一个简单的示例,演示如何使用动态编译:
public class DynamicCompiler {
public static void main(String[] args) throws Exception {
// 创建 JavaCompiler 对象
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 创建 DiagnosticCollector 对象,用于收集编译时的诊断信息
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
// 创建 JavaFileManager 对象,用于管理编译过程中的文件
StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);
// 创建 JavaFileObject 对象,用于表示要编译的 Java 源代码
String code = "public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World!"); } }";
JavaFileObject source = new JavaSourceFromString("HelloWorld", code);
// 获取 CompilationTask 对象
Iterable<? extends JavaFileObject> compilationUnits = Arrays.asList(source);
CompilationTask task = compiler.getTask(null, fileManager, diagnostics, null, null, compilationUnits);
// 编译 Java 源代码
boolean success = task.call();
// 获取诊断信息
List<Diagnostic<? extends JavaFileObject>> messages = diagnostics.getDiagnostics();
for (Diagnostic<? extends JavaFileObject> message : messages) {
System.out.println(message.getMessage(null));
}
// 处理编译结果
if (success) {
System.out.println("Compilation was successful.");
} else {
System.out.println("Compilation failed.");
}
fileManager.close();
}
}
class JavaSourceFromString extends SimpleJavaFileObject {
final String code;
JavaSourceFromString(String name, String code) {
super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.code = code;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
}
运行结果:
Hello World!
Compilation was successful.
1.3 动态编译应用场景:
-
代码生成:动态生成代码并编译执行,例如模板引擎。
-
在线 hot-swap:在不重启应用的情况下更新代码和重新加载类。
-
动态数据库访问:编译运行时生成的 SQL 或 NoSQL 查询。
-
安全环境:在安全沙箱中执行不受信任的代码。
-
动态语言支持:支持类似 Ruby、Groovy 等动态语言的特性。
2、如何结合 springboot 项目使用
上面展示了如何简单使用 Java 的动态编译功能,但是在日常项目开发中,会面对更多的场景。结合前言中我所遇到的问题,我简单的给大家介绍下我在项目中是如何使用 Java 的动态编译功能来解决我所遇到的问题的。
我当时的想法是这样的:
这样,各个业务方就可以自己管理自己的代码块,与外部对接或者修改代码无需在发布应用,彻底解放了我,让我有更多的精力给公司做更重要的事情!
2.1、动态编译在项目中遇到的问题
2.1.1、必须重写类加载器新编译的代码才能生效
在 Java 中使用动态编译功能时,重写类加载器是必要的。这是因为动态编译生成的类需要加载到 JVM 中执行,而默认的类加载器无法加载动态生成的类。
在 Java 中,类加载器分为三种:启动类加载器、扩展类加载器和应用程序类加载器。默认情况下,Java 使用应用程序类加载器来加载类。应用程序类加载器只能加载预先编译好的类,无法加载动态生成的类。因此,我们需要重写类加载器,使其能够加载动态生成的类。
重写类加载器有两种方式:继承 ClassLoader 类或实现 ClassLoader 接口。一般情况下,我们建议使用继承 ClassLoader 类的方式,因为这样可以更方便地控制类加载的过程。
当我们重写类加载器时,需要实现 findClass 方法。findClass 方法用于查找指定名称的类。如果类已经被加载过,可以直接返回已加载的类;否则,需要使用动态编译生成类的字节码,并通过 defineClass 方法将其加载到 JVM 中执行。
2.1.2、没有依赖的简单代码可以编译成功,但是一旦有依赖关系,编译就会失败
Java 编译器是通过 JavaFileManager 来加载相关依赖类的,如果不重写使用的是默认的 JavaFileManager 来获取 springboot 的 jarFile 来读取嵌套 jar,自然是获取不到的,需要我们重写 JavaFileManager,去获取编译代码所需的依赖,具体写法详见 2.2 代码示例。
2.2、动态编译的应用
(1)、从源码文件编译得到字节码文件
刚才我们使用动态编译完成了输入一个Java源文件(.java),再到输出字节码文件(.class)的操作。这是从源码文件编译得到字节码文件的方式,实质上也是从磁盘输入,再输出到磁盘的方式。
(2)、从源码字符串编译得到字节码文件
假如现在有一串字符串形式的Java代码,那如何使用动态编译将这些字符串代码编译成字节码文件?这是从源码字符串编译得到字节码文件的方式,实质上也是从内存中得到源码,再输出到磁盘的方式。
根据刚才的代码,我们知道编译任务getTask()
这个方法一共有 6 个参数,它们分别是:
Writer out
:编译器的一个额外的输出 Writer,为 null 的话就是 System.err;JavaFileManager fileManager
:文件管理器;DiagnosticListener<? super JavaFileObject> diagnosticListener
:诊断信息收集器;Iterable<String> options
:编译器的配置;Iterable<String> classes
:需要被 annotation processing 处理的类的类名;Iterable<? extends JavaFileObject> compilationUnits
:要被编译的单元们,就是一堆 JavaFileObject。
根据getTask()
的参数,我们知道编译器执行编译所需要的对象类型并不是文件File
对象,而是JavaFileObject
对象。因此,要实现从字符串源码编译得到字节码文件,只需要把字符串源码变为JavaFileObject
对象即可。
但JavaFileObject
是一个接口,它的标准实现类SimpleJavaFileObject
提供的一些方法是面向类源码文件(.java)和字节码文件(.class)的,而我们进行动态编译时输入的是字符串源码,所以我们需要自行实现JavaFileObject
,以使JavaFileObject
对象能装入我们的字符串源码。
具体的实现方法就是可以直接继承SimpleJavaFileObject
类,再重写其中的一些方法使它能够装入字符串即可。
可以通过查看compiler.getTask().call()
的源代码来查看具体用到了SimpleJavaFileObject
的那些方法,这样我们才知道需要重写 SimpleJavaFileObject
的哪些方法。
一篇大佬分析getTask().call()
源代码执行流程的文章介绍得很十分详细,强烈推荐:Java 类运行时动态编译技术.https://seanwangjs.github.io/2018/03/13/java-runtime-compile.html。
简单的流程如下:
在上图中,getTask().call()
会通过调用作为参数传入的JavaFileObject
对象的getCharContent()
方法获得字符串序列,即源码的读取是通过 JavaFileObject
的 getCharContent()
方法,那我们只需要重写getCharContent()
方法,即可将我们的字符串源码装进JavaFileObject
了。
构造SourceJavaFileObject
实现定制的JavaFileObject
对象,用于存储字符串源码:
public class SourceJavaFileObject extends SimpleJavaFileObject {
private String source; //源码字符串
//返回源码字符串
public SourceJavaFileObject(String name, String sourceStr){
super(URI.create("String:///" + name + Kind.SOURCE.extension),Kind.SOURCE);
this.source = sourceStr;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException{
if(source == null) throw new IllegalArgumentException("source == null");
else return source;
}
}
则创建JavaFileObject
对象时,变为了:
//使用重写getCharContent方法后的JavaFileObject构造参数
JavaFileObject sourceFileObject = new SourceJavaFileObject(className, source);
//执行编译
Boolean result = compiler.getTask(null, fileManager, null, null, null, Arrays.asList(sourceFileObject)).call();
由于我们自定了JavaFileObject
,文件管理器 fileManager
更像是一个工具类用于把 File
对象数组自动转换成JavaFileObject 列表
,换成手动生成 compilationUnits
列表并传入也是可行的。(上述代码就是使用了Arrays.asList()
手动生成 compilationUnits
列表)。
至此,只需要调用getTask().call()
就能将字符串形式的源码编译成字节码文件了。
(3)、从源码字符串编译得到字节码数组
如果我们进行动态编译时,想要直接输入源码字符串并且输出的是字节码数组,而不是输出字节码文件,又该如何实现?实际上,这是从内存中得到源码,再输出到内存的方式。
在getTask().call()
源代码执行流程图中,我们可以发现JavaFileObject
的 openOutputStream()
方法控制了编译后字节码的输出行为,编译完成后会调用openOutputStream
获取输出流,并写数据(字节码)。所以我们需要重写JavaFileObject
的 openOutputStream()
方法。
同时在执行流程图中,我们还发现用于输出的JavaFileObject
对象是JavaFileManager
的getJavaFileForOutput()
方法提供的,所以为了让编译器编译完成后,将编译得到的字节码输出到我们自己构造的JavaFileObject
对象,我们还需要重写JavaFileManager
。
构造ClassFileObject
,实现定制的JavaFileObject
对象,用于存储编译后得到的字节码:
public static class ClassFileObject extends SimpleJavaFileObject {
private ByteArrayOutputStream byteArrayOutputStream; //字节数组输出流
//编译完成后会回调OutputStream,回调成功后,我们就可以通过下面的getByteCode()方法获取编译后的字节码字节数组
@Override
public OutputStream openOutputStream() throws IOException {
return byteArrayOutputStream;
}
//将输出流中的字节码转换为字节数组
public byte[] getCompiledBytes() {
return byteArrayOutputStream.toByteArray();
}
}
这样,我们就拥有了自定义的用于存储字节码的JavaFileObject
。同时还通过添加getByteCode()
方法来获得JavaFileObject
对象中用于存放字节码的输出流,并将其转换为字节数组。
接下来,就需要重写JavaFileManager
,使编译器编译完成后,将字节码存放在我们的ClassFileObject
。具体做法是直接继承ForwardingJavaFileManager
,再重写需要的getJavaFileForOutput()
方法即可。
public static class MyJavaObjectManager extends ForwardingJavaFileManager<JavaFileManager>{
private ClassFileObject classObject; //我们自定义的JavaFileObject
//重写该方法,使其返回我们的ClassJavaFileObject
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind,
FileObject sibling) throws IOException {
classObject= new ClassJavaFileObject (className, kind);
return classObject;
}
}
构造完毕,接下来直接传入getTask执行即可:
//执行编译
Boolean result = compiler.getTask(null,new MyJavaObjectManager(), null, null, null, Arrays.asList(sourceFileObject)).call();
注意这里传入的JavaFileObject
,是前面构造的存储字符串源码的sourceFileObject
,而不是我们用来存储字节码的sourceFileObject
。
至此,我们使用动态编译完成了将字符串源码编译成字节码数组。随后我们可以使用类加载器加载 byte[]中的字节码即可。
2.3、代码示例一
// 通过调用这个方法即可实现 java 的动态编译功能啦
public static Class compile(String className, String code) {
try (MemoryClassLoader loader = MemoryClassLoader.genInstance()) {
loader.registerJava(className, code);
return MemoryClassLoader.getInstance().loadClass(className);
} catch (Exception e) {
// ignore
}
}
}
public class MemoryClassLoader extends URLClassLoader {
private static final Map<String, byte[]> classBytes = new ConcurrentHashMap<>();
private MemoryClassLoader() {
super(new URL[0], MemoryClassLoader.class.getClassLoader());
}
private static final Map<String, MemoryClassLoader> CLASSLOADER_MAP = new ConcurrentHashMap<String, MemoryClassLoader>() {{
put(KEY_CLASSLOADER, new MemoryClassLoader());
}};
private static final String KEY_CLASSLOADER = "key_classloader";
/**
* 注册 Java 字符串到内存类加载器中
*/
public void registerJava(String className, String javaCode) {
try {
Map<String, byte[]> compile = compile(className, javaCode);
if (null != compile) {
classBytes.putAll(compile);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 编译 Java 代码
*/
private static Map<String, byte[]> compile(String className, String javaCode) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager stdManager = getStandardFileManager(null, null, null);
try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) {
JavaFileObject javaFileObject = manager.makeStringSource(className, javaCode);
JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, Collections.singletonList(javaFileObject));
Boolean result = task.call();
if (result != null && result) {
return manager.getClassBytes();
}
}
return null;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] buf = classBytes.get(name);
if (buf == null) {
return super.findClass(name);
}
return defineClass(name, buf, 0, buf.length);
}
@Override
public void close() {
classBytes.clear();
CLASSLOADER_MAP.clear();
}
/**
* 自定义 Java 文件管理器
*/
public static SpringJavaFileManager getStandardFileManager(DiagnosticListener<? super JavaFileObject> var1, Locale var2, Charset var3) {
Context var4 = new Context();
var4.put(Locale.class, var2);
if (var1 != null) {
var4.put(DiagnosticListener.class, var1);
}
PrintWriter var5 = var3 == null ? new PrintWriter(System.err, true) : new PrintWriter(new OutputStreamWriter(System.err, var3), true);
var4.put(Log.outKey, var5);
return new SpringJavaFileManager(var4, true, var3);
}
/**
* 获取实例
*/
public static MemoryClassLoader getInstance() {
return CLASSLOADER_MAP.get(KEY_CLASSLOADER);
}
/**
* 生成新的实例
*/
public static MemoryClassLoader genInstance() {
MemoryClassLoader classLoader = new MemoryClassLoader();
CLASSLOADER_MAP.put(KEY_CLASSLOADER, new MemoryClassLoader());
return classLoader;
}
public static String getPath() {
ApplicationHome home = new ApplicationHome(MemoryJavaFileManager.class);
String path = home.getSource().getPath();
return path;
}
public static boolean isJar() {
return getPath().endsWith(".jar");
}
}
class MemoryJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
// compiled classes in bytes:
final Map<String, byte[]> classBytes = new HashMap<>();
final Map<String, List<JavaFileObject>> classObjectPackageMap = new HashMap<>();
private JavacFileManager javaFileManager;
/**
* key 包名 value javaobj 主要给 jdk 编译 class 的时候找依赖 class 用
*/
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 -> jarEntry.getName().endsWith(".jar")).collect(Collectors.toList());
JarFile libTempJarFile;
List<JavaFileObject> onePackageJavaFiles;
String packageName;
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 {
packageName = classPath.substring(0, jarEntry.getName().lastIndexOf("/"));
onePackageJavaFiles = CLASS_OBJECT_PACKAGE_MAP.containsKey(packageName) ? CLASS_OBJECT_PACKAGE_MAP.get(packageName) : new ArrayList<>();
onePackageJavaFiles.add(new MemorySpringBootInfoJavaClassObject(jarEntry.getName().replace("/", ".").replace(".class", ""),
new URL(libTempJarFile.getUrl(), jarEntry.getName()), javaFileManager));
CLASS_OBJECT_PACKAGE_MAP.put(packageName, onePackageJavaFiles);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
isInit = true;
}
MemoryJavaFileManager(JavaFileManager fileManager) {
super(fileManager);
this.javaFileManager = (JavacFileManager) fileManager;
}
public Map<String, byte[]> getClassBytes() {
return new HashMap<>(this.classBytes);
}
@Override
public void flush() {
}
@Override
public void close() {
classBytes.clear();
classObjectPackageMap.clear();
CLASS_OBJECT_PACKAGE_MAP.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<JavaFileObject.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(JavaFileObject.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, 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), JavaFileObject.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);
}
};
}
}
}
class MemorySpringBootInfoJavaClassObject extends BaseFileObject {
private final String className;
private URL url;
MemorySpringBootInfoJavaClassObject(String className, URL url, JavacFileManager javacFileManager) {
super(javacFileManager);
this.className = className;
this.url = url;
}
@Override
public Kind getKind() {
return Kind.valueOf("CLASS");
}
@Override
public URI toUri() {
try {
return url.toURI();
} catch (URISyntaxException e) {
e.printStackTrace();
}
return null;
}
@Override
public String getName() {
return className;
}
@Override
public InputStream openInputStream() {
try {
return url.openStream();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
public OutputStream openOutputStream() throws IOException {
return null;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return null;
}
@Override
public Writer openWriter() throws IOException {
return null;
}
@Override
public long getLastModified() {
return 0;
}
@Override
public boolean delete() {
return false;
}
@Override
public String getShortName() {
return className.substring(className.lastIndexOf("."));
}
@Override
protected String inferBinaryName(Iterable<? extends File> iterable) {
return className;
}
@Override
public boolean equals(Object o) {
return false;
}
@Override
public int hashCode() {
return 0;
}
@Override
public boolean isNameCompatible(String simpleName, Kind kind) {
return false;
}
}
// 自定义 springboot 的类加载器
class SpringJavaFileManager extends JavacFileManager {
public SpringJavaFileManager(Context context, boolean b, Charset charset) {
super(context, b, charset);
}
@Override
public ClassLoader getClassLoader(Location location) {
nullCheck(location);
Iterable var2 = this.getLocation(location);
if (var2 == null) {
return null;
} else {
ListBuffer var3 = new ListBuffer();
Iterator var4 = var2.iterator();
while (var4.hasNext()) {
File var5 = (File) var4.next();
try {
var3.append(var5.toURI().toURL());
} catch (MalformedURLException var7) {
throw new AssertionError(var7);
}
}
return this.getClassLoader((URL[]) var3.toArray(new URL[var3.size()]));
}
}
protected ClassLoader getClassLoader(URL[] var1) {
ClassLoader var2 = this.getClass().getClassLoader();
try {
Class loaderClass = Class.forName("org.springframework.boot.loader.LaunchedURLClassLoader");
Class[] var4 = new Class[]{URL[].class, ClassLoader.class};
Constructor var5 = loaderClass.getConstructor(var4);
return (ClassLoader) var5.newInstance(var1, var2);
} catch (Throwable var6) {
}
return new URLClassLoader(var1, var2);
}
}
2.4、代码示例二
现在定义一个接口MysqlInfoMapper,用于动态执行一条已知的SQL,很简单,就是查询MySQL的系统表mysql里面的用户信息SELECT Host,User FROM mysql.user:
假设现在只提供一个MySQL的驱动包(mysql:mysql-connector-java:jar:8.0.20),暂时不能依赖任何高层次的框架,要动态实现MysqlInfoMapper接口,优先整理需要的组件:
需要一个连接管理器去管理MySQL的连接。需要一个SQL执行器用于执行查询SQL。需要一个结果处理器去提取和转换查询结果。为了简单起见,笔者在定义这三个组件接口的时候顺便在接口中通过单例进行实现(部分配置完全写死):
接着需要动态编译MysqlInfoMapper的实现类,它的源文件的字符串内容如下(注意不要在类路径下新建这个DefaultMysqlInfoMapper类):
然后编写一个客户端进行动态编译和执行:
最终的输出结果是:
编译[club.throwable.compile.DefaultMysqlInfoMapper]结果:true[{"host":"%","user":"canal"},{"host":"%","user":"doge"},{"host":"localhost","user":"mysql.infoschema"},{"host":"localhost","user":"mysql.session"},{"host":"localhost","user":"mysql.sys"},{"host":"localhost","user":"root"}]
然后笔者查看本地安装的MySQL中的结果,验证该查询结果是正确的。
这里笔者为了简化整个例子,没有在MysqlInfoMapper#selectAllMysqlUsers()方法中添加查询参数,可以尝试一下查询的SQL是SELECT Host,User FROM mysql.user WHERE User = 'xxx'场景下的编码实现。
如果把动态实现的DefaultMysqlInfoMapper注册到IOC容器中,就可以实现MysqlInfoMapper按照类型自动装配。如果把SQL和参数处理可以抽离到单独的文件中,并且实现一个对应的文件解析器,那么就可以把类文件和SQL隔离,Mybatis和Hibernate都是这样做的。
总结
动态编译或者更底层的面向字节码层面的编程,其实是一个十分有挑战性但是可以创造无限可能的领域,本文只是简单分析了一下Java源码编译的过程,并且通过一些简单的例子进行动态编译的模拟,离使用于实际应用中还有不少距离,后面需要花更多的时间去分析一下相关领域的知识。
动态编译是在Java程序运行时编译源代码,动态编译配合类加载器就可以在程序运行时编译源代码,并动态加载。
JDK提供了对应的JavaComplier接口来实现动态编译。
动态编译中存放源码和字节码的对象都是JavaFileObject
,因此如果我们想要修改源码的输入方式或者字节码的输出方式的,可以自主实现JavaFileObject
接口。同时,由于编译器是通过JavaFileManager
来管理输入输出的,因此也需要自主实现JavaFileManager
接口。
由于能力有限,可能存在错误,感谢指出。以上内容为本人在学习过程中所做的笔记。参考的书籍、文章或博客如下:
[1]seanwangjs. Java 类运行时动态编译技术.https://seanwangjs.github.io/2018/03/13/java-runtime-compile.html
[2]Throwable.深入理解Java的动态编译.博客园.https://www.cnblogs.com/throwable/p/13053582.html
[3]执笔记忆的空白.java动态编译实现.腾讯云云社区.https://cloud.tencent.com/developer/article/1764721?from=information.detail