1. 步骤

假如我们需要动态执行一段字符串形式的java代码,大概需要这样几步:

  • 生成文件(具体的文件/内存中的文件)
  • 调用javac编译
  • 通过反射执行

2. 生成文件

  • 确定字符串的形式

    • 字符串已经是完整的.java形式,那么就不再需要再做操作,写入文件即可
    • 字符串只是一个方法,需要听过UUID为其包装出一个类名,再写入文件
    • 字符串只是一段字符串,就需要即为其加上方法名,又要加上类名,然后再写入文件

2.1 IO写入具体的磁盘文件

  • 阻塞字节流输出
public class ToFile {//字节流方式public static void main(String[] args) throws IOException {String code = new String("public class A{}");FileOutputStream outputStream = new FileOutputStream("E:\\spring\\dynamicJava\\src\\main\\resources\\T1.java");byte[] codeBytes = code.getBytes(StandardCharsets.UTF_8);outputStream.write(codeBytes);outputStream.close();}
}
  • 字符流输出
public class ToFileByWriter {//字符流方式public static void main(String[] args) throws IOException {String code = new String("public class B{}");FileWriter writer = new FileWriter("E:\\spring\\dynamicJava\\src\\main\\resources\\T2.java");writer.write(code);writer.flush();writer.close();}
}
  • RandomAccessFile的方式

RandomAccessFile 是 Java 输入/输出流体系中功能最丰富的文件内容访问类,它提供了众多的方法来访问文件内容,它既可以读取文件内容,也可以向文件输出数据。由于 RandomAccessFile可以从任意位置访问文件,所以在只需要访问文件部分内容的情况下,使用 RandonAccessFile 类是一个很好的选择。

RandomAccessFile 对象包含了一个记录指针,用以标识当前读写处的位置,当程序新创建一个 RandomAccessFile 对象时,该对象的文件记录指针位于文件头(也就是 0 处),当读/写了 n 个字节后,文件记录指针将会向后移动 n 个字节。除此之外,RandonAccessFile 可以自由移动该记录指针,既可以向前移动,也可以向后移动。

public class ToFileByRandomAccessFile {public static void main(String[] args) throws IOException {String code = new String("public class C{}");File file = new File("E:\\spring\\dynamicJava\\src\\main\\resources\\T3.java");RandomAccessFile accessFile = new RandomAccessFile(file,"rw");accessFile.write(code.getBytes());accessFile.close();}
}
  • NIO的方式
public class ToFileByNio {//通过javaNio的方式进行写入public static void main(String[] args) throws IOException {String code = new String("public class D{}");//1. 打开channelRandomAccessFile accessFile = new RandomAccessFile("E:\\spring\\dynamicJava\\src\\main\\resources\\T4.java", "rw");FileChannel channel = accessFile.getChannel();//2. 创建buffer对象并填入内容(buffer默认是写入模式)ByteBuffer buffer = ByteBuffer.allocate(1024);buffer.put(code.getBytes(StandardCharsets.UTF_8));//3. 反转bufferbuffer.flip();//4. 写入channelwhile (buffer.hasRemaining()) {channel.write(buffer);}//5. 关闭channelchannel.close();}
}

2.2 生成内存的JavaFileObject对象

  • JavaFileObject源码
/*** File abstraction for tools operating on Java&trade; programming language* source and class files.** <p>All methods in this interface might throw a SecurityException if* a security exception occurs.** <p>Unless explicitly allowed, all methods in this interface might* throw a NullPointerException if given a {@code null} argument.** @author Peter von der Ah&eacute;* @author Jonathan Gibbons* @see JavaFileManager* @since 1.6*/
public interface JavaFileObject extends FileObject {enum Kind {SOURCE(".java"),CLASS(".class"),HTML(".html"),OTHER("");public final String extension;private Kind(String extension) {extension.getClass(); // null checkthis.extension = extension;}};Kind getKind();boolean isNameCompatible(String simpleName, Kind kind);NestingKind getNestingKind();Modifier getAccessLevel();
}

可以把一个JavaFileObject当成是一个不同类型文件在内存中的抽象。一般情况下,我们自己新建一个类完成SimpleJavaFileObject的创建,当然需要继承SimpleJavaFileObject

public class MySimpleJavaFileObject extends SimpleJavaFileObject {private String contents = null;private String className;public MySimpleJavaFileObject(String className, String contents) {super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);this.className = className;this.contents = contents;}public CharSequence getCharContent(boolean ignoredEncodingErrors) throws IOException {return contents;}public String getClassName() {return className;}
}

3. 动态编译

由于编译对象的不同,具体的动态编译的输出形式也不同,可以实现:

  • 从源文件到字节码文件的编译方式
  • 从源文件到内存的编译方式
  • 从内存到内存的编译方式

3.1 JavaCompiler.run()

javax.tools包下的tools类中实现了一个run方法,如下:

/*** Common interface for tools that can be invoked from a program.* A tool is traditionally a command line program such as a compiler.* The set of tools available with a platform is defined by the* vendor.** <p>Tools can be located using {@link* java.util.ServiceLoader#load(Class)}.** @author Neal M Gafter* @author Peter von der Ah&eacute;* @author Jonathan Gibbons* @since 1.6*/
public interface Tool {/*** Run the tool with the given I/O channels and arguments. By* convention a tool returns 0 for success and nonzero for errors.* Any diagnostics generated will be written to either {@code out}* or {@code err} in some unspecified format.** @param in "standard" input; use System.in if null* @param out "standard" output; use System.out if null* @param err "standard" error; use System.err if null* @param arguments arguments to pass to the tool* @return 0 for success; nonzero otherwise* @throws NullPointerException if the array of arguments contains* any {@code null} elements.*/int run(InputStream in, OutputStream out, OutputStream err, String... arguments);/*** Gets the source versions of the Java&trade; programming language* supported by this tool.* @return a set of supported source versions*/Set<SourceVersion> getSourceVersions();}

可以发现,Tool接口是一个可以从程序中调用的工具的公共接口,其中JavaCompiler(从程序中调用Java编程语言编译器的接口)Diagnostic(用于来自工具的诊断的接口)都是继承的他,其中有一个run()方法,接收参数由四个,分别为标准输入,标准输出,标准错误输出,最后一个参数为具体接口/实现类的参数。

JavaCompiler.run()类似于执行javac,第四个参数argument就是javac ./test.javatest.java。通过这样的方式可以在当前文件的目录下生成一个class文件。

public class CompilerRun {public static void main(String[] args) {JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();int run = compiler.run(null, null, null, "E:\\spring\\dynamicJava\\src\\main\\java\\com\\yuan\\compiler\\User.java");System.out.println("result = " + run);}
}

最终执行结果:

0表示编译成功,同时对应目录下出现相应的User.class

3.2 JavaCompiler.getTask()编译硬盘中的代码

使用JavaCompiler.run()方法非常简单,但它确不能更有效地得到我们所需要的信息。一般来说我们都会使用StandardJavaFileManager类(jdk 6或以上),这个类可以很好地控制输入、输出,并且可以通过DiagnosticListener得到诊断信息,而DiagnosticCollector类就是listener(监听)的实现。

需要注意的是DiagnosticCollector在被解析的java文件没问题的情况下是不会收集信息的,我们这里引入一个lombok,相当于java文件中有不存在的包,可以发现以下的诊断信息:

执行前目录结构如下,目标是把编译后的结果都放置在下面的classes包中。

public class Test {public static void main(String[] args) throws IOException {Test.compiler();}public static void compiler() throws IOException {//1.获得系统编译器JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();//2. 建立DiagnosticCollector对象DiagnosticCollector<Object> diagnosticCollector = new DiagnosticCollector<>();StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnosticCollector, null, null);//3. 建立源文件对象,每一个文件都被保存在一个JavaFileObject继承的类中Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromStrings(Arrays.asList("E:\\spring\\dynamicJava\\src\\main\\java\\com\\yuan\\getTaskByIO\\Person.java","E:\\spring\\dynamicJava\\src\\main\\java\\com\\yuan\\getTaskByIO\\Student.java"));//4. 确定options命令行选项List<String> options = Arrays.asList("-d", "E:\\spring\\dynamicJava\\src\\main\\java\\com\\yuan\\getTaskByIO\\classes");//5. 获取编译任务JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnosticCollector, options, null, compilationUnits);//6. 编译源程序Boolean success = task.call();fileManager.close();System.out.println(success ? "编译成功" : "编译失败");//7. 打印信息for (Diagnostic<?> diagnostic : diagnosticCollector.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));}}
}

结果如下:

可以发现,由于我们的每一个类的.java文件中是如下规定的:

所以会在classes目录下建立相应的子包,删除这一行package试试,结果正常了。

3.3 JavaCompoler.getTask()编译内存中的代码

JavaCompiler不仅可以编译硬盘上的Java文件,而且还可以编译内存中的Java代码,然后使用reflection来运行它们。我们可以编写一个MyJavaSimpleObject类,通过这个类可以输入Java源代码。

public class MySimpleJavaFileObject extends SimpleJavaFileObject {private String contents = null;private String className;public MySimpleJavaFileObject(String className, String contents) {super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);this.className = className;this.contents = contents;}public CharSequence getCharContent(boolean ignoredEncodingErrors) throws IOException {return contents;}public String getClassName() {return className;}
}
public class Test {public static void main(String[] args) throws Exception {Test.compiler2();}public static void compiler2() throws IOException, IllegalAccessException, IllegalArgumentException,InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException {JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();DiagnosticCollector diagnostics = new DiagnosticCollector();//自己手写java代码String code = "public class HelloWorld{" +"public static void main(String[] args){" +"System.out.println(\"Hello World\");}" +"}";StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);MySimpleJavaFileObject simpleJavaFileObject = new MySimpleJavaFileObject("HelloWorld", code);Iterable compilationUnits = Arrays.asList(simpleJavaFileObject);// options命令行选项Iterable<String> options = Arrays.asList("-d","E:\\spring\\dynamicJava\\src\\main\\resources");// 指定的路径一定要存在,javac不会自己创建文件夹JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null,compilationUnits);boolean success = task.call();System.out.println((success) ? "编译成功" : "编译失败");for (Object object : diagnostics.getDiagnostics()) {Diagnostic diagnostic = (Diagnostic) object;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));}}
}

最终结果如下:

3.4 总结

其实可以发现动态编译的基本流程如下:

public class CompileFileToFile{public static void main(String[] args) {//获取系统Java编译器JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();//获取Java文件管理器StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);//定义要编译的源文件File file = new File("/path/to/file");//通过源文件获取到要编译的Java类源码迭代器,包括所有内部类,其中每个类都是一个 JavaFileObject,也被称为一个汇编单元Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(file);//生成编译任务JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);//执行编译任务task.call();}
}

其实由上面的执行流程可以发现,重要的就是task.call(),可以来跟踪一下task.call()的执行流程。

可以发现最终是由输出流写入到实际的磁盘class文件中的。

4. 输出到内存中而不是磁盘

从前面的分析我们看到,JavaFileObject 的 openOutputStream() 方法控制了编译后字节码的输出行为,也就意味着我们可以根据需要定制自己的 Java 文件对象。比如,当编译完源文件之后,我们不想将字节码输出到文件,而是留在内存中以便后续加载,那么我们可以实现自己的输出文件类 JavaFileObject。由于输出文件对象是从文件管理器的 getJavaFileForOutput() 方法获取的,所以我们还应该重写文件管理器的这一行为,综合起来的代码如下:

JavaFileManager jfm = new ForwardingJavaFileManager(fileManager) {public JavaFileObject getJavaFileForOutput(JavaFileManager.Location location,String className,JavaFileObject.Kind kind,FileObject sibling) throws IOException {if(kind == JavaFileObject.Kind.CLASS) {return new SimpleJavaFileObject(URI.create(className + ".class"), JavaFileObject.Kind.CLASS) {public OutputStream openOutputStream() {return new FilterOutputStream(new ByteArrayOutputStream()) {public void close() throws IOException{out.close();ByteArrayOutputStream bos = (ByteArrayOutputStream) out;bytes.put(className, bos.toByteArray());}};}};}else{return super.getJavaFileForOutput(location, className, kind, sibling);}}};

我们以前会默认将class文件存储到磁盘是由于以下两个原因:

  1. FileManger返回的时默认的JavaFileObject
  2. 默认的JavaFileObjectopenOutStream的方法如下:
@Override @DefinedBy(Api.COMPILER)
public OutputStream openOutputStream() throws IOException {
fileManager.updateLastUsedTime();
fileManager.flushCache(this);
ensureParentDirectoriesExist();
return Files.newOutputStream(path);
}
`ClassWriter`所用的输出流时NIO包的File类,会默认存储到我们设定的路径上。

写入到内存:

所以如果我们需要写入到内存,就需要修改JavaFileObjectopenOutStream()方法;同时这个JavaFileObject()是通过文件管理器的getJavaFileForOutput()方法获得的,默认的文件管理器获得的JavaFileObject中的输出方法就是输出到磁盘上,所以我们还需要一个实现一个自身的FileManager

5. 从内存到内存的动态编译+动态执行

5.1. 自定义的Java源码文件类

public class MySimpleJavaFileObject extends SimpleJavaFileObject {private String contents = null;private String className;public MySimpleJavaFileObject(String className, String contents) {super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);this.className = className;this.contents = contents;}public CharSequence getCharContent(boolean ignoredEncodingErrors) throws IOException {return contents;}public String getClassName() {return className;}
}

5.2. 自定义的Java字节码文件类

这里需要重写openOutStream()方法,不输出字节码文件到文件,而是直接保存在一个输出流中。

public class MyJavaClassFileObject extends SimpleJavaFileObject {private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();public MyJavaClassFileObject(String name, JavaFileObject.Kind kind) {super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind);}public byte[] getBytes() {return outputStream.toByteArray();}//编译时候会调用openOutputStream获取输出流,并写数据@Overridepublic OutputStream openOutputStream() throws IOException {return outputStream;}
}

5.3 自定义的文件管理器

这里需要重写的方法时getJavaFileOutput()方法,输出我们自己写的Java字节码文件类

public class MyFileManager extends ForwardingJavaFileManager {private MyJavaClassFileObject javaClassObject;protected MyFileManager(StandardJavaFileManager standardJavaFileManager) {super(standardJavaFileManager);}public MyJavaClassFileObject getJavaClassObject(){return javaClassObject;}@Overridepublic JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind,FileObject sibling) {this.javaClassObject = new MyJavaClassFileObject(className, kind);return javaClassObject;}
}

