笔者在最近的项目中对一个用户任意指定的Java项目或Java文件进行测试,这就涉及到编译和运行这些Java文件,折腾一段时间后实现了这个功能,在这记录下使用到的技术点。

编译Java文件

对于一个给定的java文件进行编译,首先想到的是javac命令,其使用形式如下所示:

javac -d destDir -classpath usedjars javaFilePath|@fileName
  • -d 指定编译后的class存放目录
  • -classpath 指定程序运行需要的包或资源存放目录
  • javaFilePath 指定一个需要编译的Java文件路径
  • @fileName 则指定一个文件,其中包含有所有需要编译的文件的路径

以上路径使用相对路径和绝对路径都可以,在这里笔者使用绝对路径。

编译单个文件

例如我们需要编译C:\HelloWorld.java文件,而这个Java文件引用了包C:\Junit.jar, 输出目录到D:\compileOutput,使用命令行

javac -d D:\compileOutput -classpath C:\Junit.jar C:\HelloWorld.java

即可完成编译任务


编译Java项目

使用通配符

如果我们需要编译复杂结构的Java项目,对每个java文件单独编译是不现实的,我们需要一种批量编译的方法。笔者曾经尝试过一种错误的方法,即使用通配符对指定目录下的所有Java文件进行编译,命令如下:

javac currentDirectory\*.java

这种编译方法会尝试使用javac去编译目录下所有的java文件,而当某个java文件对一个尚未编译的java文件引用时,编译就会失败,跳过此文件。因此,这种方法仅仅适用于需要编译的java文件之间没有相互引用

使用@File选项

注意到javac命令中有一个选项@File,能够编译File所包含的所有代码。那么如果我们将项目中的所有代码的路径都写进一个File,然后使用javac @File 就能够完全编译整个项目,文件之间的依赖关系和编译顺序,系统会自己处理。

javac @File

在Java代码中运行命令行

这时可以通过组合使用Java提供的Runtime类和Process类的方法实现。下面是一种比较典型的程序模式:

String command = "javac -d " + dir.getAbsolutePath() + " @" + sourceCodeList.getAbsolutePath()  
Process   process   =   Runtime.getRuntime().exec(command);   
process.waitfor();

在上面的程序中,第一行的command是要执行的编译指令,Runtime.getRuntime()返回当前应用程序的Runtime对象,该对象的exec()方法指示Java虚拟机创建一个子进程执行指定的可执行程序,并返回与该子进程对应的Process对象实例。通过Process可以控制该子进程的执行或获取该子进程的信息。最后的waitfor等待子进程完成再往下执行。

运行Java Class

使用命令行

运行class

java命令行为我们提供了一种运行class文件的方法,基本使用方式如下所示:

java -cp dir classFileFullName
  • 这里的-cp的作用和-classpath作用类似,都指定了class文件所依赖的资源目录,通常命令行运行的当前目录不是class文件的目录所在,所以在-cp后指定class文件目录。
  • classFullName则是class文件的名称,不带后缀,再加上类的包路径,例如HelloWorld.javapackage demo,那么全名是demo.HelloWorld

使用基本的java命令,结合Process process = Runtime.getRuntime().exec(command)就能运行指定的class文件,但是有个限制,运行的程序必须要有Main方法

与运行程序交互

通过对子进程process进行读写操作来对需要运行的程序进行输入和输出控制,示例代码如下:

String runCommond = "java -cp " + ProjectConfig.CompileClassPath  
                       + " "  + packageName  + mainFileName.replace(".java", "");
     final Process proc = Runtime.getRuntime().exec(runCommond);

     //transport input into process, params is string array of input
       OutputStream stdin = proc.getOutputStream();
       if(params != null){
           for (int i = 0; i < params.length ; i++) {
               stdin.write((params[i] + "\n").getBytes());
           }
       }
       stdin.flush();

      //get output from process
      BufferedReader stdout = new BufferedReader(
                             new InputStreamReader(proc.getInputStream()));
      int count = 0;
      String[] outputs = new String[outputNumber];
      for (String line;  (line = stdout.readLine()) != null 
          && count < outputs.length; count++){
          outputs[count] = line;
      }
      proc.waitFor();

注意 子进程Process默认通过System.in 和 System.out来读取和输出数据,这就对运行程序的语句书写提出了严格的要求,适用性不广


使用Java反射

Java反射机制使得我们可以根据class文件,在内存中建立起该类的实例。例如我们需要将dir目录下的class建立实例,代码如下所示:

// Convert File to a URL
URL url = dir.toURI().toURL();
URL[] urls = new URL[]{url};

// Create a new class loader with the directory
ClassLoader cl = new URLClassLoader(urls);
Class cls = cl.loadClass(fullClassName);

上面代码首先建立了带有指定目录的classLoader,然后利用此classLoader加载想运行的class。在拥有class在内存中的实例cls后,我们就能通过 cls.getDeclaredMethod 来获取该类中的所有方法,并且能够引用具有public访问域的方法,并在传递相关参数后运行该方法。

Method[] method = cls.getDeclaredMethods();
for (Method me : method) {
    if(me.getName().equalsIgnoreCase(“MethodNameYouWantRun”) {

    //获得指定方法的引用
    Class<?>[] paramtypes = me.getParameterTypes();
    Method invokedMethod = cls.getDeclaredMethod(“MethodNameYouWantRun”,paramtypes);
    invokedMethod.setAccessible(true);

    //将所有参数封装进objects
    objects[0] = Integer
    objects[1] = int[]

    //运行指定方法, 
    invokedMethod.invoke(cls.newInstance(), objects);
    }
}

这种使用java反射的方法能够实现真正的定制,可以加载任意一个class文件,并且获得该类实例,运行一个或多个方法( 前提是拥有权限),但是这种方法也有局限性,运行方法的输出只能依靠方法的返回值,当需要返回大量数据的时候,就需要返回一种复杂的数据结构,而主程序需要预先知道这种复杂的数据结构,以便在接受方法返回的时候能够解析。


使用Ant

思路:通过动态生成antbuild文件,或使用外部文件自带的build文件,然后在业务代码中调用ant的命令行执行ant构建,实现对外部java文件或项目的动态编译和执行

参考

Javac编译命令详解 Java动态编译 在CMD下使用Java运行Class Java中使用Runtime和Process运行外部程序 通过Java反射调用方法 Java ClassLoader机制解析