首先在运行java程序之前,肯定要想办法把.java
的文件使用编译器,编译成.class
的字节码文件。
运气好的是,强大的Java已经具备类似的API,就是JavaCompiler
类,下面做一点简单介绍:
JavaCompiler是java语言自带的一个接口,大概是一个对Java编译器的一个抽象,通过ToolProvider 类的静态方法获取其实现对象:
public interface JavaCompiler extends Tool, OptionChecker
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
稍微看一下源码
private static final String defaultJavaCompilerName
= "com.sun.tools.javac.api.JavacTool";
private static synchronized ToolProvider instance() {
if (instance == null)
instance = new ToolProvider();
return instance;
}
/**
* Gets the Java™ programming language compiler provided
* with this platform.
* @return the compiler provided with this platform or
* {@code null} if no compiler is provided
*/
public static JavaCompiler getSystemJavaCompiler() {
return instance().getSystemTool(JavaCompiler.class, defaultJavaCompilerName);
}
可以知道,返回的是一个JavacTool对象,是一个接口实现类
public final class JavacTool implements JavaCompiler {
这个类实现了run
方法
public interface Tool {
int run(InputStream in, OutputStream out, OutputStream err, String... arguments);
}
各个参数的意思分别是
in
out
err
arguments
前面三个参数如果,为null
则会用默认标准输入输出代替。网上到处都搜的到不做累述。
于是就有了第一种在线编译运行的实现思路,使用文件IO来动态生成.java
格式的文件与路径,然后写入代码内容。
最初我便是打算姑且使用这种方式,由于数据封装对象UserDto与Question都具有一个唯一的Id属性,因此 xx.userId.questionId
似乎挺适合用来做生成文件的类路径的,类名就可以统一学习leetcode使用Solution ,于是一番努力后写出了我的Compilerv1.0
然而这种方式就给人感觉很low,“java动态编译”听起来还挺屌的,结果一细看,就这?
而且,这样的实现,每次前端给一个请求过来都要进行文件读写操作,如果之前没有建立好相应路径与文件,还得重新新建,于是,当用户和题目多起来了以后那将是一个庞大的文件数量(最大值:用户数X题目数X2),甚至并发量稍微有一点还不知道会出现什么问题。
于是我开始寻找第二种解决方式,这里不得不感谢一个大佬的博客,其实我在线编译的代码很多都是参照这一篇文章的,后期也有自己的研究,但是这篇文章关于JavaCompiler类的介绍,给我开启了新的大门,感兴趣的朋友,链接在下面(应该是原创):
我是链接
我下面要介绍的内容其实和上面的博客可能会有点雷同,就是看了上面博客后有的一点自己的理解:
在线编译最理想的情况是:前端表单传给你需要编译的java文件字符串内容,然后将数据直接交给自定义编译器,编译器经过编译后返回Class对象,然后你再进行相应操作。
为了实现这个功能,除了JavaCompiler
还需要去了解如下对象:
JavaFileObject
(大概就是java文件的抽象)JavaFileManager
(大概就是Java文件管理操作的封装)相关内容是从上面博客链接学会的,我自己再做了些改动:
1.需要自定义一个JavaFileObject
重写一些方法
2.需要自定义一个JavaFileManager
重写一些方法
大致原理就是,由于Java封装的特性,只要类的行为正确,可以关心类的内部细节,所以,获取.java
文件内容,最初是从文件中获取,如果我们重写相应方法,意味着我们可以将要编译的String内容,直接返回给相应处理程序,只要调用相应方法,返回的内容正确,其实并不用关心,数据到底是从哪来的。
下面是我的
JavaFileObject
实现:
public class JavaFileObjectBean extends SimpleJavaFileObject {
/**
* Construct a SimpleJavaFileObject of the given kind and with the
* given URI.
*
* @param uri the URI for this file object
* @param kind the kind of this file object
*/
private String javaCode;
private ByteArrayOutputStream outputStream;
public JavaFileObjectBean(String className, String javaCode) {
super(URI.create("string:///"+className.replace(".","/")+Kind.SOURCE.extension), Kind.SOURCE);
// System.out.println("string:///" + className.replace(".", "/") + Kind.SOURCE.extension);
this.javaCode=javaCode;
//this.outputStream=new ByteArrayOutputStream();
}
protected JavaFileObjectBean(String className, Kind kind) {
super(URI.create("string:///"+className.replace(".","/")+kind.extension), kind);
// System.out.println("!!");
this.outputStream=new ByteArrayOutputStream();
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return this.javaCode;
}
@Override
public OutputStream openOutputStream() throws IOException {
return this.outputStream;
}
public byte[] getBytes(){
return this.outputStream.toByteArray();
}
}
继承自SimpleJavaFileObject
public class SimpleJavaFileObject implements JavaFileObject
新加了几个属性
private String javaCode;
private ByteArrayOutputStream outputStream;
重写了两个构造器方法:
public JavaFileObjectBean(String className, String javaCode) {
super(URI.create("string:///"+className.replace(".","/")+Kind.SOURCE.extension), Kind.SOURCE);
// System.out.println("string:///" + className.replace(".", "/") + Kind.SOURCE.extension);
this.javaCode=javaCode;
//this.outputStream=new ByteArrayOutputStream();
}
protected JavaFileObjectBean(String className, Kind kind) {
super(URI.create("string:///"+className.replace(".","/")+kind.extension), kind);
// System.out.println("!!");
this.outputStream=new ByteArrayOutputStream();
}
第一个构造方法:用于自己创建对象时使用,调用父类构造方法的同时初始化属性:javaCode
完成初始化以后,相关对象会调用重写的方法
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return this.javaCode;
}
然后进行编译。
第二个构造方法是给相关API调用,然后会调用重写的方法
@Override
public OutputStream openOutputStream() throws IOException {
return this.outputStream;
}
将编译结果写入提供的IO流
重写好的JavaFileObject类配合自定义JavaFileManager使用
public class JavaFileManagerBean extends ForwardingJavaFileManager {
private JavaFileObjectBean javaFileObjectBean;
/**
* Creates a new instance of ForwardingJavaFileManager.
*
* @param fileManager delegate to this file manager
*/
protected JavaFileManagerBean(JavaFileManager fileManager) {
super(fileManager);
//this.javaFileObjectBean= new JavaFileObjectBean();
}
@Override
public ClassLoader getClassLoader(Location location) {
return new SecureClassLoader(){
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = javaFileObjectBean.getBytes();
return super.defineClass(name,bytes,0,bytes.length);
}
};
}
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
this.javaFileObjectBean = new JavaFileObjectBean(className,kind);
return this.javaFileObjectBean;
}
}
相关API会调用
public JavaFileObject getJavaFileForOutput
此时,内置IO流属性会被初始化,然后写入编译的Class对象的二进制流信息,最后自定义一下类加载器的findClass
方法,利用loadClass
方法获取编译后得到的结果
cls=manager.getClassLoader(null).loadClass(className);
由于没有实际文件,最后会由下面的代码,寻找到需要加载到的类信息就会调用之前的重写的findClass方法得到Class 对象(defineClass方法用来将二进制流信息还原为Class对象)
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
暂时就介绍这么多,剩下的内容以后有时间再整理,如果又没说清楚的地方欢迎指正,觉得还不错的动动小手指点个赞。