Java动态编译(一)

http://chrui.iteye.com/blog/1008242


一般情况下对java源文件的编译均是在代码完成后使用javac编译的,不管是使用IDE还是直接使用命令行。这里要说的情况是比较特别的,就是在代码内动态的编译一些代码。比如你想通过在某个目录下通过放置一些源代码的方式来实现对程序功能的动态扩展,那么你的程序就需要具有一种对源代码的编译、加载、运行的能力,可能就需要本文介绍的3种方法。

        可以和JAVA的类加载器结合使用,动态编译、动态加载。
        方法1:通过调用本机的javac命令来编译
       
Java代码   收藏代码
  1. javac –encoding char_set –classpath/-cp classpath –d dir src  

        在java程序中调用javac命令可以通过调用Runtime类的exec或是ProcessBuilder类的start方法来完成,这两个类的功能 基本相同,用法也比较相似。如果是JDK1.5之前的版本请使用Runtime类完成相同的功能。
Java代码   收藏代码
  1. public ProcessBuilder(String... command)  

        利用指定的操作系统程序和参数构造一个进程生成器。这是一个有用的构造方法,它将进程生成器的命令设置为与 command 数组包含相同字符串的字符串列表,且顺序相同。不必检查 command 是否为一个有效的操作系统命令。
Java代码   收藏代码
  1. Process p = new ProcessBuilder("command""arg1" [, arg2] [,arg3] [,……]).start();  

Java代码   收藏代码
  1. Process proc = new ProcessBuilder("javac""-d""d:\\test\\bin""d:\\test\\src\\Test.java").start();  

         利用修改过的工作目录和环境启动进程的例子:

Java代码   收藏代码
  1. ProcessBuilder pb = new ProcessBuilder("myCommand""myArg1""myArg2");  
  2. Map<String, String> env = pb.environment();  
  3. env.put("VAR1""myValue");  
  4. env.remove("OTHERVAR");  
  5. env.put("VAR2", env.get("VAR1") + "suffix");  
  6. pb.directory(new File("myDir"));  
  7. Process p = pb.start();  

         pb.environment()返回此进程生成器环境的字符串映射视图。 无论进程生成器何时创建,都需要将环境初始化为一份当前进程环境的副本(请参阅 System.getenv())。由此对象的 start() 方法启动的后续子进程将使用这一映射作为它们的环境。
Java代码   收藏代码
  1. public class Runtime extends Object  

         每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。可以通过 getRuntime 方法获取当前运行时。
Java代码   收藏代码
  1. runtime.exec("javac -d d:\\test\\dir d:\\test\\src\\Test.java");  

        方法2:使用Sun的 tools.jar 包的com.sun.tools.javac.Main类完成对代码的编译
        其构造方法为public Main(OutputStream outputstream, String s)。使用tools.jar的com.sun.tools.javac.Main编译时,主要是调用该类的compile(String [] args)/compile(String [] args, PrintWirter pw)。
Java代码   收藏代码
  1. int returnValue = Main.compile(new String[] {"-classpath", compilepath, "-d", dir, src});  

Java代码   收藏代码
  1. PrintWriter pw = new PrintWriter("d:\\test\\src\\test.txt");  
  2. int returnValue = Main.compile(new String[] {"-classpath", compilepath, "-d", dir, src}, pw);  
  3. //暂时没有弄明白pw在运行中的作用  
