使用 javax.tools 建立動態應用程序

轉自:https://www.ibm.com/developerworks/cn/java/j-jcomp/?S_TACT=105AGX52&S_CMP=tut-cto

本文永久地址:https://my.oschina.net/bysu/blog/1552935java

簡介

javax.tools 包是一種添加到 Java SE 6 的標準 API,能夠實現 Java 源代碼編譯,使您可以添加動態功能來擴展靜態應用程序。本文將探查 javax.tools 包中提供的主要類,並演示如何使用它們建立一個 façade,以從 Java StringStringBuffer 或其餘 CharSequence 中編譯 Java 源代碼,而不是從文件中編譯。以後,使用這個 façade 構建一個交互式繪圖應用程序,經過該應用程序,用戶可使用任何有效的數值 Java 表達式表示一個數值函數 y = f(x)。最後,本文將討論與動態源代碼編譯相關的安全風險以及應對方法。git

經過編譯和加載 Java 擴展來對應用程序進行擴展,這種想法並不新鮮,而且一些現有框架也支持這一功能。Java Platform, Enterprise Edition (Java EE) 中的 JavaServer Pages (JSP) 技術就是一種廣爲人知的動態框架,可以生成並編譯 Java 類。JSP 轉換器經過中間產物即源代碼文件將 .jsp 文件轉換爲 Java servlet,JSP 引擎隨後將源代碼文件編譯並加載到 Java EE servlet 容器中。編譯過程一般是經過直接調用 javac 編譯器完成的,這須要安裝 Java Development Kit (JDK) 或者調用 com.sun.tools.javac.Main(可經過 Sun 的 tools.jar 得到)。Sun 的許可證容許跟隨完整的 Java 運行時環境(Java Runtime Environment,JRE)一塊兒從新分發 tools.jar。其餘實現動態功能的方法包括使用可與應用程序實現語言(參見 參考資料)集成的現有動態腳本編制語言(例如 JavaScript 或 Groovy),或者編寫特定於域的語言和相關的語言解釋器和編譯器。express

其餘框架(例如 NetBeans 和 Eclipse)支持開發人員使用 Java 語言直接編寫擴展,可是這些框架須要外部靜態編譯,並須要管理 Java 代碼及其工件的源代碼和二進制文件。Apache Commons JCI 提供了一種機制能夠將 Java 類編譯並加載到運行的應用程序中。Janino 和 Javassist 也提供了相似的動態功能,可是 Janino 只限於 Java 1.4 以前的語言,而 Javassist 只能工做在 Java 類抽象級別,而不能在源代碼級別工做(參見 參考資料 中有關這些項目的連接)。然而,Java 開發人員已經熟悉如何使用 Java 語言編寫程序,若是一種系統可以動態生成 Java 源代碼並進行編譯和加載,那麼它能夠保證最短的學習曲線並提供最大程度的靈活性。安全

使用 javax.tools 的優勢

使用 javax.tools 具備如下好處:服務器

  • 它是通過承認的 Java SE 擴展,這意味着它是 Java Community Process(按照 JSR 199 規範)開發的標準 API。com.sun.tools.javac.Main API 不屬於 通過文件歸檔的 Java 平臺 API,所以沒有必要在其餘供應商的 JDK 中提供或保證在將來版本的 Sun JDK 中提供該 API。
  • 您能夠應用已經掌握的知識:Java 源代碼,而不是字節碼。不須要學習生成有效字節碼的複雜規則或者新的類對象模型、方法、語句和表達式,經過生成有效的 Java 源代碼,您就能夠建立正確的 Java 類。
  • 它簡化了一種受支持機制,並進行了標準化,使您不用侷限於基於文件的源代碼就可生成並加載代碼。
  • 它能夠在 JDK Version 6 和更高版本的各類供應商實現之間移植,而且未來也支持這種移植性。
  • 它使用通過驗證的 Java 編譯器。
  • 與基於解釋器的系統不一樣,所加載的類能夠從 JRE 的運行時優化中受益。

