您的位置:首页 > 编程语言 > Java开发

使用 javax.tools 创建动态应用程序

2017-10-18 00:00 531 查看

转自:https://www.ibm.com/developerworks/cn/java/j-jcomp/?S_TACT=105AGX52&S_CMP=tut-cto

本文永久地址:https://my.oschina.net/bysu/blog/1552935

简介

javax.tools
包是一种添加到 Java SE 6 的标准 API,可以实现 Java 源代码编译,使您能够添加动态功能来扩展静态应用程序。本文将探查 javax.tools 包中提供的主要类,并演示如何使用它们创建一个 façade,以从 Java
String
StringBuffer
或其他
CharSequence
中编译 Java 源代码,而不是从文件中编译。之后,使用这个 façade 构建一个交互式绘图应用程序,通过该应用程序,用户可以使用任何有效的数值 Java 表达式表示一个数值函数 y = f(x)。最后,本文将讨论与动态源代码编译相关的安全风险以及应对方法。

通过编译和加载 Java 扩展来对应用程序进行扩展,这种想法并不新鲜,并且一些现有框架也支持这一功能。Java Platform, Enterprise Edition (Java EE) 中的 JavaServer Pages (JSP) 技术就是一种广为人知的动态框架,能够生成并编译 Java 类。JSP 转换器通过中间产物即源代码文件将 .jsp 文件转换为 Java servlet,JSP 引擎随后将源代码文件编译并加载到 Java EE servlet 容器中。编译过程通常是通过直接调用
javac
编译器完成的,这需要安装 Java Development Kit (JDK) 或者调用
com.sun.tools.javac.Main
(可通过 Sun 的 tools.jar 获得)。Sun 的许可证允许跟随完整的 Java 运行时环境(Java Runtime Environment,JRE)一起重新分发 tools.jar。其他实现动态功能的方法包括使用可与应用程序实现语言(参见 参考资料)集成的现有动态脚本编制语言(例如 JavaScript 或 Groovy),或者编写特定于域的语言和相关的语言解释器和编译器。

其他框架(例如 NetBeans 和 Eclipse)支持开发人员使用 Java 语言直接编写扩展,但是这些框架需要外部静态编译,并需要管理 Java 代码及其工件的源代码和二进制文件。Apache Commons JCI 提供了一种机制可以将 Java 类编译并加载到运行的应用程序中。Janino 和 Javassist 也提供了类似的动态功能,但是 Janino 只限于 Java 1.4 之前的语言,而 Javassist 只能工作在 Java 类抽象级别,而不能在源代码级别工作(参见 参考资料 中有关这些项目的链接)。然而,Java 开发人员已经熟悉如何使用 Java 语言编写程序,如果一种系统能够动态生成 Java 源代码并进行编译和加载,那么它可以保证最短的学习曲线并提供最大程度的灵活性。

使用
javax.tools
的优点

使用
javax.tools
具有以下好处:

它是经过认可的 Java SE 扩展,这意味着它是 Java Community Process(按照 JSR 199 规范)开发的标准 API。
com.sun.tools.javac.Main
API 不属于 经过文件归档的 Java 平台 API,因此没有必要在其他供应商的 JDK 中提供或保证在未来版本的 Sun JDK 中提供该 API。

您可以应用已经掌握的知识:Java 源代码,而不是字节码。不需要学习生成有效字节码的复杂规则或者新的类对象模型、方法、语句和表达式,通过生成有效的 Java 源代码,您就可以创建正确的 Java 类。

它简化了一种受支持机制,并进行了标准化,使您不用局限于基于文件的源代码就可生成并加载代码。

它可以在 JDK Version 6 和更高版本的各种供应商实现之间移植,并且将来也支持这种移植性。

它使用经过验证的 Java 编译器。

与基于解释器的系统不同,所加载的类可以从 JRE 的运行时优化中受益。

Java 编译:概念和实现

要理解
javax.tools
包,回顾 Java 编译概念以及如何使用包实现编译将非常有帮助。
javax.tools
包以一种通用的方式对这些概念进行了抽象化,使您能够从备用的源代码对象提供源代码,而不要求源代码必须位于文件系统中。

编译 Java 源代码需要用到以下组件:

