java动态编译技术原理分析
1.动态编译技术
从 JDK 1.6 开始引入了用 Java 代码重写的编译器接口,使得我们可以在运行时编译 Java 源码,然后用类加载器进行加载,让 Java 语言更具灵活性,能够完成许多高级的操作。
2.本次要实现的功能
动态编译和运行输入的java代码 测试版,项目下载:http://www.jiajiajia.club/file/info/fxwPbs/101
Java动态编译-动态运行-代码检测-算法练习 springboot项目,下载:http://www.jiajiajia.club/file/info/RUyiak/97
截图:
3.探索jdk为我们提供的接口和类
1.编译相关接口和类
ToolProvider:编译器的提供者,类似一个工具返回JavaCompiler类的实例。
JavaCompiler:java的编译器。
JavaCompiler.CompilationTask:一个编译任务,从JavaCompiler对象获取一个编译任务。并调用call方法执行编译任务。
2.代码源文件相关接口和类
FileObject:它代表了对文件的一种抽象,在此主要是看作是对编译前源文件的抽象和编译后生成的字节码文件的抽象。其中定义了一些对文件读取、删除等的操作。
JavaFileObject:它是FileObject接口的一个子接口,增加了对java源文件和字节码文件特有的api,是编程语言工具的文件抽象。
SimpleJavaFileObject :该类是JavaFileObject接口的一个实现类,为JavaFileObject中的大多数方法提供简单的实现。从源码中可以看出该实现类的构造器用protected修饰,所以如果要想实现更加复杂的功能就必须要扩展这个类。后面的示例中会提到。
3.文件的创建和管理相关接口和类
JavaFileManager:java语言的文件管理器,包括源文件和类文件,用于创建JavaFileObject,在构建新的JavaFileObjects时,文件管理器必须确定创建它们的位置(包括输入位置和输出位置)。
ForwardingJavaFileManager:该类是JavaFileManager的一个子类,它的目的也是提高扩展性。
4.诊断信息收集的相关接口和类
Diagnostic,DiagnosticCollector用于定位和输出编译过程中的问题。
4.准备编译器对象
获取编译器对象要用到tools.jar,要注意。
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
获取一个编译任务:
JavaCompiler.CompilationTask task = compiler.getTask(null, javaFileManager, diagnosticsCollector, null, null, Arrays.asList(javaFileObject));
5.构建源代码对象
由于JDK提供的FileObject、ForwardingFileObject、JavaFileObject、ForwardingJavaFileObject,SimpleJavaFileObject都无法直接使用,所以我们需要根据需求自定义,此时我们要明白SimpleJavaFileObject类中的哪些方法是必须要覆盖的。首先在com.sun.tools.javac包中Main.class中的main方法开始,直到在com.sun.tools.javac.main. JavaCompiler类中找到一个叫readSource的方法:
public CharSequence readSource(JavaFileObject paramJavaFileObject)
{
try
{
this.inputFiles.add(paramJavaFileObject);
return paramJavaFileObject.getCharContent(false);
}
catch (IOException localIOException)
{
this.log.error("error.reading.file", new Object[] { paramJavaFileObject, JavacFileManager.getMessage(localIOException) });
}
return null;
}
源码中可以看到该方法通过JavaFileObject的getCharContent方法获取要编译的源码。所以我们在构建源码对象的时候要重写getCharContent方法;
import java.io.IOException;
import java.net.URI;
import javax.tools.SimpleJavaFileObject;
/**
* java源代码对象
* @author 硅谷探秘者(jia)
*
*/
public class SourcesJavaFileObject extends SimpleJavaFileObject {
/**
* 待编译的java源代码
*/
private String contents;
protected SourcesJavaFileObject(String className, String contents) {
super(URI.create("string:///" + className.replaceAll("\\.", "/") + Kind.SOURCE.extension), Kind.SOURCE);
this.contents = contents;
}
/**
* 编译器在编译的时候会调用 getCharContent 方法
*/
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
// TODO Auto-generated method stub
return contents;
}
}
6.构造编译后的字节码文件对象
在构建完成源码对象以,并且编译完成后需要调用writeClass方法将字节码文件输出出来,从源码中可以看出调用的是JavaFileObject类的openOutputStream方法。因此在构建字节码文件对象的时候必须要重写openOutputStream方法。
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import javax.tools.SimpleJavaFileObject;
/**
* 编译后的字节码对象
* @author 硅谷探秘者(jia)
*/
public class ClassByteJavaFileObject extends SimpleJavaFileObject{
/**
* 存放 编译后的clas字节码
*/
private ByteArrayOutputStream outPutStream;
protected ClassByteJavaFileObject(String className, Kind kind) {
super(URI.create("string:///" + className.replaceAll("\\.", "/") + Kind.SOURCE.extension), kind);
}
/**
* 编译器编译完成之后要将编译完成的字节码输出,通过打开一个输出流OutputStream来完成该过程。
* 因此openOutputStream()这个方法也是必须实现的。
*/
@Override
public OutputStream openOutputStream() throws IOException {
// TODO Auto-generated method stub
outPutStream=new ByteArrayOutputStream();
return outPutStream;
}
/**
* 获取class字节码对象的字节数组
* @return
*/
public byte[] getClassByte() {
return outPutStream.toByteArray();
}
}
7.构建文件管理器对象
为了设计的方便我们还得定义一个自己的文件管理器。因为我们要从代码中动态输入源码,并且要直接获取输出的字节码文件,然后使用自定义的类加载器加载字节码文件,所以jdk提供的例如ForwardingJavaFileManager文件管理器肯定不满足我们的需求,因此我们还需要自定义一个文件管理器,管理源文件或字节码文件的输入和输出。目前仅仅知道需要自定义文件管理器还不够,我们还需要知道JavaFileManager在内存中编译时的运行过程。
- 在编译过程中,首先是编译器会遍历JavaFileManager对象,获取指定位置的所有符合要求的JavaFileObject对象,过程中会扫面所有涉及的到的包,包括一个类和它实现的接口和继承的类。
- 之后根据获取到的JavaFileObject对象,获取它的二进制表示的名称,通过调用inferBinaryName()方法。
- 输出编译后的类,也是一个JavaFileObject对象。实现类是我们定义的ClassByteJavaFileObject。底层调用的是openOutputStream方法。
import java.io.IOException;
import java.util.Map;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
/**
* 自定义java文件管理器
* @author 硅谷探秘者(jia)
*
*/
@SuppressWarnings("rawtypes")
public class MyJavaFileManage extends ForwardingJavaFileManager{
private final Map<String, JavaFileObject> fileObjectMap;
@SuppressWarnings("unchecked")
protected MyJavaFileManage(JavaFileManager fileManager,final Map<String, JavaFileObject> fileObjectMap) {
super(fileManager);
this.fileObjectMap=fileObjectMap;
}
/**
* 编译器在编译的时候会嗲用getJavaFileForOutput方法获取一个文件对象,底层会遍历源文件所有涉及的到的包,包括一个类和它实现的接口和继承的类。
* 在此用于字节数组的输出。
*/
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling)
throws IOException {
// TODO Auto-generated method stub
JavaFileObject sourcesJavaFileObject=new ClassByteJavaFileObject(className, kind);
fileObjectMap.put(className,sourcesJavaFileObject);
return sourcesJavaFileObject;
}
}
8.字节码输出以后我们还需要自定义类加载器去加载输出的字节码对象
import java.util.Map;
import javax.tools.JavaFileObject;
/**
* 自定义类加载器
* @author 硅谷探秘者(jia)
*/
public class MyClassLoader extends ClassLoader{
private final Map<String, JavaFileObject> fileObjectMap;
public MyClassLoader(final Map<String, JavaFileObject> fileObjectMap) {
// TODO Auto-generated constructor stub
this.fileObjectMap=fileObjectMap;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
ClassByteJavaFileObject fileObject = (ClassByteJavaFileObject)fileObjectMap.get(name);
if (fileObject != null) {
byte[] bytes = fileObject.getClassByte();
return defineClass(name, bytes, 0, bytes.length);
}
try {
return ClassLoader.getSystemClassLoader().loadClass(name);
} catch (Exception e) {
return super.findClass(name);
}
}
}
9.进入主题,主要代码
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
/**
* java动态编译
* @author 硅谷探秘者(jia)
*/
public class MyJavaCompiler {
//全类名
private String fullClassName;
//java源代码
private String sourceCode;
//存放编译过程中输出的信息,如编译过程中存在警告或异常信息
private DiagnosticCollector<JavaFileObject> diagnosticsCollector = new DiagnosticCollector<>();
//存放控制台输出的内容
private String result;
//编译耗时(单位ms)
private long compilerTakeTime;
//运行耗时(单位ms)
private long runTakeTime;
//存放 编译后的 ClassByteJavaFileObject 对象
private static final Map<String, JavaFileObject> fileObjectMap = new ConcurrentHashMap<>();
/**
* 获取java编译器,注意如果获取java编译器为null 说明你的jdk或jre中缺少 tools.jar
* 此时将tools.jar拷贝到jdk安装目录的lib文件夹下即可
*/
private JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
MyJavaCompiler(String sourceCode){
this.sourceCode=sourceCode;
this.fullClassName=getFullClassName(sourceCode);
}
/**
* 执行编译任务
* @return
*/
public boolean compiler() {
//标准的内容管理器,更换成自己的实现,覆盖部分方法
StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(diagnosticsCollector, null, null);
JavaFileManager javaFileManager = new MyJavaFileManage(standardFileManager,fileObjectMap);
//构造源代码对象
JavaFileObject javaFileObject = new SourcesJavaFileObject(fullClassName, sourceCode);
long startTime = System.currentTimeMillis();
//获取一个编译任务
JavaCompiler.CompilationTask task = compiler.getTask(null, javaFileManager, diagnosticsCollector, null, null, Arrays.asList(javaFileObject));
compilerTakeTime = System.currentTimeMillis() - startTime;
return task.call();
}
/**
* 运行目标程序
* @throws ClassNotFoundException
* @throws NoSuchMethodException
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws UnsupportedEncodingException
*/
public void runMainMethod() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, UnsupportedEncodingException {
PrintStream out = System.out;
try {
long startTime = System.currentTimeMillis();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PrintStream printStream = new PrintStream(outputStream);
System.setOut(printStream);
MyClassLoader scl = new MyClassLoader(fileObjectMap);
Class<?> aClass = scl.findClass(fullClassName);
Method main = aClass.getMethod("main", String[].class);
Object[] pars = new Object[]{1};
pars[0] = new String[]{};
main.invoke(null, pars); //调用main方法
//设置运行耗时
runTakeTime = System.currentTimeMillis() - startTime;
//设置打印输出的内容
result = new String(outputStream.toByteArray(), "utf-8");
} finally {
//还原默认打印的对象
System.setOut(out);
}
}
/**
* 获取编译信息 (异常/警告)
* @return
*/
public String getCompilerMessage() {
StringBuilder sb = new StringBuilder();
List<Diagnostic<? extends JavaFileObject>> diagnostics = diagnosticsCollector.getDiagnostics();
for (@SuppressWarnings("rawtypes") Diagnostic diagnostic : diagnostics) {
sb.append(diagnostic.toString()).append("\r\n");
}
return sb.toString();
}
/**
* 获取类的全类名
* @param sourceCode java源代码
* @return
*/
public static String getFullClassName(String sourceCode) {
String className = "";
Pattern pattern = Pattern.compile("package\\s+\\S+\\s*;");
Matcher matcher = pattern.matcher(sourceCode);
if (matcher.find())
className = matcher.group().replaceFirst("package", "").replace(";", "").trim() + ".";
pattern = Pattern.compile("class\\s+\\S+\\s+\\{");
matcher = pattern.matcher(sourceCode);
if (matcher.find())
className += matcher.group().replaceFirst("class", "").replace("{", "").trim();
return className;
}
public String getResult() {
return result;
}
public long getRunTakeTime() {
return runTakeTime;
}
public long getCompilerTakeTime() {
return compilerTakeTime;
}
}
10.测试代码
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
public class MainTest {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, UnsupportedEncodingException {
String sourceCode="public class MainTest {\r\n" +
" public static void main(String[] args) {\r\n" +
" A a=new A();\r\n" +
" System.out.println(a.a);\r\n" +
" }\r\n" +
"}\r\n" +
"class A{\r\n" +
" public int a=9;\r\n" +
"}";
MyJavaCompiler c=new MyJavaCompiler(sourceCode);
if(c.compiler()) {
c.runMainMethod();
System.out.println("编译时间(ms):"+c.getCompilerTakeTime());
System.out.println("运行时间(ms):"+c.getRunTakeTime());
System.out.println("运行结果输出:"+c.getResult());
}
System.out.println("编译信息:"+c.getCompilerMessage());
}
}
输出:
编译时间(ms):25
运行时间(ms):1
运行结果输出:9
编译信息: