Java300StudyNote(3)-Java动态编译(DynamicCompile)、动态运行、反射调用mian方法、JavaCompiler.run()空指针问题

来源:互联网 发布:java sql注入原理 编辑:程序博客网 时间:2024/06/14 23:09

动态编译的应用场景

以W3School为例,动态编译常用的场景如下:即输入代码,发送到服务器进行动态编译并返回结果

这里写图片描述

动态编译相关API

//获取系统的编译器
JavaCompiler compile = ToolProvider.getSystemJavaCompiler();

/**
* 执行编译
* @param in “standard” input; use System.in if null 标准的输入流
* @param out “standard” output; use System.out if null 标准的输出流
* @param err “standard” error; use System.err if null 标准的错误输出流
* @param arguments arguments to pass to the tool 将要编译的文件名,支持可变参数
* @Return 0表示编译成功,非0表示编译失败
**/
compile.run(in, out, err, arguments);

动态编译程序测试

下面我们通过程序来直观的体验一下:
1、首先我在E盘创建了一个Test文件夹,里面放了一个Test.java文件,文件内容如下图所示:
这里写图片描述

Test.java
这里写图片描述

2、创建好了文件接下来我们来写测试程序

package com.maple.DynamicCompile;import javax.tools.JavaCompiler;import javax.tools.ToolProvider;public class Demo02 {    @org.junit.Test    public void test() {        //获取编译器        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();        /**         * 设置为null表示使用系统默认的输入输入与错误输出流         */        int result = compiler.run(null, null, null, "E:/Test/Test.java");        System.out.println(result==0?"编译成功":"编译失败");    }}

3、执行以上代码不出意外你会出现以下的错误
java.lang.NullPointerException
这里写图片描述

JavaCompile.run()出现空指针异常问题分析与解决

首先出现以上错误是很正常的,并不是你程序写的有问题,我们先来说如何解决这个问题

解决方案

1、进入到jdk和jre的安装目录

这里写图片描述

2、先进入jdk目录,进入lib目录,找到tools.jar包,点击复制

这里写图片描述

3、退出进入到jre的安装目录,同样也是进入jre的lib目录,将刚刚复制的tools.jar包复制到lib目录中即可

这里写图片描述

4、再次运行程序,程序执行成功

这里写图片描述

空指针问题分析

首先我们要知道程序的每一步的执行和出现的结果都不是平白无故的出现的,所以出现问题我们最好的方式是分析他的源代码,顺腾摸瓜找到问题所在的根源。那么接下来就让我们进入源代码来分析一下问题出在哪儿
首先我们debug发现,空指针问题发生在JavaCompiler compiler上,由于获取到编译器是空的,而我们调用了compile.run()方法自然就出现了空指针的问题。好,到这里问题似乎有点眉目了,我们再通过查看ToolProvider.getSystemJavaCompiler()方法的源代码,一个个往下看,这里我就不讲过程一一展示,我就将我发现的结果直接展示给你们看,你们要想自己查看可以进入ToolProvider.class的181行查看源代码。

private static final String[] defaultToolsLocation = { "lib", "tools.jar" };    private Class<?> findSystemToolClass(String toolClassName)        throws MalformedURLException, ClassNotFoundException    {        // try loading class directly, in case tool is on the bootclasspath        try {            return Class.forName(toolClassName, false, null);        } catch (ClassNotFoundException e) {            trace(FINE, e);            // if tool not on bootclasspath, look in default tools location (tools.jar)            ClassLoader cl = (refToolClassLoader == null ? null : refToolClassLoader.get());            if (cl == null) {                File file = new File(System.getProperty("java.home"));                if (file.getName().equalsIgnoreCase("jre"))                    file = file.getParentFile();                for (String name : defaultToolsLocation)                    file = new File(file, name);                // if tools not found, no point in trying a URLClassLoader                // so rethrow the original exception.                if (!file.exists())                    throw e;                URL[] urls = { file.toURI().toURL() };                trace(FINE, urls[0].toString());                cl = URLClassLoader.newInstance(urls);                refToolClassLoader = new WeakReference<ClassLoader>(cl);            }            return Class.forName(toolClassName, false, cl);        }

代码分析

首先看到这段代码我们并不要求全部能够看到,能解决我们的问题就行

那么以上的代码的意思是什么呢,意识是他是通过调用tools.jar包来进行编译操作的,那他返回为null,说明他没有找到这个工具,那我们就推测是不是它找工具的路径有问题。我们发现这个tools.jar包工具在jdk的lib中存在,在jre的lib不存在,我们知道jre是java运行时环境,注意是运行时,而我们java的动态编译操作其实也可以理解是一种运行时的编译操作,所以在执行这个动态编译操作的时候它会去jre中找而不是jdk中找这个工具,而实际中在jre中不存在这个工具,所以就出现了空指针错误,所以我们将jdk中的tools.jar工具复制到jre中,就可以将这个问题解决。

动态编译结果查看

执行动态编译程序成功后,你会发现在你的Test文件中多了一个Test.class文件,这就说明完成了编译操作。

这里写图片描述

动态运行编译好的类

方法一:通过Runtime.getRuntime()运行启动新的进程

测试代码如下:

package com.maple.DynamicCompile;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;public class Demo03 {    @org.junit.Test    public void test() throws IOException {        //Returns the runtime object associated with the current Java application.        //返回程序运行时对象        Runtime run = Runtime.getRuntime();        //Executes the specified string command in a separate process.        //执行指定的命令在一个新的线程        Process process = run.exec("java -cp E:\\Test Test");        //通过IO流将输入流中的结果信息输出在控制台        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));        String info = "";        while(null!=(info=reader.readLine())) {            System.out.println(info);        }    }}

运行结果
这里写图片描述

方法二:通过反射API动态运行编译好的程序

测试代码:

package com.maple.DynamicCompile;import java.lang.reflect.Method;import java.net.URL;import java.net.URLClassLoader;import org.junit.Test;public class Demo04 {    @Test    public void test() throws Exception {        //获取Test目录下的URL        //注意这里不要忘记Test后面的/否则将找不到类Test        URL urls[] = new URL[]{new URL("file:/"+"E:/Test/")};        //调用类加载器        URLClassLoader classLoader = new URLClassLoader(urls);        //通过类加载器加载编译好的Test类        Class<?> clazz = classLoader.loadClass("Test");        //通过反射API获取main方法        Method m = clazz.getDeclaredMethod("main", String[].class);        //指定main方法        m.invoke(null, (Object)new String[] {});    }}

运行结果:

这里写图片描述

反射调用main方法问题

通过以上程序展示反射调用main方法,那么在这里把要注意的几点再罗列几下

m.invoke(null, (Object)new String[] {“aa”,”bb”});注意这行代码,第一个参数我们设置为null,这是main方法专属,这是一点,还有就是在new String[]数组对象我们加了一个Object的强制转型,原因就在于由于可变参数的出现,它在执行这个反射方法的时候,如果不加Object强转,他会编译为main(“aa”,”bb”)这显然就是另外一个main方法了,所以这里注意要加上Object强转,不过稍微进阶一点的程序员往往采用JUNT来进行程序的测试,所以这个main方法的反射调用我觉得用处不大。

字符串代码动态编译

最后再讲一个如何将字符串中的java代码进行动态编译,由于比较简单,我这里简单的介绍一下,其实处理字符代码的思路处理文本文件代码的思路是相同的,即我们可以把字符串代码通过IO流将其写入一个临时文件,然后再动态编译这个临时文件即可,实际项目中,服务器端的动态编译很多采用的也是这个思路

    String str = "public class Test{\r\n" +                 "   public static void main(String [] args){\r\n" +                 "       System.out.println(\"nihao\");\r\n" +                 "   }\r\n" +                 "}";