结合 Java Swing 实现 Java 文件和 JAR 包的拖拽执行

发布时间:2024年01月05日

相关文章:

之前开发了一个简易的分布式任务调度框架,任务的调度源头是通过各个业务服务的主动注册来实现的。当时有朋友提出可以添加一种任务来源,让用户能够手动将代码上传到调度平台并执行。所以在当时就“立项”了,然而,时光荏苒,转眼到了24年,这个“项”却依然未能完成。时间过得真是匆匆忙忙。

在这里插入图片描述

要实现这个功能,主要有以下几步:

  1. 解析上传的文件
    • 这涉及到类加载的问题
  2. 执行上传的文件
    • 这需要使用到反射技术

首先,会实现一个基础功能:拖拽一个 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

欢迎关注公众号:
在这里插入图片描述

文章来源:https://blog.csdn.net/Dongguabai/article/details/135415406
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。