相关文章:
- 自己动手写分布式任务调度框架
之前开发了一个简易的分布式任务调度框架,任务的调度源头是通过各个业务服务的主动注册来实现的。当时有朋友提出可以添加一种任务来源,让用户能够手动将代码上传到调度平台并执行。所以在当时就“立项”了,然而,时光荏苒,转眼到了24年,这个“项”却依然未能完成。时间过得真是匆匆忙忙。
要实现这个功能,主要有以下几步:
- 解析上传的文件
- 这涉及到类加载的问题
- 执行上传的文件
- 这需要使用到反射技术
首先,会实现一个基础功能:拖拽一个 Java 文件并自动执行其 main
函数。由于编写前端界面比较麻烦,这里选择使用 Java Swing,这是我个人非常喜欢的一个工具。
拖拽一个 Java 文件,执行 main 函数
这段实现其实比较简单:
package blog.dongguabai.others.drag_run;
import javax.swing.*;
/**
* @author dongguabai
* @date 2024-01-03 12:38
*/
import javax.tools.*;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.dnd.*;
import java.io.*;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
public class DragRunDemo1 extends JFrame {
public DragRunDemo1() {
JTextArea textArea = new JTextArea();
textArea.setDropTarget(new DropTarget() {
@Override
public synchronized void drop(DropTargetDropEvent evt) {
evt.acceptDrop(DnDConstants.ACTION_COPY);
try {
List<File> droppedFiles = (List<File>) evt.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
droppedFiles.forEach(file -> compileAndRun(file, textArea));
} catch (Exception ex) {
ex.printStackTrace();
}
}
});
JScrollPane scrollPane = new JScrollPane(textArea);
JPanel panel = new JPanel(new BorderLayout());
panel.setBorder(BorderFactory.createTitledBorder("Dongguabai-代码拖拽执行"));
panel.add(scrollPane, BorderLayout.CENTER);
this.add(panel);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(600, 900);
this.setVisible(true);
}
private void compileAndRun(File file, JTextArea textArea) {
try {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
compiler.run(null, null, null, file.getPath());
URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{file.getParentFile().toURI().toURL()});
String className = file.getName().replace(".java", "");
Class<?> cls = Class.forName(className, true, classLoader);
Method method = cls.getDeclaredMethod("main", String[].class);
PrintStream originalOut = System.out;
PrintStream out = new PrintStream(new OutputStream() {
@Override
public void write(int b) {
textArea.append(String.valueOf((char) b));
}
});
//重定向
System.setOut(out);
textArea.append("->" + file.getName() + ":\n");
method.invoke(null, (Object) new String[]{});
textArea.append("\n");
System.setOut(originalOut);
} catch (Throwable e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(DragRunDemo1::new);
}
}
测试代码:
import java.util.Date;
/**
* @author dongguabai
* @date 2024-01-03 17:02
*/
public class SimpleTest {
public static void main(String[] args) {
System.out.println(new Date().toLocaleString());
}
}
支持同时拖拽多个文件,运行效果如下:
然而,需要注意一个问题。在实际的开发场景中,逻辑通常会非常复杂,我们通常会将这些逻辑打包成 JAR 文件来执行。因此,这里的工具也需要能够执行 JAR 包中指定包下所有类的 main
函数。
拖拽一个 JAR 文件,执行指定 package 下所有类的 main 函数
实现如下:
package blog.dongguabai.others.drag.run;
import javax.swing.*;
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDropEvent;
import java.io.File;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* @author dongguabai
* @date 2024-01-03 19:24
*/
public class DragRunDemo2 extends JFrame {
public DragRunDemo2() {
JTextArea textArea = new JTextArea();
textArea.setDropTarget(new DropTarget() {
@Override
public synchronized void drop(DropTargetDropEvent evt) {
evt.acceptDrop(DnDConstants.ACTION_COPY);
try {
List<File> droppedFiles = (List<File>) evt.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
for (File file : droppedFiles) {
if (file.getName().endsWith(".java")) {
compileAndRunJava(file, textArea);
} else if (file.getName().endsWith(".jar")) {
String[] parts = file.getName().split("#");
if (parts.length > 1) {
String packageName = parts[1].replace("_", ".").replace(".jar", "");
runJar(file, textArea, packageName);
}
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
});
JScrollPane scrollPane = new JScrollPane(textArea);
JPanel panel = new JPanel(new BorderLayout());
panel.setBorder(BorderFactory.createTitledBorder("Dongguabai-代码拖拽执行"));
panel.add(scrollPane, BorderLayout.CENTER);
this.add(panel);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(600, 900);
this.setVisible(true);
}
private void compileAndRunJava(File file, JTextArea textArea) {
try {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
compiler.run(null, null, null, file.getPath());
URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{file.getParentFile().toURI().toURL()});
String className = file.getName().replace(".java", "");
Class<?> cls = Class.forName(className, true, classLoader);
Method method = cls.getDeclaredMethod("main", String[].class);
PrintStream originalOut = System.out;
PrintStream out = new PrintStream(new OutputStream() {
@Override
public void write(int b) {
textArea.append(String.valueOf((char) b));
}
});
//重定向
System.setOut(out);
textArea.append("->" + file.getName() + ":\n");
method.invoke(null, (Object) new String[]{});
textArea.append("\n");
System.setOut(originalOut);
} catch (Throwable e) {
e.printStackTrace();
}
}
private void runJar(File file, JTextArea textArea, String packageName) {
try {
URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{file.toURI().toURL()});
JarFile jarFile = new JarFile(file);
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().endsWith(".class")) {
String className = entry.getName().replace("/", ".").replace(".class", "");
if (className.startsWith(packageName)) {
Class<?> cls = Class.forName(className, true, classLoader);
for (Method method : cls.getDeclaredMethods()) {
if (method.getName().equals("main") && Modifier.isStatic(method.getModifiers())) {
PrintStream originalOut = System.out;
PrintStream out = new PrintStream(new OutputStream() {
@Override
public void write(int b) {
textArea.append(String.valueOf((char) b));
}
});
System.setOut(out);
textArea.append("->" + file.getName() + ", class " + className + ":\n");
method.invoke(null, (Object) new String[]{});
textArea.append("\n");
System.setOut(originalOut);
}
}
}
}
}
jarFile.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(DragRunDemo2::new);
}
}
每个拖拽的 JAR 文件都是通过一个新的 URLClassLoader
实例来加载的。每个 URLClassLoader
实例都有自己的类加载空间,它们之间是相互隔离的。
构建一个简单的 Jar 文件:
package blog.dongguabai.drag.run;
import java.util.Date;
/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args )
{
System.out.println(new Date().toLocaleString());
}
}
打包:
➜ dongguabai-others-drag-run mvn clean install
...
[INFO] Building jar: /Users/dongguabai/IdeaProjects/gitee/dongguabai-others-drag-run/target/testJa#blog_dongguabai_drag_run.jar
...
生成的 Jar 名称是:testJa#blog_dongguabai_drag_run.jar,所以会执行 blog.dongguabai.drag.run
包下所有类的 main
函数。
拖拽执行:
依赖类加载问题
这里其实存在一个依赖类的加载问题,比如我在 JAR 中新增 Guava 依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
在要执行的 App
中使用 Guava 的 Lists
:
package blog.dongguabai.drag.run;
import com.google.common.collect.Lists;
import java.util.Date;
/**
* Hello world!
*/
public class App {
public static void main(String[] args) {
System.out.println(Lists.newArrayList(new Date().toLocaleString()));
}
}
重新打包后拖拽执行,会出现异常:
Caused by: java.lang.NoClassDefFoundError: com/google/common/collect/Lists
at blog.dongguabai.drag.run.App.main(App.java:12)
... 39 more
Caused by: java.lang.ClassNotFoundException: com.google.common.collect.Lists
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.net.FactoryURLClassLoader.loadClass(URLClassLoader.java:817)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 40 more
这是因为我这里使用 URLClassLoader
直接加载了 JAR 文件,它只会加载这个 JAR 文件中的所有类,但是它不会加载 JAR 文件的依赖项。
胖 JAR
可以基于 Maven 创建一个包含所有依赖项的 “胖” JAR 文件,增加如下配置:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<!--只包含实际被使用的依赖项(不一定准,比如反射等特殊调用)-->
<minimizeJar>true</minimizeJar>
</configuration>
</execution>
</executions>
</plugin>
这里把胖 JAR 和瘦 JAR 做一个对比,可以看到胖 JAR 大很多(增加 minimizeJar
参数后,胖 JAR 实际只有 1.2M 了 ),而且将依赖类也加载了:
重新拖拽执行,效果不错:
类冲突问题
通过上文可以看出,支撑拖拽执行的核心类是 blog.dongguabai.others.drag.run.DragRunDemo2
。
但如果上传的 JAR 中也有一个同名类:
package blog.dongguabai.others.drag.run;
import com.google.common.collect.Lists;
import java.util.Date;
/**
* @author dongguabai
* @date 2024-01-04 10:23
*/
public class DragRunDemo2 {
public static void main(String[] args) {
System.out.println(Lists.newArrayList(new Date().toLocaleString()));
}
}
JAR 名称:
<finalName>testJa#blog_dongguabai_others_drag_run</finalName>
拖拽执行:
这是因为使用了 URLClassLoader
来加载 JAR 文件中的类。URLClassLoader
遵循双亲委派模型。当尝试加载一个类时,URLClassLoader
会首先尝试在其父类加载器中查找这个类,如果找到了就直接返回,否则才会尝试自己加载这个类。
因此如果上传的 JAR 文件中包含一个与当前运行环境中的类完全相同的类,那么 URLClassLoader
就会直接返回当前运行环境中的类,而不会加载 JAR 文件中的类。
打破双亲委派机制
自定义类加载器:
package blog.dongguabai.others.drag.run;
import java.net.URL;
import java.net.URLClassLoader;
/**
* @author dongguabai
* @date 2024-01-04 11:56
*/
public class CustomizedClassLoader extends URLClassLoader {
public CustomizedClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
return findClass(name);
} catch (ClassNotFoundException e) {
return super.loadClass(name);
}
}
}
修改加载类逻辑:
// URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{file.toURI().toURL()});
CustomizedClassLoader classLoader = new CustomizedClassLoader(new URL[]{file.toURI().toURL()}, ClassLoader.getSystemClassLoader().getParent());
再次拖拽运行:
效果不错。
在这里加上前后的类加载机制对比图(网上发现一张图画的不错,这里直接在其基础上进行修改,原文链接在文末):
之前:
打破双亲委派后:
总结
本文实现了一个简单的工具,用户可以通过拖拽的方式执行 Java 文件和 JAR 包。同时,通过使用胖 JAR 来解决依赖类的加载问题;通过自定义类加载器打破双亲委派机制,从而解决了类冲突问题。
源码地址:https://gitee.com/dongguabai/blog/tree/master/others
References
- https://blog.csdn.net/qq_33591903/article/details/120088757
欢迎关注公众号: