笔者在最近的项目中对一个用户任意指定的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.java
中package 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
思路:通过动态生成ant
的build
文件,或使用外部文件自带的build
文件,然后在业务代码中调用ant
的命令行执行ant
构建,实现对外部java
文件或项目的动态编译和执行
Javac编译命令详解 Java动态编译 在CMD下使用Java运行Class Java中使用Runtime和Process运行外部程序 通过Java反射调用方法 Java ClassLoader机制解析