Java代码   收藏代码
  1. public class DynamicCompileTest {  
  2.     public static void main(String[] args) {  
  3.         try {  
  4.             String className = "RunTime";  
  5.             String classDir = System.getProperty("user.dir");  
  6.             File file = new File(classDir, className + ".java");  
  7.             PrintWriter out = new PrintWriter(new FileOutputStream(file));  
  8.             // 代码  
  9.             StringBuffer sbf = new StringBuffer(128);  
  10.             sbf.append("public class ");  
  11.             sbf.append(className);  
  12.             sbf.append("{");  
  13.             sbf.append("public void hello () {");  
  14.             sbf.append("System.out.println(\"DynamicCompile Success.\");");  
  15.             sbf.append("}");  
  16.             sbf.append("}");  
  17.             String code = sbf.toString();  
  18.             out.println(code);  
  19.             out.flush();  
  20.             out.close();  
  21.             // 编译  
  22.             com.sun.tools.javac.Main.compile(new String[] { "-d", classDir, file.getName() });// JAVA_HOME/jdk/lib/tools.jar  
  23.             URL url = new URL("file:/" + classDir + File.separator);  
  24.             // 动态加载/执行  
  25.             URLClassLoader loader = new URLClassLoader(new URL[] { url });  
  26.             Class<?> clazz = loader.loadClass(className);  
  27.             Object obj = clazz.newInstance();  
  28.             Method method = clazz.getMethod("hello");  
  29.             method.invoke(obj);  
  30.         } catch (Exception e) {  
  31.             e.printStackTrace();  
  32.         }  
  33.     }  
  34. }  
方法3:使用javax.tools包
         从上面可以看到方法2的缺点就是tools.jar需要我们自行导入。而在Java SE6中为我们提供了标准的包来操作Java编译器,这就是javax.tools包。使用这个包,我们可以不用将jar文件路径添加到classpath中了。 使用这个类的方法和上面的类很相似,我只需要将
Java代码   收藏代码
  1. Main.compile(new String[]{"-encoding""UTF-8","-d", binDir, filePath});  

         替换成
         //将编译参数通过数组传递到编译方法中,该函数的方法和javac的参数完成一致
Java代码   收藏代码
  1. JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();    
  2. compiler.run(nullnullnull"-encoding""UTF-8","-d", binDir, filePath);  

         可以通过ToolProvider类的静态方法getSystemJavaCompiler来得到一个JavaCompiler接口的实例。
Java代码   收藏代码
  1. JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();   

         JavaCompiler中最核心的方法是run。通过这个方法可以编译java源程序。这个方法有3个固定参数和1个可变参数(可变参数是从 Jave SE5开始提供的一个新的参数类型,用type… args表示)。前3个参数分别用来为java编译器提供参数、得到Java编译器的输出信息以及接收编译器的错误信息,后面的可变参数可以传入一个或多个Java源程序文件。如果run编译成功,返回0。
Java代码   收藏代码
  1. int run(InputStream in, OutputStream out, OutputStream err, String... arguments)  

Java代码   收藏代码
  1. int result = compiler.run(nullnullnull"-encoding""UTF-8""-cp""C:/Java/jdk1.6.0_18/lib/tools.jar""-d", binDir, filePath);    



在上一篇(Java动态编译(一))中我们提到了动态编译的三种方法,在这篇文章中讲解一些扩展的知识。

Java代码   收藏代码
  1. public abstract class Process extends Object  

        ProcessBuilder.start() 和 Runtime.exec 方法创建一个本机进程,并返回 Process 子类的一个实例,该实例可用来控制进程并获得相关信息。
        子进程没有自己的终端或控制台。它的所有标准io(即 stdin、stdout 和 stderr)操作都将通过三个流 (getOutputStream()、getInputStream() 和 getErrorStream()) 重定向到父进程。
        waitFor() 导致当前线程等待,如有必要,一直要等到由该Process对象表示的进程已经终止。但是如果我们在调用此方法时,如果不注意的话,很容易出现主线程阻塞,Process也挂起的情况。在调用waitFor() 的时候,Process需要向主线程汇报运行状况,所以要注意清空缓存区,即InputStream和ErrorStream,在网上,很多只提到处理InputStream,忽略了ErrorStream。


举例1.

