W3docs

Anotações Personalizadas em Java

Defina seus próprios tipos de anotação em Java, configure retenção e alvos, e leia-os em tempo de execução com reflection.

Uma anotação personalizada é um tipo de anotação que você mesmo declara, em vez de uma fornecida pelo JDK (como @Override) ou por um framework (como @Test). A sintaxe lembra uma interface, mas as regras são mais rígidas. Após declarada, sua anotação se torna um tipo real que pode ser anexado ao código, consultado via reflection e processado em tempo de compilação.

Este capítulo é o guia prático para criar as suas próprias: a palavra-chave @interface, quais tipos de elemento são permitidos, como elementos obrigatórios e opcionais diferem, e como um processador lê os valores em tempo de execução. Se você ainda não conhece anotações, comece com anotações em Java e as anotações embutidas; para controlar onde sua anotação pode aparecer e quanto tempo ela vive, consulte meta-anotações.

A declaração @interface

Um tipo de anotação é declarado com a palavra-chave @interface:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Audited {
  String value();                                       // required element
  String level() default "INFO";                        // element with a default
  String[] tags() default {};                           // array element with default
}

Isso declara um novo tipo de anotação chamado Audited, cujos elementos parecem métodos de uma interface, mas se comportam como valores nomeados nos locais de uso. Cada "método" é um elemento.

Use assim:

@Audited("UserService.login")                            // value omitted name → "value" element
public User login(String user, String password) { ... }

@Audited(value = "Service.save", level = "WARN", tags = {"db", "write"})
public void save(Entity e) { ... }

O atalho value (@Audited("...") em vez de @Audited(value = "...")) só está disponível quando o elemento se chama literalmente value — é por isso que tantas anotações usam exatamente esse nome para o seu parâmetro principal.

Quais elementos são permitidos

O corpo de um @interface é um conjunto fechado de declarações de elementos. O tipo de retorno de cada elemento deve ser um dos seguintes:

  • Um primitivo (int, long, double, boolean, ...).
  • String.
  • Class ou um Class<?> parametrizado.
  • Um tipo enum.
  • Outro tipo de anotação.
  • Um array de qualquer um dos tipos acima.

Valores padrão são escritos com default. O padrão deve ser uma constante de tempo de compilação do tipo correto:

@interface RetryPolicy {
  int attempts() default 3;
  long delayMs() default 100;
  Class<? extends Exception>[] on() default {Exception.class};
  Level level() default Level.WARN;
  enum Level { DEBUG, INFO, WARN, ERROR }
}

O que você não pode declarar em uma anotação:

  • Métodos que recebem parâmetros (os () são obrigatórios, mas sempre vazios).
  • Elementos genéricos (<T> T value(); é inválido).
  • Cláusulas throws.
  • Herança de outra interface (anotações estendem implicitamente java.lang.annotation.Annotation).
  • Construtores.

Você pode aninhar tipos dentro de uma declaração de anotação — o enum Level acima vive dentro de @RetryPolicy. É um idioma útil: mantém as opções relacionadas no escopo da anotação que as usa.

Elementos obrigatórios vs. opcionais

Um elemento sem default é obrigatório nos locais de uso. O compilador falha se você o omitir:

@interface Issue { String id(); }                       // required

@Issue                                                  // compile error: missing 'id'
public void brokenLogin() { }

@Issue(id = "JIRA-123")                                 // OK: 'id' supplied
public void fixedLogin() { }

Uma dica de estilo: se há um único valor óbvio, nomeie o elemento value e torne-o obrigatório. Se há vários parâmetros, nomeie-os e forneça padrões sensatos para que a chamada comum continue curta.

Anotações marcadoras

Uma anotação sem nenhum elemento é uma marcadora:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface ThreadSafe { }

Anotações marcadoras não carregam dados; sua presença ou ausência é o sinal completo. A reflection pergunta "esta classe tem @ThreadSafe?" com getAnnotation(ThreadSafe.class) != null ou isAnnotationPresent(ThreadSafe.class).

Lendo anotações em tempo de execução

Para uma anotação RUNTIME, a reflection expõe vários métodos em Class, Method, Field, Constructor e Parameter (consulte lendo anotações com reflection para a superfície completa):

  • isAnnotationPresent(Class) — sim/não rápido.
  • getAnnotation(Class) — retorna a instância da anotação ou null.
  • getAnnotations() — retorna todas as anotações do elemento (declaradas + herdadas via @Inherited).
  • getDeclaredAnnotations() — apenas as declaradas diretamente no elemento, ignorando @Inherited.
  • getAnnotationsByType(Class) — trata o caso @Repeatable corretamente.

A leitura tem o mesmo formato independentemente do tipo de alvo com que você trabalha:

Method m = ...;
if (m.isAnnotationPresent(Audited.class)) {
  Audited a = m.getAnnotation(Audited.class);
  log(a.value(), a.level(), a.tags());
}

O Audited retornado é um proxy gerado pela JVM — os métodos de elemento (value(), level(), tags()) são chamadas de método reais nele.

Igualdade, identidade e toString de anotações

Os valores de anotação implementam equals, hashCode e toString conforme definido por java.lang.annotation.Annotation:

  • Duas instâncias de anotação são iguais quando são do mesmo tipo e cada elemento compara como igual (com igualdade profunda de array).
  • hashCode é derivado dos valores de elemento de forma definida.
  • toString produz uma renderização estável, semelhante ao código-fonte — útil para logging.

A reflection às vezes retorna o mesmo proxy para consultas repetidas no mesmo elemento, e às vezes retorna um novo. Use equals, nunca ==, ao comparar instâncias de anotação.

Um exemplo completo: declarar, anexar e refletir

O programa declara duas anotações (@Audited e @Retry), as usa em uma classe e percorre os métodos com reflection — executando cada método dentro de um wrapper de auditoria ou com um loop de retry. As anotações são puro metadado; o comportamento vive no executor.

java— editable, runs on the server

O que observar na execução:

  • greet carregava apenas @Audited, então o executor imprimiu um par de entrada/saída ao redor do método mas não fez retry. O mesmo executor tratou save detectando @Retry além de @Audited: a primeira invocação lançou exceção (saveCalls == 1), o auxiliar registrou a falha e repetiu, e a segunda tentativa retornou saved: data. As anotações em si não fizeram nada — o auxiliar invoke forneceu o comportamento.
  • unannotated passou pelo mesmo loop porque o executor é uniforme. isAnnotationPresent retornou false para ambas as anotações, então o auxiliar não registrou nem fez retry; o método simplesmente executou uma vez. Esse é o padrão para processadores: examinar anotações, adotar padrões sensatos quando ausentes, nunca tratar especialmente o "caminho anotado."
  • Cada acessor de elemento (a.value(), r.attempts(), r.when()) retornou o valor escrito no código-fonte. Retry.when() voltou como a constante enum ALWAYS porque o local de chamada usou o padrão. Os padrões são incorporados no proxy de anotação pelo compilador; o chamador não consegue distinguir se um valor foi explícito ou padrão.
  • O toString de Audited imprimiu uma forma semelhante ao código-fonte, como @...Audited(level="WARN", value="Service.save"). Essa é uma propriedade de todo proxy de anotação — útil para logging e para assertEquals em testes. (A ordem em que os elementos aparecem dentro dos parênteses não é garantida e varia entre versões do JDK, portanto não faça assert na string exata.)
  • As duas anotações são completamente independentes no nível do código-fonte: um método carrega ambas ao mesmo tempo e a reflection retornou as duas sem problemas. Não há hierarquia de herança entre tipos de anotação; combinar comportamentos é feito empilhando anotações no mesmo elemento, não estendendo uma anotação de outra.

Onde isso para de funcionar

Algumas surpresas comuns:

  • Retenção de código-fonte não pode ser refletida. Se você esquecer @Retention(RUNTIME), a reflection silenciosamente retorna null. O padrão é CLASS, não RUNTIME.
  • Os alvos devem corresponder. Se @Target(METHOD) e você colocar a anotação em uma classe, o compilador recusa.
  • Os padrões de elemento devem ser constantes de tempo de compilação. Você não pode usar new ArrayList<>() como padrão; pode usar {} para array, uma constante enum, um literal Class ou um literal primitivo.
  • Anotações não podem referenciar a si mesmas de forma cíclica. Um elemento do tipo MyAnn dentro de @interface MyAnn é rejeitado.

O próximo capítulo, processamento de anotações, mostra o lado em tempo de compilação — gerando novos arquivos-fonte em resposta às suas anotações personalizadas, em vez de (ou além de) lê-las em tempo de execução.

Prática

Prática
Você declara `@Cached { int ttlSeconds(); }` e coloca em um método. Em tempo de execução, `m.getAnnotation(Cached.class)` retorna `null`, mesmo que o código-fonte claramente tenha `@Cached(ttlSeconds = 60)`. Qual é a causa mais provável?
Você declara `@Cached { int ttlSeconds(); }` e coloca em um método. Em tempo de execução, `m.getAnnotation(Cached.class)` retorna `null`, mesmo que o código-fonte claramente tenha `@Cached(ttlSeconds = 60)`. Qual é a causa mais provável?
Was this page helpful?