Java编译器API
本文是我們名為“ 高級Java ”的學院課程的一部分。
本課程旨在幫助您最有效地使用Java。 它討論了高級主題,包括對象創建,并發,序列化,反射等。 它將指導您完成Java掌握的過程! 在這里查看 !
目錄
1.簡介 2. Java編譯器API 3.注釋處理器 4.元素掃描儀 5. Java編譯器樹API 6.接下來 7.下載1.簡介
在本部分的教程中,我們將對Java編譯器API進行10000英尺的觀察。 該API提供了對Java編譯器本身的編程訪問,并允許開發人員從應用程序代碼中即時從源文件編譯Java類。
更有趣的是,我們還將遍歷Java編譯器樹API,該API提供對Java語法分析器功能的訪問。 通過使用此API,Java開發人員可以直接插入語法分析階段并對正在編譯的Java源代碼進行后期分析。 它是一個非常強大的API,許多靜態代碼分析工具都大量使用它。
Java Compiler API還支持注釋處理(有關更多詳細信息,請參閱本教程的第5部分 , 如何和何時使用Enums和Annotations ,更多內容將在本教程的第14部分 , Annotation Processors中提供 ),并且分為三個不同的包,如下表所示。
| javax.annotation.processing | 注釋處理。 |
| javax.lang.model | 注釋處理和Compiler Tree API中使用的語言模型(包括Java語言元素,類型和實用程序類)。 |
| javax.tools | Java編譯器API本身。 |
另一方面,Java編譯器樹API托管在com.sun.source package下,并且遵循Java標準庫的命名約定,被認為是非標準的(專有的或內部的)。 通常,這些API沒有得到很好的文檔記錄或支持,并且可能隨時更改。 而且,它們與特定的JDK / JRE版本相關聯,并且可能會限制使用它們的應用程序的可移植性。
2. Java編譯器API
我們的探索將從Java Compiler API開始,該文件已被很好地記錄且易于使用。 Java Compiler API的入口點是ToolProvider類 , 該類允許獲取系統中可用的Java編譯器實例( 官方文檔是熟悉典型用法場景的一個很好的起點)。 例如:
final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); for( final SourceVersion version: compiler.getSourceVersions() ) {System.out.println( version ); }此小代碼段獲取Java編譯器實例,并在控制臺上打印出受支持的Java源版本的列表。 對于Java 7編譯器,輸出如下所示:
RELEASE_3 RELEASE_4 RELEASE_5 RELEASE_6 RELEASE_7它對應于多個公知的Java版本方案:1.3,1.4,5,6和7。 對于Java 8編譯器,受支持的版本列表看起來更長:
RELEASE_3 RELEASE_4 RELEASE_5 RELEASE_6 RELEASE_7 RELEASE_8一旦Java編譯器實例可用,就可以將其用于對Java源文件集執行不同的編譯任務。 但是在此之前,應準備好一組編譯單元和診斷收集器(以收集所有遇到的編譯錯誤)。 為了進行試驗,我們將編譯存儲在SampleClass.java源文件中的這個簡單Java類:
public class SampleClass {public static void main(String[] args) {System.out.println( "SampleClass has been compiled!" );} }創建此源文件后,讓我們實例化診斷收集器并配置要編譯的源文件列表(僅包含SampleClass.java )。
final DiagnosticCollector< JavaFileObject > diagnostics = new DiagnosticCollector<>(); final StandardJavaFileManager manager = compiler.getStandardFileManager( diagnostics, null, null );final File file = new File( CompilerExample.class.getResource("/SampleClass.java").toURI() );final Iterable< ? extends JavaFileObject > sources = manager.getJavaFileObjectsFromFiles( Arrays.asList( file ) );準備工作完成后,最后一步基本上是調用Java編譯器任務,將診斷收集器和源文件列表傳遞給它,例如:
final CompilationTask task = compiler.getTask( null, manager, diagnostics, null, null, sources ); task.call();基本上就是這樣。 編譯任務完成后,應該在target / classes文件夾中提供SampleClass.class 。 我們可以運行它以確保編譯已成功執行:
java -cp target/classes SampleClass以下輸出將顯示在控制臺上,確認源文件已正確編譯為字節碼:
SampleClass has been compiled!如果在編譯過程中遇到任何錯誤,它們將通過診斷收集器變為可用(默認情況下,任何其他編譯器輸出也將被打印到System.err )。 為了說明這一點,讓我們嘗試編譯示例Java源文件,該文件有意包含一些錯誤( SampleClassWithErrors.java ):
private class SampleClassWithErrors {public static void main(String[] args) {System.out.println( "SampleClass has been compiled!" );} }編譯過程應該失敗,并且可以從診斷收集器中檢索錯誤消息(包括行號和源文件名),例如:
for( final Diagnostic< ? extends JavaFileObject > diagnostic: diagnostics.getDiagnostics() ) {System.out.format("%s, line %d in %s", diagnostic.getMessage( null ),diagnostic.getLineNumber(),diagnostic.getSource().getName() ); }在SampleClassWithErrors.java源文件上調用編譯任務將在控制臺上打印出以下示例錯誤描述:
modifier private not allowed here, line 1 in SampleClassWithErrors.java最后但并非最不重要的一點是,為了正確完成Java Compiler API的使用,請不要忘記關閉文件管理器:
manager.close();甚至更好的是,始終使用try-with-resources構造(本教程的第8部分“ 如何以及何時使用Exceptions”已經介紹過):
try( final StandardJavaFileManager manager = compiler.getStandardFileManager( diagnostics, null, null ) ) {// Implementation here }簡而言之,這些是Java Compiler API的典型使用場景。 在處理更復雜的示例時,有一些細微但非常重要的細節,可以極大地加快編譯過程。 要了解更多信息,請參考官方文檔 。
3.注釋處理器
幸運的是,編譯過程不僅限于編譯。 Java Compiler支持注釋處理器,可以將其視為編譯器插件。 顧名思義,注釋處理器可以對正在編譯的代碼執行加法處理(通常由注釋驅動)。
在本教程的第14部分“ 注釋處理器”中 ,我們將更全面地介紹注釋處理器。 目前,請參閱官方文檔以獲取更多詳細信息。
4.元素掃描儀
有時,在編譯過程中,有必要對所有語言元素(類,方法/構造函數,字段,參數,變量等)進行淺層分析。 為此,Java編譯器API提供了元素掃描器的概念。 元素掃描器是圍繞訪問者模式構建的,基本上需要實現單個掃描器(和訪問者)。 為了簡化實現,請提供一組基類。
我們將要開發的示例非常簡單,足以展示元素掃描儀用法的基本概念,并將計算所有編譯單元中的所有類,方法和字段。 基本的掃描器/訪問者實現擴展了ElementScanner7類,并且僅覆蓋其感興趣的方法:
public class CountClassesMethodsFieldsScanner extends ElementScanner7< Void, Void > {private int numberOfClasses;private int numberOfMethods;private int numberOfFields;public Void visitType( final TypeElement type, final Void p ) {++numberOfClasses;return super.visitType( type, p );}public Void visitExecutable( final ExecutableElement executable, final Void p ) {++numberOfMethods;return super.visitExecutable( executable, p );}public Void visitVariable( final VariableElement variable, final Void p ) {if ( variable.getEnclosingElement().getKind() == ElementKind.CLASS ) {++numberOfFields;}return super.visitVariable( variable, p );} }關于元素掃描器的快速說明: ElementScannerX類家族對應于特定的Java版本。 例如, ElementScanner8對應于Java 8 , ElementScanner7對應于Java 7 , ElementScanner6對應于Java 6 ,依此類推。 所有這些類的確有一個visitXxx方法族,其中包括:
| visitPackage | 訪問包元素。 |
| visitType | 訪問類型元素。 |
| visitVariable | 訪問變量元素。 |
| visitExecutable | 訪問可執行元素。 |
| visitTypeParameter | 訪問類型參數元素。 |
在編譯過程中調用掃描器(和訪問者)的一種方法是使用注釋處理器。 讓我們通過擴展AbstractProcessor類來定義一個(請注意,注釋處理器也與特定的Java版本緊密相關,在我們的示例中為Java 7 ):
@SupportedSourceVersion( SourceVersion.RELEASE_7 ) @SupportedAnnotationTypes( "*" ) public class CountElementsProcessor extends AbstractProcessor {private final CountClassesMethodsFieldsScanner scanner;public CountElementsProcessor( final CountClassesMethodsFieldsScanner scanner ) {this.scanner = scanner;}public boolean process( final Set< ? extends TypeElement > types, final RoundEnvironment environment ) {if( !environment.processingOver() ) {for( final Element element: environment.getRootElements() ) {scanner.scan( element );}}return true;} }基本上,注釋處理器只是將所有艱苦的工作委托給我們之前定義的掃描程序實現(在本教程的第14部分“ 注釋處理器”中 ,我們將提供更全面的注釋處理器示例)。
SampleClassToParse.java文件是示例,我們將在其中編譯和計算所有類,方法/構造函數和字段:
public class SampleClassToParse {private String str;private static class InnerClass { private int number;public void method() {int i = 0;try {// Some implementation here} catch( final Throwable ex ) {// Some implementation here}}}public static void main( String[] args ) {System.out.println( "SampleClassToParse has been compiled!" );} }編譯過程看起來與我們在Java Compiler API部分中所看到的完全一樣。 唯一的區別是,編譯任務應配置有注釋處理器實例。 為了說明這一點,讓我們看一下下面的代碼片段:
final CountClassesMethodsFieldsScanner scanner = new CountClassesMethodsFieldsScanner(); final CountElementsProcessor processor = new CountElementsProcessor( scanner );final CompilationTask task = compiler.getTask( null, manager, diagnostics, null, null, sources ); task.setProcessors( Arrays.asList( processor ) ); task.call();System.out.format( "Classes %d, methods/constructors %d, fields %d",scanner.getNumberOfClasses(),scanner.getNumberOfMethods(), scanner.getNumberOfFields() );針對SampleClassToParse.java源文件執行編譯任務將在控制臺中輸出以下消息:
Classes 2, methods/constructors 4, fields 2這很有意義:聲明了兩個類, SampleClassToParse和InnerClass 。 SampleClassToParse類具有default constructor (隱式定義),方法main和字段str 。 反過來, InnerClass類也具有default constructor (隱式定義),方法method和字段number 。
這個例子很幼稚,但是它的目的不是展示花哨的東西,而是介紹基本概念(教程的第14部分 , 注釋處理器 ,將包括更完整的例子)。
5. Java編譯器樹API
元素掃描儀非常有用,但是它們提供的訪問權限非常有限。 有時需要將Java源文件解析為抽象語法樹(或AST )并執行更深入的分析。 Java編譯器樹API是我們實現它所需要的工具。 Java編譯器樹API與Jav??a編譯器API緊密合作,并使用javax.lang.model包。
Java編譯器樹API的用法與“元素掃描器”部分中的元素掃描器非常相似,并且是按照相同的模式構建的。 讓我們重用Element Scanners部分中的示例源文件SampleClassToParse.java ,并計算所有編譯單元中存在多少空try/catch塊。 為此,我們必須通過擴展TreePathScanner基類來定義樹路徑掃描器(和訪問者),類似于元素掃描器(和訪問者)。
public class EmptyTryBlockScanner extends TreePathScanner< Object, Trees > {private int numberOfEmptyTryBlocks;@Overridepublic Object visitTry(final TryTree tree, Trees trees) {if( tree.getBlock().getStatements().isEmpty() ){++numberOfEmptyTryBlocks;}return super.visitTry( tree, trees );}public int getNumberOfEmptyTryBlocks() {return numberOfEmptyTryBlocks;} }與元素掃描器相比, visitXxx方法的數量要豐富visitXxx (大約50種方法),并且涵蓋所有Java語言語法構造。 與元素掃描器一樣,調用樹路徑掃描器的方法之一也是通過定義專用注釋處理器,例如:
@SupportedSourceVersion( SourceVersion.RELEASE_7 ) @SupportedAnnotationTypes( "*" ) public class EmptyTryBlockProcessor extends AbstractProcessor {private final EmptyTryBlockScanner scanner;private Trees trees;public EmptyTryBlockProcessor( final EmptyTryBlockScanner scanner ) {this.scanner = scanner;}@Overridepublic synchronized void init( final ProcessingEnvironment processingEnvironment ) {super.init( processingEnvironment );trees = Trees.instance( processingEnvironment );}public boolean process( final Set< ? extends TypeElement > types, final RoundEnvironment environment ) {if( !environment.processingOver() ) {for( final Element element: environment.getRootElements() ) {scanner.scan( trees.getPath( element ), trees );}}return true;} }初始化過程變得有點復雜,因為我們必須獲取Trees類的實例并將每個元素轉換為樹路徑表示形式。 此時,編譯步驟應該看起來非常熟悉并且足夠清晰。 為了使它更具趣味性,讓我們針對到目前為止我們已經嘗試過的所有源文件運行它: SampleClassToParse.java和SampleClass.java 。
final EmptyTryBlockScanner scanner = new EmptyTryBlockScanner(); final EmptyTryBlockProcessor processor = new EmptyTryBlockProcessor( scanner );final Iterable<? extends JavaFileObject> sources = manager.getJavaFileObjectsFromFiles( Arrays.asList( new File(CompilerExample.class.getResource("/SampleClassToParse.java").toURI()),new File(CompilerExample.class.getResource("/SampleClass.java").toURI())) );final CompilationTask task = compiler.getTask( null, manager, diagnostics, null, null, sources ); task.setProcessors( Arrays.asList( processor ) ); task.call();System.out.format( "Empty try/catch blocks: %d", scanner.getNumberOfEmptyTryBlocks() );一旦針對多個源文件運行,上面的代碼段將在控制臺中打印以下輸出:
Empty try/catch blocks: 1Java編譯器樹API可能看起來有點低級,的確如此。 另外,作為內部API,它沒有受支持的文檔。 但是,它提供了對抽象語法樹的完全訪問權限,當您需要執行深入的源代碼分析和后處理時,它可以節省生命。
6.接下來
在本教程的這一部分中,我們研究了從Java應用程序內部對Java Compiler API的編程訪問。 我們還挖掘了更深層次,更動人的注釋處理器和未發現的Java編譯器樹API,該API提供了對正在編譯的Java源文件(編譯單元)的抽象語法樹的完整訪問。 在本教程的下一部分中,我們將以同樣的方式繼續,并進一步注解注釋處理器及其適用性。
7.下載
這是Java編譯器API的課程,是高級Java課程的第13部分。 您可以在此處下載課程的源代碼: advanced-java-part-13
翻譯自: https://www.javacodegeeks.com/2015/09/java-compiler-api.html
總結
以上是生活随笔為你收集整理的Java编译器API的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 简述GPS,GSM,GPRS和GNSS的
- 下一篇: 一招教你用电脑系统修复硬盘如何修复电脑硬