Java代码   收藏代码
  1. public class RuntimeExecDemo {  
  2.     public static void main(String[] args) throws InterruptedException {  
  3.         Runtime runtime = Runtime.getRuntime();     
  4.         Process process = null;  
  5.         try {  
  6.             process = runtime.exec("cmd /c dir"nullnew File("g:/"));  
  7.         } catch (IOException e) {  
  8.             e.printStackTrace();  
  9.         }  
  10.           
  11.         int exitVal = process.waitFor();  
  12.         System.out.println("Process exitValue: " + exitVal);    
  13.     }  
  14. }  

        上面的程序并没有按照我们预想的那样列出g盘下的目录,而是挂起了,为什么会出现这种情况呢?java文档上说,创建进程的方法可能无法针对某些本机平台上的特定进程很好地工作,比如,本机窗口进程,守护进程,Microsoft Windows 上的 Win16/DOS 进程,或者 shell 脚本。创建的子进程没有自己的终端或控制台。它的所有标准io(即 stdin、stdout 和 stderr)操作都将通过三个流 (getOutputStream()、getInputStream() 和 getErrorStream()) 重定向到父进程。父进程使用这些流来提供到子进程的输入和获得从子进程的输出。因为 有些本机平台仅针对标准输入和输出流提供有限的缓冲区大小,如果读写子进程的输出流或输入流迅速出现失败,则可能导致子进程阻塞,甚至产生死锁
         当进程启动后,就会打开标准输出流和错误输出流准备输出,当进程结束时,就会关闭他们。举例1中,由于标准输出流有数据需要输出,但是我们却没有读取其中的数据,标准输出流就会一直等待数据被读取,程序挂起。


举例2.处理标准输出流

Java代码   收藏代码
  1. public class RuntimeExecDemo {  
  2.     public static void main(String[] args) throws InterruptedException {  
  3.         Runtime runtime = Runtime.getRuntime();  
  4.         Process process = null;  
  5.         try {  
  6.             process = runtime.exec("cmd /c dir"nullnew File("g:/"));  
  7.         } catch (IOException e) {  
  8.             e.printStackTrace();  
  9.         }  
  10.         try {  
  11.             BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));  
  12.             String line = null;  
  13.             while ((line = br.readLine()) != null) {  
  14.                 System.out.println(line);  
  15.             }  
  16.         } catch (IOException e) {  
  17.             e.printStackTrace();  
  18.         }  
  19.         int exitVal = process.waitFor();  
  20.         System.out.println("Process exitValue: " + exitVal);  
  21.     }  
  22. }  

        当我们处理了标准输出后,则程序可以正常运行了。但是,如果我们的命令是错误的,那么情况又会如何呢?即这里将dir修改成dira,然后执行。程序仍然可以正常运行,只不过结束的时候的exitVal不再是0。
        为什么会出现这种情况呢?这里就是当我们执行程序的时候,先打开的是标准输出流,然后等待我们从中读取数据,而我们对需要读取的数据做了读取处理。那么当接下来打开错误输出流的时候,尽管没有被读取,但是却不再阻塞,程序异常终止。
        但是,如果命令是正确的(即dira已经改回dir)再将process.getInputStream()修改成process.getErrorStream()呢?这次程序又一起挂起了。这是由于标准输出流是先打开并需要输出而,但是却并没有被读取;读取的是错误输出流,那么此时标准输出流的数据没有被读取导致程序不会结束。
        为了解决这个问题,可以根据输出的实际先后,先读取标准输出流,然后读取错误输出流。
        但是,很多时候不能很明确的知道输出的先后,特别是要操作标准输入的时候,情况就会更为复杂。这时候可以 采用线程来对标准输出、错误输出和标准输入进行分别处理,根据他们之间在业务逻辑上的关系决定读取那个流或者写入数据。
         针对标准输出流和错误输出流所造成的问题,还可以 使用ProcessBuilder的redirectErrorStream()方法将他们合二为一,这时候只要读取标准输出的数据就可以了。


举例3.使用多线程