5.4 自定义的类加载器

这里需要注意,默认的ClassLoader的defineClass()方法第一个参数接受的是全限定类名,classData是字节数组。

public class MyClassLoader extends ClassLoader {public Class loadClass(String fullName, MyJavaClassFileObject javaClassObject) {byte[] classData = javaClassObject.getBytes();return this.defineClass(fullName, classData, 0, classData.length);}
}

5.5 自定义的Java编译器

我们这里将编译器抽象出来。

public class DynamicCompiler {/*** 编译出类** @param fullClassName 全路径的类名* @param javaCode      java代码* @return 目标类*/public Class<?> compileToClass(String fullClassName, String javaCode) throws Exception {JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();MyFileManager fileManager = new MyFileManager(compiler.getStandardFileManager(diagnostics, null, null));List<JavaFileObject> jfiles = new ArrayList<>();jfiles.add(new MySimpleJavaFileObject(fullClassName, javaCode));List<String> options = new ArrayList<>();options.add("-encoding");options.add("UTF-8");JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, jfiles);boolean success = task.call();if (success) {MyJavaClassFileObject javaClassObject = fileManager.getJavaClassObject();MyClassLoader dynamicClassLoader = new MyClassLoader();//加载至内存return dynamicClassLoader.loadClass(fullClassName, javaClassObject);} else {for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {String error = compileError(diagnostic);throw new RuntimeException(error);}throw new RuntimeException("compile error");}}private String compileError(Diagnostic diagnostic) {StringBuilder res = new StringBuilder();res.append("LineNumber:[").append(diagnostic.getLineNumber()).append("]\n");res.append("ColumnNumber:[").append(diagnostic.getColumnNumber()).append("]\n");res.append("Message:[").append(diagnostic.getMessage(null)).append("]\n");return res.toString();}
}

步骤依旧和动态编译的时候一致,只不过在编译成功的时候,通过文件管理器获得我们自己写的class源码文件类,然后通过自定义的classLoader读取字节数组,实现创建一个类。

5.6 Main

public class Main {public static void main(String[] args) throws Exception {DynamicCompiler dynamicCompiler = new DynamicCompiler();String code = "package com.yuan.dynamic;\n" +"\n" +"/**\n" +" * Created by yuantb on 21/12/14.\n" +" */\n" +"public class Test {\n" +"    @Override\n" +"    public String toString() {\n" +"        return \"yuantb\"\n" + ";" +"    }\n" +"}\n";Class<?> clazz = dynamicCompiler.compileToClass("com.yuan.dynamic.Test", code);System.out.println(clazz.newInstance());}
}

结果:

Java动态执行代码字符串相关推荐

  1. php执行查询的代码,PHP实现动态执行代码的方法

    本文实例讲述了PHP实现动态执行代码的方法.分享给大家供大家参考,具体如下: 这里介绍的PHP动态执行,即在页面上直接输入代码,点击执行,返回执行结果 方法很简单,主要使用了: $newfunc = ...

  2. java动态执行逻辑_动态执行代码逻辑

    动态执行逻辑的方法据我所知有一下两种方式 QLExpress Groovy QLExpress QLExpress是阿里开源的动态脚本执行的项目. 由阿里的电商业务规则.表达式(布尔组合).特殊数学公 ...

  3. python动态执行代码_第6.6节 Python动态执行小结

    一.    Python动态执行支持通过输入数据流或文件传入Python源代码串,进行编译后执行,可以通过这种方式扩展Python程序的功能: 二.    动态执行方法可能导致恶意攻击,因此使用时需要 ...

  4. Java动态代理代码快速上手

    动态代理的两个核心的点是:代理的行为 和 代理机构. 举个例子,上大学的时候,很多同学吃午饭的时候都是叫别人带饭,有一个人H特别热心肠,想了一个办法,他在门口挂了个公示牌,每天有谁想要找人带饭就写公告 ...

  5. python控制台执行代码字符串_编写Python脚本以使用控制台命令执行

    研究是在底部,阅读之前...谢谢.在 我必须编写一个运行SQL查询的Python脚本.我创建了一个主类,名为SQLQuery.每个SQLQuery实例表示一个查询.脚本的结构必须如下所示:class ...

  6. Java动态代理代码案例:使用jdk自带的Proxy只能对有接口的类进行动态代理

  7. Java动态代理代码案例:使用cglib实现对无接口的类进行动态代理

  8. php 间隔时间执行任务,PHP间隔一段时间执行代码的方法

    PHP间隔一段时间执行代码的方法 发布于 2015-11-16 18:26:46 | 230 次阅读 | 评论: 0 | 来源: 网友投递 PHP开源脚本语言PHP(外文名: Hypertext Pr ...

  9. java 动态字符串_Java动态编译执行一串字符串,类似于Javascript里的eval函数

    Javascript里的eval函数能动态执行一串js脚本. 那Java里怎么做到呢. 有两种方法: 一种是使用可以执行js脚本的Java类 ScriptEngineManagerpublic sta ...

最新文章

  1. java内连接外连接_SQL中的内连接与外连接--Java学习网
  2. activity生命周期及数据保存
  3. jquery调用click事件的三种方式
  4. issubclass和isinstance 反射 内置方法(魔术方法)
  5. 参数迁移表达的第三种形态相互作用
  6. 澳洲 计算机 本科学费,澳大利亚墨尔本大学一年学费和生活费清单
  7. JavaScript: 取得 function 的所有参数名
  8. 10款中小企业必备的开源免费安全工具
  9. arcmap添加WMTS服务
  10. [转]Windows 7 蓝屏后获取 MEMORY.DMP 文件及注意事项
  11. scrapy使用crawlspider
  12. echarts单个实例包含多个grid,标题分别居中
  13. 导航可与红绿灯结合起来
  14. Improving Opencv10 More Morphology Transformations
  15. K8S-删除Terminating状态的namespace
  16. XPS数据分析问题收集及解答
  17. IntelliJ IDEA常见问题解决办法汇总
  18. 计算机系统概论(原书第2版)完整课后习题答案(一、二章)
  19. 微信小程序之扫普通链接二维码打开小程序实现动态传递参数及踩坑总结
  20. 微信扫一扫二维码直接打开手机外部浏览器

热门文章

  1. AIX 网卡绑定聚合具体操作步骤
  2. 【愚公系列】2023年02月 WMS智能仓储系统-012.登录功能的实现
  3. 靶机渗透(一)Lazysysadmin
  4. ARINC429总线收发器 -- HI-3593调试记录
  5. python 中文显示乱码如何处理
  6. 分享一个 Python + Django 实现的电商小项目
  7. 计算机四年大学规划书,大学四年规划书
  8. 解线性方程组的直接方法:LU分解法及其C语言算法实现
  9. 【图像分割】基于snake模型的图像分割matlab 源码
  10. 关于excel表格输入身份证的问题