Java 編譯:概念和實現

要理解 javax.tools 包,回顧 Java 編譯概念以及如何使用包實現編譯將很是有幫助。javax.tools 包以一種通用的方式對這些概念進行了抽象化,使您可以從備用的源代碼對象提供源代碼,而不要求源代碼必須位於文件系統中。app

編譯 Java 源代碼須要用到如下組件:框架

  • 類路徑,編譯器從其中解析庫類。編譯器類路徑一般由一個有序的文件系統目錄列表和一些歸檔文件(JAR 或 ZIP 文件)組成,歸檔文件中包含先前編譯過的 .class 文件。類路徑由一個 JavaFileManager 實現,後者管理多個源代碼和類 JavaFileObject 實例以及傳遞給 JavaFileManager 構造函數的 ClassLoaderJavaFileObject 是一個 FileObject,專門處理如下任一種由編譯器使用的 JavaFileObject.Kind 枚舉類型:
    • SOURCE
    • CLASS
    • HTML
    • OTHER
    每一個源文件提供一個 openInputStream() 方法,能夠做爲 InputStream訪問源代碼。
  • javac 選項,以 Iterable<String> 的形式傳遞
  • 源文件 — 待編譯的一個或多個 .java 源文件。JavaFileManager 提供了一個抽象的文件系統,能夠將源文件和輸出文件的文件名映射到 JavaFileObject 實例(其中,文件 表示一個唯一名稱和一串字節之間的關聯。客戶機不須要使用實際的文件系統)。在本文的示例中,JavaFileManager 管理類名與 CharSequence 實例之間的映射,後者包含待編譯的 Java 源代碼。JavaFileManager.Location 包含一個文件名和一個標記,該標記能夠代表該位置是源代碼仍是一個輸出位置。ForwardingJavaFileManager 實現 Chain of Responsibility 模式(參見 參考資料),容許將文件管理器連接在一塊兒,就像類路徑和源路徑將 JAR 和目錄連接起來同樣。若是在這條鏈的第一個元素中沒有發現 Java 類,那麼將對鏈中的其餘元素進行查找。
  • 輸出目錄,編譯器在其中編寫生成的 .class 文件。做爲輸出類文件的集合,JavaFileManager 也保存表示編譯過的 CLASS文件的 JavaFileObject 實例。
  • 編譯器JavaCompiler 建立 JavaCompiler.CompilationTask 對象,後者從 JavaFileManager 中的 JavaFileObjectSOURCE 對象編譯源代碼,建立新的輸出 JavaFileObject CLASS 文件和 Diagnostic(警告和錯誤)。靜態 ToolProvider.getSystemJavaCompiler() 方法返回編譯器實例。
  • 編譯器警告和錯誤,這些內容經過 Diagnostic 和 DiagnosticListener 實現。Diagnostic 是編譯器發出的警告或編譯錯誤。Diagnostic 指定如下內容:
    • KindERRORWARNINGMANDATORY_WARNINGNOTE 或 OTHER
    • 源代碼中的位置(包括行號和列號)
    • 消息
    客戶機向編譯器提供一個 DiagnosticListener,編譯器可經過它向客戶機發回診斷信息。DiagnosticCollector 是一個簡單的 DiagnosticListener 實現。

圖 1 展現了 javax.tools 中的 javac 概念與其實現之間的映射:jsp

圖 1. javac 概念如何映射到 javax.tools 接口ide

javac 概念如何映射到 javax.tools 接口。

瞭解了這些概念,咱們如今看一下如何實現一個 façade 來編譯 CharSequence函數

編譯 CharSequence 實例中的 Java 源代碼

