本文曾发表于天极网:http://dev.yesky.com/451/3039451.shtml
在很多Java
应用中需要在程序中调用Java
编译器来编译和运行。但在早期的版本中(Java SE5
及以前版本)中只能通过tools.jar
中的com.sun.tools.javac
包来调用Java
编译器,但由于tools.jar
不是标准的Java
库,在使用时必须要设置这个jar
的路径。而在Java SE6
中为我们提供了标准的包来操作Java
编译器,这就是javax.tools
包。使用这个包,我们可以不用将jar
文件路径添加到classpath
中了。
一、使用
JavaCompiler
接口来编译
Java
源程序
使用
Java API
来编译Java
源程序有很多方法,现在让我们来看一种最简单的方法,通过JavaCompiler
进行编译。
我们可以通过ToolProvider
类的静态方法getSystemJavaCompiler
来得到一个JavaCompiler
接口的实例。
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
JavaCompiler
中最核心的方法是run
。通过这个方法可以编译java
源程序。这个方法有3
个固定参数和1
个可变参数(可变参数是从Jave SE5
开始提供的一个新的参数类型,用type… argu
表示)。前3
个参数分别用来为java
编译器提供参数、得到Java
编译器的输出信息以及接收编译器的错误信息,后面的可变参数可以传入一个或多个Java
源程序文件。如果run
编译成功,返回0
。
int run(InputStream in, OutputStream out, OutputStream err, String... arguments)
如果前3
个参数传入的是null
,那么run
方法将以标准的输入、输出代替,即System.in
、System.out
和System.err
。如果我们要编译一个test.java
文件,并将使用标准输入输出,run
的使用方法如下:
int results = tool.run(null, null, null, "test.java");
下面是使用JavaCompiler
的完整代码。
import
java.io.
*
;
import
javax.tools.
*
;
public
class
test_compilerapi
{
public
static
void
main(String args[])
throws
IOException
{
JavaCompiler compiler
=
ToolProvider.getSystemJavaCompiler();
int
results
=
compiler.run(
null
,
null
,
null
,
"
test.java
"
);
System.out.println((results
==
0
)
?
"
编译成功
"
:
"
编译失败
"
);
//
在程序中运行test
Runtime run
=
Runtime.getRuntime();
Process p
=
run.exec(
"
java test
"
);
BufferedInputStream in
=
new
BufferedInputStream(p.getInputStream());
BufferedReader br
=
new
BufferedReader(
new
InputStreamReader(in));
String s;
while
((s
=
br.readLine())
!=
null
)
System.out.println(s);
}
}
public
class
test
{
public
static
void
main(String[] args)
throws
Exception
{
System.out.println(
"
JavaCompiler测试成功!
"
);
}
}
编译成功的输出结果:
编译成功
test.java:9:
找不到符号
符号:
方法 printlnln(java.lang.String)
位置:
类 java.io.PrintStream
System.out.printlnln("JavaCompiler
测试成功!");
^
1
错误
编译失败
二、使用
StandardJavaFileManager
编译
Java
源程序
在第一部分我们讨论调用java
编译器的最容易的方法。这种方法可以很好地工作,但它确不能更有效地得到我们所需要的信息,如标准的输入、输出信息。而在Java SE6
中最好的方法是使用StandardJavaFileManager
类。这个类可以很好地控制输入、输出,并且可以通过DiagnosticListener
得到诊断信息,而DiagnosticCollector
类就是listener
的实现。
使用StandardJavaFileManager
需要两步。首先建立一个DiagnosticCollector
实例以及通过JavaCompiler
的getStandardFileManager()
方法得到一个StandardFileManager
对象。最后通过CompilationTask
中的call
方法编译源程序。
在使用这种方法调用Java
编译时最复杂的方法就是getTask
,下面让我们讨论一下getTask
方法。这个方法有如下所示的6
个参数。
getTask(Writer out,
JavaFileManager fileManager,
DiagnosticListener
<?
super
JavaFileObject
>
diagnosticListener,
Iterable
<
String
>
options,
Iterable
<
String
>
classes,
Iterable
<?
extends
JavaFileObject
>
compilationUnits)
这些参数大多数都可为null
。它们的含义所下。
1. out:
:用于输出错误的流,默认是System.err
。
2. fileManager:
:标准的文件管理。
3.
diagnosticListener:
编译器的默认行为。
4.
options:
编译器的选项
5. classes
:参与编译的class
。
最后一个参数compilationUnits
不能为null
,因为这个对象保存了你想编译的Java
文件。
在使用完getTask
后,需要通过StandardJavaFileManager
的getJavaFileObjectsFromFiles
或getJavaFileObjectsFromStrings
方法得到compilationUnits
对象。调用这两个方法的方式如下:.
Iterable
<?
extends
JavaFileObject
>
getJavaFileObjectsFromFiles(
Iterable
<?
extends
File
>
files)
Iterable
<?
extends
JavaFileObject
>
getJavaFileObjectsFromStrings(
Iterable
<
String
>
names)
String[] filenames
=
…;
Iterable
<?
extends
JavaFileObject
>
compilationUnits
=
fileManager.getJavaFileObjectsFromFiles(Arrays.asList(filenames));
JavaCompiler.CompilationTask task
=
compiler.getTask(
null
, fileManager,
diagnostics, options,
null
, compilationUnits);
最后需要关闭fileManager.close();
下面是一个完整的演示程序。
import
java.io.
*
;
import
java.util.
*
;
import
javax.tools.
*
;
public
class
test_compilerapi
{
private
static
void
compilejava()
throws
Exception
{
JavaCompiler compiler
=
ToolProvider.getSystemJavaCompiler();
//
建立DiagnosticCollector对象
DiagnosticCollector
<
JavaFileObject
>
diagnostics
=
new
DiagnosticCollector
<
JavaFileObject
>
();
StandardJavaFileManager fileManager
=
compiler.getStandardFileManager(
diagnostics,
null
,
null
);
//
建立用于保存被编译文件名的对象
//
每个文件被保存在一个从JavaFileObject继承的类中
Iterable
<?
extends
JavaFileObject
>
compilationUnits
=
fileManager
.getJavaFileObjectsFromStrings(Arrays asList(
"
test3.java
"
));
JavaCompiler.CompilationTask task
=
compiler.getTask(
null
, fileManager,
diagnostics,
null
,
null
, compilationUnits);
//
编译源程序
boolean
success
=
task.call();
fileManager.close();
System.out.println((success)
?
”编译成功”:”编译失败”);
}
public
static
void
main(String args[])
throws
Exception
{
compilejava();
}
}
如果想得到具体的编译错误,可以对Diagnostics
进行扫描,代码如下:
for
(Diagnostic diagnostic : diagnostics.getDiagnostics())
System.out.printf(
"
Code: %s%n
"
+
"
Kind: %s%n
"
+
"
Position: %s%n
"
+
"
Start Position: %s%n
"
+
"
End Position: %s%n
"
+
"
Source: %s%n
"
+
"
Message: %s%n
"
,
diagnostic.getCode(), diagnostic.getKind(),
diagnostic.getPosition(), diagnostic.getStartPosition(),
diagnostic.getEndPosition(), diagnostic.getSource(),
diagnostic.getMessage(
null
));
被编译的test.java
代码如下:
public
class
test
{
public
static
void
main(String[] args)
throws
Exception
{
aa;
//
错误语句
System.out.println(
"
JavaCompiler测试成功!
"
);
}
}
在这段代码中多写了个aa
,得到的编译错误为:
Code: compiler.err.not.stmt
Kind: ERROR
Position: 89
Start Position: 89
End Position: 89
Source: test.java
Message: test.java:5:
不是语句
Success: false
通过JavaCompiler
进行编译都是在当前目录下生成.class
文件,而使用编译选项可以改变这个默认目录。编译选项是一个元素为String
类型的Iterable
集合。如我们可以使用如下代码在D
盘根目录下生成.class
文件。
Iterable
<
String
>
options
=
Arrays.asList(
"
-d
"
,
"
d:\\
"
);
JavaCompiler.CompilationTask task
=
compiler.getTask(
null
, fileManager,
diagnostics, options,
null
, compilationUnits);
在上面的例子中options
处的参数为null
,而要传递编译器的参数,就需要将options
传入。
有时我们编译一个Java
源程序文件,而这个源程序文件需要另几个Java
文件,而这些Java
文件又在另外一个目录,那么这就需要为编译器指定这些文件所在的目录。
Iterable<String> options = Arrays.asList("-sourcepath", "d:""src");
上面的代码指定的被编译Java
文件所依赖的源文件所在的目录。
三、在内存中编译
JavaCompiler
不仅可以编译硬盘上的Java
文件,而且还可以编译内存中的Java
代码,然后使用reflection
来运行它们。我们可以编写一个JavaSourceFromString
类,通过这个类可以输入Java
源代码。一但建立这个对象,你可以向其中输入任意的Java
代码,然后编译和运行,而且无需向硬盘上写.class
文件。
import
java.lang.reflect.
*
;
import
java.io.
*
;
import
javax.tools.
*
;
import
javax.tools.JavaCompiler.CompilationTask;
import
java.util.
*
;
import
java.net.
*
;
public
class
test_compilerapi
{
private
static
void
compilerJava()
throws
Exception
{
JavaCompiler compiler
=
ToolProvider.getSystemJavaCompiler();
DiagnosticCollector
<
JavaFileObject
>
diagnostics
=
new
DiagnosticCollector
<
JavaFileObject
>
();
//
定义一个StringWriter类,用于写Java程序
StringWriter writer
=
new
StringWriter();
PrintWriter out
=
new
PrintWriter(writer);
//
开始写Java程序
out.println(
"
public class HelloWorld {
"
);
out.println(
"
public static void main(String args[]) {
"
);
out.println(
"
System.out.println(\
"
Hello, World\
"
);
"
);
out.println(
"
}
"
);
out.println(
"
}
"
);
out.close();
//
为这段代码取个名子:HelloWorld,以便以后使用reflection调用
JavaFileObject file
=
new
JavaSourceFromString(
"
HelloWorld
"
, writer
.toString());
Iterable
<?
extends
JavaFileObject
>
compilationUnits
=
Arrays
.asList(file);
JavaCompiler.CompilationTask task
=
compiler.getTask(
null
,
null
,
diagnostics,
null
,
null
, compilationUnits);
boolean
success
=
task.call();
System.out.println(
"
Success:
"
+
success);
//
如果成功,通过reflection执行这段Java程序
if
(success)
{
System.out.println(
"
-----输出-----
"
);
Class.forName(
"
HelloWorld
"
).getDeclaredMethod(
"
main
"
,
new
Class[]
{ String[].
class
}).invoke(
null
,
new
Object[]
{
null
});
System.out.println(
"
-----输出 -----
"
);
}
}
public
static
void
main(String args[])
throws
Exception
{
compilerJava();
}
}
//
用于传递源程序的JavaSourceFromString类
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;
}
}