Anotação Java Processando e Criando um Construtor

Anotação Java Processando e Criando um Construtor

1. Introdução

Este artigo éan intro to Java source-level annotation processinge fornece exemplos de como usar essa técnica para gerar arquivos de origem adicionais durante a compilação.

2. Aplicações de processamento de anotação

O processamento da anotação no nível da fonte apareceu pela primeira vez no Java 5. É uma técnica útil para gerar arquivos de origem adicionais durante o estágio de compilação.

Os arquivos de origem não precisam ser arquivos Java - você pode gerar qualquer tipo de descrição, metadados, documentação, recursos ou qualquer outro tipo de arquivo, com base nas anotações no seu código-fonte.

O processamento de anotações é usado ativamente em muitas bibliotecas Java onipresentes, por exemplo, para gerar metaclasses no QueryDSL e JPA, para aumentar as classes com código padrão na biblioteca Lombok.

Uma coisa importante a se notar éthe limitation of the annotation processing API — it can only be used to generate new files, not to change existing ones.

A exceção notável é a bibliotecaLombok, que usa o processamento de anotações como um mecanismo de inicialização para se incluir no processo de compilação e modificar o AST por meio de algumas APIs de compilador interno. Essa técnica hacky não tem nada a ver com o objetivo pretendido do processamento de anotações e, portanto, não é discutida neste artigo.

3. API de processamento de anotação

O processamento da anotação é realizado em várias rodadas. Cada rodada começa com o compilador pesquisando as anotações nos arquivos de origem e escolhendo os processadores de anotação adequados para essas anotações. Cada processador de anotação, por sua vez, é chamado nas fontes correspondentes.

Se algum arquivo for gerado durante esse processo, outra rodada será iniciada com os arquivos gerados como entrada. Esse processo continua até que nenhum arquivo novo seja gerado durante o estágio de processamento.

Cada processador de anotação, por sua vez, é chamado nas fontes correspondentes. Se algum arquivo for gerado durante esse processo, outra rodada será iniciada com os arquivos gerados como entrada. Esse processo continua até que nenhum arquivo novo seja gerado durante o estágio de processamento.

A API de processamento de anotações está localizada no pacotejavax.annotation.processing. A principal interface que você terá que implementar é a interfaceProcessor, que tem uma implementação parcial na forma de classeAbstractProcessor. Essa classe é a que estenderemos para criar nosso próprio processador de anotações.

4. Configurando o Projeto

Para demonstrar as possibilidades do processamento de anotações, desenvolveremos um processador simples para gerar construtores de objetos fluentes para classes anotadas.

Vamos dividir nosso projeto em dois módulos Maven. Um deles, o móduloannotation-processor, conterá o próprio processador junto com a anotação, e outro, o móduloannotation-user, conterá a classe anotada. Este é um caso de uso típico do processamento de anotações.

As configurações para o móduloannotation-processor são as seguintes. Vamos usar a bibliotecaauto-service do Google para gerar o arquivo de metadados do processador, que será discutido mais tarde, e omaven-compiler-plugin ajustado para o código-fonte Java 8. As versões dessas dependências são extraídas para a seção de propriedades.

As versões mais recentes da bibliotecaauto-service emaven-compiler-plugin podem ser encontradas no repositório Maven Central:


    1.0-rc2
    
      3.5.1
    




    
        com.google.auto.service
        auto-service
        ${auto-service.version}
        provided
    




    

        
            org.apache.maven.plugins
            maven-compiler-plugin
            ${maven-compiler-plugin.version}
            
                1.8
                1.8
            
        

    

O módulo Mavenannotation-user com as fontes anotadas não precisa de nenhum ajuste especial, exceto adicionar uma dependência no módulo do processador de anotação na seção de dependências:


    com.example
    annotation-processing
    1.0.0-SNAPSHOT

5. Definindo uma Anotação

Suponha que temos uma classe POJO simples em nosso móduloannotation-user com vários campos:

public class Person {

    private int age;

    private String name;

    // getters and setters …

}

Queremos criar uma classe auxiliar do construtor para instanciar a classePerson com mais fluência:

Person person = new PersonBuilder()
  .setAge(25)
  .setName("John")
  .build();

Esta classePersonBuilder é uma escolha óbvia para uma geração, pois sua estrutura é completamente definida pelos métodos setterPerson.

Vamos criar uma anotação@BuilderProperty no móduloannotation-processor para os métodos setter. Isso nos permitirá gerar a classeBuilder para cada classe que tem seus métodos setter anotados:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface BuilderProperty {
}

A anotação@Target com o parâmetroElementType.METHOD garante que essa anotação só possa ser colocada em um método.

A política de retençãoSOURCE significa que essa anotação está disponível apenas durante o processamento da origem e não está disponível no tempo de execução.

A classePerson com propriedades anotadas com a anotação@BuilderProperty terá a seguinte aparência:

public class Person {

    private int age;

    private String name;

    @BuilderProperty
    public void setAge(int age) {
        this.age = age;
    }

    @BuilderProperty
    public void setName(String name) {
        this.name = name;
    }

    // getters …

}

6. Implementando umProcessor

6.1. Criação de uma subclasseAbstractProcessor

Começaremos estendendo a classeAbstractProcessor dentro do módulo Mavenannotation-processor.

Primeiro, devemos especificar anotações que este processador é capaz de processar e também a versão do código-fonte suportada. Isso pode ser feito implementando os métodosgetSupportedAnnotationTypesegetSupportedSourceVersion da interfaceProcessor ou anotando sua classe com anotações@SupportedAnnotationTypese@SupportedSourceVersion.

A anotação@AutoService faz parte da bibliotecaauto-service e permite gerar os metadados do processador que serão explicados nas seções seguintes.

@SupportedAnnotationTypes(
  "com.example.annotation.processor.BuilderProperty")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class BuilderProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set annotations,
      RoundEnvironment roundEnv) {
        return false;
    }
}

Você pode especificar não apenas os nomes das classes de anotação concretas, mas também curingas, como“com.example.annotation. para processar anotações dentro do pacotecom.example.annotation e todos os seus subpacotes, ou mesmo“” para processar todas as anotações.

O único método que teremos que implementar é o métodoprocess que faz o próprio processamento. É chamado pelo compilador para cada arquivo de origem que contém as anotações correspondentes.

As anotações são passadas como o primeiro argumentoSet<? extends TypeElement> annotations, e as informações sobre a rodada de processamento atual são passadas como o argumentoRoundEnviroment roundEnv.

O valor de retornoboolean deve sertrue se o seu processador de anotação processou todas as anotações passadas e você não quer que elas sejam passadas para outros processadores de anotação na lista.

6.2. Juntando informação

Nosso processador ainda não faz nada de útil, então vamos preenchê-lo com código.

Primeiro, precisaremos iterar por todos os tipos de anotação encontrados na classe - em nosso caso, o conjuntoannotations terá um único elemento correspondente à anotação@BuilderProperty, mesmo que essa anotação ocorra várias vezes no arquivo de origem.

Ainda assim, é melhor implementar o métodoprocess como um ciclo de iteração, para fins de completude:

@Override
public boolean process(Set annotations,
  RoundEnvironment roundEnv) {

    for (TypeElement annotation : annotations) {
        Set annotatedElements
          = roundEnv.getElementsAnnotatedWith(annotation);

        // …
    }

    return true;
}

Neste código, usamos a instânciaRoundEnvironment para receber todos os elementos anotados com a anotação@BuilderProperty. No caso da classePerson, esses elementos correspondem aos métodossetNameesetAge.

O usuário da anotação de@BuilderProperty pode anotar erroneamente métodos que não são realmente configuradores. O nome do método setter deve começar comset, e o método deve receber um único argumento. Então, vamos separar o joio do trigo.

No código a seguir, usamos o coletorCollectors.partitioningBy() para dividir os métodos anotados em duas coleções: setters anotados corretamente e outros métodos anotados erroneamente:

Map> annotatedMethods = annotatedElements.stream().collect(
  Collectors.partitioningBy(element ->
    ((ExecutableType) element.asType()).getParameterTypes().size() == 1
    && element.getSimpleName().toString().startsWith("set")));

List setters = annotatedMethods.get(true);
List otherMethods = annotatedMethods.get(false);

Aqui, usamos o métodoElement.asType() para receber uma instância da classeTypeMirror, que nos dá alguma capacidade de introspectar tipos, embora estejamos apenas no estágio de processamento de origem.

Devemos alertar o usuário sobre métodos anotados incorretamente, então vamos usar a instânciaMessager acessível a partir do campo protegidoAbstractProcessor.processingEnv. As seguintes linhas produzirão um erro para cada elemento anotado erroneamente durante o estágio de processamento da origem:

otherMethods.forEach(element ->
  processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
    "@BuilderProperty must be applied to a setXxx method "
      + "with a single argument", element));

Obviamente, se a coleção correta de setters estiver vazia, não há sentido em continuar a iteração atual do conjunto de elementos de tipo:

if (setters.isEmpty()) {
    continue;
}

Se a coleção setters tiver pelo menos um elemento, vamos usá-la para obter o nome completo da classe a partir do elemento envolvente, que no caso do método setter parece ser a própria classe de origem:

String className = ((TypeElement) setters.get(0)
  .getEnclosingElement()).getQualifiedName().toString();

A última informação que precisamos para gerar uma classe de construtor é um mapa entre os nomes dos setters e os nomes de seus tipos de argumento:

Map setterMap = setters.stream().collect(Collectors.toMap(
    setter -> setter.getSimpleName().toString(),
    setter -> ((ExecutableType) setter.asType())
      .getParameterTypes().get(0).toString()
));

6.3. Gerando o arquivo de saída

Agora temos todas as informações necessárias para gerar uma classe de construtor: o nome da classe de origem, todos os seus nomes de setter e seus tipos de argumento.

Para gerar o arquivo de saída, usaremos a instânciaFiler fornecida novamente pelo objeto na propriedade protegidaAbstractProcessor.processingEnv:

JavaFileObject builderFile = processingEnv.getFiler()
  .createSourceFile(builderClassName);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
    // writing generated file to out …
}

O código completo do métodowriteBuilderFile é fornecido abaixo. Precisamos apenas calcular o nome do pacote, o nome completo da classe do construtor e os nomes simples da classe de origem e da classe do construtor. O restante do código é bem direto.

private void writeBuilderFile(
  String className, Map setterMap)
  throws IOException {

    String packageName = null;
    int lastDot = className.lastIndexOf('.');
    if (lastDot > 0) {
        packageName = className.substring(0, lastDot);
    }

    String simpleClassName = className.substring(lastDot + 1);
    String builderClassName = className + "Builder";
    String builderSimpleClassName = builderClassName
      .substring(lastDot + 1);

    JavaFileObject builderFile = processingEnv.getFiler()
      .createSourceFile(builderClassName);

    try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {

        if (packageName != null) {
            out.print("package ");
            out.print(packageName);
            out.println(";");
            out.println();
        }

        out.print("public class ");
        out.print(builderSimpleClassName);
        out.println(" {");
        out.println();

        out.print("    private ");
        out.print(simpleClassName);
        out.print(" object = new ");
        out.print(simpleClassName);
        out.println("();");
        out.println();

        out.print("    public ");
        out.print(simpleClassName);
        out.println(" build() {");
        out.println("        return object;");
        out.println("    }");
        out.println();

        setterMap.entrySet().forEach(setter -> {
            String methodName = setter.getKey();
            String argumentType = setter.getValue();

            out.print("    public ");
            out.print(builderSimpleClassName);
            out.print(" ");
            out.print(methodName);

            out.print("(");

            out.print(argumentType);
            out.println(" value) {");
            out.print("        object.");
            out.print(methodName);
            out.println("(value);");
            out.println("        return this;");
            out.println("    }");
            out.println();
        });

        out.println("}");
    }
}

7. Executando o Exemplo

Para ver a geração de código em ação, você deve compilar ambos os módulos da raiz pai comum ou primeiro compilar o móduloannotation-processor e depois o móduloannotation-user.

A classePersonBuilder gerada pode ser encontrada dentro do arquivoannotation-user/target/generated-sources/annotations/com/example/annotation/PersonBuilder.java e deve ter a seguinte aparência:

package com.example.annotation;

public class PersonBuilder {

    private Person object = new Person();

    public Person build() {
        return object;
    }

    public PersonBuilder setName(java.lang.String value) {
        object.setName(value);
        return this;
    }

    public PersonBuilder setAge(int value) {
        object.setAge(value);
        return this;
    }
}

8. Formas alternativas de registrar um processador

Para usar seu processador de anotações durante o estágio de compilação, você tem várias outras opções, dependendo do seu caso de uso e das ferramentas utilizadas.

8.1. Usando a ferramenta do processador de anotação

A ferramentaapt era um utilitário de linha de comando especial para processar arquivos de origem. Fazia parte do Java 5, mas desde o Java 7 foi preterido em favor de outras opções e removido completamente no Java 8. Não será discutido neste artigo.

8.2. Usando a chave do compilador

A chave de compilador-processor é um recurso JDK padrão para aumentar o estágio de processamento de origem do compilador com seu próprio processador de anotação.

Observe que o próprio processador e a anotação já devem ser compilados como classes em uma compilação separada e presentes no caminho de classe; portanto, a primeira coisa que você deve fazer é:

javac com/example/annotation/processor/BuilderProcessor
javac com/example/annotation/processor/BuilderProperty

Em seguida, você faz a compilação real de suas fontes com a chave-processor especificando a classe do processador de anotação que acabou de compilar:

javac -processor com.example.annotation.processor.MyProcessor Person.java

Para especificar vários processadores de anotação de uma só vez, você pode separar seus nomes de classe com vírgulas, assim:

javac -processor package1.Processor1,package2.Processor2 SourceFile.java

8.3. Usando o Maven

Omaven-compiler-plugin permite especificar processadores de anotação como parte de sua configuração.

Aqui está um exemplo de adição de processador de anotação para o plug-in do compilador. Você também pode especificar o diretório para colocar as fontes geradas, usando o parâmetro de configuraçãogeneratedSourcesDirectory.

Observe que a classeBuilderProcessor já deve ser compilada, por exemplo, importada de outro jar nas dependências de construção:


    

        
            org.apache.maven.plugins
            maven-compiler-plugin
            3.5.1
            
                1.8
                1.8
                UTF-8
                ${project.build.directory}
                  /generated-sources/
                
                    
                        com.example.annotation.processor.BuilderProcessor
                    
                
            
        

    

8.4. Adicionando um Jar do processador ao Classpath

Em vez de especificar o processador de anotação nas opções do compilador, você pode simplesmente adicionar um jar especialmente estruturado com a classe do processador ao caminho de classe do compilador.

Para buscá-lo automaticamente, o compilador precisa saber o nome da classe do processador. Portanto, você deve especificá-lo no arquivoMETA-INF/services/javax.annotation.processing.Processor como um nome de classe totalmente qualificado do processador:

com.example.annotation.processor.BuilderProcessor

Você também pode especificar vários processadores deste jar para serem selecionados automaticamente, separando-os com uma nova linha:

package1.Processor1
package2.Processor2
package3.Processor3

Se você usar o Maven para construir este jar e tentar colocar esse arquivo diretamente no diretóriosrc/main/resources/META-INF/services, encontrará o seguinte erro:

[ERROR] Bad service configuration file, or exception thrown while
constructing Processor object: javax.annotation.processing.Processor:
Provider com.example.annotation.processor.BuilderProcessor not found

Isso ocorre porque o compilador tenta usar este arquivo durante o estágiosource-processing do próprio módulo, quando o arquivoBuilderProcessor ainda não foi compilado. O arquivo deve ser colocado dentro de outro diretório de recursos e copiado para o diretórioMETA-INF/services durante o estágio de cópia de recursos da compilação do Maven ou (melhor ainda) gerado durante a compilação.

A biblioteca Googleauto-service, discutida na seção seguinte, permite gerar este arquivo usando uma anotação simples.

8.5. Usando a biblioteca do Googleauto-service

Para gerar o arquivo de registro automaticamente, você pode usar a anotação@AutoService da bibliotecaauto-service do Google, assim:

@AutoService(Processor.class)
public BuilderProcessor extends AbstractProcessor {
    // …
}

Essa anotação é processada pelo processador de anotação da biblioteca de autoatendimento. Este processador gera o arquivoMETA-INF/services/javax.annotation.processing.Processor contendo o nome da classeBuilderProcessor.

9. Conclusão

Neste artigo, demonstramos o processamento de anotações no nível da fonte usando um exemplo de geração de uma classe Builder para um POJO. Também fornecemos várias maneiras alternativas de registrar processadores de anotação em seu projeto.

O código-fonte do artigo está disponívelon GitHub.