教你怎么实现java语⾔的在线编译
⽬录
⼀、前⾔
⼆、前期准备
三、JavaCompiler V1.0
四、JavaCompiler V2.0
五、JavaFileObject实现
⼀、前⾔
使⽤过leetcode或者类似在线编译⽹站功能的⼈,或许会⽐较感兴趣,关于在线编译的实现原理,由于我⽐较头铁,所以⼀冲动之下毕业设计的项⽬选择制作⼀个类似于在线编译的⼀个⽹站。
在决定做这个之前,⼤概对这⽅⾯的东西⼀窍不通,⽹上的资料很多也是⽐较千篇⼀律,给我这种萌新带来的难度不是⼀点半点,当然,最终收获还是挺⼤的,所以想写⼀点东西,作为梳理,也给以后想学的⼈
做⼀个参考作⽤(其实在写的过程中还是踩了⼀些坑的)。
最终,其实成果挺⽔的,做出来的成品,就只是实现了⼀个简陋的Java语⾔的在线编译功能,这⾥也想吐槽⼀下,其实leetcode,⽀持那么多语⾔的在线编译真的挺厉害的。
⼆、前期准备
⾸先在运⾏java程序之前,肯定要想办法把.java的⽂件使⽤编译器,编译成.class的字节码⽂件。
运⽓好的是,强⼤的Java已经具备类似的API,就是JavaCompiler类,下⾯做⼀点简单介绍:
JavaCompiler是java语⾔⾃带的⼀个接⼝,⼤概是⼀个对Java编译器的⼀个抽象,通过ToolProvider 类的静态⽅法获取其实现对象:
public interface JavaCompiler extends Tool, OptionChecker
JavaCompiler compiler = SystemJavaCompiler();
稍微看⼀下源码
private static final String defaultJavaCompilerName
= "ls.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, arguments);
}
各个参数的意思分别是
in
java编译器提供信息
out
⽤于获取输出信息
err
⽤于获取错误信息
arguments
编译的⽂件(路径)
前⾯三个参数如果,为null则会⽤默认标准输⼊输出代替。⽹上到处都搜的到不做累述。
三、JavaCompiler V1.0
于是就有了第⼀种在线编译运⾏的实现思路,使⽤⽂件IO来动态⽣成.java格式的⽂件与路径,然后写⼊代码内容。
最初我便是打算姑且使⽤这种⽅式,由于数据封装对象UserDto与Question都具有⼀个唯⼀的Id属性,因此
xx.userId.questionId似乎挺适合⽤来做⽣成⽂件的类路径的,类名就可以统⼀学习leetcode使⽤Solution ,于是⼀番努⼒后写出了我的Compilerv1.0
然⽽这种⽅式就给⼈感觉很low,“java动态编译”听起来还挺屌的,结果⼀细看,就这?
⽽且,这样的实现,每次前端给⼀个请求过来都要进⾏⽂件读写操作,如果之前没有建⽴好相应路径与⽂件,还得重新新建,于是,当⽤户和题⽬多起来了以后那将是⼀个庞⼤的⽂件数量(最⼤值:⽤户数X题⽬数X2),甚⾄并发量稍微有⼀点还不知道会出现什么问题。
四、JavaCompiler V2.0
在线编译最理想的情况是:
前端表单传给你需要编译的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) {
ate("string:///"+place(".","/")+sion), Kind.SOURCE);
// System.out.println("string:///" + place(".", "/") + sion);
this.javaCode=javaCode;
//this.outputStream=new ByteArrayOutputStream();
}
protected JavaFileObjectBean(String className, Kind kind)  {
ate("string:///"+place(".","/")+sion), 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 ByteArray();
}
}
继承⾃SimpleJavaFileObject
public class SimpleJavaFileObject implements JavaFileObject
新加了⼏个属性
private String javaCode;
private ByteArrayOutputStream outputStream;
javaCode:⽤来保存需要编译的Java⽂件内容
outputStream:⽤来保存编译后,Class对象的⼆进制流
重写了两个构造器⽅法:
public JavaFileObjectBean(String className, String javaCode) {
ate("string:///"+place(".","/")+sion), Kind.SOURCE);
// System.out.println("string:///" + place(".", "/") + sion);
this.javaCode=javaCode;
//this.outputStream=new ByteArrayOutputStream();
}
protected JavaFileObjectBean(String className, Kind kind)  {
ate("string:///"+place(".","/")+sion), 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 = Bytes();
return super.defineClass(name,bytes,0,bytes.length);
java做什么的
}
};
}
@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⽅法获取编译后得到的结果
ClassLoader(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.ParentDelegationTime().addTime(t1 - t0);
sun.FindClassTime().addElapsedTimeFrom(t1);
sun.FindClasses().increment();
}
暂时就介绍这么多,剩下的内容以后有时间再整理,如果⼜没说清楚的地⽅欢迎指正。
到此这篇关于教你怎么实现java语⾔的在线编译的⽂章就介绍到这了,更多相关实现java语⾔的在线编译内容请搜索以前的⽂章或继续浏览下⾯的相关⽂章希望⼤家以后多多⽀持!