值提取系列
值提取系列
字符串值提取工具-04-java 调用 java? Janino 编译工具
代码地址
标题:Janino:轻量级且高效的Java编译器
Janino 是一个超小型、超快速的Java编译器。
Janino 不仅能像JAVAC一样将一组源文件编译成一组类文件,而且还能编译一个Java表达式、一个代码块、一个类体、一个.java文件或一组.java文件在内存中,加载字节码并在运行中的JVM中直接执行。
<integration>
<commons-jci>Apache Commons JCI ("Java Compiler Interface")</commons-jci>
<jboss-rules>JBoss Rules / Drools</jboss-rules>
</integration>
Janino 还可以用于静态代码分析或代码操作。
快速入门指南
简介:
这是关于什么的?查看这个PDF演示文稿以快速开始。(请注意,一些信息已经过时,例如对CODEHAUS的引用,它在2015年的某个时候已经不存在了。)
属性:
主要设计目标是在保持编译器小巧简单的前提下,提供一个(几乎)完整的Java编译器。
以下是Java编程语言实现的元素(或注明部分实现的):
Java 1.4语言特性:
- 包声明,导入声明
- 类声明
- 接口声明
- 继承(扩展和实现)
- 静态成员类型声明
- 内部类声明(非静态成员、局部、匿名)
- 类初始化器,实例初始化器
- 字段声明,方法声明
- 局部变量声明
- 类变量初始化器,实例变量初始化器
- 块语句({…})
- if … else 语句
- 基本的for循环
- while循环
- do … while循环
- try … catch … finally 语句
- 抛出语句
- 返回语句
- 断点语句
- 继续语句
- switch语句
- 同步语句
- 所有基本类型(布尔,字符,字节,短整型,整型,长整型,浮点型,双精度浮点型)
- 赋值运算符 =
-
复合赋值运算符 +=, -=, *=, /=, &=, =, ^=, %=, «=, »=, »>= -
条件运算符 ? … :, &&, -
布尔逻辑运算符 &, ^, -
整型位运算符 &, ^, - 数值运算符 *, /, %, +, -, «, », »
- 字符串连接运算符 +
- 运算符 ++ 和 –
- 类型比较运算符 instanceof
- 一元运算符 +, -, ~, !
- 括号表达式
- 字段访问(如 System.out)
- 超类成员访问(super.meth(), super.field)
- this(当前实例的引用)
- 调用其他构造器(this(a, b, c);)
- 超类构造器调用(super(a, b, c);)
- 方法调用(System.out.println(“Hello”))
- 类实例创建(new Foo())
- 原始数组创建(new int[10][5][])
- 类或接口数组创建(new Foo[10][5][])
- 数组访问(args[0])
- 局部变量访问
- 整数字面量(十进制,十六进制和八进制)
- 浮点字面量(十进制)
- 布尔,字符,字符串字面量
- null字面量
- 数值转换:一元,二元,扩展,缩小
- 引用转换:扩展,缩小
- 赋值转换
- 字符串转换(用于字符串连接)
- 强制类型转换
- 常量表达式
- 块作用域,方法作用域,类作用域,全局作用域
- 抛出子句
- 数组初始化器(String[] a = { “x”, “y”, “z” })
- 原始类字面量(int.class)
- 非原始类字面量(String.class)
- 未编译编译单元之间的引用
- 行号表(”-g:lines”)
- 源文件信息(”-g:source”)
- 局部变量信息(”-g:vars”)
- 处理@deprecated文档注释标签
- 可访问性检查(public, protected, private, 默认)
- “确定赋值”检查
- 编译成超过32KB的方法
- assert(部分实现 - 断言始终启用,就像JVM以”-ea”命令行选项启动一样)
- 用于字符串连接的StringBuilder类
Java 5语言特性:
- 参数化类型声明
- 类型参数(例如 List
):已解析,但其他方面被忽略。最显著的限制是,您必须从方法调用中强制转换返回值,例如 "(String) myMap.get(key)"
Java 7语言特性:
- 二进制整数字面量(JLS7 3.10.1)
- 数值字面量中的下划线(JLS7 3.10.1)
- 字符串switch语句(JLS7 14.11)
- try-with-resources语句(JLS7 14.20.3)
- 捕获和重新抛出多个异常类型:部分实现;已解析和未解析,但不可编译
- 泛型实例创建的类型推断(即“菱形运算符”)(JLS11 15.9.1)
Java 8语言特性:
- Lambda表达式:部分实现;已解析和未解析,但不可编译
- 方法引用:部分实现;已解析和未解析,但不可编译
- 默认方法
- 静态接口方法
Java 9语言特性:
- 私有接口方法
- 增强的try-with-resources语句,允许VariableAccesses作为资源(JLS9 14.20.3)
Java 10语言特性:
- 局部变量类型推断:部分实现;已解析和未解析,但不可编译
Java 11语言特性:
- Lambda参数类型推断:部分实现;已解析和未解析,但不可编译
Java 15语言特性:
- 文本块
即使JANINO在旧版本的JRE上运行,它也支持所有这些语言特性!
Java编程语言未实现或部分实现的特性
以下是Java编程语言未实现(或注明部分实现)的元素:
Java 1.4语言特性:
- assert:部分实现 - 断言始终启用,就像JVM是以”-ea”命令行选项启动的一样。
Java 5语言特性:
- 类型参数:已解析,但其他方面被忽略。最显著的限制是,您必须从方法调用中强制转换返回值,例如 “(String) myMap.get(key)”。
Java 7语言特性:
- 捕获并重新抛出多个异常类型:部分实现;已解析和未解析,但不可编译。
- 泛型实例创建的类型推断(例如
Map<String, Integer> map = new HashMap<>();
)。
Java 8语言特性:
- Lambda表达式:部分实现;已解析和未解析,但不可编译。
- 方法引用:部分实现;已解析和未解析,但不可编译。
- 重复注解。
- 类型注解。
Java 9语言特性:
- 内部类使用的菱形运算符。
- 模块:部分实现;模块化编译单元已解析和未解析,但不可编译。
Java 10语言特性:
- 局部变量类型推断:部分实现;已解析和未解析,但不可编译。
Java 11语言特性:
- Lambda参数类型推断:部分实现;已解析和未解析,但不可编译。
许可:
- JANINO在新BSD许可证下可用。
下载
- 包 如果您使用MAVEN,请在您的POM文件中添加此条目:
<!-- MAVEN依赖配置示例 -->
<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>janino</artifactId>
<version>版本号</version>
</dependency>
请注意,上面的版本号
应替换为实际使用的Janino库的版本号。
入门例子
ExpressionEvaluator ee = new ExpressionEvaluator();
ee.cook("3 + 4");
System.out.println(ee.evaluate()); // Prints "7".
基础示例:Janino 作为表达式求值器
Janino作为一个表达式求值器:
假设你正在构建一个电子商务系统,该系统计算用户放入购物车中的商品的运费。由于你在实现时不知道商家的运费计算模型,你可以实现一组你能想到的运费计算模型(固定收费、按重量、按商品数量等),并在运行时选择其中之一。
实际上,你会发现你实现的运费计算模型很少会符合商家的要求,所以你必须添加自定义模型,这些模型是特定于商家的。如果商家的模型稍后有变化,你必须更改你的代码,重新编译并重新分发你的软件。
因为这种方式非常不灵活,运费表达式应在运行时指定,而不是在编译时。这意味着表达式必须在运行时扫描、解析和评估,这就是为什么你需要一个表达式求值器。
一个简单的表达式求值器会解析一个表达式并创建一个“语法树”。例如,表达式 “a + b * c” 将编译成一个“求和”对象,其第一个操作数是参数 “a”,第二个操作数是一个“乘积”对象,其操作数是参数 “b” 和 “c”。这样的语法树可以相对快速地评估。然而,运行时性能大约比直接由JVM执行的“本地”Java代码低100倍。这限制了这种表达式求值器在简单应用程序中的使用。
此外,你可能不仅想要进行简单的算术运算,如 “a + b * c % d”,而且可能想要进一步发展这个概念,拥有一个真正的“脚本”语言,这增加了你的应用程序的灵活性。由于你已经知道Java编程语言,你可能想要一个与Java编程语言语法类似的语言。
所有这些考虑都导致了在运行时编译Java代码,就像一些引擎(例如JSP引擎)已经做的那样。然而,使用ORACLE的JDK编译Java程序是一个相对资源密集型的过程(磁盘访问,CPU时间等)。这就是Janino发挥作用的地方……一个轻量级,“嵌入式”Java编译器,它在内存中编译简单程序到JVM字节码,这些字节码在运行程序的JVM中执行。
好的,现在你好奇了……这就是你如何使用ExpressionEvaluator的方式:(接下来的内容应该是具体的使用示例,但原文中并未给出具体的代码或步骤,所以这里无法继续翻译。如果需要进一步的示例代码或步骤,请提供详细信息。)
package foo;
import java.lang.reflect.InvocationTargetException;
import org.codehaus.commons.compiler.CompileException;
import org.codehaus.janino.ExpressionEvaluator;
public class Main {
public static void
main(String[] args) throws CompileException, InvocationTargetException {
// Now here's where the story begins...
ExpressionEvaluator ee = new ExpressionEvaluator();
// The expression will have two "int" parameters: "a" and "b".
ee.setParameters(new String[] { "a", "b" }, new Class[] { int.class, int.class });
// And the expression (i.e. "result") type is also "int".
ee.setExpressionType(int.class);
// And now we "cook" (scan, parse, compile and load) the fabulous expression.
ee.cook("a + b");
// Eventually we evaluate the expression - and that goes super-fast.
int result = (Integer) ee.evaluate(new Object[] { 19, 23 });
System.out.println(result);
}
}
注意:如果你传递一个字符串字面量作为表达式,一定要转义所有的 Java 特殊字符,特别是反斜杠。
在我的机器上(2 GHz P4),编译表达式需要 670 微秒,而评估表达式仅需 0.35 微秒(大约比编译快 2000 倍)。
有一个示例程序 “ExpressionDemo” 你可以用来试验 ExpressionEvaluator,或者你可以研究 ExpressionDemo 的源代码来了解 ExpressionEvaluator 的 API。
$ java -cp janino.jar:commons-compiler.jar \
> org.codehaus.commons.compiler.samples.ExpressionDemo \
> -help
Usage:
ExpressionDemo { <option> } <expression> { <parameter-value> }
Compiles and evaluates the given expression and prints its value.
Valid options are
-et <expression-type> (default: any)
-pn <comma-separated-parameter-names> (default: none)
-pt <comma-separated-parameter-types> (default: none)
-te <comma-separated-thrown-exception-types> (default: none)
-di <comma-separated-default-imports> (default: none)
-help
The number of parameter names, types and values must be identical.
$ java -cp janino.jar:commons-compiler.jar \
> org.codehaus.commons.compiler.samples.ExpressionDemo \
> -et double \
> -pn x \
> -pt double \
> "Math.sqrt(x)" \
> 99
Result = 9.9498743710662
$
Janino 作为脚本评估器
类似于表达式评估器,ScriptEvaluator API 存在于编译和处理 Java “块”(即方法体)中。
如果定义了一个非 “void” 的返回值,那么该块必须返回该类型的值。
作为一个特殊功能,它允许声明方法。方法声明的位置和顺序无关紧要。
示例:
package foo;
import java.lang.reflect.InvocationTargetException;
import org.codehaus.commons.compiler.CompileException;
import org.codehaus.janino.ScriptEvaluator;
public class Main {
public static void
main(String[] args) throws CompileException, NumberFormatException, InvocationTargetException {
ScriptEvaluator se = new ScriptEvaluator();
se.cook(
""
+ "static void method1() {\n"
+ " System.out.println(1);\n"
+ "}\n"
+ "\n"
+ "method1();\n"
+ "method2();\n"
+ "\n"
+ "static void method2() {\n"
+ " System.out.println(2);\n"
+ "}\n"
);
se.evaluate();
}
}
Janino 作为类体评估器
类似于表达式评估器和脚本评估器,ClassBodyEvaluator 用于编译和处理 Java 类体,即一系列方法和变量声明。
如果你规定类体应该定义一个名为 “main()” 的方法,那么你的脚本将几乎像一个 “C” 程序一样:
public static void
main(String[] args) {
System.out.println(java.util.Arrays.asList(args));
}
“ClassBodyDemo” 程序(源代码)演示了这一点:
$ java -cp janino.jar:commons-compiler.jar \
> org.codehaus.commons.compiler.samples.ClassBodyDemo -help
Usage:
ClassBodyDemo <class-body> { <argument> }
ClassBodyDemo -help
If <class-body> starts with a '@', then the class body is read
from the named file.
The <class-body> must declare a method "public static void main(String[])"
to which the <argument>s are passed. If the return type of that method is
not VOID, then the returned value is printed to STDOUT.
$ java -cp janino.jar:commons-compiler.jar \
> org.codehaus.commons.compiler.samples.ClassBodyDemo '
> public static void
> main(String[] args) {
> System.out.println(java.util.Arrays.asList(args));
> }' \
> a b c
[a, b, c]
$
Janino 作为简单编译器
SimpleCompiler 编译单个 .java 文件(“编译单元”)。
与常规的 Java 编译不同,该编译单元可以声明多个公共类型。
示例:
// This is file "Hello.java", but it could have any name.
public
class Foo {
public static void
main(String[] args) {
new Bar().meth();
}
}
public
class Bar {
public void
meth() {
System.out.println("HELLO!");
}
}
它返回一个 ClassLoader,你可以从中获取已编译的类。
要运行此程序,请输入:
$ java -cp janino.jar:commons-compiler.jar \
> org.codehaus.janino.SimpleCompiler -help
Usage:
org.codehaus.janino.SimpleCompiler <source-file> <class-name> { <argument> }
Reads a compilation unit from the given <source-file> and invokes method
"public static void main(String[])" of class <class-name>, passing the
given <argument>s.
$ java -cp janino.jar:commons-compiler.jar \
> org.codehaus.janino.SimpleCompiler \
> Hello.java Foo
HELLO!
$
Janino 作为编译器
Compiler 编译一组 .java 文件(“编译单元”),并生成 .class 文件。
每个编译单元可以声明不同的包,且这些编译单元可以相互引用,甚至可以循环引用。
示例:
ICompiler compiler = compilerFactory.newCompiler();
compiler.compile(new File("pkg1/A.java"), new File("pkg2/B.java"));
然而,Compiler 可以重新配置,以从不同的源读取编译单元,和/或将类存储在不同的位置,例如存储到一个 Map 中,然后通过 ClassLoader 将其加载到虚拟机中:
ICompiler compiler = compilerFactory.newCompiler();
// Store generated .class files in a Map:
Map<String, byte[]> classes = new HashMap<String, byte[]>();
compiler.setClassFileCreator(new MapResourceCreator(classes));
// Now compile two units from strings:
compiler.compile(new Resource[] {
new StringResource(
"pkg1/A.java",
"package pkg1; public class A { public static int meth() { return pkg2.B.meth(); } }"
),
new StringResource(
"pkg2/B.java",
"package pkg2; public class B { public static int meth() { return 77; } }"
),
});
// Set up a class loader that uses the generated classes.
ClassLoader cl = new ResourceFinderClassLoader(
new MapResourceFinder(classes), // resourceFinder
ClassLoader.getSystemClassLoader() // parent
);
Assert.assertEquals(77, cl.loadClass("pkg1.A").getDeclaredMethod("meth").invoke(null));
高级示例
Janino 作为源代码类加载器
JavaSourceClassLoader 扩展了 Java 的 java.lang.ClassLoader 类,具有直接从源代码加载类的功能。
具体来说,如果通过这个类加载器加载一个类,它会在给定的“源路径”中的任何目录中搜索匹配的 “.java” 文件,读取、扫描、解析并编译它,并在 JVM 中定义生成的类。必要时,更多的类将通过父类加载器和/或源路径加载。
文件系统中不会创建任何中间文件。
示例:
// srcdir/pkg1/A.java
package pkg1;
import pkg2.*;
public class A extends B {
}
// srcdir/pkg2/B.java
package pkg2;
public class B implements Runnable {
public void run() {
System.out.println("HELLO");
}
}
// Sample code that reads, scans, parses, compiles and loads
// "A.java" and "B.java", then instantiates an object of class
// "A" and invokes its "run()" method.
ClassLoader cl = new JavaSourceClassLoader(
this.getClass().getClassLoader(), // parentClassLoader
new File[] { new File("srcdir") }, // optionalSourcePath
(String) null, // optionalCharacterEncoding
DebuggingInformation.NONE // debuggingInformation
);
// Load class A from "srcdir/pkg1/A.java", and also its superclass
// B from "srcdir/pkg2/B.java":
Object o = cl.loadClass("pkg1.A").newInstance();
// Class "B" implements "Runnable", so we can cast "o" to
// "Runnable".
((Runnable) o).run(); // Prints "HELLO" to "System.out".
如果 Java 源代码不在文件中,而是在其他存储(如数据库、主内存等)中,你可以指定一个自定义的 ResourceFinder,而不是基于目录的源路径。
如果你有许多源文件,并且想要减少编译时间,可以使用 CachingJavaSourceClassLoader,它利用应用程序提供的缓存来存储类文件以便重复使用。
jsh - Java shell
JANINO 有一个姊妹项目 “jsh”,它实现了一个类似于 “bash”、”ksh”、”csh” 等的 “shell” 程序,但使用 Java 语法。
Janino 作为命令行 Java 编译器
Compiler 类模仿了 ORACLE 的 javac 工具的行为。它将一组“编译单元”(即 Java 源文件)编译成一组类文件。
使用 “-warn” 选项,Janino 会发出一些可能非常有趣的警告,这些警告可能帮助你“清理”源代码。
BASH 脚本 “bin/janinoc” 实现了一个 ORACLE 的 JAVAC 工具的替代品:
$ janinoc -sourcepath src -d classes src/com/acme/MyClass.java
$ janinoc -help
A drop-in replacement for the JAVAC compiler, see the documentation for JAVAC
Usage:
java java.lang.Compiler [ <option> ] ... <source-file> ...
Supported <option>s are:
-d <output-dir> Where to save class files
-sourcepath <dirlist> Where to look for other source files
-classpath <dirlist> Where to look for other class files
-extdirs <dirlist> Where to look for other class files
-bootclasspath <dirlist> Where to look for other class files
-encoding <encoding> Encoding of source files, e.g. "UTF-8" or "ISO-8859-1"
-verbose
-g Generate all debugging info
-g:none Generate no debugging info (the default)
-g:{source,lines,vars} Generate only some debugging info
-rebuild Compile all source files, even if the class files
seem up-to-date
-help
The default encoding in this environment is "UTF-8".
$
Janino 作为 ANT 编译器
你可以通过 AntCompilerAdapter
类将 Janino 插入到 ANT 工具中。
只需确保 janino.jar
在类路径中,然后使用以下命令行选项运行 ANT:
-Dbuild.compiler=org.codehaus.janino.AntCompilerAdapter
Janino 作为 TOMCAT 编译器
如果你想在 TOMCAT 中使用 Janino,只需将 janino.jar
文件复制到 TOMCAT 的 common/lib
目录中,然后在 TOMCAT 的 conf/web.xml
文件中的 JSP servlet 定义部分添加以下初始化参数段:
<init-param>
<param-name>compiler</param-name>
<param-value>org.codehaus.janino.AntCompilerAdapter</param-value>
</init-param>
Janino 作为代码分析器
除了编译 Java 代码之外,Janino 还可以用于静态代码分析:
基于解析器生成的 AST(“抽象语法树”),遍历器会遍历 AST 的所有节点,派生类可以对它们进行各种分析,例如计算声明数量:
$ java org.codehaus.janino.samples.DeclarationCounter DeclarationCounter.java
Class declarations: 1
Interface declarations: 0
Fields: 4
Local variables: 4
$
这就是所有这些精巧的代码度量和样式检查的基础。
Janino 作为代码操控器
如果你想将一个 Java 编译单元读入内存,对其进行操控,然后将其写回文件以进行编译,只需执行以下步骤:
// 从 Reader "r" 中读取编译单元到内存中。
Java.CompilationUnit cu = new Parser(new Scanner(fileName, r)).parseCompilationUnit();
// 在内存中操控 AST。
// ...
// 将 AST 转换回文本。
UnparseVisitor.unparse(cu, new OutputStreamWriter(System.out));
AstTest.testMoveLocalVariablesToFields()
测试用例演示了如何操控 AST。
替代编译器实现
Janino 可以配置为使用其他 Java 编译器实现,而不是其自身的编译器。这些替代实现基本上需要实现 ICompilerFactory
接口。其中一个替代实现基于 javax.tools
API,并作为 Janino 发行版的一部分提供:commons-compiler-jdk.jar
。
基本上有两种方式可以切换实现:
-
使用
org.codehaus.commons.compiler.jdk.ExpressionEvaluator
及其相关类,而不是org.codehaus.janino.ExpressionEvaluator
;在编译时和运行时的类路径中使用commons-compiler-jdk.jar
代替janino.jar
。(commons-compiler.jar
必须始终在类路径中,因为它包含所有实现所需的基本类。) -
使用
org.codehaus.commons.compiler.CompilerFactoryFactory.getDefaultFactory().newExpressionEvaluator()
,并且只对commons-compiler.jar
进行编译(不需要具体实现)。在运行时,添加一个实现(janino.jar
或commons-compiler-jdk.jar
)到类路径中,getDefaultFactory()
会在运行时找到它。
基于 JDK 的实现生成的代码性能与 Janino 实现的性能相同;然而,编译速度则慢了 22 倍(通过编译表达式“a + b”两个整数来测量,见下文)。主要原因是 javax.tools
编译器通过类路径加载所需的 JRE 类,而 Janino 则使用已经加载到运行中的 JVM 中的类。因此,CompilerDemo
(JAVAC 的直接替代品)并不比 JAVAC 更快(通过编译 Janino 源代码来测量,见下文)。
活动 | Janino | JDK |
---|---|---|
编译表达式“a + b” | 0.44 ms | 9.84 ms |
编译 Janino 源代码 | 6.417 s | 5.864 s |
(使用 JDK 17 和 JRE 17 测量。)
安全性
警告: JRE 17 报告“System::setSecurityManager
将在未来版本中移除”,因此你不应该在 JRE 17 及更高版本中使用 Janino 沙箱。
由于 Janino 生成的字节码可以完全访问 JRE,因此如果正在编译和执行的表达式、脚本、类体或编译单元包含用户输入,可能会产生安全问题。
如果用户是一个有经验的系统管理员,可以预期他或她会负责任地使用 Janino,并遵循你提供的文档和注意事项;然而,如果用户是来自内部网或互联网的用户,无法假定他们不会表现得笨拙、轻率、富有创造性、一根筋,甚至是恶意的。
Janino 包含一个非常易于使用的安全 API,可以用来将表达式、脚本、类体和编译单元锁定在一个“沙箱”中,该沙箱由 Java 安全管理器保护。
import java.security.Permissions;
import java.security.PrivilegedAction;
import java.util.PropertyPermission;
import org.codehaus.janino.ScriptEvaluator;
import org.codehaus.commons.compiler.Sandbox;
public class SandboxDemo {
public static void
main(String[] args) throws Exception {
// Create a JANINO script evaluator. The example, however, will work as fine with
// ExpressionEvaluators, ClassBodyEvaluators and SimpleCompilers.
ScriptEvaluator se = new ScriptEvaluator();
se.setDebuggingInformation(true, true, false);
// Now create a "Permissions" object which allows to read the system variable
// "foo", and forbids everything else.
Permissions permissions = new Permissions();
permissions.add(new PropertyPermission("foo", "read"));
// Compile a simple script which reads two system variables - "foo" and "bar".
PrivilegedAction<?> pa = se.createFastEvaluator((
"System.getProperty(\"foo\");\n" +
"System.getProperty(\"bar\");\n" +
"return null;\n"
), PrivilegedAction.class, new String[0]);
// Finally execute the script in the sandbox. Getting system property "foo" will
// succeed, and getting "bar" will throw a
// java.security.AccessControlException: access denied (java.util.PropertyPermission bar read)
// in line 2 of the script. Et voila!
Sandbox sandbox = new Sandbox(permissions);
sandbox.confine(pa);
}
}
Java 安全管理器的官方文档由 ORACLE 提供,标题为《Java Essentials: The Security Manager》。由权限保护的操作包括:
- 接受来自指定主机和端口号的套接字连接
- 修改线程(更改其优先级、停止它等)
- 打开到指定主机和端口号的套接字连接
- 创建新的类加载器
- 删除指定文件
- 创建新进程
- 使应用程序退出
- 加载包含本地方法的动态库
- 在指定的本地端口号上等待连接
- 从指定包中加载类(由类加载器使用)
- 将新类添加到指定包中(由类加载器使用)
- 访问或修改系统属性
- 访问指定的系统属性
- 从指定文件读取
- 向指定文件写入
这些操作是攻击者可能进行的“真正恶意的行为”。然而,未受到保护的操作包括:
- 分配内存
- 创建新线程
- 过度使用 CPU 时间
幸运的是,Thread
构造函数会进行一些反射操作,因此可以通过不允许新的 RuntimePermission("accessDeclaredMembers")
来防止脚本创建新线程。
内存分配无法直接受到保护,不过,com.sun.management.ThreadMXBean.getThreadAllocatedBytes(long threadId)
似乎可以统计线程所分配的字节数,因此它可能是检测运行中脚本过度内存分配的一个有效措施。
debug 调试
即使生成的类是动态创建的,也可以进行交互式调试。
只需在启动 JVM 时设置两个系统属性,例如:
$ java \
> ... \
> -Dorg.codehaus.janino.source_debugging.enable=true \
> -Dorg.codehaus.janino.source_debugging.dir=C:\tmp \
> ...
(第二个属性是可选的;如果未设置,则临时文件将创建在默认的临时文件目录中。)
当 Janino 扫描表达式、脚本、类体或编译单元时,它会将源代码的副本存储在一个临时文件中,调试器通过其源路径访问该文件。(JVM 终止时,临时文件将被删除。)
参考资料
https://janino-compiler.github.io/janino/#getting_started