在本節中,我將爲 javax.tools.JavaCompiler 構造一個 façade。javaxtools.compiler.CharSequenceCompiler 類(參見 下載)能夠編譯保存在任何 java.lang.CharSequence 對象(例如 StringStringBuffer 和 StringBuilder)中的 Java 源代碼,並返回一個 ClassCharSequenceCompiler 提供瞭如下 API:

  • public CharSequenceCompiler(ClassLoader loader, Iterable<String> options):該構造函數接收傳遞給 Java 編譯器的 ClassLoader,容許它解析相關類。Iterable options 容許客戶機傳遞額外的編譯器選項,這些選項均對應於 javac 選項。
  • public Map<String, Class<T>> compile(Map<String, CharSequence> classes, final DiagnosticCollector<JavaFileObject> diagnostics) throws CharSequenceCompilerException, ClassCastException:這是經常使用的編譯方法,支持同時編譯多個源代碼。注意,Java 編譯器必須處理類的循環依賴性,例如 A.java 依賴 B.java,B.java 依賴 C.java,而 C.java 又依賴 A.java。該方法的第一個參數是 Map,它的鍵爲徹底限定類名,而相應的值爲包含該類源代碼的 CharSequence。例如:
    • "mypackage.A" ⇒ "package mypackage; public class A { ... }";
    • "mypackage.B" ⇒ "package mypackage; class B extends A implements C { ... }";
    • "mypackage.C" ⇒ "package mypackage; interface C { ... }"
    編譯器將 Diagnostic 添加到 DiagnosticCollector。您但願對類進行強制轉換的主要類型是泛型類型參數 Tcompile()被另外一個方法重載,其參數爲一個類名和待編譯的 CharSequence
  • public ClassLoader getClassLoader():該方法返回編譯器在生成 .class 文件時組裝的類加載器,所以,能夠從其中加載其餘類或資源。
  • public Class<T> loadClass(final String qualifiedClassName) throws ClassNotFoundException:因爲 compile() 方法能夠定義多個類(包括公共的嵌套類),所以容許加載輔助類。

爲了支持 CharSequenceCompiler API,我使用 JavaFileObjectImpl 類和 JavaFileManagerImpl 實現了 javax.tools 接口,其中,JavaFileObjectImpl 類用於保存 CharSequence 源代碼和編譯器產生的 CLASS 輸出,而 JavaFileManagerImpl 用於將名稱映射到 JavaFileObjectImpl 實例,從而管理源代碼和編譯器產生的字節碼。

JavaFileObjectImpl

清單 1 中的 JavaFileObjectImpl 實現 JavaFileObject 並保存 CharSequence source(用於 SOURCE)或一個 ByteArrayOutputStream byteCode(用於 CLASS 文件)。關鍵方法是 CharSequence getCharContent(final boolean ignoreEncodingErrors),編譯器經過它得到源代碼文本。參見 下載,獲取全部代碼示例的源代碼。

清單 1. JavaFileObjectImpl(只顯示部分源代碼)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

final class JavaFileObjectImpl extends SimpleJavaFileObject {

   private final CharSequence source;

 

   JavaFileObjectImpl(final String baseName, final CharSequence source) {

      super(CharSequenceCompiler.toURI(baseName + ".java"), Kind.SOURCE);

      this.source = source;

   }

   @Override

   public CharSequence getCharContent(final boolean ignoreEncodingErrors)

         throws UnsupportedOperationException {

      if (source == null)

         throw new UnsupportedOperationException("getCharContent()");

      return source;

   }

}

FileManagerImpl

FileManagerImpl(參見清單 2)對 ForwardingJavaFileManager 進行了擴展,將限定的類名映射到 JavaFileObjectImpl實例:

清單 2. FileManagerImpl(只顯示部分源代碼)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

final class FileManagerImpl extends ForwardingJavaFileManager<JavaFileManager> {

   private final ClassLoaderImpl classLoader;

   private final Map<URI, JavaFileObject> fileObjects

           = new HashMap<URI, JavaFileObject>();

 

