Эта статья является частью нашего Академического курса под названием Advanced Java .
Этот курс призван помочь вам наиболее эффективно использовать Java. В нем обсуждаются сложные темы, включая создание объектов, параллелизм, сериализацию, рефлексию и многое другое. Он проведет вас через ваше путешествие в мастерство Java! Проверьте это здесь !
Содержание
1. Введение
В этой части руководства мы рассмотрим API компилятора Java. Этот API обеспечивает программный доступ к самому компилятору Java и позволяет разработчикам компилировать классы Java из исходных файлов на лету из кода приложения.
Что еще интереснее, мы также собираемся пройти через API дерева компиляторов Java, который предоставляет доступ к функциональности синтаксического анализатора Java. Используя этот API, разработчики Java могут напрямую подключаться к этапу синтаксического анализа и компилировать исходный код Java после анализа. Это очень мощный API, который активно используется многими инструментами статического анализа кода.
Java Compiler API также поддерживает обработку аннотаций (более подробную информацию см. В части 5 учебника, « Как и когда использовать перечисления и аннотации» , подробнее в части 14 учебника, « Процессоры аннотаций» ) и разделен на три различных пакета. , показано в таблице ниже.
пакет | Описание |
---|---|
javax.annotation.processing | Обработка аннотаций. |
javax.lang.model | Языковая модель, используемая в обработке аннотаций и API дерева компиляторов (включая элементы языка Java, типы и служебные классы). |
javax.tools | Сам API компилятора Java. |
С другой стороны, API дерева компиляторов Java размещается в com.sun.source package
и, в соответствии с соглашениями об именах стандартной библиотеки Java, считается нестандартным (проприетарным или внутренним). В целом, эти API не очень хорошо документированы или поддерживаются и могут измениться в любое время. Более того, они привязаны к конкретной версии JDK / JRE и могут ограничивать переносимость приложений, которые их используют.
2. Java Compiler API
Наше исследование начнется с Java Compiler API, который достаточно хорошо документирован и прост в использовании. Точкой входа в API Java Compiler является класс ToolProvider , который позволяет получить экземпляр Java-компилятора, доступный в системе ( официальная документация является отличной отправной точкой для ознакомления с типичными сценариями использования). Например:
1
2
3
4
|
final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); for ( final SourceVersion version: compiler.getSourceVersions() ) { System.out.println( version ); } |
Этот небольшой фрагмент кода получает экземпляры компилятора Java и выводит на консоль список поддерживаемых версий исходного кода Java. Для компилятора Java 7 вывод выглядит следующим образом:
1
2
3
4
5
|
RELEASE_3 RELEASE_4 RELEASE_5 RELEASE_6 RELEASE_7 |
Это соответствует более известной схеме версий Java: 1.3 , 1.4 , 5 , 6 и 7 . Для компилятора Java 8 список поддерживаемых версий выглядит немного длиннее:
1
2
3
4
5
6
|
RELEASE_3 RELEASE_4 RELEASE_5 RELEASE_6 RELEASE_7 RELEASE_8 |
Как только экземпляр Java-компилятора станет доступен, его можно будет использовать для выполнения различных задач компиляции над набором исходных файлов Java. Но перед этим необходимо подготовить набор модулей компиляции и диагностического сборщика (для сбора всех обнаруженных ошибок компиляции). Для экспериментов мы собираемся скомпилировать этот простой класс Java, хранящийся в исходном файле SampleClass.java
:
1
2
3
4
5
|
public class SampleClass { public static void main(String[] args) { System.out.println( "SampleClass has been compiled!" ); } } |
Создав этот исходный файл, давайте создадим экземпляр диагностического сборщика и сконфигурируем список исходных файлов (который включает только SampleClass.java
) для компиляции.
1
2
3
4
5
6
7
8
9
|
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, передав ей сборщик диагностики и список исходных файлов, например:
1
2
3
|
final CompilationTask task = compiler.getTask( null , manager, diagnostics, null , null , sources ); task.call(); |
То есть, в основном, это. После завершения задачи компиляции SampleClass.class
должен быть доступен в папке target / classes . Мы можем запустить его, чтобы убедиться, что компиляция была выполнена успешно:
1
|
java - cp target /classes SampleClass |
Следующий вывод будет показан на консоли, подтверждая, что исходный файл был правильно скомпилирован в байт-код:
1
|
SampleClass has been compiled! |
В случае каких-либо ошибок, обнаруженных в процессе компиляции, они станут доступны через диагностический сборщик (по умолчанию любые дополнительные выходные данные компилятора будут также напечатаны в System.err
). Чтобы проиллюстрировать это, давайте попробуем скомпилировать пример исходного файла Java, который намеренно содержит некоторые ошибки ( SampleClassWithErrors.java
):
1
2
3
4
5
|
private class SampleClassWithErrors { public static void main(String[] args) { System.out.println( "SampleClass has been compiled!" ); } } |
Процесс компиляции должен завершиться неудачно, и сообщение об ошибке (включая номер строки и имя исходного файла) может быть получено из диагностического сборщика, например:
1
2
3
4
5
6
7
8
|
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
выведет на консоль следующее примерное описание ошибки:
1
|
modifier private not allowed here, line 1 in SampleClassWithErrors.java |
И последнее, но не менее важное: чтобы правильно завершить работу с API Java Compiler, не забудьте закрыть файловый менеджер:
1
|
manager.close(); |
Или, что еще лучше, всегда используйте конструкцию try-with-resources
(которая была рассмотрена в части 8 руководства « Как и когда использовать исключения» ):
1
2
3
4
|
try ( final StandardJavaFileManager manager = compiler.getStandardFileManager( diagnostics, null , null ) ) { // Implementation here } |
В двух словах, это типичные сценарии использования API Java Compiler. При работе с более сложными примерами есть пара тонких, но довольно важных деталей, которые могут значительно ускорить процесс компиляции. Чтобы узнать больше об этом, пожалуйста, обратитесь к официальной документации .
3. Процессоры аннотаций
К счастью, процесс компиляции не ограничивается только компиляцией. Компилятор Java поддерживает процессоры аннотаций, которые можно рассматривать как плагины компилятора. Как следует из названия, обработчики аннотаций могут выполнять дополнительную обработку (обычно управляемую аннотациями) над компилируемым кодом.
В части 14 руководства « Процессоры аннотаций» мы увидим гораздо более подробное описание и примеры процессоров аннотаций. На данный момент, пожалуйста, обратитесь к официальной документации, чтобы получить более подробную информацию.
4. Элемент Сканеры
Иногда возникает необходимость выполнить поверхностный анализ всех элементов языка (классов, методов / конструкторов, полей, параметров, переменных и т. Д.) В процессе компиляции. Специально для этого в Java Compiler API предусмотрена концепция сканеров элементов. Сканеры элементов построены вокруг шаблона посетителя и в основном требуют реализации одного сканера (и посетителя). Для упрощения реализации любезно предоставлен набор базовых классов.
Пример, который мы собираемся разработать, достаточно прост, чтобы продемонстрировать основные концепции использования сканеров элементов, и в нем будут учтены все классы, методы и поля во всех единицах компиляции. Базовая реализация сканера / посетителя расширяет класс ElementScanner7 и переопределяет только интересующие его методы:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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 ):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
@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
является примером, который мы собираемся скомпилировать и сосчитать все классы, методы / конструкторы и поля в:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
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!" ); } } |
Процедура компиляции выглядит точно так же, как мы видели в разделе API Java Compiler . Единственное отличие состоит в том, что задача компиляции должна быть настроена с использованием экземпляров процессора аннотаций. Чтобы проиллюстрировать это, давайте взглянем на фрагмент кода ниже:
01
02
03
04
05
06
07
08
09
10
11
12
|
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
выведет на консоль следующее сообщение:
1
|
Classes 2, methods /constructors 4, fields 2 |
Это имеет смысл: объявлены два класса, SampleClassToParse
и InnerClass
. Класс SampleClassToParse
имеет default constructor
(определенный неявно), метод main
и поле str
. В свою очередь, класс InnerClass
также имеет default constructor
(определенный неявно), метод method
и number
поля.
Этот пример очень наивен, но его цель состоит не в том, чтобы продемонстрировать что-то причудливое, а в том, чтобы представить основополагающие концепции ( часть 14 руководства, « Процессоры аннотаций» , будет включать более полные примеры).
5. API дерева компиляторов Java
Сканеры элементов довольно полезны, но детали, к которым они предоставляют доступ, весьма ограничены. Время от времени возникает необходимость проанализировать исходные файлы Java в абстрактных синтаксических деревьях (или AST ) и выполнить более глубокий анализ. Java Compiler Tree API — это инструмент, который нам нужен, чтобы это произошло. API Java Compiler Tree тесно работает с Java Compiler API и использует пакет javax.lang.model
.
Использование API дерева компиляторов Java очень похоже на сканеры элементов из раздела Сканеры элементов и построено по тем же шаблонам. Давайте повторно используем пример исходного файла SampleClassToParse.java
из раздела « Сканеры элементов » и посчитаем, сколько пустых блоков try/catch
присутствует во всех единицах компиляции. Чтобы сделать это, мы должны определить сканер пути дерева (и посетителя), подобно сканеру элемента (и посетителю), расширив базовый класс TreePathScanner .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
public class EmptyTryBlockScanner extends TreePathScanner< Object, Trees > { private int numberOfEmptyTryBlocks; @Override public Object visitTry( final TryTree tree, Trees trees) { if ( tree.getBlock().getStatements().isEmpty() ){ ++numberOfEmptyTryBlocks; } return super .visitTry( tree, trees ); } public int getNumberOfEmptyTryBlocks() { return numberOfEmptyTryBlocks; } } |
Количество методов visitXxx
значительно больше (около 50 методов) по сравнению со сканерами элементов и охватывает все синтаксические конструкции языка Java. Как и в случае со сканерами элементов, одним из способов вызова сканеров путей дерева также является определение выделенного процессора аннотаций, например:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@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; } @Override public 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
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
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() ); |
После запуска с несколькими исходными файлами приведенный выше фрагмент кода выведет следующий вывод в консоли:
1
|
Empty try /catch blocks: 1 |
API дерева компиляторов Java может выглядеть немного низкоуровневым, и это, безусловно, так. Кроме того, поскольку он является внутренним API, он не имеет хорошо поддерживаемой документации. Тем не менее, он дает полный доступ к абстрактным синтаксическим деревьям и спасает жизнь, когда вам необходимо выполнить глубокий анализ исходного кода и постобработку.
6. Что дальше
В этой части руководства мы рассмотрели программный доступ к API компилятора Java из приложений Java. Мы также углубились в процессоры аннотаций и раскрыли API дерева компиляторов Java, который обеспечивает полный доступ к абстрактным синтаксическим деревьям компилируемых исходных файлов Java (единицам компиляции). В следующей части урока мы продолжим в том же духе и рассмотрим процессоры аннотаций и их применимость.
7. Скачать
Это был урок для API компилятора Java, часть 13 Advanced Java Course. Вы можете скачать исходный код урока здесь: advanced-java-part-13