Java动态编译的实现

这里就不讲解啥是动态编译了,原理,区别的文章也有很多。

这里主要使用 线程池;重写 System.out的输出,并保存输出的结果。然后返回到客户端。

1.获取字符串类型的源码,转化成编译后的byte[]数组.

String source :客户端传过来的字符串类型的代码,DiagnosticCollector:编译结果收集器。

    private static Pattern CLASS_PATTERN = Pattern.compile("class\\s+([$_a-zA-Z][$_a-zA-Z0-9]*)\\s*");

    public static byte[] compile(String source, DiagnosticCollector compileCollector) {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        JavaFileManager javaFileManager =
                new TmpJavaFileManager(compiler.getStandardFileManager(compileCollector, null, null));

        // 从源码字符串中匹配类名
        Matcher matcher = CLASS_PATTERN.matcher(source);
        String className;
        if (matcher.find()) {
            className = matcher.group(1);
        } else {
            throw new IllegalArgumentException("No valid class");
        }

        // 把源码字符串构造成JavaFileObject,供编译使用
        JavaFileObject sourceJavaFileObject = new TmpJavaFileObject(className, source);

        Boolean result = compiler.getTask(null, javaFileManager, compileCollector,
                null, null, Arrays.asList(sourceJavaFileObject)).call();

        JavaFileObject bytesJavaFileObject = fileObjectMap.get(className);
        if (result && bytesJavaFileObject != null) {
            return ((TmpJavaFileObject) bytesJavaFileObject).getCompiledBytes();
        }
        return null;
    }

上面 使用到 jdk自带的tools包下的工具:ToolProvider.getSystemJavaCompiler() 

JavaFileManager 接口:java类文件管理器,管理JavaFileObject对象的工具, 需要自己 继承 ForwardingJavaFileManager 来实现。重写 getJavaFileForInput 和 getJavaFileForOutput 

JavaFileObject 接口:把源码字符串构造成JavaFileObject. 需要自己 继承 SimpleJavaFileObject 重写 getCharContent

openOutputStream 方法。这样就可以获取 源码转换成字节码的 byte[] 数组。((TmpJavaFileObject) bytesJavaFileObject).getCompiledBytes();

2.在创建的线程池,执行 刚编译好的字节码。

private static final int N_THREAD = 5;

private static final ExecutorService pool = new ThreadPoolExecutor(N_THREAD, N_THREAD,
            60L, TimeUnit.SECONDS, new ArrayBlockingQueue(N_THREAD));

//这样保证了 多客户端运行时互相不受影响。


       // 运行字节码的main方法
        Callable runTask = new Callable() {
            @Override
            public String call() throws Exception {
                // classBytes 源码字节数组
                return execute(classBytes);
            }
        };
        Future res = pool.submit(runTask);

        // 获取运行结果,处理非客户端代码错误
        String runResult;
        try {
            runResult = res.get(15, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            runResult = "Program interrupted.";
        } catch (ExecutionException e) {
            runResult = e.getCause().getMessage();
        } catch (TimeoutException e) {
            runResult = "Time Limit Exceeded.";
        }

// 重点在于 res.get() 获取的返回是哪的返回。这里就需要用到 System.out.println() 的方法来作为返回的结果。

3. execute(classBytes) 方法的实现。使用字节码修改器 替换掉 java/lang/System 的,自己继承 printStream类实现MyPrintSysteam。

类 MyPrintStream extends printStream{

private ThreadLocal out;
// 使用 ThreadLocal 保证每个线程的输出流互不影响。
private ThreadLocal trouble;
// 判断当前线程是否抛io错误。

}

类 MySystem
private final static printStream out = new MyPrintStream;

//调用 out.toString(); 就可以知道 自己System.out.println(123);输出的结果。

4. new 一个类加载器,把字节数组加载为Class对象。

Class clazz = classLoader.loadByte(bytes);

5.通过反射调用Class对象的main方法。

我们这里统一 已  main函数做入口。

 try {
            Method mainMethod = clazz.getMethod("main", new Class[] { String[].class });
            mainMethod.invoke(null, new String[] { null });
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.getCause().printStackTrace(MySystem.err);
        }
        //  从MySystem中获取返回结果
        String res = MySystem.getBufferString();
        MySystem.closeBuffer();

引用外部的说法来说:

我们在使用动态编译时,需要注意以下几点:

(1)在框架中谨慎使用

比如要在Struts中使用动态编译,动态实现一个类,它若继承自ActionSupport就希望它成为一个Action。能做到,但是debug很困难;再比如在Spring中,写一个动态类,要让它动态注入到Spring容器中,这是需要花费老大功夫的。

(2)不要在要求高性能的项目使用

动态编译毕竟需要一个编译过程,与静态编译相比多了一个执行环节,因此在高性能项目中不要使用动态编译。不过,如果是在工具类项目中它则可以很好地发挥其优越性,比如在Eclipse工具中写一个插件,就可以很好地使用动态编译,不用重启即可实现运行、调试功能,非常方便。

(3)动态编译要考虑安全问题

如果你在Web界面上提供了一个功能,允许上传一个Java文件然后运行,那就等于说:“我的机器没有密码,大家都来看我的隐私吧”,这是非常典型的注入漏洞,只要上传一个恶意Java程序就可以让你所有的安全工作毁于一旦。

(4)记录动态编译过程

建议记录源文件、目标文件、编译过程、执行过程等日志,不仅仅是为了诊断,还是为了安全和审计,对Java项目来说,空中编译和运行是很不让人放心的,留下这些依据可以更好地优化程序。

个人觉得,线上项目还是尽量不要使用,可以在开发环境使用,就像jsp那样,即写即用。方便查询bug等。

你可能感兴趣的:(基础知识)