在线运行 Java 代码的原理及实现
本文最后更新于 2023年2月21日 上午
简介
在线运行 Java 代码是指用户在浏览器中输入 Java 代码,通过在线编译和加载,最终在服务器上运行代码并返回结果。这种技术被广泛应用于在线编程学习、在线面试和在线评测等场景。
原理
动态编译
Java 的动态编译是指在运行时将 Java 代码编译成字节码的过程。Java 提供了一个标准的 API:JavaCompiler
和 ToolProvider
,可以用来进行动态编译。在动态编译时,需要将 Java 代码转换为 JavaFileObject
对象,然后通过 JavaCompiler.getTask()
方法来编译 JavaFileObject
对象。在编译过程中,可以使用 DiagnosticCollector
类来收集编译过程中的错误和警告信息。
动态加载
Java 的动态加载是指在运行时将编译好的字节码加载到内存中,并生成对应的 class 对象的过程。Java 提供了一个标准的 API:ClassLoader
,可以用来进行动态加载。通过自定义 ClassLoader
类来加载字节码,然后调用 ClassLoader.loadClass()
方法即可加载类。
线程的限制
在线运行 Java 代码需要考虑线程的限制和安全性控制。为了避免在线运行的代码对服务器产生过多的负载,可以使用线程池来限制并发访问以及设置超时时间停止线程。
安全性控制
为了保证在线运行的代码安全性,需要限制在线运行的代码只能访问一些受控的资源,并且禁止访问其他资源。Java 提供了一个安全管理器(SecurityManager
)来控制代码的安全性,可以在代码运行前启用安全管理器,限制代码的访问权限。
实现
编译器
ScriptCompiler
类实现了一个动态编译和执行 Java 代码,主要功能包括:
- 编译 Java 代码字符串为 Java Class,并加载该 Class。
- 执行该 Class 中的 Main 方法,并将输出结果返回。
包括以下几个主要步骤:
- 使用 Java Compiler API 编译 Java 代码字符串为 Java Class。
- 利用 Java Compiler API 获取系统默认的
JavaCompiler
,然后创建一个DiagnosticCollector
用于收集编译过程中的诊断信息。 - 使用
StandardJavaFileManager
创建一个JavaFileObject
对象,表示一个源代码文件,将 Java 代码字符串作为文件内容。 - 设置编译选项,这里设置了编译输出目录和编译源文件列表。
- 调用
CompilationTask
的call()
方法编译 Java 代码,如果编译失败,则将诊断信息拼接成字符串并抛出异常。
- 利用 Java Compiler API 获取系统默认的
- 使用自定义的
ClassLoader
加载编译好的 Java Class。ScriptLoader
首先将类名转换成类文件名,然后从指定的路径中加载对应的类文件,返回该类的 class 对象。
- 执行编译好的 Java Class 的 Main 方法,并将输出结果返回。
- 创建一个
ByteArrayOutputStream
对象用于缓存执行结果。 - 使用
System.setOut()
方法将System.out
的输出重定向到缓存输出流中。 - 通过反射获取 Main 方法并执行,将传入的参数作为 Main 方法的参数,执行过程中会输出内容到缓存输出流中。
- 将缓存输出流中的内容转换成字符串并返回,同时将
System.out
重定向回原来的输出流。
- 创建一个
- 对执行 Main 方法进行安全控制,防止代码执行恶意操作。
- 在执行 Main 方法之前和之后,调用
ScriptSecurityManager
的相关方法进行安全控制,限制了代码执行的权限和行为,防止代码执行恶意操作。
- 在执行 Main 方法之前和之后,调用
点击查看代码
1 |
|
常量
ScriptConstant
类的作用是为编译脚本文件提供一个固定的目录,该目录下的脚本文件会被编译为 Java 类并在运行时执行。
CLASS_NAME
常量为字符串 “Main”。CLASS_PATH
常量为调用了createScriptDir()
方法的返回值。createScriptDir()
是一个静态方法,它的作用是创建一个名为 “custom-script” 的目录,并返回该目录的路径作为 CLASS_PATH 常量的值。具体实现如下:- 通过
ScriptConstant.class.getProtectionDomain().getCodeSource().getLocation().getPath()
方法获取当前类的绝对路径。 - 将路径字符串按照 UTF-8 编码方式进行解码,以避免因为路径中存在特殊字符导致的问题。
- 获取当前路径的父目录和其父目录的路径,即
resource
目录。 - 将
resource + File.separator + "custom-script" + File.separator
赋值给customScriptPath
变量,表示要创建的目录名。 - 创建
customScript
目录,并将customScriptPath
作为 CLASS_PATH 常量的值返回。
- 通过
点击查看代码
1 |
|
异常
ScriptException
的异常类继承自 SecurityException
类。该类通过 public ScriptException(String message)
构造函数提供了一个带有字符串参数的构造函数,用于创建一个新的 ScriptException
对象,这个对象包含了给定的字符串消息。
这个自定义的异常类可能用于在处理脚本时发生错误时抛出异常。例如,当脚本执行时发生安全性异常时,就可以抛出这个自定义的异常,以便在调用脚本的代码中处理异常并采取适当的措施。
点击查看代码
1 |
|
类加载器
继承自 ClassLoader
的 ScriptLoader
类,用于在运行时动态加载自定义脚本。
该类重写了 findClass()
方法,在此方法中,将类名转换为类文件的路径,然后通过 getClassFileBytes()
方法读取该路径下的 class 文件,并返回其字节码。最后,使用 defineClass()
方法将字节码转化为 Java 类的实例,并返回该类的 class 对象。
getClassFileBytes()
方法使用了 NIO 的方式读取 class 文件。该方法通过 FileInputStream
打开 class 文件,然后通过 FileChannel
读取文件数据,并使用 ByteBuffer
缓存数据,最后通过 WritableByteChannel
将数据写入到 ByteArrayOutputStream
中,并返回其字节数组。
该脚本加载器通过在 findClass()
方法中动态加载 class 文件,使得程序可以在运行时动态的调用一些自定义的 Java 脚本。
点击查看代码
1 |
|
安全管理器
继承自 SecurityManager
的 ScriptSecurityManager
类是一个用于控制 Java 应用程序的安全权限的自定义安全管理器,通过 initPermission()
和 destroyPermission()
方法设置安全管理器,并通过 check()
方法检查和控制权限请求。
initPermission()
方法用于设置应用程序的安全管理器,如果还没有设置,则会创建一个 ScriptSecurityManager
实例并将其设置为应用程序的安全管理器。它需要一个线程 ID 参数作为标识,以便在 check()
方法中检查权限时确定当前线程是否具有特定权限。
destroyPermission()
方法用于撤销应用程序的安全管理器,它将之前设置的安全管理器设置为 null,并且将 destroy
标志设置为 true。
ScriptSecurityManager
类重写了 checkPermission()
方法并在里面调用 check()
方法 ,在 check()
方法中,根据权限的类型和名称,执行不同的检查。
如果请求的权限不被允许,它将抛出一个 SecurityException
异常,以防止应用程序的不安全行为。
如果权限是 RuntimePermission
,它会检查请求的名称是否包含 setSecurityManager
并且 destroy
标志为 false,如果是,则不允许设置新的安全管理器。
对于其他权限类型,它会检查请求的名称或操作是否包含特定权限的名称,并且如果包含,则不允许该请求。如果请求的权限被允许,则不会发生任何操作。
点击查看代码
1 |
|