类路径,编译器从其中解析库类。编译器类路径通常由一个有序的文件系统目录列表和一些归档文件(JAR 或 ZIP 文件)组成,归档文件中包含先前编译过的 .class 文件。类路径由一个
JavaFileManager
实现,后者管理多个源代码和类
JavaFileObject
实例以及传递给
JavaFileManager
构造函数的
ClassLoader
JavaFileObject
是一个
FileObject
,专门处理以下任一种由编译器使用的
JavaFileObject.Kind
枚举类型:

SOURCE


CLASS


HTML


OTHER


每个源文件提供一个
openInputStream()
方法,可以作为
InputStream
访问源代码。

javac
选项
,以
Iterable<String>
的形式传递

源文件 — 待编译的一个或多个 .java 源文件。
JavaFileManager
提供了一个抽象的文件系统,可以将源文件和输出文件的文件名映射到
JavaFileObject
实例(其中,文件 表示一个惟一名称和一串字节之间的关联。客户机不需要使用实际的文件系统)。在本文的示例中,
JavaFileManager
管理类名与
CharSequence
实例之间的映射,后者包含待编译的 Java 源代码。
JavaFileManager.Location
包含一个文件名和一个标记,该标记可以表明该位置是源代码还是一个输出位置。
ForwardingJavaFileManager
实现 Chain of Responsibility 模式(参见 参考资料),允许将文件管理器链接在一起,就像类路径和源路径将 JAR 和目录链接起来一样。如果在这条链的第一个元素中没有发现 Java 类,那么将对链中的其他元素进行查找。

输出目录,编译器在其中编写生成的 .class 文件。作为输出类文件的集合,
JavaFileManager
也保存表示编译过的
CLASS
文件的
JavaFileObject
实例。

编译器
JavaCompiler
创建
JavaCompiler.CompilationTask
对象,后者从
JavaFileManager
中的
JavaFileObject
SOURCE
对象编译源代码,创建新的输出
JavaFileObject
CLASS
文件和
Diagnostic
(警告和错误)。静态
ToolProvider.getSystemJavaCompiler()
方法返回编译器实例。

编译器警告和错误,这些内容通过
Diagnostic
DiagnosticListener
实现。
Diagnostic
是编译器发出的警告或编译错误。
Diagnostic
指定以下内容:

Kind
ERROR
WARNING
MANDATORY_WARNING
NOTE
OTHER


源代码中的位置(包括行号和列号)

消息

客户机向编译器提供一个
DiagnosticListener
,编译器可通过它向客户机发回诊断信息。
DiagnosticCollector
是一个简单的
DiagnosticListener
实现。

图 1 展示了
javax.tools
中的
javac
概念与其实现之间的映射:

图 1.
javac
概念如何映射到
javax.tools
接口



了解了这些概念,我们现在看一下如何实现一个 façade 来编译
CharSequence


编译
CharSequence
实例中的 Java 源代码

在本节中,我将为
javax.tools.JavaCompiler
构造一个 façade。
javaxtools.compiler.CharSequenceCompiler
类(参见 下载)可以编译保存在任何
java.lang.CharSequence
对象(例如
String
StringBuffer
StringBuilder
)中的 Java 源代码,并返回一个
Class
CharSequenceCompiler
提供了以下 API:

public CharSequenceCompiler(ClassLoader loader, Iterable<String>options)
:该构造函数接收传递给 Java 编译器的
ClassLoader
,允许它解析相关类。
Iterable
options
允许客户机传递额外的编译器选项,这些选项均对应于
javac
选项。

public Map<String, Class<T>>compile(Map<String, CharSequence> classes, final DiagnosticCollector<JavaFileObject> diagnostics) throws CharSequenceCompilerException, ClassCastException
:这是常用的编译方法,支持同时编译多个源代码。注意,Java 编译器必须处理类的循环依赖性,例如 A.java 依赖 B.java,B.java 依赖 C.java,而 C.java 又依赖 A.java。该方法的第一个参数是
Map
,它的键为完全限定类名,而相应的值为包含该类源代码的
CharSequence
。例如:

"mypackage.A"
"package mypackage; public class A { ... }";


"mypackage.B"
"package mypackage; class B extends A implements C { ... }";


"mypackage.C"
"package mypackage; interface C { ... }"


编译器将
Diagnostic
添加到
DiagnosticCollector
。您希望对类进行强制转换的主要类型是泛型类型参数
T
compile()
被另一个方法重载,其参数为一个类名和待编译的
CharSequence


public ClassLoader getClassLoader()
:该方法返回编译器在生成 .class 文件时组装的类加载器,因此,可以从其中加载其他类或资源。

