W3docs

Anotaciones personalizadas en Java

Define tus propios tipos de anotación en Java, configura su retención y destinos, y léelos en tiempo de ejecución con reflexión.

Una anotación personalizada es un tipo de anotación que declaras tú mismo, en lugar de una suministrada por el JDK (como @Override) o un framework (como @Test). La sintaxis se parece a una interfaz, pero las reglas son más estrictas. Una vez declarada, tu anotación se convierte en un tipo real que puedes adjuntar al código, buscar mediante reflexión y procesar en tiempo de compilación.

Este capítulo es la guía práctica para escribir las tuyas propias: la palabra clave @interface, qué tipos de elementos están permitidos, en qué se diferencian los elementos requeridos y opcionales, y cómo un procesador lee los valores en tiempo de ejecución. Si aún no conoces las anotaciones, empieza con anotaciones de Java y las anotaciones integradas; para controlar dónde puede aparecer tu anotación y cuánto tiempo vive, consulta las meta-anotaciones.

La declaración @interface

Un tipo de anotación se declara con la palabra clave @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
}

Esto declara un nuevo tipo de anotación llamado Audited cuyos elementos parecen métodos de una interfaz, pero se comportan como valores con nombre en los sitios de uso. Cada "método" es un elemento.

Úsalo así:

@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) { ... }

El atajo value (@Audited("...") en lugar de @Audited(value = "...")) solo está disponible cuando el elemento se llama literalmente value, que es por eso que tantas anotaciones usan exactamente ese nombre para su parámetro principal.

Qué elementos están permitidos

El cuerpo de un @interface es un conjunto cerrado de declaraciones de elementos. El tipo de retorno de cada elemento debe ser uno de los siguientes:

  • Un primitivo (int, long, double, boolean, ...).
  • String.
  • Class o una Class<?> parametrizada.
  • Un tipo enum.
  • Otro tipo de anotación.
  • Un array de cualquiera de los anteriores.

Los valores predeterminados se escriben con default. El valor predeterminado debe ser una constante en tiempo de compilación del tipo correcto:

@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 }
}

Lo que no puedes declarar en una anotación:

  • Métodos que acepten parámetros (los () son obligatorios pero siempre están vacíos).
  • Elementos genéricos (<T> T value(); es ilegal).
  • Cláusulas throws.
  • Herencia de otra interfaz (las anotaciones extienden implícitamente java.lang.annotation.Annotation).
  • Constructores.

Puedes anidar tipos dentro de una declaración de anotación — el enum Level del ejemplo anterior vive dentro de @RetryPolicy. Es un patrón muy útil: mantiene las opciones relacionadas vinculadas a la anotación que las usa.

Elementos requeridos vs. opcionales

Un elemento sin default es requerido en los sitios de uso. El compilador falla si lo olvidas:

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

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

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

Una pequeña nota de estilo: si hay un único valor obvio, nómbralo value y hazlo requerido. Si hay varios ajustes, nómbralos y dales valores predeterminados sensatos para que la llamada más común sea breve.

Anotaciones marcadoras

Una anotación sin elementos es un marcador:

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

Las anotaciones marcadoras no llevan datos; su presencia o ausencia es la señal completa. La reflexión pregunta "¿tiene esta clase @ThreadSafe?" con getAnnotation(ThreadSafe.class) != null o isAnnotationPresent(ThreadSafe.class).

Leer anotaciones en tiempo de ejecución

Para una anotación RUNTIME, la reflexión expone varios métodos en Class, Method, Field, Constructor y Parameter (consulta leer anotaciones con reflexión para la superficie completa):

  • isAnnotationPresent(Class) — respuesta rápida sí/no.
  • getAnnotation(Class) — devuelve la instancia de la anotación, o null.
  • getAnnotations() — devuelve todas las anotaciones del elemento (declaradas + heredadas mediante @Inherited).
  • getDeclaredAnnotations() — solo las declaradas directamente en el elemento, ignorando @Inherited.
  • getAnnotationsByType(Class) — maneja correctamente el caso @Repeatable.

La lectura tiene la misma forma independientemente del tipo de destino con el que trabajes:

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

El Audited devuelto es un proxy generado por la JVM — los métodos de elemento (value(), level(), tags()) son llamadas a métodos reales sobre él.

Igualdad, identidad y toString de anotaciones

Los valores de las anotaciones implementan equals, hashCode y toString tal como los define java.lang.annotation.Annotation:

  • Dos instancias de anotación son iguales cuando son del mismo tipo y cada elemento compara igual (con igualdad profunda de arrays).
  • hashCode se deriva de los valores de los elementos de manera definida.
  • toString produce una representación estable similar a la del código fuente — útil para el registro.

La reflexión a veces devuelve el mismo proxy para búsquedas repetidas en el mismo elemento, y a veces devuelve uno nuevo. Usa equals, nunca ==, al comparar instancias de anotaciones.

Un ejemplo completo: definir, adjuntar y reflejar

El programa declara dos anotaciones (@Audited y @Retry), las usa en una clase y recorre los métodos con reflexión — ejecutando cada método dentro de un envoltorio de auditoría o con un bucle de reintentos. Las anotaciones son puro metadata; el comportamiento vive en el ejecutor.

java— editable, runs on the server

Qué extraer de la ejecución:

  • greet solo llevaba @Audited, por lo que el ejecutor imprimió un par entrada/salida alrededor del método pero no realizó reintentos. El mismo ejecutor manejó save detectando @Retry además de @Audited: la primera invocación lanzó una excepción (saveCalls == 1), el auxiliar registró el fallo y repitió el bucle, y el segundo intento devolvió saved: data. Las anotaciones en sí no hicieron nada — el auxiliar invoke proporcionó el comportamiento.
  • unannotated pasó por el mismo bucle porque el ejecutor es uniforme. isAnnotationPresent devolvió false para ambas anotaciones, por lo que el auxiliar ni registró ni reintentó; el método simplemente se ejecutó una vez. Ese es el patrón para los procesadores: examinar las anotaciones, actuar razonablemente cuando están ausentes, nunca tratar de manera especial el "camino anotado".
  • Cada acceso a elemento (a.value(), r.attempts(), r.when()) devolvió el valor escrito en el código fuente. Retry.when() devolvió la constante del enum ALWAYS porque el sitio de llamada usó el valor predeterminado. Los valores predeterminados son incorporados en el proxy de la anotación por el compilador; el llamador no puede distinguir si un valor fue explícito o predeterminado.
  • El toString de Audited imprimió una forma similar al código fuente como @...Audited(level="WARN", value="Service.save"). Esa es una propiedad de cada proxy de anotación — útil para el registro y para assertEquals en pruebas. (El orden en que los elementos aparecen dentro de los paréntesis no está garantizado y varía entre versiones del JDK, así que no hagas aserciones sobre la cadena exacta.)
  • Las dos anotaciones son completamente independientes a nivel de código fuente: un método lleva ambas a la vez y la reflexión devolvió felizmente ambas. No hay jerarquía de herencia entre tipos de anotación; combinar comportamientos se logra apilando anotaciones en el mismo elemento, no extendiendo una anotación de otra.

Dónde esto deja de funcionar

Algunas sorpresas comunes:

  • La retención SOURCE no puede reflejarse. Si olvidas @Retention(RUNTIME), la reflexión silenciosamente devuelve null. El valor predeterminado es CLASS, no RUNTIME.
  • Los destinos deben coincidir. Si @Target(METHOD) y pones la anotación en una clase, el compilador lo rechaza.
  • Los valores predeterminados de los elementos deben ser constantes en tiempo de compilación. No puedes usar new ArrayList<>() como predeterminado; sí puedes usar {} para un array, una constante de enum, un literal Class o un literal primitivo.
  • Las anotaciones no pueden referenciarse a sí mismas cíclicamente. Un elemento del tipo MyAnn dentro de @interface MyAnn es rechazado.

El siguiente capítulo, procesamiento de anotaciones, muestra el lado en tiempo de compilación — generar nuevos archivos fuente en respuesta a tus anotaciones personalizadas, en lugar de (o además de) leerlas en tiempo de ejecución.

Práctica

Práctica
Declaras `@Cached { int ttlSeconds(); }` y la colocas en un método. En tiempo de ejecución `m.getAnnotation(Cached.class)` devuelve `null` aunque el código fuente tiene claramente `@Cached(ttlSeconds = 60)`. ¿Cuál es la causa más probable?
Declaras `@Cached { int ttlSeconds(); }` y la colocas en un método. En tiempo de ejecución `m.getAnnotation(Cached.class)` devuelve `null` aunque el código fuente tiene claramente `@Cached(ttlSeconds = 60)`. ¿Cuál es la causa más probable?
Was this page helpful?