Java代码   收藏代码
  1. public class RuntimeExecDemo {  
  2.     public static void main(String[] args) {  
  3.         Runtime runtime = Runtime.getRuntime();     
  4.         Process process = null;  
  5.         try {  
  6.             process = runtime.exec("cmd /c dir"nullnew File("g:/"));  
  7.         } catch (IOException e) {  
  8.             e.printStackTrace();  
  9.         }  
  10.         InputStream is = process.getInputStream();     
  11.         InputStream es = process.getErrorStream();  
  12.         StringBuffer out = new StringBuffer(128);  
  13.         StreamGobbler isGobbler = new StreamGobbler(is, "gbk", out);  
  14.         StringBuffer err = new StringBuffer(128);  
  15.         StreamGobbler errorGobbler = new StreamGobbler(es, "gbk", err);  
  16.         isGobbler.start();  
  17.         errorGobbler.start();  
  18.           
  19.         int exitVal = -1;  
  20.         try {  
  21.             exitVal = process.waitFor();  
  22.         } catch (InterruptedException e) {  
  23.             e.printStackTrace();  
  24.         }  
  25.         System.out.println("out : \r\n" + out.toString());  
  26.         System.out.println("err : \r\n" + err.toString());  
  27.         System.out.println("Process exitValue: " + exitVal);    
  28.     }  
  29. }  

Java代码   收藏代码
  1. class StreamGobbler extends Thread {  
  2.     InputStream is;  
  3.     String encoding = "utf-8";  
  4.     StringBuffer result = new StringBuffer();  
  5.   
  6.     /** 
  7.      * 
  8.      * @param is process.getErrorStream() or process.getInputStream() 
  9.      * @param encoding default is "utf-8", if the result include chinese, need to use "gbk" 
  10.      */  
  11.     StreamGobbler(InputStream is, String encoding, StringBuffer result) {  
  12.         this.is = is;  
  13.         this.encoding = encoding;  
  14.         this.result = result;  
  15.     }  
  16.   
  17.     public void run() {  
  18.         try {  
  19.             InputStreamReader isr = new InputStreamReader(is, encoding);  
  20.             BufferedReader br = new BufferedReader(isr);  
  21.             String line=null;  
  22.             while ( (line = br.readLine()) != null) {  
  23.                 result.append(line).append("\r\n");  
  24.             }  
  25.         } catch (IOException e) {  
  26.             e.printStackTrace();  
  27.         }  
  28.     }  
  29. }  

        举例4.是用ProcessBuilder.redirectErrorStream()
Java代码   收藏代码
  1. public boolean redirectErrorStream()  
通知进程生成器是否合并标准错误和标准输出。
        如果此属性为 true,则任何由通过此对象的 start() 方法启动的后续子进程生成的错误输出都将与标准输出合并,因此两者均可使用 Process.getInputStream() 方法读取。这使得关联错误消息和相应的输出变得更容易。初始值为 false。
Java代码   收藏代码
  1. public class ProcessBuilderDemo {  
  2.     public static void main(String[] args) {  
  3.         ProcessBuilder pb = new ProcessBuilder("cmd""/c""dira");  
  4.         pb.directory(new File("g:\\"));  
  5.         pb.redirectErrorStream(true);  
  6.         Process process = null;  
  7.         try {  
  8.             process = pb.start();  
  9.         } catch (IOException e1) {  
  10.             e1.printStackTrace();  
  11.         }  
  12.         try {  
  13.             BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));  
  14.             String line = null;  
  15.             while ((line = br.readLine()) != null) {  
  16.                 System.out.println(line);  
  17.             }  
  18.         } catch (IOException e) {  
  19.             e.printStackTrace();  
  20.         }  
  21.         int exitVal = -1;  
  22.         try {  
  23.             exitVal = process.waitFor();  
  24.         } catch (InterruptedException e) {  
  25.             e.printStackTrace();  
  26.         }  
  27.         System.out.println("Process exitValue: " + exitVal);  
  28.     }  


你可能感兴趣的:(Java动态编译(一))