教你怎么实现java语言的在线编译

一、前言

  • 使用过leetcode或者类似在线编译网站功能的人,或许会比较感兴趣,关于在线编译的实现原理,由于我比较头铁,所以一冲动之下毕业设计的项目选择制作一个类似于在线编译的一个网站。
  • 在决定做这个之前,大概对这方面的东西一窍不通,网上的资料很多也是比较千篇一律,给我这种萌新带来的难度不是一点半点,当然,最终收获还是挺大的,所以想写一点东西,作为梳理,也给以后想学的人做一个参考作用(其实在写的过程中还是踩了一些坑的)。
  • 最终,其实成果挺水的,做出来的成品,就只是实现了一个简陋的Java语言的在线编译功能,这里也想吐槽一下,其实leetcode,支持那么多语言的在线编译真的挺厉害的。

二、前期准备

首先在运行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

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) {
        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;
  • javaCode:用来保存需要编译的Java文件内容
  • outputStream:用来保存编译后,Class对象的二进制流

重写了两个构造器方法:

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();
                }

暂时就介绍这么多,剩下的内容以后有时间再整理,如果又没说清楚的地方欢迎指正。

到此这篇关于教你怎么实现java语言的在线编译的文章就介绍到这了,更多相关实现java语言的在线编译内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 改善Java代码之慎用java动态编译

    动态编译一直是Java的梦想,从Java 6版本它开始支持动态编译了,可以在运行期直接编译.java文件,执行.class,并且能够获得相关的输入输出,甚至还能监听相关的事件.不过,我们最期望的还是给定一段代码,直接编译,然后运行,也就是空中编译执行(on-the-fly),来看如下代码: public class Client { public static void main(String[] args) throws Exception { //Java源代码 String sourceS

  • IDEA 中使用 ECJ 编译出现 java.lang.IllegalArgumentException的错误问题

    IDEA 中使用 ECJ 编译出现 java.lang.IllegalArgumentException IDEA内置的ECJ版本太高了 把图中红框部分的内容替换一个合适的版本jar包就行了.这个jar除了可以单独下载,在tomcat / lib 目录下也存在 ps:IDEA中的tomcat启动出现java.lang.IllegalArgumentException,java.util.zip.ZipException 我的错误:war中包含的的jar版本与pom.xml中配置的版本不一致.wa

  • java编译后的文件出现xx$1.class的原因及解决方式

    java编译后的文件名字带有$接数字的就是匿名内部类的编译结果,接名字的就是内部类的编译结果 例如: TestFrame$1.class是匿名内部类的编译结果,TestFrame$MyJob.class则是内部类MyJob编译后得到的. 使用内部类可以隐藏一些实现的细节, 等等, 还有其他一些好处. 使用匿名类的时候, 要注意代码的可读性 补充知识:JNI之javah使用时报错:找不到类文件 初学java,想使用JNI,在用javah生成头文件时,总是报错找不到类: 看了javah的help,本

  • 手动编译并运行Java项目实现过程解析

    现在Java开发基本上就是IDE调试,如果跨平台打个jar包过去运行一般就可以了,但是有些情况比如需要引入外部依赖的时候,这个时候是不能直接运行的,还需要引入一些外部的参数,并不是简单的javac和java的关系了,下面来详细说一下 一般情况下,在本地都是使用eclipse开发工具进行开发,很多东西基本上就不用我们考虑了,如果我们要将项目放到Linux下运行的话,那么就需要进行转移的操作,当然有Maven.Ant这样的自动化部署工具,简直是太方便了,为了做到更进一步认识的话,我们纯手动的去打包一

  • java实现动态编译并动态加载

    在D盘test目录下有个java文件:AlTest.java public class AlTest { public String sayHello(){ System.out.println("AlTest类 sayHello()方法正在执行...."); return "hello word"; } } 现需要实现在工程已经运行过程中,进行java文件到class文件的编译操作,并运行AlTest类的方法 package com.piao.job; impor

  • java利用JEXL实现动态表达式编译

    背景 做项目突然遇到这样的需求: 系统要获取多个数据源的数据,并进行处理,最后输出多个字段.字段的计算规则一般是简单的取值最多加一点条件判断. 而且需要动态变动!!例如一个字段a的取值,如果a > 10的时候输出10,a <= 10则输出a.这里的10可能在一天后改成8,也可能在后天就改成了12.当然,如果只是一个数字的变动还好说,我们可以使用数据库进行存储.但是,万一哪天需求突然变成了a < 10的时候输出10,a >=10 则输出a,就需要对代码改动,再测试再发布才能到生产环境

  • 2020年支持java8的Java反编译工具汇总(推荐)

    大多商业软件,会对程序进行加密.加壳等安全措施以防范软件被破解,从而使得反编译越来越难.反编译是一个对目标可执行程序进行逆向分析,从而得到源代码的过程.尤其是像Java这样的运行在虚拟机上的编程语言,更容易进行反编译得到源代码. 我们知道,在代码支撑方面,JDK 1.7引入了字符串Switch.泛型接口改进等新功能,1.8增加了lambda表达式.方法传递.多重注解等新特性,这使得反编译工具的编写难度加大.今天我们盘点一下目前仍然可用的.相对功能很强大的Java反编译工具(Eclipse插件不做

  • IDEA不编译除了.java之外的文件的解决办法(推荐)

    - 遇到问题 今天在IDEA里面运行项目的时候报了一个错,如下图所示: - 找到问题根源 其实控制台给出的错误信息提示说的很明显:类加载器加载文件的时候没有找到指定的文件,于是我看了一下项目编译的二进制文件,如下图所示: 我们可以很清楚的看到我的源文件sixbit和threebit目录和目录下的文件并没有被编译到target二进制文件里面去,所以项目在运行的时候要用到sixbit和threebit目录下的文件时,需要去编译号的classes里面去找相对应的二进制文件,发现根本没有,于是就报错了.

  • java编译命令基础知识点

    我们在对计算机下达指令时,人类的语言它是不能够明白,需要通过编译的时候翻译成计算机能听懂的语言.编译过程中会调用javac命令,这点大家可能接触的不多,毕竟是是计算机程序内部运行时的操作.下面我们就编译的概念.命令带来讲解,然后分享一个编译实例给大家练习. 1.编译概念 通过流程图可以看出其实java的执行可以分为两大步骤,第一是编译,这一过程就是调用的javac命令,编译成对应的.class文件.第二是解释执行,这一过程是调用的java命令. 2.编译命令 (1)linux rm -rf Ma

  • java编译器和JVM的区别

    Java虚拟机(JVM)是可运行Java代码的假想计算机.只要根据JVM规格描述将解释器移植到特定的计算机上,就能保证经过编译的任何Java代码能够在该系统上运行.java编译器把java编译成字节码,也就是.class文件,然后JVM给编译成的字节码提供运行环境.java的源代码是无法直接在JVM上运行的. 1.java编译器 Java语言写的源程序通过Java编译器,编译成与平台无关的'字节码程序'(.class文件,也就是0,1二进制程序),然后在OS之上的Java解释器中解释执行. 也相

  • 详解Java的编译执行与解释执行

    一.前言 编程语言分为低级语言和高级语言,机器语言.汇编语言是低级语言,C.C++.java.python等是高级语言. 机器语言是最底层的语言,能够直接执行.而我们编写的源代码是人类语言,计算机只能识别某些特定的二进制指令,在程序真正运行之前必须将源代码转换成二进制指令.汇编语言通过汇编器翻译成机器指令后执行,一条汇编指令,对应着一条机器指令. 高级语言编程的程序有三种执行方式: 1.一种是编译执行,源程序先通过编译器(负责将源程序翻译成目标机器指令)翻译成机器指令,通过编译-->链接-->

  • 利用 Docker 构建简单的 java 开发编译环境的方法详解

    目前 Java 语言的版本很多,除了常用的 Java 8,有一些遗留项目可能使用了 Java 7,也可能有一些比较新的的项目使用了 Java 10 以上的版本.如果想切换自己本地的 Java 开发环境,折腾起来还是需要花费一些时间的,并且日后在不同版本间切换每次都要折腾一次. Docker 的出现让我们维护不同版本的开发编译环境变得简单,如果你还不知道什么是 Docker 可以看看 Docker 入门介绍.我们可以采用两种方式来构建 java 的开发环境,一种是在容器内编译运行,一种是在容器外编

随机推荐