Ломбок часто используется, но знаете ли вы, как это работает? (два)

Java

в предыдущем постеЛомбок часто используется, но знаете ли вы, как это работает?Кратко представлен обработчик аннотаций, который представляет собой инструмент, используемый для обработки аннотаций во время компиляции. Мы сами сгенерировали некоторый код, но он отличается от Lombok, потому что Lombok добавляет некоторые классы на основе исходных классов. Как Lombok модифицирует содержание исходного класса? Далее мы углубимся в принцип Ломбока.

Принцип Javac

Теперь, когда мы работаем с классами во время компиляции, нам нужно понять, что Javac делает с программами на Java. Процесс компиляции кода Javac на самом деле написан на Java. Мы можем просмотреть его исходный код для простого анализа, как загрузить исходный код, и я не буду здесь анализировать исходный код отладки. Я рекомендую хорошо написанную статью.Руководство по отладке исходного кода Javac.

Процесс компиляции условно делится на три этапа

  • Разобрать и заполнить таблицу символов
  • Обработка аннотаций
  • Анализ и генерация байт-кода

Процесс взаимодействия этих трех стадий показан на следующем рисунке.

Разобрать и заполнить таблицу символов

Этот шаг состоит из двух шагов, включая синтаксический анализ и заполнение символов, где синтаксический анализ делится налексический анализиРазбордва шага.

Лексический анализ и синтаксический анализ

Лексический анализ заключается в преобразовании потока символов исходного кода в набор токенов (Token) в Java.Одиночный символ — это наименьший элемент в процессе написания программы, а токен (Token) — наименьший элемент в процессе компиляции. Ключевое слово, имя переменной, литералы и операторы могут быть токенами. Например в Явеint a = b+2Этот код указывает на 6 тегов.Token, соответственноint、a、=、b、+、2. Хотя ключевое слово int состоит из трех символов, это всего лишь токен, и его нельзя разделить.

Синтаксический анализ — это процесс построения абстрактного дерева объектов в соответствии с последовательностью токенов.Абстрактное синтаксическое дерево — это метод представления дерева, используемый для описания синтаксической структуры кода.Каждый узел синтаксического дерева представляет собой синтаксическую конструкцию, такую ​​как пакет, тип, модификатор, оператор, интерфейс, возвращаемое значение или даже комментарий к коду — это синтаксическая конструкция.

Древовидная структура, анализируемая синтаксическим анализом, состоит изJCTreeЧтобы представить, мы можем посмотреть, каковы его подклассы.

Мы строим класс сами, и в процессе компиляции мы можем наблюдать, какую структуру он представляет древовидной структурой.

 1public class HelloJvm {
2
3    private String a;
4    private String b;
5
6    public static void main(String[] args) {
7        int c = 1+2;
8        System.out.println(c);
9        print();
10    }
11
12    private static void print(){
13
14    }
15}

Все обратите внимание, где я рисую красную линию, вы можете видеть, что это все подклассы JCTree. Мы можем знать, что дерево во время компиляцииJCCompilationUnitЭто корневой узел, а затем составные элементы класса, такие как методы, закрытые переменные и классы классов, которые все используются как своего рода дерево.

Заполнить таблицу символов

Заполнение таблицы символов имеет мало общего с нашим принципом Ломбока, просто поймите его здесь.

После завершения синтаксического анализа и лексического анализа следующим шагом является процесс заполнения таблицы символов.Таблица символов представляет собой таблицу, состоящую из набора адресов символов и информации о символах.Его можно представить как пару значений KV в хэше table (таблица символов не обязательно является реализацией хэш-таблицы, это может быть упорядоченная таблица символов, древовидная таблица символов, таблица символов со стековой структурой и т. д.). Информация, зарегистрированная в таблице символов, используется на разных этапах компиляции.В семантическом анализе содержимое, зарегистрированное в таблице символов, будет использоваться для семантической проверки (например, для проверки соответствия использования имени исходному описанию) и создание промежуточного кода. На этапе генерации объектного кода таблица символов является основой для распределения адресов, когда выделяется имя символа.

Процессор аннотаций

После завершения первого шага разбора и заполнения таблицы символов следующим шагом является процессор аннотаций. Потому что этот шаг является ключом к принципу реализации Ломбока.

После JDK1.5 язык Java обеспечивает поддержку аннотаций, которые, как и обычный код Java, играют роль во время выполнения. Спецификация JSR-269 реализована в JDK1.6, предоставляя стандартный API для набора подключаемых процессоров аннотаций для обработки аннотаций во время компиляции.Мы можем думать об этом как о наборе подключаемых модулей компилятора, в этих , вы можете читать, изменять и добавлять любой элемент в абстрактное синтаксическое дерево.

Если эти подключаемые модули вносят изменения в синтаксическое дерево во время обработки аннотации, компилятор вернется к анализу и заполнению таблицы символов и повторной обработке до тех пор, пока все подключаемые процессоры аннотаций не закончат изменения в синтаксическом дереве. Каждая петля становится Раундом.

При стандартном API обработки аннотаций компилятора наш код может мешать поведению компилятора.Поскольку к любому элементу синтаксического дерева, даже к комментариям кода, можно получить доступ в плагине, он обрабатывается через аннотацию плагина. У подключаемого модуля, реализованного устройством, есть много возможностей для игры с точки зрения функций. При достаточной изобретательности программисты могут использовать подключаемые процессоры аннотаций для достижения многих целей, которые в противном случае можно было бы сделать только в кодировании.

Семантический анализ и генерация байт-кода

После анализа синтаксиса компилятор получает представление кода программы в виде абстрактного синтаксического дерева.Синтаксическое дерево может представлять собой абстракцию правильно структурированной исходной программы, но оно не может гарантировать, что исходная программа является логичной. Основной задачей семантического анализа является выполнение контекстно-зависимых проверок структурно правильных исходных программ, таких как проверка типов.

Например, у нас есть следующий код

1int a = 1;
2boolean b = false;
3char c = 2;

Ниже у нас могут быть следующие операции

1int d = b+c;

На самом деле приведенный выше код может сформировать точное по структуре синтаксическое дерево, но следующая операция семантически неверна. Поэтому, если он запущен, компиляция завершится ошибкой и не сможет быть скомпилирована.

Реализуйте простой ломбок самостоятельно

Мы поняли процесс javac выше, затем мы можем напрямую написать простой гаджет для добавления кода в существующие классы, и мы только генерируем метод set. Сначала напишите собственный класс аннотаций.

1@Retention(RetentionPolicy.SOURCE) // 注解只在源码中保留
2@Target(ElementType.TYPE) // 用于修饰类
3public @interface MySetter {
4}

Затем напишите класс процессора аннотаций для этого класса аннотаций.

 1@SupportedSourceVersion(SourceVersion.RELEASE_8)
2@SupportedAnnotationTypes("aboutjava.annotion.MySetter")
3public class MySetterProcessor extends AbstractProcessor {
4
5    private Messager messager;
6    private JavacTrees javacTrees;
7    private TreeMaker treeMaker;
8    private Names names;
9
10    /**
11     * @Description: 1. Message 主要是用来在编译时期打log用的
12     *              2. JavacTrees 提供了待处理的抽象语法树
13     *              3. TreeMaker 封装了创建AST节点的一些方法
14     *              4. Names 提供了创建标识符的方法
15     */
16    @Override
17    public synchronized void init(ProcessingEnvironment processingEnv) {
18        super.init(processingEnv);
19        this.messager = processingEnv.getMessager();
20        this.javacTrees = JavacTrees.instance(processingEnv);
21        Context context = ((JavacProcessingEnvironment)processingEnv).getContext();
22        this.treeMaker = TreeMaker.instance(context);
23        this.names = Names.instance(context);
24    }
25
26    @Override
27    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
28        return false;
29    }
30}

Здесь мы отмечаем, что мы получаем некоторую информацию об окружении на этапе компиляции в методе init. Мы извлекаем некоторые ключевые классы из окружения, описанные ниже.

  • JavacTrees: предоставляет абстрактное синтаксическое дерево для обработки
  • TreeMaker: инкапсулирует некоторые методы управления абстрактным синтаксическим деревом AST.
  • Names: Предоставляет методы для создания идентификаторов
  • Messager: В основном используется для регистрации в компиляторе.

Затем мы используем предоставленный класс инструментов для изменения существующего абстрактного синтаксического дерева AST. Основная логика модификации находится вprocessметод, если возвращается true, то процесс javac снова начнется с разбора и заполнения таблицы символов.processЛогика метода в основном следующая

 1@Override
2    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
3        Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(MySetter.class);
4        elementsAnnotatedWith.forEach(e->{
5            JCTree tree = javacTrees.getTree(e);
6            tree.accept(new TreeTranslator(){
7                @Override
8                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
9                    List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
10                    // 在抽象树中找出所有的变量
11                    for (JCTree jcTree : jcClassDecl.defs){
12                        if (jcTree.getKind().equals(Tree.Kind.VARIABLE)){
13                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) jcTree;
14                            jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
15                        }
16                    }
17                    // 对于变量进行生成方法的操作
18                    jcVariableDeclList.forEach(jcVariableDecl -> {
19                        messager.printMessage(Diagnostic.Kind.NOTE,jcVariableDecl.getName()+"has been processed");
20                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeSetterMethodDecl(jcVariableDecl));
21                    });
22                    super.visitClassDef(jcClassDecl);
23                }
24            });
25        });
26        return true;
27    }

На самом деле это выглядит сложнее, принцип относительно прост, в основном потому, что мы не знакомы с API, поэтому он кажется сложным для понимания, но основной смысл заключается в следующем.

  1. оказаться@MySetterКласс, помеченный аннотацией, получает свое синтаксическое дерево
  2. Пройдите его синтаксическое дерево, чтобы найти его узел параметра
  3. Создайте узел метода самостоятельно и добавьте его в синтаксическое дерево.

С точки зрения диаграмм мы построили тестовый классTestMySetter, мы знаем, что общая структура его синтаксического дерева показана на рисунке ниже.

Тогда наша цель превратить его дерево грамматики, как показано ниже, потому что окончательно генерирующий байт-код генерируется в соответствии с деревом грамматики, поэтому мы добавляем метод метода в дерево синтаксиса, тогда при генерации кода байт-кода байт-код соответствующего метода будет быть сгенерирован.

Код для создания узла метода выглядит следующим образом.

 1private JCTree.JCMethodDecl makeSetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl){
2
3    ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
4    // 生成表达式 例如 this.a = a;
5    JCTree.JCExpressionStatement aThis = makeAssignment(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName()));
6    statements.append(aThis);
7    JCTree.JCBlock block = treeMaker.Block(0, statements.toList());
8
9    // 生成入参
10    JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), jcVariableDecl.getName(), jcVariableDecl.vartype, null);
11    List<JCTree.JCVariableDecl> parameters = List.of(param);
12
13    // 生成返回对象
14    JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType());
15    return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC),getNewMethodName(jcVariableDecl.getName()),methodType,List.nil(),parameters,List.nil(),block,null);
16
17}
18
19private Name getNewMethodName(Name name){
20    String s = name.toString();
21    return names.fromString("set"+s.substring(0,1).toUpperCase()+s.substring(1,name.length()));
22}
23
24private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
25    return treeMaker.Exec(
26            treeMaker.Assign(
27                    lhs,
28                    rhs
29            )
30    );
31}

Наконец, мы выполняем следующие три команды

1javac -cp $JAVA_HOME/lib/tools.jar aboutjava/annotion/MySetter* -d
2javac -processor aboutjava.annotion.MySetterProcessor aboutjava/annotion//TestMySetter.java
3javap -p aboutjava/annotion/TestMySetter.class

Вы можете увидеть вывод следующим образом

1Compiled from "TestMySetter.java"
2public class aboutjava.annotion.TestMySetter {
3  private java.lang.String name;
4  public void setName(java.lang.String);
5  public aboutjava.annotion.TestMySetter();
6}

Вы можете видеть, что то, что нам нужно, было сгенерировано в байт-коде.setNameметод.

кодовый адрес

Суммировать

Пока что принцип Ломбок был примерно объяснен, что на самом деле различные операции на абстрактном синтаксическом дереве. На самом деле, вы также можете использовать время компиляции, чтобы сделать много вещей, таких как проверка спецификации кода и тому подобное. Здесь я только писал о создании метода набора. Если вы заинтересованы, вы можете написать код самостоятельно и попробовать создание метода получения ломбок.

Если вам интересно, можете обратить внимание на мой новый паблик аккаунт и поискать [Сумка с сокровищами программиста]. Или просто отсканируйте код ниже.

Ссылаться на