   public FileManagerImpl(JavaFileManager fileManager, ClassLoaderImpl classLoader) {

      super(fileManager);

      this.classLoader = classLoader;

   }

 

   @Override

   public FileObject getFileForInput(Location location, String packageName,

         String relativeName) throws IOException {

      FileObject o = fileObjects.get(uri(location, packageName, relativeName));

      if (o != null)

         return o;

      return super.getFileForInput(location, packageName, relativeName);

   }

 

   public void putFileForInput(StandardLocation location, String packageName,

         String relativeName, JavaFileObject file) {

      fileObjects.put(uri(location, packageName, relativeName), file);

   }

}

CharSequenceCompiler

若是 ToolProvider.getSystemJavaCompiler() 不能建立 JavaCompiler

若是 tools.jar 不在應用程序類路徑中,ToolProvider.getSystemJavaCompiler()方法能夠返回 nullCharStringCompiler 類檢測到這一配置問題後將拋出一個異常,並給出修復建議。注意,Sun 許可證容許跟隨 JRE 一塊兒從新分發 tools.jar。

經過這些支持類,如今能夠定義 CharSequenceCompiler,可以使用運行時 ClassLoader 和編譯器選項構建。它使用 ToolProvider.getSystemJavaCompiler() 得到 JavaCompiler 實例,而後對轉發給編譯器標準文件管理器的 JavaFileManagerImpl 進行實例化。

compile() 方法對輸入映射進行迭代,從每一個名稱/CharSequence 中構建一個 JavaFileObjectImpl,並將其添加到 JavaFileManager 中,所以,在調用文件管理器的 getFileForInput() 方法時,JavaCompiler 將找到它們。compile() 方法隨後將建立一個 JavaCompiler.Task 實例並運行該實例。故障被做爲 CharSequenceCompilerException 拋出。而後,對於傳遞給 compile() 方法的全部源代碼,加載產生的 Class 類並放在結果 Map中。

與 CharSequenceCompiler (參見清單 3)相關的類加載器是一個 ClassLoaderImpl 實例,它在 JavaFileManagerImpl 實例中查找類的字節碼,返回編譯器建立的 .class 文件:

清單 3. CharSequenceCompiler(只顯示部分源代碼)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

public class CharSequenceCompiler<T> {

   private final ClassLoaderImpl classLoader;

   private final JavaCompiler compiler;

   private final List<String> options;

   private DiagnosticCollector<JavaFileObject> diagnostics;

   private final FileManagerImpl javaFileManager;

 

   public CharSequenceCompiler(ClassLoader loader, Iterable<String> options) {

      compiler = ToolProvider.getSystemJavaCompiler();

      if (compiler == null) {

         throw new IllegalStateException(

               "Cannot find the system Java compiler. "

               + "Check that your class path includes tools.jar");

      }

      classLoader = new ClassLoaderImpl(loader);

      diagnostics = new DiagnosticCollector<JavaFileObject>();

      final JavaFileManager fileManager = compiler.getStandardFileManager(diagnostics,

            null, null);

      javaFileManager = new FileManagerImpl(fileManager, classLoader);

      this.options = new ArrayList<String>();

      if (options != null) {

         for (String option : options) {

            this.options.add(option);

         }

      }

   }

 

   public synchronized Map<String, Class<T>>

          compile(final Map<String, CharSequence> classes,

                  final DiagnosticCollector<JavaFileObject> diagnosticsList)