public Class<T> loadClass(final String qualifiedClassName) throws ClassNotFoundException
:由于
compile()
方法可以定义多个类(包括公共的嵌套类),因此允许加载辅助类。

为了支持
CharSequenceCompiler
API,我使用
JavaFileObjectImpl
类和
JavaFileManagerImpl
实现了
javax.tools
接口,其中,
JavaFileObjectImpl
类用于保存
CharSequence
源代码和编译器产生的
CLASS
输出,而
JavaFileManagerImpl
用于将名称映射到
JavaFileObjectImpl
实例,从而管理源代码和编译器产生的字节码。

JavaFileObjectImpl

清单 1 中的
JavaFileObjectImpl
实现
JavaFileObject
并保存
CharSequence source
(用于
SOURCE
)或一个
ByteArrayOutputStream byteCode
(用于
CLASS
文件)。关键方法是
CharSequence getCharContent(final boolean ignoreEncodingErrors)
,编译器通过它获得源代码文本。参见 下载,获取所有代码示例的源代码。

清单 1.
JavaFileObjectImpl
(只显示部分源代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final class JavaFileObjectImpl extends SimpleJavaFileObject {

private final CharSequence source;


JavaFileObjectImpl(final String baseName, final CharSequence source) {

super(CharSequenceCompiler.toURI(baseName + ".java"), Kind.SOURCE);

this.source = source;

}

@Override

public CharSequence getCharContent(final boolean ignoreEncodingErrors)

throws UnsupportedOperationException {

if (source == null)

throw new UnsupportedOperationException("getCharContent()");

return source;

}

}

FileManagerImpl

FileManagerImpl
(参见清单 2)对
ForwardingJavaFileManager
进行了扩展,将限定的类名映射到
JavaFileObjectImpl
实例:

清单 2.
FileManagerImpl
(只显示部分源代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
final class FileManagerImpl extends ForwardingJavaFileManager<
JavaFileManager
> {

private final ClassLoaderImpl classLoader;

private final Map<
URI
, JavaFileObject> fileObjects

= new HashMap<
URI
, JavaFileObject>();


public FileManagerImpl(JavaFileManager fileManager, ClassLoaderImpl classLoader) {

super(fileManager);

this.classLoader = classLoader;

}


@Override

public FileObject getFileForInput(Location location, String packageName,

String relativeName) throws IOException {

FileObject o = fileObjects.get(uri(location, packageName, relativeName));

if (o != null)

return o;

return super.getFileForInput(location, packageName, relativeName);

}


public void putFileForInput(StandardLocation location, String packageName,

String relativeName, JavaFileObject file) {

fileObjects.put(uri(location, packageName, relativeName), file);

}

}

CharSequenceCompiler

如果 ToolProvider.getSystemJavaCompiler() 不能创建 JavaCompiler

如果 tools.jar 不在应用程序类路径中,
ToolProvider.getSystemJavaCompiler()
方法可以返回
null
CharStringCompiler
类检测到这一配置问题后将抛出一个异常,并给出修复建议。注意,Sun 许可证允许跟随 JRE 一起重新分发 tools.jar。

通过这些支持类,现在可以定义
CharSequenceCompiler
,可使用运行时
ClassLoader
和编译器选项构建。它使用
ToolProvider.getSystemJavaCompiler()
获得
JavaCompiler
实例,然后对转发给编译器标准文件管理器的
JavaFileManagerImpl
进行实例化。

compile()
方法对输入映射进行迭代,从每个名称/
CharSequence
中构建一个
JavaFileObjectImpl
,并将其添加到
JavaFileManager
中,因此,在调用文件管理器的
getFileForInput()
方法时,
JavaCompiler
将找到它们。
compile()
方法随后将创建一个
JavaCompiler.Task
实例并运行该实例。故障被作为
CharSequenceCompilerException
抛出。然后,对于传递给
compile()
方法的所有源代码,加载产生的
Class
类并放在结果
Map
中。

CharSequenceCompiler
(参见清单 3)相关的类加载器是一个
ClassLoaderImpl
实例,它在
JavaFileManagerImpl
实例中查找类的字节码,返回编译器创建的 .class 文件:

清单 3.
CharSequenceCompiler
(只显示部分源代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class CharSequenceCompiler<
T
> {

private final ClassLoaderImpl classLoader;

private final JavaCompiler compiler;

private final List<
String
>options;

private DiagnosticCollector<
JavaFileObject
> diagnostics;

private final FileManagerImpl javaFileManager;


public CharSequenceCompiler(ClassLoader loader, Iterable<
String
>options) {

compiler = ToolProvider.getSystemJavaCompiler();

if (compiler == null) {

throw new IllegalStateException(

"Cannot find the system Java compiler. "

+ "Check that your class path includes tools.jar");

}

classLoader = new ClassLoaderImpl(loader);

diagnostics = new DiagnosticCollector<
JavaFileObject
>();

final JavaFileManager fileManager = compiler.getStandardFileManager(diagnostics,

null, null);

javaFileManager = new FileManagerImpl(fileManager, classLoader);

this.options = new ArrayList<
String
>();

if (options != null) {

for (String option :options) {

this.options.add(option);

}

}

}


public synchronized Map<
String
, Class<T>>

 
compile(final Map<
String
, CharSequence> classes,

final DiagnosticCollector<
JavaFileObject
> diagnosticsList)

 
throws CharSequenceCompilerException, ClassCastException {

List<
JavaFileObject
> sources = new ArrayList<
JavaFileObject
>();

for (Entry<
String
, CharSequence> entry : classes.entrySet()) {

String qualifiedClassName = entry.getKey();

CharSequence javaSource = entry.getValue();

if (javaSource != null) {

final int dotPos = qualifiedClassName.lastIndexOf('.');

final String className = dotPos == -1

? qualifiedClassName

: qualifiedClassName.substring(dotPos + 1);

final String packageName = dotPos == -1

? ""

: qualifiedClassName .substring(0, dotPos);

final JavaFileObjectImpl source =

new JavaFileObjectImpl(className, javaSource);

sources.add(source);

javaFileManager.putFileForInput(StandardLocation.SOURCE_PATH, packageName,

className + ".java", source);

}

}

final CompilationTask task = compiler.getTask(null, javaFileManager, diagnostics,

 
options, null, sources);

final Boolean result = task.call();

if (result == null || !result.booleanValue()) {

throw new CharSequenceCompilerException("Compilation failed.",

 
classes.keySet(), diagnostics);

}

try {

Map<
String
, Class<T>>compiled = 

new HashMap<
String
, Class<T>>();

for (Entry<
String
, CharSequence> entry : classes.entrySet()) {

String qualifiedClassName = entry.getKey();

final Class<
T
> newClass = loadClass(qualifiedClassName);

compiled.put(qualifiedClassName, newClass);

}

return compiled;

} catch (ClassNotFoundException e) {

throw new CharSequenceCompilerException(classes.keySet(), e, diagnostics);

} catch (IllegalArgumentException e) {

throw new CharSequenceCompilerException(classes.keySet(), e, diagnostics);

} catch (SecurityException e) {

throw new CharSequenceCompilerException(classes.keySet(), e, diagnostics);

}

}

}

Plotter 应用程序

现在,我有了一个可以编译源代码的简单 API,我将通过创建函数绘制应用程序(使用 Swing 编写)来发挥其功用。图 2 展示了该应用程序使用图形表示
x * sin(x) * cos(x)
函数:

图 2. 使用 javaxtools.compiler 包的动态应用程序



该应用程序使用清单 4 中定义的
Function
接口:

清单 4.
Function
接口

1
2
3
4
package javaxtools.compiler.examples.plotter;

public interface Function {

double f(double x);

}

应用程序提供了一个文本字段,用户可以向其中输入一个 Java 表达式,后者根据隐式声明的
double x
输入参数返回一个
double
值。在清单 5 所示的代码模板中,应用程序将表达式文本插入到以
$expression
标记的位置。并且每次生成一个惟一的类名,替代模板中的
$className
。包名也是一个模板变量。

清单 5.
Function
模板

1
2
3
4
5
6
7
8
package $packageName;

import static java.lang.Math.*;

public class $className

 
implements javaxtools.compiler.examples.plotter.Function {

public double f(double x) {

 
return ($expression) ;

}

}

应用程序使用
fillTemplate(packageName, className, expr)
函数填充模板,它返回一个
String
对象,然后应用程序使用
CharSequenceCompiler
进行编译。异常或编译器诊断信息被传递给
log()
方法或直接写入到应用程序中可滚动的
errors
组件。

清单 6 显示的
newFunction()
方法将返回一个对象,它将实现
Function
接口(参见 清单 5 中的源代码模板):

清单 6.
Plotter
Function newFunction(String expr)
方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Function newFunction(final String expr) {

errors.setText("");

try {

// generate semi-secure unique package and class names

final String packageName = PACKAGE_NAME + digits();

final String className = "Fx_" + (classNameSuffix++) + digits();

final String qName = packageName + '.' + className;

// generate the source class as String

final String source = fillTemplate(packageName, className, expr);

// compile the generated Java source

final DiagnosticCollector<
JavaFileObject
> errs =

new DiagnosticCollector<
JavaFileObject
>();

Class<
Function
> compiledFunction = stringCompiler.compile(qName, source, errs,

new Class<?>[] { Function.class });

log(errs);

return compiledFunction.newInstance();

} catch (CharSequenceCompilerException e) {

log(e.getDiagnostics());

} catch (InstantiationException e) {

errors.setText(e.getMessage());

} catch (IllegalAccessException e) {

errors.setText(e.getMessage());

} catch (IOException e) {

errors.setText(e.getMessage());

}

return NULL_FUNCTION;

}

您通常会生成一些源类,使用它们扩展已有的基类或实现特定接口,从而可以将实例转换为已知的类型并通过一个类型安全 API 调用其方法。注意,在实例化
CharSequenceCompiler<T>
时,
Function
类被作为泛型类型参数
T
使用。因此,也可以将
compiledFunction
输入作为
Class<Function>
compiledFunction.newInstance()
,以返回
Function
实例,而不需要进行强制转换。

动态生成一个
Function
实例后,应用程序使用它针对一系列 x 值生成 y 值,然后使用开源的 JFreeChart API(参见 参考资料)描绘(x,y)值。Swing 应用程序的完整源代码可以通过
javaxtools.compiler.examples.plotter
包的 可下载源代码 部分中获得。

这个应用程序的源代码生成需求非常普通。更为复杂的源代码模板工具可以更好地满足其他应用程序的需求,例如 Apache Velocity (参见 参考资料)。

安全风险和策略

如果应用程序允许用户随意输入 Java 源代码,那么会存在一些内在的安全风险。类似 SQL 注入(参见 参考资料),如果系统允许用户或其他代理提供原始的 Java 源代码来生成代码,那么恶意用户可能会利用这一点。例如,在本文的
Plotter
应用程序中,一个有效的 Java 表达式可能包含匿名的嵌套类,它可以访问系统资源、在受到拒绝服务攻击时产生大量线程或者执行其他行为。这些行为被称为 Java 注入。这种应用程序不应该部署在非信任用户可以随意访问的不可靠位置,例如作为 servlet 或 applet 的 Java EE 服务器。相反,
javax.tools
的大多数客户机应该限制用户输入并将用户请求转换为安全的源代码。

使用这种包时可以采用的安全策略包括:

使用定制的
SecurityManager
ClassLoader
阻止加载匿名类或其他无法直接控制的类。

使用源代码扫描程序或其他预处理程序,删除含有可疑代码构造的输入。例如,
Plotter
可以使用
java.io.StreamTokenizer
并删除含有
{
(左侧大括号)字符的输入,从而有效阻止了匿名或嵌套类的声明。

使用
javax.tools
API,
JavaFileManager
可以删除任何预料之外的
CLASS
文件的写入。例如,当编译某个特定类时,对于要求保存预料之外的类文件的任何调用,
JavaFileManager
将抛出一个
SecurityExeception
异常,并只允许生成用户无法猜测或欺骗的包名或类名。
Plotter
newFunction
方法使用的就是这种策略。

结束语

在本文中,我解释了
javax.tools
包的概念和重要接口,并展示了一个 façade,使用它编译保存在
String
或其他
CharSequence
中的 Java 源代码,然后使用这个库类开发可以描绘任意 f(x) 函数的样例应用程序。可以使用这种技术创建其他有用的应用程序:

根据数据描述语言生成二进制文件阅读程序/写入程序。

生成类似于 Java Architecture for XML Binding (JAXB) 或持久性框架的格式转换程序。

通过执行源代码与 Java 语言的转换、Java 源代码编译和加载(类似于 JSP 技术),实现特定于域的语言解释器。

实现规则引擎。

您可以想像得到的任何内容。

下一次开发应用程序时,如果需要使用动态行为,请尝试
javax.tools
提供的多样性和灵活性。

下载资源

本文的样例代码 (j-jcomp.zip | 166KB)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: