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.Classou umClass<?>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 ounull.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@Repeatablecorretamente.
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.toStringproduz 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.
O que observar na execução:
greetcarregava 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 tratousavedetectando@Retryalém de@Audited: a primeira invocação lançou exceção (saveCalls == 1), o auxiliar registrou a falha e repetiu, e a segunda tentativa retornousaved: data. As anotações em si não fizeram nada — o auxiliarinvokeforneceu o comportamento.unannotatedpassou pelo mesmo loop porque o executor é uniforme.isAnnotationPresentretornoufalsepara 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 enumALWAYSporque 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
toStringdeAuditedimprimiu 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 paraassertEqualsem 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 retornanull. O padrão éCLASS, nãoRUNTIME. - 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 literalClassou um literal primitivo. - Anotações não podem referenciar a si mesmas de forma cíclica. Um elemento do tipo
MyAnndentro 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.