          throws CharSequenceCompilerException, ClassCastException {

      List<JavaFileObject> sources = new ArrayList<JavaFileObject>();

      for (Entry<String, CharSequence> entry : classes.entrySet()) {

         String qualifiedClassName = entry.getKey();

         CharSequence javaSource = entry.getValue();

         if (javaSource != null) {

            final int dotPos = qualifiedClassName.lastIndexOf('.');

            final String className = dotPos == -1

                  ? qualifiedClassName

                  : qualifiedClassName.substring(dotPos + 1);

            final String packageName = dotPos == -1

                  ? ""

                  : qualifiedClassName .substring(0, dotPos);

            final JavaFileObjectImpl source =

                  new JavaFileObjectImpl(className, javaSource);

            sources.add(source);

            javaFileManager.putFileForInput(StandardLocation.SOURCE_PATH, packageName,

                  className + ".java", source);

         }

      }

      final CompilationTask task = compiler.getTask(null, javaFileManager, diagnostics,

                                                    options, null, sources);

      final Boolean result = task.call();

      if (result == null || !result.booleanValue()) {

         throw new CharSequenceCompilerException("Compilation failed.",

                                                 classes.keySet(), diagnostics);

      }

      try {

         Map<String, Class<T>> compiled =

                        new HashMap<String, Class<T>>();

         for (Entry<String, CharSequence> entry : classes.entrySet()) {

            String qualifiedClassName = entry.getKey();

            final Class<T> newClass = loadClass(qualifiedClassName);

            compiled.put(qualifiedClassName, newClass);

         }

         return compiled;

      } catch (ClassNotFoundException e) {

         throw new CharSequenceCompilerException(classes.keySet(), e, diagnostics);

      } catch (IllegalArgumentException e) {

         throw new CharSequenceCompilerException(classes.keySet(), e, diagnostics);

      } catch (SecurityException e) {

         throw new CharSequenceCompilerException(classes.keySet(), e, diagnostics);

      }

   }

}

Plotter 應用程序

如今,我有了一個能夠編譯源代碼的簡單 API,我將經過建立函數繪製應用程序(使用 Swing 編寫)來發揮其功用。圖 2 展現了該應用程序使用圖形表示 x * sin(x) * cos(x) 函數:

圖 2. 使用 javaxtools.compiler 包的動態應用程序

Plotter Swing 應用程序屏幕截圖

該應用程序使用清單 4 中定義的 Function 接口:

清單 4. Function 接口

1

2

3

4

package javaxtools.compiler.examples.plotter;

public interface Function {

   double f(double x);

}

應用程序提供了一個文本字段,用戶能夠向其中輸入一個 Java 表達式,後者根據隱式聲明的 double x 輸入參數返回一個 double 值。在清單 5 所示的代碼模板中,應用程序將表達式文本插入到以 $expression 標記的位置。而且每次生成一個唯一的類名,替代模板中的 $className。包名也是一個模板變量。

清單 5. Function 模板

1

2

3

4

5

6

7

8

package $packageName;

import static java.lang.Math.*;

public class $className

             implements javaxtools.compiler.examples.plotter.Function {

  public double f(double x) {

    return ($expression) ;

  }

}

應用程序使用 fillTemplate(packageName, className, expr) 函數填充模板,它返回一個 String 對象,而後應用程序使用 CharSequenceCompiler 進行編譯。異常或編譯器診斷信息被傳遞給 log() 方法或直接寫入到應用程序中可滾動的 errors組件。

清單 6 顯示的 newFunction() 方法將返回一個對象,它將實現 Function 接口(參見 清單 5 中的源代碼模板):

清單 6. Plotter 的 Function newFunction(String expr) 方法

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

Function newFunction(final String expr) {

   errors.setText("");

   try {

      // generate semi-secure unique package and class names

      final String packageName = PACKAGE_NAME + digits();

      final String className = "Fx_" + (classNameSuffix++) + digits();

      final String qName = packageName + '.' + className;

      // generate the source class as String

      final String source = fillTemplate(packageName, className, expr);

      // compile the generated Java source

      final DiagnosticCollector<JavaFileObject> errs =

            new DiagnosticCollector<JavaFileObject>();

      Class<Function> compiledFunction = stringCompiler.compile(qName, source, errs,

            new Class<?>[] { Function.class });

      log(errs);

      return compiledFunction.newInstance();

   } catch (CharSequenceCompilerException e) {

      log(e.getDiagnostics());

   } catch (InstantiationException e) {

      errors.setText(e.getMessage());

   } catch (IllegalAccessException e) {

      errors.setText(e.getMessage());

   } catch (IOException e) {

      errors.setText(e.getMessage());

   }

   return NULL_FUNCTION;

}

您一般會生成一些源類,使用它們擴展已有的基類或實現特定接口,從而能夠將實例轉換爲已知的類型並經過一個類型安全 API 調用其方法。注意,在實例化 CharSequenceCompiler<T> 時,Function 類被做爲泛型類型參數 T 使用。所以,也能夠將 compiledFunction 輸入做爲 Class<Function> 和 compiledFunction.newInstance(),以返回 Function 實例,而不須要進行強制轉換。

動態生成一個 Function 實例後,應用程序使用它針對一系列 x 值生成 y 值,而後使用開源的 JFreeChart API(參見 參考資料)描繪(x,y)值。Swing 應用程序的完整源代碼能夠經過 javaxtools.compiler.examples.plotter 包的 可下載源代碼 部分中得到。

這個應用程序的源代碼生成需求很是普通。更爲複雜的源代碼模板工具能夠更好地知足其餘應用程序的需求,例如 Apache Velocity (參見 參考資料)。

安全風險和策略

若是應用程序容許用戶隨意輸入 Java 源代碼,那麼會存在一些內在的安全風險。相似 SQL 注入(參見 參考資料),若是系統容許用戶或其餘代理提供原始的 Java 源代碼來生成代碼,那麼惡意用戶可能會利用這一點。例如,在本文的 Plotter 應用程序中,一個有效的 Java 表達式可能包含匿名的嵌套類,它能夠訪問系統資源、在受到拒絕服務攻擊時產生大量線程或者執行其餘行爲。這些行爲被稱爲 Java 注入。這種應用程序不該該部署在非信任用戶能夠隨意訪問的不可靠位置,例如做爲 servlet 或 applet 的 Java EE 服務器。相反,javax.tools 的大多數客戶機應該限制用戶輸入並將用戶請求轉換爲安全的源代碼。

使用這種包時能夠採用的安全策略包括:

  • 使用定製的 SecurityManager 或 ClassLoader 阻止加載匿名類或其餘沒法直接控制的類。
  • 使用源代碼掃描程序或其餘預處理程序,刪除含有可疑代碼構造的輸入。例如,Plotter 可使用 java.io.StreamTokenizer 並刪除含有 {(左側大括號)字符的輸入,從而有效阻止了匿名或嵌套類的聲明。
  • 使用 javax.tools API,JavaFileManager 能夠刪除任何預料以外的 CLASS 文件的寫入。例如,當編譯某個特定類時,對於要求保存預料以外的類文件的任何調用,JavaFileManager 將拋出一個 SecurityExeception 異常,並只容許生成用戶沒法猜想或欺騙的包名或類名。Plotter 的 newFunction 方法使用的就是這種策略。

結束語

在本文中,我解釋了 javax.tools 包的概念和重要接口,並展現了一個 façade,使用它編譯保存在 String 或其餘 CharSequence 中的 Java 源代碼,而後使用這個庫類開發能夠描繪任意 f(x) 函數的樣例應用程序。可使用這種技術建立其餘有用的應用程序:

  • 根據數據描述語言生成二進制文件閱讀程序/寫入程序。
  • 生成相似於 Java Architecture for XML Binding (JAXB) 或持久性框架的格式轉換程序。
  • 經過執行源代碼與 Java 語言的轉換、Java 源代碼編譯和加載(相似於 JSP 技術),實現特定於域的語言解釋器。
  • 實現規則引擎。
  • 您能夠想像獲得的任何內容。

下一次開發應用程序時,若是須要使用動態行爲,請嘗試 javax.tools 提供的多樣性和靈活性。

下載資源

相關文章
相關標籤/搜索