La lección anterior cerró con una carencia concreta: los genéricos le hablan al compilador y solo al compilador, y hay información sobre el código que no cabe en un sistema de tipos. "Este campo se exporta al CSV en la tercera columna con el encabezado ISBN". "Esta operación hay que registrarla en la auditoría". "Este método está obsoleto desde la versión 3 y desaparecerá en la 4". "Esta clase es un servicio que hay que instanciar al arrancar".

Nada de eso es un tipo. Es información sobre el código, y el mecanismo de Java para expresarla se llama anotación.

Las anotaciones son, probablemente, la característica del lenguaje cuya importancia real más se subestima al aprenderla. Vistas de cerca parecen un adorno: @Override encima de un método, @Deprecated encima de otro. Vistas de lejos, son el pilar sobre el que está construido el Java empresarial moderno entero. Cuando en el módulo 11 escribas @Entity sobre una clase y Hibernate cree una tabla, @Autowired sobre un campo y Spring inyecte una dependencia, o @Test sobre un método y JUnit lo ejecute, estarás usando exactamente el mecanismo que aprendes aquí.

Y hay una idea que conviene fijar desde la primera línea, porque es la fuente de casi toda la confusión: una anotación no hace nada por sí sola. @Test no ejecuta nada. @Entity no crea ninguna tabla. Son etiquetas inertes. Lo que ocurre es que alguien las lee —el compilador, una herramienta de construcción, o un framework en tiempo de ejecución— y actúa en consecuencia. Esta lección te enseña a escribir las etiquetas; la siguiente, a escribir el lector.

Al terminar, BiblioTech tendrá @CampoCsv y @Auditable, definidas y colocadas. Y estarán sin usar, esperando al motor de 10-03.

Contenido

  1. Qué es una anotación: metadatos sobre el código
  2. Sintaxis de uso: marcador, valor único y varios elementos
  3. Las anotaciones estándar del JDK
  4. @Override y el error que evita
  5. @Deprecated, con since y forRemoval
  6. @SuppressWarnings
  7. @SafeVarargs
  8. @FunctionalInterface
  9. Crear anotaciones propias: @interface
  10. Elementos: tipos permitidos, default y el elemento value
  11. Meta-anotaciones: @Retention
  12. Meta-anotaciones: @Target
  13. @Documented e @Inherited
  14. @Repeatable y su anotación contenedora
  15. Anotaciones de tipo (Java 8)
  16. Cómo se leen (I): procesadores de anotaciones en compilación
  17. Cómo se leen (II): reflexión en ejecución
  18. Para qué las usan los frameworks reales
  19. Configuración junto al código frente a XML
  20. Cuándo NO usar anotaciones
  21. BiblioTech: @CampoCsv
  22. BiblioTech: @Auditable
  23. Errores Comunes y Consejos
  24. Ejercicios

  1. Qué es una anotación: metadatos sobre el código

Una anotación es una etiqueta que se adhiere a un elemento del programa —una clase, un método, un campo, un parámetro, una variable local, un paquete— y que transporta información sobre ese elemento sin formar parte de su lógica.

La analogía útil es la de las etiquetas de un archivo físico. Un expediente en una carpeta tiene su contenido (los documentos) y tiene pegatinas en la solapa: "urgente", "revisado por Marta Ruiz", "destruir en 2030". Las pegatinas no cambian el contenido del expediente. Pero cuando alguien recorre el archivador buscando qué destruir, las lee y actúa.

Formalmente:

  • Una anotación no cambia el comportamiento del código que anota. Poner @Auditable sobre prestar() no hace que se audite nada. El método sigue haciendo exactamente lo mismo.
  • Una anotación es información que alguien consume. Ese "alguien" puede ser el compilador (@Override), una herramienta que se ejecuta durante la compilación (un procesador de anotaciones), o código que se ejecuta con tu programa (un framework, vía reflexión).
  • Una anotación se compila junto al código y, según su @Retention, puede sobrevivir hasta la ejecución.
graph TD
    A["Codigo fuente<br/>con anotaciones"] --> B["Compilador javac"]
    B -->|"RETENTION SOURCE<br/>se descartan"| C["Solo avisos y errores<br/>@Override, @SuppressWarnings"]
    B -->|"RETENTION CLASS<br/>quedan en el .class"| D["Herramientas de bytecode<br/>analizadores estaticos"]
    B -->|"RETENTION RUNTIME<br/>llegan vivas"| E["JVM en ejecucion"]
    E --> F["Reflexion: Spring, Hibernate, JUnit<br/>y el motor de 10-03"]
    B -.->|"procesador de anotaciones"| G["Genera codigo fuente nuevo<br/>ejemplo: Lombok"]
    G --> B

Ese diagrama es, en el fondo, toda la lección. El resto son detalles.

  1. Sintaxis de uso: marcador, valor único y varios elementos

Una anotación se escribe con @ seguido de su nombre, delante del elemento que anota. Hay tres formas según cuántos datos lleve:

// 1. ANOTACION MARCADORA: sin elementos. Los parentesis se omiten.
@Override
public String toString() { ... }

// 2. VALOR UNICO: si el elemento se llama 'value', se omite el nombre.
@SuppressWarnings("unchecked")
List<Libro> libros = (List<Libro>) crudo;

// equivale exactamente a:
@SuppressWarnings(value = "unchecked")

// 3. VARIOS ELEMENTOS: pares nombre = valor, separados por comas.
@Deprecated(since = "3.2", forRemoval = true)
public void prestarLibro(String isbn) { ... }

Y cuando un elemento es un array, se usan llaves (y se pueden omitir si hay un solo valor):

@SuppressWarnings({"unchecked", "rawtypes"})     // varios valores
@SuppressWarnings("unchecked")                   // uno solo: llaves opcionales

Se pueden apilar varias anotaciones sobre el mismo elemento:

@Auditable(nivel = "ALTO")
@Override
@SuppressWarnings("unchecked")
public Resultado<Prestamo> prestar(String isbn, String empleado) { ... }

Convención de estilo: las anotaciones de clases, métodos y campos van en su propia línea, encima del elemento. Las de parámetros y variables locales van en la misma línea.

  1. Las anotaciones estándar del JDK

El JDK trae un puñado de anotaciones que el compilador entiende de forma especial. No son muchas, y conviene conocerlas todas porque las vas a usar a diario.

Anotación Dónde se pone Qué hace realmente Retención
@Override Métodos El compilador verifica que realmente se sobrescribe algo; si no, error SOURCE
@Deprecated Casi cualquier elemento El compilador avisa al usarlo; aparece en el Javadoc RUNTIME
@SuppressWarnings Casi cualquier elemento Silencia avisos concretos del compilador SOURCE
@SafeVarargs Métodos static, final o constructores con varargs genéricos Suprime el aviso de heap pollution RUNTIME
@FunctionalInterface Interfaces El compilador verifica que tiene exactamente un método abstracto RUNTIME

Fíjate en la columna "qué hace realmente", porque distingue tres comportamientos muy distintos: verificar (falla la compilación si no se cumple), avisar (compila con advertencia) y silenciar (quita una advertencia).

Hay algunas más, de uso especializado, que solo merece la pena nombrar: @Native (constantes referenciadas desde código nativo) y las meta-anotaciones del apartado 11 en adelante.

  1. @Override y el error que evita

Retomamos 03-05. @Override es la anotación más usada de Java y la más útil, y su valor está en un tipo de bug particularmente cruel: el método que crees que sobrescribe y no sobrescribe nada.

public class Material {
    private final String isbn;

    @Override
    public boolean equals(Object otro) {
        if (this == otro) return true;
        if (!(otro instanceof Material)) return false;
        return isbn.equals(((Material) otro).isbn);
    }
}

Ahora mira este error, que es el clásico de los clásicos:

public class Material {

    // SIN @Override: parece que sobrescribe equals... pero NO.
    public boolean equals(Material otro) {          // ¡Material, no Object!
        return this.isbn.equals(otro.isbn);
    }
}

Esto compila perfectamente. No sobrescribe Object.equals(Object): la sobrecarga. Y el efecto es demoledor:

Material a = new Libro("Java Efectivo", "978-0000000001");
Material b = new Libro("Java Efectivo", "978-0000000001");

System.out.println(a.equals(b));        // true: llama a tu equals(Material)

List<Material> lista = new ArrayList<>();
lista.add(a);
System.out.println(lista.contains(b));  // ¡false! contains() llama a equals(Object)

Set<Material> conjunto = new HashSet<>();
conjunto.add(a);
conjunto.add(b);
System.out.println(conjunto.size());    // ¡2! duplicados en un Set

contains(), HashSet, HashMap, remove(), indexOf(): todo el framework de colecciones llama a equals(Object), que sigue siendo el de Object (identidad de referencia). Tienes duplicados en un Set, búsquedas que no encuentran nada y horas de depuración.

Con @Override, el compilador te lo corta en el sitio:

@Override
public boolean equals(Material otro) { ... }
error: method does not override or implement a method from a supertype
    @Override
    ^

Regla sin excepciones: pon @Override en absolutamente todos los métodos que sobrescriban algo. No cuesta nada y detecta errores de firma, de nombre (toSting() en lugar de toString()) y de aridad. Desde Java 6 también funciona para métodos de interfaz, así que también va en las implementaciones de Comparator.compare, Runnable.run o tus propias interfaces.

Su retención es SOURCE: cumple su función en el compilador y desaparece. No queda rastro de ella en el .class.

  1. @Deprecated, con since y forRemoval

@Deprecated marca un elemento como obsoleto: sigue funcionando, pero no deberías usarlo en código nuevo.

package com.nexussoftware.bibliotech.servicio;

public class GestorPrestamos {

    /**
     * Presta un material identificado por su ISBN.
     *
     * @deprecated Desde 3.2 usa {@link #prestar(String, String)}, que devuelve
     *             un {@code Resultado<Prestamo>} en lugar de lanzar excepciones
     *             para casos esperables. Se eliminará en la versión 4.0.
     */
    @Deprecated(since = "3.2", forRemoval = true)
    public void prestarLibro(String isbn) throws BiblioTechException {
        prestar(isbn, "desconocido");
    }

    public Resultado<Prestamo> prestar(String isbn, String empleado) { ... }
}

Al usarlo:

gestor.prestarLibro("978-0000000001");
warning: [removal] prestarLibro(String) in GestorPrestamos has been deprecated and marked for removal

Los dos elementos, añadidos en Java 9, importan más de lo que parece:

Elemento Tipo Significado
since String Versión en la que se marcó obsoleto. Ayuda a estimar cuánto tiempo lleva así
forRemoval boolean true = se va a eliminar. Cambia el aviso a la categoría removal, más ruidosa

La diferencia entre forRemoval = false (por defecto) y true es real: el primero significa "hay algo mejor", el segundo "esto desaparece, migra ya". Los IDE los muestran distinto y -Xlint:removal permite tratarlos como errores.

Dos reglas de uso:

  1. Siempre acompaña @Deprecated de @deprecated en el Javadoc, diciendo qué usar en su lugar. Una deprecación sin alternativa es una crueldad: informas del problema y no de la solución. Fíjate en que son dos cosas distintas: la anotación (para el compilador) y la etiqueta Javadoc (para el humano).
  2. No deprecies sin plan. Marcar y no eliminar nunca hace que los avisos se conviertan en ruido que todo el mundo ignora.

Su retención es RUNTIME, y eso permite que herramientas de análisis inspeccionen un .jar compilado y detecten usos de API obsoleta.

  1. @SuppressWarnings

Ya la usaste en 10-01. Silencia avisos concretos del compilador, identificados por una cadena.

@SuppressWarnings("unchecked")
T[] copia = (T[]) new Object[capacidad];

Los identificadores más útiles:

Valor Silencia
"unchecked" Operaciones sin comprobación genérica (los casts de 10-01)
"rawtypes" Uso de tipos crudos
"deprecation" Uso de API obsoleta
"removal" Uso de API marcada forRemoval = true
"serial" Clase serializable sin serialVersionUID (07-05)
"this-escape" Fuga de this en el constructor (Java 21)
"all" Todos. Prácticamente nunca es lo correcto

Los identificadores no están estandarizados más allá de unos pocos: cada compilador e IDE reconoce los suyos, y uno desconocido se ignora en silencio.

Las tres reglas (las mismas de 10-01, porque son importantes):

  1. Ámbito mínimo. Sobre la variable local, no sobre el método; sobre el método, no sobre la clase.
  2. Comentario justificando por qué la operación es segura pese al aviso.
  3. Nunca "all".
// MAL: apaga todo en toda la clase para siempre
@SuppressWarnings("all")
public class ImportadorCatalogo { ... }

// BIEN: ambito minimo y justificacion
public List<Libro> leerLibros(Path fichero) throws IOException {
    // SEGURO: el fichero solo lo escribe ExportadorCatalogoCsv, que
    // siempre serializa Libro. Cualquier otro contenido falla antes,
    // en la validacion de cabecera.
    @SuppressWarnings("unchecked")
    List<Libro> libros = (List<Libro>) leerEntidades(fichero);
    return libros;
}

  1. @SafeVarargs

Los varargs genéricos producen un aviso incómodo. Recuerda de 10-01 que no se pueden crear arrays genéricos, pero T... elementos es exactamente un T[]. El compilador lo permite creando un array de la clase borrada, y avisa de posible contaminación del montón (heap pollution):

public static <T> List<T> listaDe(T... elementos) {
    return new ArrayList<>(Arrays.asList(elementos));
}
warning: [unchecked] Possible heap pollution from parameterized vararg type T
    public static <T> List<T> listaDe(T... elementos) {
                                          ^

El aviso es legítimo, porque este método es peligroso:

// PELIGROSO: expone el array de varargs
@SafeVarargs
static <T> T[] peligroso(T... elementos) {
    return elementos;                     // devuelve el array creado por el compilador
}

public static void main(String[] args) {
    String[] textos = peligroso("a", "b");   // ClassCastException aqui
}

@SafeVarargs es tu promesa al compilador de que el método no guarda ni expone el array de varargs, solo lee de él. Si la promesa es cierta, el aviso desaparece:

@SafeVarargs
public static <T> List<T> listaDe(T... elementos) {
    return new ArrayList<>(Arrays.asList(elementos));   // solo LEE del array
}

Restricción: solo se puede poner en métodos que nadie pueda sobrescribir —static, final, private (Java 9+)— y en constructores. Es lógico: no puedes prometer nada sobre una implementación que aún no existe.

  1. @FunctionalInterface

Retomamos 04-06. Marca una interfaz como funcional, y el compilador verifica que tiene exactamente un método abstracto:

package com.nexussoftware.bibliotech.servicio;

/**
 * Calcula la multa de un prestamo con retraso.
 * Al ser funcional, se puede implementar con una lambda.
 */
@FunctionalInterface
public interface PoliticaMultas {

    double calcular(int diasDeRetraso);

    // Los default NO cuentan como abstractos: la interfaz sigue siendo funcional
    default PoliticaMultas conTopeDe(double maximo) {
        return dias -> Math.min(calcular(dias), maximo);
    }

    // Los static tampoco cuentan
    static PoliticaMultas fija(double porDia) {
        return dias -> dias * porDia;
    }
}
PoliticaMultas estandar = dias -> dias * 0.25;
PoliticaMultas acotada  = estandar.conTopeDe(10.0);

System.out.printf("30 días de retraso: %.2f €%n", estandar.calcular(30));   // 7,50 €
System.out.printf("100 días acotado:   %.2f €%n", acotada.calcular(100));   // 10,00 €

Si alguien añade un segundo método abstracto, el compilador lo impide:

@FunctionalInterface
public interface PoliticaMultas {
    double calcular(int dias);
    double calcularConDescuento(int dias, double descuento);   // segundo abstracto
}
error: Unexpected @FunctionalInterface annotation
    PoliticaMultas is not a functional interface
      multiple non-overriding abstract methods found in interface PoliticaMultas

No es obligatoria. Cualquier interfaz con un solo método abstracto se puede usar con lambdas, la lleve o no. Lo que hace @FunctionalInterface es proteger el contrato: declara la intención y evita que un compañero rompa a todos los usuarios de la interfaz añadiendo un método. Es documentación que el compilador verifica, que es la mejor clase de documentación.

  1. Crear anotaciones propias: @interface

Una anotación se declara con @interface. Formalmente es una interfaz (extiende implícitamente java.lang.annotation.Annotation), aunque no se usa como tal.

La más simple es una anotación marcadora, sin elementos:

package com.nexussoftware.bibliotech.anotaciones;

/**
 * Marca un elemento que aun no esta terminado.
 * Por si sola no hace absolutamente nada.
 */
public @interface EnConstruccion {
}
@EnConstruccion
public class ImportadorMarc21 {
    // ...
}

Compila y no pasa nada. Literalmente nada: la anotación es SOURCE por defecto... no, y este es el primer detalle que hay que fijar: por defecto la retención es CLASS, no SOURCE ni RUNTIME. Queda en el .class pero no es visible por reflexión. Es el valor por defecto menos útil de los tres, y la causa número uno de la pregunta "¿por qué mi anotación no aparece en getAnnotations()?". Volveremos a ello en el apartado 11.

Una anotación con elementos:

package com.nexussoftware.bibliotech.anotaciones;

/**
 * Documenta quien es responsable de un componente y desde cuando.
 */
public @interface Responsable {

    /** Nombre del responsable. Obligatorio: no tiene default. */
    String nombre();

    /** Equipo. Opcional. */
    String equipo() default "Plataforma";

    /** Fecha en formato ISO. Opcional. */
    String desde() default "";

    /** Personas de respaldo. Opcional, array vacio por defecto. */
    String[] respaldo() default {};
}
@Responsable(nombre = "Marta Ruiz", equipo = "Catálogo", desde = "2026-01-15",
             respaldo = {"Diego Alonso", "Nuria Vidal"})
public class CatalogoService { }

@Responsable(nombre = "Nuria Vidal")     // los demas toman su default
public class ServicioAvisos { }

Fíjate en la sintaxis de los elementos: se declaran como métodos sin cuerpo (String nombre();), no como campos. Eso es porque una anotación es una interfaz, y al leerla por reflexión llamarás a anotacion.nombre().

Un elemento sin default es obligatorio. Omitirlo es un error de compilación:

@Responsable(equipo = "Catálogo")
error: annotation @Responsable is missing a default value for the element 'nombre'

  1. Elementos: tipos permitidos, default y el elemento value

Tipos permitidos

Los tipos que puede tener un elemento de anotación están muy restringidos, y no es arbitrario: su valor debe poder guardarse en el fichero .class como una constante y ser conocido en compilación.

Permitido Ejemplo
Primitivos int orden(); boolean obligatorio(); double factor();
String String nombre();
Class o Class<?> Class<?> conversor();
Tipos enum Gravedad nivel();
Otras anotaciones Responsable propietario();
Arrays de todo lo anterior String[] alias(); Class<?>[] grupos();

No permitido: cualquier otra cosa. Nada de List<String>, Object, LocalDate, tus propias clases o tipos genéricos.

public @interface Malo {
    List<String> etiquetas();     // ERROR: invalid type for annotation member
    LocalDate creado();           // ERROR
    Libro libro();                // ERROR
}

Por eso las fechas en anotaciones se escriben como String en formato ISO ("2026-01-15") y se parsean al leerlas (verás cómo en 10-05), y las listas se declaran como arrays.

Los valores deben ser constantes de tiempo de compilación. Esto compila:

@Responsable(nombre = "Marta " + "Ruiz")           // concatenacion de literales: constante

Esto no:

@Responsable(nombre = obtenerResponsable())        // ERROR: no es una constante

Ningún elemento puede ser null. No hay forma de expresarlo, ni como valor ni como default. La convención es usar la cadena vacía o un valor centinela:

public @interface CampoCsv {
    String nombre() default "";      // "" significa "usa el nombre del campo"
    int orden() default Integer.MAX_VALUE;   // centinela: "al final"
}

El elemento value y su sintaxis abreviada

Si una anotación tiene un elemento llamado exactamente value, quien la usa puede omitir el nombre:

public @interface Auditable {
    String value();     // el nombre magico
}
@Auditable("prestar")            // forma abreviada
@Auditable(value = "prestar")    // forma completa: equivalentes

La abreviatura funciona también si hay más elementos, siempre que todos los demás tengan default y solo se especifique value:

public @interface Auditable {
    String value();
    Gravedad nivel() default Gravedad.MEDIA;
}
@Auditable("prestar")                                 // OK: nivel toma su default
@Auditable(value = "prestar", nivel = Gravedad.ALTA)  // OK: forma completa obligatoria
// @Auditable("prestar", nivel = Gravedad.ALTA)       // ERROR: no se puede mezclar

Regla de diseño: si tu anotación tiene un elemento claramente principal, llámalo value. @SuppressWarnings("unchecked") y @RequestMapping("/libros") de Spring se leen bien precisamente por esto.

  1. Meta-anotaciones: @Retention

Una meta-anotación es una anotación que anota otra anotación. Son cinco, y las dos primeras son las que de verdad definen el comportamiento de la tuya.

@Retention decide hasta dónde vive tu anotación. Es la decisión más importante que tomarás al diseñarla.

Política Presente en el .java Presente en el .class Visible por reflexión Para qué
RetentionPolicy.SOURCE No No Anotaciones para el compilador o para procesadores: @Override, @SuppressWarnings, Lombok
RetentionPolicy.CLASS No Herramientas que analizan bytecode sin cargar clases. Es el valor por defecto
RetentionPolicy.RUNTIME Frameworks que actúan en ejecución: Spring, Hibernate, JUnit, y el motor de 10-03
graph LR
    S["Codigo fuente<br/>.java"] --> C["Compilacion"]
    C --> B["Bytecode<br/>.class"]
    B --> R["Ejecucion<br/>JVM"]

    S -.->|"SOURCE llega hasta aqui"| C
    B -.->|"CLASS llega hasta aqui"| B2["fin"]
    R -.->|"RUNTIME llega hasta el final"| R2["getAnnotation funciona"]

La trampa del valor por defecto. Si no pones @Retention, tu anotación es CLASS y isAnnotationPresent() devolverá false sin ninguna explicación:

public @interface Auditable { }      // sin @Retention -> CLASS

// en otra parte
boolean tiene = metodo.isAnnotationPresent(Auditable.class);
System.out.println(tiene);           // false, y no entiendes por que

Si tu anotación la va a leer código en ejecución —que es el 90 % de los casos de una anotación propia— pon @Retention(RetentionPolicy.RUNTIME) siempre. Es el error de principiante más frecuente con anotaciones y cuesta media tarde encontrarlo.

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Auditable { }      // ahora si

  1. Meta-anotaciones: @Target

@Target restringe dónde se puede poner tu anotación. Recibe un array de ElementType:

ElementType Se puede poner en
TYPE Clase, interfaz, enum, record, anotación
FIELD Campo (incluidas constantes de enum)
METHOD Método
PARAMETER Parámetro de un método o constructor
CONSTRUCTOR Constructor
LOCAL_VARIABLE Variable local
ANNOTATION_TYPE Otra anotación (para meta-anotaciones)
PACKAGE Paquete (en package-info.java)
TYPE_PARAMETER Parámetro de tipo genérico (Java 8)
TYPE_USE Cualquier uso de un tipo (Java 8)
MODULE Declaración de módulo (Java 9)
RECORD_COMPONENT Componente de un record (Java 16)
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.RECORD_COMPONENT})
public @interface CampoCsv {
    String nombre() default "";
    int orden();
}

Con eso, ponerla donde no toca es un error de compilación:

@CampoCsv(orden = 1)      // ERROR: no aplicable a clases
public class Libro { }
error: annotation type not applicable to this kind of declaration

Sin @Target, la anotación se puede poner casi en cualquier sitio, lo que suele ser un descuido y no una decisión. Poner @Target es documentar la intención y evitar usos absurdos.

Un detalle sobre RECORD_COMPONENT: cuando anotas el componente de un record, la anotación se propaga al campo, al parámetro del constructor canónico y al método de acceso, según a qué ElementType sea aplicable. Es una comodidad importante para que las anotaciones funcionen con records.

  1. @Documented e @Inherited

Dos meta-anotaciones menores pero con efectos concretos.

@Documented: hace que la anotación aparezca en el Javadoc generado del elemento anotado. Sin ella, quien lea la documentación de tu clase no verá que lleva @Responsable.

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Responsable {
    String nombre();
}

Regla simple: si la anotación forma parte del contrato público de lo que anota, ponla. Si es un detalle interno, no.

@Inherited: hace que la anotación se herede por las subclases. Solo funciona en anotaciones de TYPE, y solo se hereda de clases, no de interfaces.

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Auditado {
    String modulo();
}

@Auditado(modulo = "catálogo")
public abstract class Material { }

public class Libro extends Material { }    // sin anotacion propia
System.out.println(Libro.class.isAnnotationPresent(Auditado.class));   // true, gracias a @Inherited
System.out.println(Libro.class.getAnnotation(Auditado.class).modulo()); // catálogo

Sin @Inherited, ese primer println daría false.

Cuidado con la asimetría: getAnnotations() incluye las heredadas; getDeclaredAnnotations() solo las puestas directamente. Es la misma distinción entre getFields y getDeclaredFields que verás en 10-03, y confunde igual.

  1. @Repeatable y su anotación contenedora

Por defecto, una anotación no se puede repetir sobre el mismo elemento:

@Responsable(nombre = "Marta Ruiz")
@Responsable(nombre = "Diego Alonso")     // ERROR antes de Java 8
public class CatalogoService { }
error: Responsable is not a repeatable annotation type

Antes de Java 8 la solución era manual y fea: definir una segunda anotación que contuviera un array. Java 8 automatizó el patrón con @Repeatable, pero sigue habiendo dos anotaciones: la repetible y su contenedora.

package com.nexussoftware.bibliotech.anotaciones;

import java.lang.annotation.*;

/** La anotacion CONTENEDORA: guarda un array de las repetibles. */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Responsables {
    Responsable[] value();     // el elemento DEBE llamarse value y ser un array
}
package com.nexussoftware.bibliotech.anotaciones;

import java.lang.annotation.*;

/** La anotacion REPETIBLE: declara quien la contiene. */
@Repeatable(Responsables.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Responsable {
    String nombre();
    String equipo() default "Plataforma";
}

Ahora sí:

@Responsable(nombre = "Marta Ruiz",   equipo = "Catálogo")
@Responsable(nombre = "Diego Alonso", equipo = "Préstamos")
public class CatalogoService { }

Qué hace el compilador por debajo: cuando ve dos o más @Responsable, las envuelve automáticamente en un @Responsables({...}). Y esto tiene una consecuencia práctica que sorprende al leerlas por reflexión:

// Con UNA sola @Responsable
CatalogoService.class.getAnnotation(Responsable.class);    // devuelve la anotacion
CatalogoService.class.getAnnotation(Responsables.class);   // null

// Con DOS @Responsable
CatalogoService.class.getAnnotation(Responsable.class);    // ¡null! estan dentro del contenedor
CatalogoService.class.getAnnotation(Responsables.class);   // devuelve el contenedor

Para no tener que distinguir los dos casos, Java 8 añadió getAnnotationsByType, que funciona igual con una o con varias:

Responsable[] todos = CatalogoService.class.getAnnotationsByType(Responsable.class);
for (Responsable r : todos) {
    System.out.println(r.nombre() + " (" + r.equipo() + ")");
}
Marta Ruiz (Catálogo)
Diego Alonso (Préstamos)

Regla: para anotaciones repetibles, usa siempre getAnnotationsByType. Es la única forma de no equivocarse.

Requisitos que el compilador impone a la contenedora:

  1. Debe tener un elemento value que sea un array del tipo repetible.
  2. Su @Retention debe ser igual o más amplia que la de la repetible.
  3. Su @Target debe incluir todos los destinos de la repetible.

  1. Anotaciones de tipo (Java 8)

Hasta Java 8, una anotación se ponía sobre declaraciones. Java 8 añadió dos ElementType que permiten ponerlas sobre cualquier uso de un tipo:

  • TYPE_USE: en cualquier sitio donde aparezca un tipo.
  • TYPE_PARAMETER: sobre un parámetro de tipo genérico (el <T> de 10-01).
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
public @interface NoNulo { }

Y ahora se puede escribir:

// En una declaracion de variable
@NoNulo String titulo = "Java Efectivo";

// En el tipo de un parametro generico
List<@NoNulo Libro> catalogo = new ArrayList<>();

// En un cast
String s = (@NoNulo String) objeto;

// En un new
Libro libro = new @NoNulo Libro("Refactorización", "978-0000000003");

// En un throws y en un implements
public class Catalogo implements @NoNulo Identificable { }

// En arrays: anota el TIPO DE ELEMENTO
@NoNulo String[] nombres;         // array de String no nulos
String @NoNulo [] nombres2;       // array no nulo de String

Con TYPE_PARAMETER:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_PARAMETER)
public @interface Entidad { }

public class Repositorio<@Entidad T extends Identificable> { }

¿Para qué sirve esto? No para el compilador de Java, que las ignora. Sirve para verificadores de tipos externos (pluggable type checkers) que analizan el código en busca de errores que el sistema de tipos de Java no puede detectar. El caso emblemático es el Checker Framework, capaz de demostrar en compilación que un programa no puede lanzar NullPointerException gracias a anotaciones @Nullable/@NonNull sobre los usos de tipo.

En el día a día del desarrollo empresarial las verás sobre todo en forma de @NotNull de Bean Validation o del IDE. No las necesitarás para escribir tus propias anotaciones de negocio, pero conviene reconocerlas cuando aparecen en firmas ajenas.

  1. Cómo se leen (I): procesadores de anotaciones en compilación

Aquí está la primera de las dos formas de que una anotación "haga algo". Un procesador de anotaciones es un programa que javac ejecuta durante la compilación, al que le entrega los elementos anotados y que puede generar código fuente nuevo, que a su vez se compila.

El mecanismo vive en javax.annotation.processing y funciona por rondas:

graph TD
    A["Fuentes .java con anotaciones"] --> B["javac ronda 1"]
    B --> C{"Hay procesadores<br/>registrados?"}
    C -->|"si"| D["El procesador recibe los elementos anotados"]
    D --> E["Genera nuevos .java"]
    E --> F["javac ronda 2 compila lo generado"]
    F --> C
    C -->|"no quedan"| G["Bytecode final .class"]

Un procesador, en esqueleto:

package com.nexussoftware.bibliotech.procesador;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import java.util.Set;

@SupportedAnnotationTypes("com.nexussoftware.bibliotech.anotaciones.CampoCsv")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class ProcesadorCampoCsv extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> anotaciones, RoundEnvironment entorno) {

        for (Element elemento : entorno.getElementsAnnotatedWith(CampoCsv.class)) {

            // Comprobacion en TIEMPO DE COMPILACION
            if (elemento.getModifiers().contains(Modifier.STATIC)) {
                processingEnv.getMessager().printMessage(
                        Diagnostic.Kind.ERROR,
                        "@CampoCsv no se puede poner en un campo estático",
                        elemento);
            }
            // Aqui se podria generar codigo con
            // processingEnv.getFiler().createSourceFile(...)
        }
        return true;   // true = estas anotaciones ya estan procesadas
    }
}

Se registra en META-INF/services/javax.annotation.processing.Processor y se activa con -processorpath.

El ejemplo canónico es Lombok. Cuando escribes:

@Data
public class Libro {
    private String isbn;
    private String titulo;
}

...el procesador de Lombok genera durante la compilación los getIsbn(), getTitulo(), setIsbn(), setTitulo(), equals(), hashCode() y toString(). El .class resultante los contiene todos, aunque en tu fuente no aparezca ninguno. Por eso las anotaciones de Lombok son SOURCE: cumplen su función en el compilador y desaparecen. Verás Lombok en 11-07.

Aspecto Procesador (compilación) Reflexión (ejecución)
Cuándo actúa Durante javac Con el programa en marcha
Retención necesaria SOURCE basta RUNTIME obligatoria
Puede generar código No (salvo proxies, 10-03)
Puede fallar la compilación No
Coste en ejecución Cero Sí, medible
Ejemplos Lombok, MapStruct, Dagger Spring, Hibernate, JUnit

Esta tabla explica una tendencia real del ecosistema: cada vez más frameworks mueven trabajo de la ejecución a la compilación (Micronaut, Quarkus, Spring AOT), porque arrancar más rápido y consumir menos memoria importa mucho en contenedores.

  1. Cómo se leen (II): reflexión en ejecución

La segunda forma, y la que desarrollarás por completo en la próxima lección. En ejecución, cualquier elemento anotado responde a estos métodos:

import java.lang.reflect.Method;

Method metodo = GestorPrestamos.class.getMethod("prestar", String.class, String.class);

// ¿Tiene la anotacion?
if (metodo.isAnnotationPresent(Auditable.class)) {

    // Obtenerla y leer sus elementos
    Auditable auditable = metodo.getAnnotation(Auditable.class);
    System.out.println("Operación: " + auditable.value());
    System.out.println("Nivel:     " + auditable.nivel());
}

// Todas las anotaciones (incluidas heredadas)
for (Annotation a : metodo.getAnnotations()) {
    System.out.println(a);
}

// Solo las declaradas directamente
for (Annotation a : metodo.getDeclaredAnnotations()) { }

// Repetibles
Responsable[] responsables = CatalogoService.class.getAnnotationsByType(Responsable.class);
Operación: prestar
Nivel:     ALTA
@com.nexussoftware.bibliotech.anotaciones.Auditable(value="prestar", nivel=ALTA)

Requisito absoluto: @Retention(RetentionPolicy.RUNTIME). Sin ella, isAnnotationPresent devuelve false y no hay ningún mensaje que te explique por qué.

Esto es exactamente lo que hacen Spring, Hibernate y JUnit al arrancar: recorren las clases, buscan sus anotaciones y construyen a partir de ellas la configuración de la aplicación. En 10-03 escribirás uno.

  1. Para qué las usan los frameworks reales

Un recorrido rápido, solo para que reconozcas el patrón. Todo esto es el módulo 11; aquí solo interesa ver que son anotaciones corrientes leídas por reflexión.

Hibernate / JPA (11-03) — mapean clases a tablas:

@Entity
@Table(name = "materiales")
public class Libro {

    @Id
    @Column(name = "isbn", length = 17)
    private String isbn;

    @Column(name = "titulo", nullable = false)
    private String titulo;
}

Al arrancar, Hibernate lee estas anotaciones por reflexión y construye el mapeo objeto-relacional: sabe qué tabla, qué columnas, cuál es la clave primaria y cómo generar el SQL.

Spring (11-02) — declaran componentes e inyección de dependencias:

@Service
public class GestorPrestamos {

    private final Repositorio<Material> catalogo;

    @Autowired
    public GestorPrestamos(Repositorio<Material> catalogo) {
        this.catalogo = catalogo;
    }

    @Transactional
    public Resultado<Prestamo> prestar(String isbn, String empleado) { ... }
}

Spring escanea el classpath, encuentra las clases con @Service, las instancia, resuelve sus dependencias por tipo y las inyecta. Y @Transactional hace algo que reconocerás en 10-03: envuelve el objeto en un proxy dinámico que abre una transacción antes de cada llamada y la confirma después.

JUnit (11-04) — marcan qué ejecutar:

class GestorPrestamosTest {

    @BeforeEach
    void prepararCatalogo() { ... }

    @Test
    @DisplayName("Prestar un material disponible devuelve éxito")
    void prestarDisponible() { ... }
}

JUnit recorre las clases de prueba, busca los métodos con @Test y los invoca por reflexión.

El patrón es siempre el mismo:

graph LR
    A["Tu escribes una anotacion<br/>sobre tu clase"] --> B["El framework escanea<br/>al arrancar"]
    B --> C["Lee la anotacion<br/>por reflexion"]
    C --> D["Construye configuracion<br/>o comportamiento"]
    D --> E["Instancia, inyecta,<br/>envuelve en proxy, invoca"]

Cuando en 10-03 escribas el ExportadorAnotado de BiblioTech, estarás haciendo exactamente los pasos 2, 3 y 4 con tus propias manos. Después de eso, Spring dejará de ser magia.

  1. Configuración junto al código frente a XML

Merece un apartado propio porque explica por qué el ecosistema se movió hacia las anotaciones.

Antes de las anotaciones (Java 5, 2004), toda la configuración de los frameworks vivía en XML separado del código:

<bean id="gestorPrestamos" class="com.nexussoftware.bibliotech.servicio.GestorPrestamos">
    <constructor-arg ref="repositorioCatalogo"/>
</bean>
<bean id="repositorioCatalogo" class="com.nexussoftware.bibliotech.servicio.Repositorio">
    <constructor-arg value="catálogo"/>
</bean>

Frente a:

@Service
public class GestorPrestamos {
    public GestorPrestamos(Repositorio<Material> catalogo) { ... }
}
Criterio Anotaciones XML externo
Proximidad La configuración está donde está el código Hay que saltar entre dos ficheros
Verbosidad Mínima Mucho ceremonial
Refactorización El IDE renombra clase y anotación a la vez El XML se queda con el nombre viejo y falla en ejecución
Verificación El compilador comprueba tipos y nombres Cadenas de texto sin comprobar
Cambiar sin recompilar Imposible
Distinta config por entorno Difícil Natural
Ver toda la config de un vistazo Imposible, está repartida Sí, en un fichero

Ninguna de las dos gana en todo, y por eso los frameworks modernos usan las dos: anotaciones para lo estructural (qué es un servicio, qué se inyecta, qué mapea a qué tabla) y ficheros externos para lo que cambia entre entornos (URL de la base de datos, credenciales, tamaños de pool). Es exactamente lo que ya hiciste en 07-07 con Configuracion y ReglasNegocio sobre Properties, y lo que verás formalizado en 11-02 con application.properties.

  1. Cuándo NO usar anotaciones

Las anotaciones son atractivas y se abusa de ellas. Cuatro casos claros donde son la herramienta equivocada:

1. Lógica de negocio. Una anotación no puede contener código. Intentar expresar reglas complejas en elementos de anotación acaba en cadenas que hay que parsear:

// MAL: has inventado un lenguaje dentro de una cadena
@Regla("si dias > 15 && empleado.categoria != 'DIRECCION' entonces multa = dias * 0.25")
public double calcularMulta(Prestamo p) { ... }

Eso es código disfrazado, sin comprobación del compilador, sin depurador, sin autocompletado. Escríbelo en Java.

2. Configuración que cambia por entorno. Una anotación se compila dentro del .class. Cambiarla exige recompilar y redesplegar:

// MAL: cambiar de servidor obliga a recompilar
@ConexionBd(url = "jdbc:postgresql://produccion.nexus:5432/bibliotech")
public class AlmacenPrestamos { }

Eso va en un .properties o en una variable de entorno.

3. Valores que cambian con frecuencia. Umbrales, límites, plazos. @LimitePrestamos(5) parece cómodo hasta que dirección quiere 7 un martes por la tarde.

4. Cuando una interfaz lo expresa mejor. Si quieres decir "esta clase se puede exportar", una interfaz Exportable te da comprobación del compilador, autocompletado y polimorfismo. Una anotación @Exportable solo te da una etiqueta que hay que leer por reflexión.

Usa una anotación cuando... Usa otra cosa cuando...
La información es estructural y estable El valor cambia por entorno → Properties
Es transversal a muchas clases (auditoría, transacciones) Es lógica → escríbela en Java
La va a consumir una herramienta o framework El comportamiento es polimórfico → interfaz
Va ligada al elemento del código Es una lista larga y jerárquica → fichero externo

  1. BiblioTech: @CampoCsv

Hora de aplicarlo. En 07-07 escribiste ExportadorCatalogoCsv a mano, y tiene un problema real:

public class ExportadorCatalogoCsv {

    public String aLinea(Libro libro) {
        return String.join(";",
                libro.getIsbn(),
                libro.getTitulo(),
                libro.getAutor(),
                String.valueOf(libro.getPaginas()));
    }

    public String cabecera() {
        return "ISBN;Título;Autor;Páginas";
    }
}

Tres defectos: hay que escribir un exportador por cada clase, el orden de la cabecera y el de los valores se pueden desincronizar sin que nadie avise, y añadir un campo a Libro obliga a tocar dos métodos en otro fichero (y si se olvida, el CSV sale mal en silencio).

La solución es marcar los campos en la propia clase:

package com.nexussoftware.bibliotech.anotaciones;

import java.lang.annotation.*;

/**
 * Marca un campo como exportable a CSV.
 *
 * La lee el ExportadorAnotado de 10-03, que recorre por reflexion
 * los campos de cualquier entidad, se queda con los anotados,
 * los ordena por 'orden' y construye cabecera y linea.
 *
 * RUNTIME es OBLIGATORIO: sin el, la reflexion no la vera.
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.RECORD_COMPONENT})
public @interface CampoCsv {

    /**
     * Encabezado de la columna.
     * Vacio = usar el nombre del campo (no se puede poner null: apartado 10).
     */
    String nombre() default "";

    /** Posicion de la columna. Los campos se ordenan por este valor. */
    int orden();

    /** Formato opcional para String.format. Vacio = String.valueOf. */
    String formato() default "";

    /** Si es true, el valor se enmascara al exportar (datos personales). */
    boolean sensible() default false;
}

Y se aplica:

package com.nexussoftware.bibliotech.dominio;

import com.nexussoftware.bibliotech.anotaciones.CampoCsv;

public class Libro extends Material {

    @CampoCsv(nombre = "ISBN", orden = 1)
    private final String isbn;

    @CampoCsv(nombre = "Título", orden = 2)
    private final String titulo;

    @CampoCsv(nombre = "Autor", orden = 3)
    private final String autor;

    @CampoCsv(nombre = "Páginas", orden = 4)
    private final int paginas;

    @CampoCsv(nombre = "Valoración", orden = 5, formato = "%.2f")
    private final double valoracion;

    // SIN anotacion: es estado interno, no se exporta
    private boolean prestado;

    // SIN anotacion: cache interna
    private transient String resumenCalculado;

    public Libro(String isbn, String titulo, String autor, int paginas, double valoracion) {
        super(isbn, titulo);
        this.isbn = isbn;
        this.titulo = titulo;
        this.autor = autor;
        this.paginas = paginas;
        this.valoracion = valoracion;
    }

    // getters...
}

Y en el record Ficha de 04-07, aprovechando RECORD_COMPONENT:

package com.nexussoftware.bibliotech.dominio;

import com.nexussoftware.bibliotech.anotaciones.CampoCsv;

public record Ficha(
        @CampoCsv(nombre = "ISBN", orden = 1) String isbn,
        @CampoCsv(nombre = "Título", orden = 2) String titulo,
        @CampoCsv(nombre = "Disponible", orden = 3) boolean disponible) {
}

Y en Prestamo, con un campo sensible:

public class Prestamo implements Identificable {

    @CampoCsv(nombre = "Referencia", orden = 1)
    private final String referencia;

    @CampoCsv(nombre = "ISBN", orden = 2)
    private final String isbn;

    @CampoCsv(nombre = "Empleado", orden = 3, sensible = true)
    private final String empleado;

    @CampoCsv(nombre = "Día préstamo", orden = 4)
    private final int diaPrestamo;      // seguira siendo int hasta 10-05

    private final List<Incidencia> incidencias = new ArrayList<>();   // no se exporta
}

Fíjate en lo que ha cambiado conceptualmente. La información "este campo se exporta y en esta posición" ha pasado de estar en ExportadorCatalogoCsv a estar junto al campo que describe. Cuando alguien añada un campo a Libro, la decisión de exportarlo o no está a la vista, en la misma línea. Y el exportador —que aún no existe— servirá para Libro, Prestamo, Ficha, SalaReuniones y cualquier cosa que venga después, sin conocerlas.

Eso es exactamente lo que hace Jackson con @JsonProperty (11-07) y lo que hace Hibernate con @Column (11-03).

  1. BiblioTech: @Auditable

El segundo caso. Nexus Software exige registrar quién hace qué con los materiales. En 06-07 lo resolviste llamando a RegistroOperaciones a mano en cada método:

public Resultado<Prestamo> prestar(String isbn, String empleado) {
    registro.registrar("prestar", empleado, isbn);      // repetido en 14 metodos
    // ... logica real
}

Catorce llamadas idénticas que hay que recordar poner, que nadie comprueba y que ensucian la lógica. Es un asunto transversal (cross-cutting concern): no pertenece a la lógica de préstamos, pero atraviesa todos sus métodos. Y es justo el caso donde una anotación brilla.

package com.nexussoftware.bibliotech.anotaciones;

import com.nexussoftware.bibliotech.dominio.Gravedad;
import java.lang.annotation.*;

/**
 * Marca una operacion que debe quedar registrada en la auditoria.
 *
 * Por si sola NO registra nada: es una etiqueta. El proxy dinamico
 * de 10-03 la lee e intercepta las llamadas a los metodos marcados.
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Auditable {

    /**
     * Nombre de la operacion en el registro.
     * Se llama 'value' para permitir la forma abreviada @Auditable("prestar").
     * Vacio = usar el nombre del metodo.
     */
    String value() default "";

    /** Importancia de la operacion. */
    Gravedad nivel() default Gravedad.MEDIA;

    /** Si es true, tambien se registran los argumentos de la llamada. */
    boolean registrarArgumentos() default false;

    /** Indices de los argumentos que NO deben aparecer en el registro. */
    int[] argumentosSensibles() default {};
}

Aplicada al servicio:

package com.nexussoftware.bibliotech.servicio;

import com.nexussoftware.bibliotech.anotaciones.Auditable;
import com.nexussoftware.bibliotech.dominio.*;

/**
 * @Auditable a nivel de TYPE: valor por defecto para toda la clase.
 * Los metodos pueden afinarlo con su propia anotacion.
 */
@Auditable(nivel = Gravedad.BAJA)
public class GestorPrestamos implements ServicioPrestamos {

    private final Repositorio<Material> catalogo;
    private final Repositorio<Prestamo> prestamos;

    public GestorPrestamos(Repositorio<Material> catalogo, Repositorio<Prestamo> prestamos) {
        this.catalogo = catalogo;
        this.prestamos = prestamos;
    }

    @Override
    @Auditable(value = "prestar", nivel = Gravedad.ALTA, registrarArgumentos = true)
    public Resultado<Prestamo> prestar(String isbn, String empleado) {
        Material material = catalogo.buscarPorId(isbn).orElse(null);
        if (material == null) {
            return Resultado.fallo("No existe ningún material con ISBN " + isbn);
        }
        if (material.estaPrestado()) {
            return Resultado.fallo("El material '" + material.getTitulo() + "' ya está prestado");
        }
        material.marcarPrestado(empleado);
        Prestamo prestamo = new Prestamo(siguienteReferencia(), isbn, empleado);
        prestamos.guardar(prestamo);
        return Resultado.exito(prestamo);
        // Ni una sola linea de auditoria: la logica esta LIMPIA
    }

    @Override
    @Auditable(value = "devolver", nivel = Gravedad.ALTA, registrarArgumentos = true)
    public Resultado<Prestamo> devolver(String referencia) { ... }

    @Override
    @Auditable(value = "cambiarClave", registrarArgumentos = true, argumentosSensibles = {1})
    public Resultado<Void> cambiarClaveEmpleado(String empleado, String claveNueva) { ... }

    // SIN @Auditable: consulta de solo lectura, no se audita
    @Override
    public List<Prestamo> listarActivos() { ... }
}

Tres detalles de diseño que merecen atención:

La anotación a nivel de clase actúa como valor por defecto. El motor de 10-03 mirará primero el método y, si no la encuentra, la clase. Es exactamente el mecanismo de @Transactional de Spring.

argumentosSensibles = {1} resuelve elegantemente lo que en 09-06 era una regla escrita en un comentario: "nunca registres tokens ni credenciales". Ahora la regla está codificada junto al método al que se aplica, y el motor no puede olvidarla.

La lógica del método está limpia. prestar() habla de préstamos y de nada más. La auditoría es una decisión declarada en una etiqueta.

Y ahora, la frase importante: todo esto no hace absolutamente nada todavía. Puedes ejecutar BiblioTechApp, prestar veinte libros y no se registrará ni una línea. @CampoCsv no exporta nada y @Auditable no audita nada, porque son etiquetas y nadie las está leyendo.

Escribir ese lector es la lección siguiente.

Errores Comunes y Consejos

1. Olvidar @Retention(RetentionPolicy.RUNTIME). El error número uno, sin discusión. La retención por defecto es CLASS: la anotación queda en el .class pero no es visible por reflexión, y isAnnotationPresent() devuelve false sin ninguna pista. Si tu anotación la va a leer código en ejecución, la meta-anotación es obligatoria.

2. Creer que una anotación hace algo. @Auditable no audita. @Transactional no abre transacciones — lo hace el proxy que Spring crea al leerla. Si pones tu anotación y no pasa nada, no está rota: es que falta el lector. Corolario: @Transactional sobre un método privado o llamado desde dentro de la misma clase no funciona, porque la llamada no pasa por el proxy.

3. Poner @SuppressWarnings("all"). Silencia todo, incluidos los avisos que aún no existen. Usa el identificador concreto y el ámbito mínimo.

4. Omitir @Override. Un equals(Material) en lugar de equals(Object) compila, parece correcto y rompe todas las colecciones. Ponla siempre.

5. Intentar usar null en un elemento. No se puede, ni como valor ni como default. Usa "", un array vacío {} o una constante centinela.

6. Intentar tipos no permitidos. List<String>, LocalDate o tus propias clases no valen. Solo primitivos, String, Class, enum, anotaciones y arrays de eso.

7. No usar getAnnotationsByType con repetibles. Con una sola aparición, getAnnotation funciona; con dos, devuelve null porque el compilador las metió en el contenedor. getAnnotationsByType funciona en los dos casos.

8. Meter configuración de entorno en anotaciones. Una URL de base de datos anotada obliga a recompilar para cambiar de servidor. Eso va en Properties.

9. Anotaciones sin @Target. Se pueden poner en cualquier sitio, incluidos los que no tienen sentido, y tu motor recibirá elementos que no espera. Declarar el destino es documentación verificada.

Consejo 1: diseña la anotación pensando en quien la escribe. Nombra value al elemento principal para permitir la forma abreviada, y pon default en todo lo demás. Comparar @CampoCsv(orden = 3) con @CampoCsv(nombre = "", orden = 3, formato = "", sensible = false) deja claro por qué.

Consejo 2: usa enum en lugar de String para valores cerrados. Gravedad nivel() da autocompletado y comprobación del compilador; String nivel() da erratas que fallan en ejecución.

Consejo 3: documenta lo que hace el lector, no lo que hace la anotación. El Javadoc de @CampoCsv debe decir "la lee ExportadorAnotado, que ordena por orden y usa nombre como cabecera". Sin eso, quien la ponga no sabe qué esperar.

Consejo 4: mantén un paquete anotaciones propio. Tenerlas juntas en com.nexussoftware.bibliotech.anotaciones hace evidente el inventario de metadatos del proyecto y evita duplicados.

Consejo 5: ante la duda entre anotación e interfaz, elige la interfaz. El compilador comprueba las interfaces. Reserva las anotaciones para lo que una interfaz no puede expresar: información con parámetros (orden = 3) o comportamiento transversal.

Ejercicios

Ejercicio 1: @Validar con reglas declarativas

Define una anotación @Validar para BiblioTech que permita declarar restricciones sobre los campos de una entidad:

  • Aplicable a campos y componentes de record, visible en ejecución.
  • Elementos: obligatorio (booleano, por defecto true), longitudMinima (entero, por defecto 0), longitudMaxima (entero, por defecto Integer.MAX_VALUE), patron (cadena, por defecto vacía) y mensaje (cadena, por defecto vacía).
  • Anota con ella los campos de Libro: el ISBN obligatorio con patrón 978-\d{10}, el título obligatorio de entre 1 y 200 caracteres, el autor opcional de máximo 120.
  • Escribe además una anotación repetible @Ejemplo(String) con su contenedora, para documentar valores válidos de cada campo, y ponla dos veces sobre el ISBN.
  • No escribas el validador: es 10-03. Sí escribe un main que imprima, usando getDeclaredFields() y getAnnotation(), qué campos están anotados y con qué valores.

Ejercicio 2: Anotaciones estándar bien puestas

Te dan esta clase de BiblioTech, escrita sin ninguna anotación y con varios problemas latentes. Corrígela añadiendo las anotaciones estándar del JDK que correspondan y arreglando lo que las anotaciones destapen.

public class CatalogoLegado {

    private final Map<String, Material> materiales = new HashMap<>();

    public boolean equals(CatalogoLegado otro) {
        return materiales.equals(otro.materiales);
    }

    public String toString() {
        return "CatalogoLegado con " + materiales.size() + " materiales";
    }

    public void anadirTodos(List... listas) {
        for (List lista : listas) {
            for (Object o : lista) {
                Material m = (Material) o;
                materiales.put(m.getId(), m);
            }
        }
    }

    public void agregarLibro(String isbn, String titulo) {
        materiales.put(isbn, new Libro(isbn, titulo));
    }

    public interface FiltroMaterial {
        boolean acepta(Material m);
    }
}

Requisitos: agregarLibro debe quedar obsoleta desde la versión 3.1, marcada para eliminación, con alternativa anadir(Material). anadirTodos debe aceptar varargs genéricos sin avisos. FiltroMaterial debe quedar protegida como interfaz funcional. Y equals debe funcionar de verdad con HashMap.

Ejercicio 3: @Reintentable con política declarativa

En 09-06 escribiste ClienteHttpResistente con la política de reintentos codificada dentro del método. Diseña una anotación que la declare:

  • @Reintentable, aplicable a métodos, visible en ejecución.
  • Elementos: intentos (por defecto 3), esperaInicialMs (por defecto 200), factorDeCrecimiento (double, por defecto 2.0), excepciones (array de Class<? extends Exception>, por defecto {Exception.class}), y soloIdempotentes (booleano, por defecto true).
  • Anota con ella tres métodos de ClienteMetadatos con políticas distintas.
  • Escribe un método describirPolitica(Class<?>) que, por reflexión, imprima una tabla legible de la política de cada método anotado, calculando y mostrando la espera acumulada máxima.

Añade además una meta-anotación propia @Transversal (aplicable solo a otras anotaciones) que marque @Auditable y @Reintentable como aspectos transversales, y demuestra que se puede leer desde Auditable.class.

Soluciones

Solución 1

package com.nexussoftware.bibliotech.anotaciones;

import java.lang.annotation.*;

/**
 * Restricciones declarativas sobre el valor de un campo.
 * El validador que las consume se escribe en 10-03.
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)          // OBLIGATORIO para leerla por reflexion
@Target({ElementType.FIELD, ElementType.RECORD_COMPONENT})
public @interface Validar {

    boolean obligatorio() default true;

    int longitudMinima() default 0;

    int longitudMaxima() default Integer.MAX_VALUE;

    /** Expresion regular. Vacio = sin patron (no se puede poner null). */
    String patron() default "";

    /** Mensaje personalizado. Vacio = el motor genera uno. */
    String mensaje() default "";
}
package com.nexussoftware.bibliotech.anotaciones;

import java.lang.annotation.*;

/** CONTENEDORA de @Ejemplo. Su value es un array del tipo repetible. */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.RECORD_COMPONENT})
public @interface Ejemplos {
    Ejemplo[] value();
}
package com.nexussoftware.bibliotech.anotaciones;

import java.lang.annotation.*;

/** Documenta un valor valido de ejemplo. Repetible. */
@Documented
@Repeatable(Ejemplos.class)                  // declara quien la contiene
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.RECORD_COMPONENT})
public @interface Ejemplo {
    String value();                          // 'value' permite @Ejemplo("978-0000000001")
}

La entidad anotada:

package com.nexussoftware.bibliotech.dominio;

import com.nexussoftware.bibliotech.anotaciones.*;

public class Libro extends Material {

    @Validar(obligatorio = true, patron = "978-\\d{10}",
             mensaje = "El ISBN debe seguir el formato 978-XXXXXXXXXX")
    @Ejemplo("978-0000000001")
    @Ejemplo("978-0000000002")
    private final String isbn;

    @Validar(obligatorio = true, longitudMinima = 1, longitudMaxima = 200)
    @Ejemplo("Java Efectivo")
    private final String titulo;

    @Validar(obligatorio = false, longitudMaxima = 120)
    private final String autor;

    // Sin anotar: no se valida
    private boolean prestado;

    public Libro(String isbn, String titulo, String autor) {
        super(isbn, titulo);
        this.isbn = isbn;
        this.titulo = titulo;
        this.autor = autor;
    }
}

El inspector:

package com.nexussoftware.bibliotech;

import com.nexussoftware.bibliotech.anotaciones.*;
import com.nexussoftware.bibliotech.dominio.Libro;

import java.lang.reflect.Field;

public class InspectorDeReglas {

    public static void main(String[] args) {

        System.out.printf("%-10s %-6s %-5s %-6s %-20s%n",
                "CAMPO", "OBLIG", "MIN", "MAX", "PATRÓN");
        System.out.println("-".repeat(56));

        for (Field campo : Libro.class.getDeclaredFields()) {

            // Sin la anotacion, este campo no se valida
            if (!campo.isAnnotationPresent(Validar.class)) {
                continue;
            }

            Validar regla = campo.getAnnotation(Validar.class);

            System.out.printf("%-10s %-6s %-5d %-6s %-20s%n",
                    campo.getName(),
                    regla.obligatorio() ? "sí" : "no",
                    regla.longitudMinima(),
                    regla.longitudMaxima() == Integer.MAX_VALUE ? "-" : regla.longitudMaxima(),
                    regla.patron().isEmpty() ? "-" : regla.patron());

            if (!regla.mensaje().isEmpty()) {
                System.out.println("           mensaje: " + regla.mensaje());
            }

            // REPETIBLES: getAnnotationsByType funciona con 0, 1 o N apariciones.
            // getAnnotation(Ejemplo.class) devolveria null cuando hay dos.
            Ejemplo[] ejemplos = campo.getAnnotationsByType(Ejemplo.class);
            if (ejemplos.length > 0) {
                StringBuilder sb = new StringBuilder("           ejemplos: ");
                for (Ejemplo e : ejemplos) {
                    sb.append(e.value()).append("  ");
                }
                System.out.println(sb);
            }
        }

        // Demostracion de la trampa de las repetibles
        System.out.println();
        try {
            Field isbn = Libro.class.getDeclaredField("isbn");
            System.out.println("isbn: getAnnotation(Ejemplo)      -> " + isbn.getAnnotation(Ejemplo.class));
            System.out.println("isbn: getAnnotation(Ejemplos)     -> presente="
                    + isbn.isAnnotationPresent(Ejemplos.class));
            System.out.println("isbn: getAnnotationsByType(Ejemplo) -> "
                    + isbn.getAnnotationsByType(Ejemplo.class).length + " elementos");

            Field titulo = Libro.class.getDeclaredField("titulo");
            System.out.println("título: getAnnotation(Ejemplo)    -> " + titulo.getAnnotation(Ejemplo.class));
        } catch (NoSuchFieldException e) {
            throw new IllegalStateException(e);
        }
    }
}
CAMPO      OBLIG  MIN   MAX    PATRÓN
--------------------------------------------------------
isbn       sí     0     -      978-\d{10}
           mensaje: El ISBN debe seguir el formato 978-XXXXXXXXXX
           ejemplos: 978-0000000001  978-0000000002
titulo     sí     1     200    -
           ejemplos: Java Efectivo
autor      no     0     120    -

isbn: getAnnotation(Ejemplo)      -> null
isbn: getAnnotation(Ejemplos)     -> presente=true
isbn: getAnnotationsByType(Ejemplo) -> 2 elementos
título: getAnnotation(Ejemplo)    -> @...Ejemplo(value="Java Efectivo")

Comentarios. La salida final es la parte instructiva.

Con dos @Ejemplo, getAnnotation(Ejemplo.class) devuelve null. El compilador las envolvió en @Ejemplos({...}), así que el campo ya no lleva directamente ninguna @Ejemplo. Con una sola (el título) sí funciona. Esta asimetría es exactamente por la que hay que usar siempre getAnnotationsByType con anotaciones repetibles.

El patrón "978-\\d{10}" lleva doble barra porque es una cadena Java que contiene una expresión regular: \\d en el fuente es \d en la cadena.

El campo prestado no aparece porque no está anotado, y el continue lo salta. Ese es el mecanismo básico de todo motor de anotaciones: recorrer todo y quedarse con lo marcado.

Solución 2

package com.nexussoftware.bibliotech.servicio;

import com.nexussoftware.bibliotech.dominio.*;
import java.util.*;

public class CatalogoLegado {

    private final Map<String, Material> materiales = new HashMap<>();

    /**
     * CORREGIDO: la firma original era equals(CatalogoLegado), que SOBRECARGA
     * en lugar de sobrescribir. Con @Override el compilador lo habria cortado.
     * HashMap, HashSet y contains() llaman a equals(Object).
     */
    @Override
    public boolean equals(Object otro) {
        if (this == otro) return true;
        if (!(otro instanceof CatalogoLegado)) return false;
        return materiales.equals(((CatalogoLegado) otro).materiales);
    }

    /**
     * Sobrescribir equals OBLIGA a sobrescribir hashCode (03-09).
     * El @Override sobre equals nos ha recordado que faltaba.
     */
    @Override
    public int hashCode() {
        return Objects.hash(materiales);
    }

    @Override
    public String toString() {
        return "CatalogoLegado con " + materiales.size() + " materiales";
    }

    /**
     * CORREGIDO: era List... (tipo crudo) con cast a Material dentro.
     * Ahora es generico y tipado. @SafeVarargs porque el metodo
     * SOLO LEE del array de varargs: no lo guarda ni lo devuelve.
     * Requiere ser final (o static/private) para poder anotarlo.
     */
    @SafeVarargs
    public final void anadirTodos(List<? extends Material>... listas) {
        for (List<? extends Material> lista : listas) {   // ? extends: solo leemos (PECS, 10-01)
            for (Material m : lista) {
                materiales.put(m.getId(), m);
            }
        }
    }

    /** Sustituto de agregarLibro. */
    public void anadir(Material material) {
        Objects.requireNonNull(material, "material");
        materiales.put(material.getId(), material);
    }

    /**
     * @deprecated Desde 3.1 usa {@link #anadir(Material)}, que acepta
     *             cualquier material y no solo libros. Se eliminará en 4.0.
     */
    @Deprecated(since = "3.1", forRemoval = true)
    public void agregarLibro(String isbn, String titulo) {
        anadir(new Libro(isbn, titulo));
    }

    /**
     * @FunctionalInterface protege el contrato: si alguien anade
     * un segundo metodo abstracto, la compilacion falla aqui
     * en vez de romper a todos los que la usan con lambdas.
     */
    @FunctionalInterface
    public interface FiltroMaterial {
        boolean acepta(Material m);

        // Los default NO cuentan como abstractos
        default FiltroMaterial y(FiltroMaterial otro) {
            return m -> this.acepta(m) && otro.acepta(m);
        }
    }
}

Demostración de que ahora funciona:

public class PruebaCatalogoLegado {

    @SuppressWarnings("removal")   // usamos a proposito el metodo obsoleto, y lo justificamos
    public static void main(String[] args) {

        CatalogoLegado a = new CatalogoLegado();
        CatalogoLegado b = new CatalogoLegado();

        List<Libro> libros = List.of(
                new Libro("978-0000000001", "Java Efectivo"),
                new Libro("978-0000000002", "Patrones de Diseño"));

        a.anadirTodos(libros);   // sin avisos: varargs genericos seguros
        b.anadirTodos(libros);

        // Con el equals correcto, esto AHORA funciona
        System.out.println("a.equals(b): " + a.equals(b));

        Set<CatalogoLegado> conjunto = new HashSet<>();
        conjunto.add(a);
        conjunto.add(b);
        System.out.println("Tamaño del Set: " + conjunto.size() + " (antes habría sido 2)");

        // El metodo obsoleto sigue funcionando, con aviso en compilacion
        a.agregarLibro("978-0000000003", "Refactorización");

        // La interfaz funcional se usa con lambda
        CatalogoLegado.FiltroMaterial disponibles = m -> !m.estaPrestado();
        CatalogoLegado.FiltroMaterial conTitulo   = m -> m.getTitulo() != null;
        CatalogoLegado.FiltroMaterial ambos = disponibles.y(conTitulo);
        System.out.println("Filtro compuesto creado: " + (ambos != null));
    }
}
a.equals(b): true
Tamaño del Set: 1 (antes habría sido 2)
Filtro compuesto creado: true

Comentarios.

@Override sobre equals destapó dos bugs, no uno. El primero, la firma incorrecta. El segundo, en cascada: al arreglarla, el contrato de Object (03-09) exige hashCode() coherente, que faltaba. Un HashSet con equals correcto y hashCode heredado seguiría dando tamaño 2.

@SafeVarargs exige final. El método pasó de public void a public final void. Sin final, static o private, el compilador rechaza la anotación: no puedes prometer que una implementación futura será segura.

El @SuppressWarnings("removal") del main está justificado en el comentario, que es la única forma legítima de usarlo: aquí llamamos a propósito al método obsoleto para demostrar que sigue funcionando.

Solución 3

package com.nexussoftware.bibliotech.anotaciones;

import java.lang.annotation.*;

/**
 * META-ANOTACION: solo se puede poner sobre otras anotaciones.
 * Marca un aspecto que atraviesa la aplicacion en lugar de
 * pertenecer a una capa concreta.
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)     // SOLO sobre anotaciones
public @interface Transversal {

    /** Orden de aplicacion cuando varios aspectos se apilan. */
    int orden() default 0;

    String descripcion() default "";
}
package com.nexussoftware.bibliotech.anotaciones;

import java.lang.annotation.*;

/**
 * Declara la politica de reintentos de una operacion de red.
 * Sustituye a la politica codificada a mano en ClienteHttpResistente (09-06).
 */
@Documented
@Transversal(orden = 20, descripcion = "Reintentos con espera creciente")
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Reintentable {

    /** Numero total de intentos, incluido el primero. */
    int intentos() default 3;

    /** Espera antes del segundo intento, en milisegundos. */
    long esperaInicialMs() default 200;

    /** Multiplicador de la espera en cada reintento. */
    double factorDeCrecimiento() default 2.0;

    /**
     * Excepciones que activan el reintento.
     * Class<? extends Exception>[] es un tipo PERMITIDO en anotaciones
     * (Class y arrays de Class lo son), y ademas esta acotado (10-01).
     */
    Class<? extends Exception>[] excepciones() default { Exception.class };

    /** Si es true, solo se reintenta si la operacion es idempotente (09-06). */
    boolean soloIdempotentes() default true;
}

Aplicada con tres políticas distintas:

package com.nexussoftware.bibliotech.red;

import com.nexussoftware.bibliotech.anotaciones.Reintentable;

import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;

public class ClienteMetadatos {

    /** Consulta de solo lectura: se puede reintentar con confianza. */
    @Reintentable(intentos = 5, esperaInicialMs = 100, factorDeCrecimiento = 2.0,
                  excepciones = { SocketTimeoutException.class, ConnectException.class })
    public String consultarPorIsbn(String isbn) throws IOException { ... }

    /** Descarga pesada: menos intentos y esperas mas largas. */
    @Reintentable(intentos = 2, esperaInicialMs = 1000, factorDeCrecimiento = 3.0,
                  excepciones = { IOException.class })
    public byte[] descargarPortada(String isbn) throws IOException { ... }

    /** POST no idempotente: NUNCA se reintenta a ciegas (09-06). */
    @Reintentable(intentos = 1, soloIdempotentes = true)
    public void publicarValoracion(String isbn, int estrellas) throws IOException { ... }

    /** Sin anotacion: sin politica de reintentos. */
    public boolean estaDisponible() { ... }
}

El describidor:

package com.nexussoftware.bibliotech;

import com.nexussoftware.bibliotech.anotaciones.*;
import com.nexussoftware.bibliotech.red.ClienteMetadatos;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;

public class DescriptorDePoliticas {

    public static void main(String[] args) {
        describirPolitica(ClienteMetadatos.class);

        System.out.println();
        describirMetaAnotaciones(Reintentable.class);
        describirMetaAnotaciones(Auditable.class);
    }

    public static void describirPolitica(Class<?> tipo) {

        System.out.println("Políticas de reintento en " + tipo.getSimpleName());
        System.out.printf("%-22s %-8s %-9s %-7s %-11s %s%n",
                "MÉTODO", "INTENTOS", "ESPERA_1ª", "FACTOR", "ESPERA_MÁX", "EXCEPCIONES");
        System.out.println("-".repeat(96));

        // getDeclaredMethods no garantiza orden: lo fijamos para que la salida sea estable
        Method[] metodos = tipo.getDeclaredMethods();
        Arrays.sort(metodos, Comparator.comparing(Method::getName));

        for (Method metodo : metodos) {

            Reintentable politica = metodo.getAnnotation(Reintentable.class);
            if (politica == null) {
                continue;                       // sin politica declarada
            }

            // Espera acumulada: e, e*f, e*f^2, ... para (intentos - 1) reintentos
            double esperaTotal = 0;
            double espera = politica.esperaInicialMs();
            for (int i = 1; i < politica.intentos(); i++) {
                esperaTotal += espera;
                espera *= politica.factorDeCrecimiento();
            }

            String excepciones = Arrays.stream(politica.excepciones())
                    .map(Class::getSimpleName)
                    .reduce((a, b) -> a + ", " + b)
                    .orElse("-");

            System.out.printf("%-22s %-8d %-9d %-7.1f %-11.0f %s%n",
                    metodo.getName(),
                    politica.intentos(),
                    politica.esperaInicialMs(),
                    politica.factorDeCrecimiento(),
                    esperaTotal,
                    excepciones);

            if (politica.soloIdempotentes() && politica.intentos() > 1) {
                System.out.println("    aviso: exige idempotencia; verifica que el método lo sea");
            }
        }
    }

    /** Lee las META-anotaciones de una anotacion: es reflexion sobre reflexion. */
    public static void describirMetaAnotaciones(Class<? extends java.lang.annotation.Annotation> anotacion) {

        System.out.println("Meta-anotaciones de @" + anotacion.getSimpleName() + ":");

        Transversal t = anotacion.getAnnotation(Transversal.class);
        if (t != null) {
            System.out.printf("  @Transversal(orden=%d) %s%n", t.orden(), t.descripcion());
        } else {
            System.out.println("  no es un aspecto transversal");
        }

        Retention r = anotacion.getAnnotation(Retention.class);
        Target    d = anotacion.getAnnotation(Target.class);
        System.out.println("  retención: " + (r == null ? "CLASS (por defecto)" : r.value()));
        System.out.println("  destinos:  " + (d == null ? "cualquiera" : Arrays.toString(d.value())));
    }
}

Añadiendo @Transversal(orden = 10, descripcion = "Auditoría de operaciones") sobre @Auditable, la salida es:

Políticas de reintento en ClienteMetadatos
MÉTODO                 INTENTOS ESPERA_1ª FACTOR  ESPERA_MÁX  EXCEPCIONES
------------------------------------------------------------------------------------------------
consultarPorIsbn       5        100       2,0     1500        SocketTimeoutException, ConnectException
descargarPortada       2        1000      3,0     1000        IOException
    aviso: exige idempotencia; verifica que el método lo sea
publicarValoracion     1        200       2,0     0           Exception

Meta-anotaciones de @Reintentable:
  @Transversal(orden=20) Reintentos con espera creciente
  retención: RUNTIME
  destinos:  [METHOD]
Meta-anotaciones de @Auditable:
  @Transversal(orden=10) Auditoría de operaciones
  retención: RUNTIME
  destinos:  [METHOD, TYPE]

Comentarios. Cuatro observaciones.

Class<? extends Exception>[] funciona porque Class es un tipo permitido, y además está acotado con genéricos (10-01): no puedes poner String.class ahí, el compilador lo impide. Es un buen ejemplo de las dos características cooperando.

publicarValoracion con intentos = 1 tiene espera máxima 0, y es exactamente lo que se busca: un POST no idempotente que no se reintenta. La anotación documenta la decisión en lugar de que sea un if perdido dentro del método.

@Transversal es una meta-anotación de verdad, con @Target(ANNOTATION_TYPE). Ponerla sobre una clase no compila. Y leerla desde Auditable.class demuestra algo conceptualmente importante: una anotación es una clase, y por tanto se puede inspeccionar como cualquier otra. Retention y Target se leen igual que las tuyas, porque no tienen nada de especial.

Y falta lo esencial: nada de esto reintenta nada. describirPolitica solo describe. Para que @Reintentable funcione de verdad hace falta interceptar la llamada, ejecutarla, capturar la excepción, esperar y repetir — es decir, un proxy dinámico. Eso es la lección siguiente.

Conclusión

Ya sabes qué es una anotación y, sobre todo, qué no es.

Una anotación es información sobre el código: una etiqueta adherida a una clase, un método, un campo o un parámetro que no cambia su comportamiento. @Auditable no audita. @Entity no crea tablas. @Test no ejecuta nada. Alguien las lee y actúa, y esa es la única forma en que una anotación produce efectos.

Conoces las estándar del JDK y qué hace realmente cada una: @Override verifica que sobrescribes algo de verdad y te salva del equals(Material) que sobrecarga en lugar de sobrescribir y rompe en silencio todas las colecciones; @Deprecated avisa, con since para saber desde cuándo y forRemoval para distinguir "hay algo mejor" de "esto desaparece"; @SuppressWarnings silencia avisos concretos, con ámbito mínimo y justificación escrita; @SafeVarargs es tu promesa de que no expones el array de varargs, y exige final, static o private para poder hacerla; @FunctionalInterface protege el contrato de una interfaz con un solo método abstracto.

Escribes las tuyas con @interface, cuyos elementos se declaran como métodos sin cuerpo, con default para hacerlos opcionales y con value como nombre mágico que habilita la forma abreviada. Sabes qué tipos se permiten —primitivos, String, Class, enum, anotaciones y arrays de eso— y que null no es un valor posible, por lo que la convención es "", {} o un centinela.

Dominas las meta-anotaciones, empezando por la que decide si tu anotación existirá siquiera: @Retention. SOURCE muere en el compilador, CLASSel valor por defecto— queda en el .class pero es invisible para la reflexión, y RUNTIME es la única que llega viva a la ejecución. Ese defecto es el error número uno con anotaciones propias y ahora no te pillará. Con @Target declaras dónde se puede poner, con @Documented que forme parte del contrato público, con @Inherited que las subclases la hereden, y con @Repeatable que pueda aparecer varias veces — sabiendo que el compilador las envuelve en la contenedora y que por eso hay que usar siempre getAnnotationsByType. Y reconoces las anotaciones de tipo de Java 8 (TYPE_USE, TYPE_PARAMETER), que existen para verificadores externos como el Checker Framework.

Sabes que hay exactamente dos formas de leerlas. En compilación, con un procesador de anotaciones que recibe los elementos marcados, puede fallar la compilación y puede generar código nuevo — que es literalmente lo que hace Lombok cuando @Data produce los getters, los setters, equals, hashCode y toString que no escribiste (11-07). Y en ejecución, con reflexión: isAnnotationPresent, getAnnotation, getAnnotationsByType. La primera cuesta cero en ejecución; la segunda es lo que hacen Spring, Hibernate y JUnit al arrancar, y es la próxima lección.

Entiendes por qué el ecosistema se movió del XML externo a la configuración junto al código —proximidad, menos ceremonial, refactorización segura, verificación del compilador— y por qué no ganó del todo: lo que cambia por entorno sigue viviendo fuera, en Properties. Y sabes cuándo no usar anotaciones: para lógica de negocio (acabas inventando un lenguaje dentro de una cadena), para configuración de entorno (obliga a recompilar), para valores que cambian a menudo, y cuando una interfaz expresaría lo mismo con comprobación del compilador.

BiblioTech tiene ahora un paquete com.nexussoftware.bibliotech.anotaciones. @CampoCsv(nombre, orden, formato, sensible) marca qué campos se exportan y en qué columna, y está puesta sobre Libro, Prestamo y el record Ficha —aprovechando RECORD_COMPONENT—, moviendo esa decisión desde ExportadorCatalogoCsv hasta la línea del campo que describe. @Auditable(value, nivel, registrarArgumentos, argumentosSensibles) marca qué operaciones se registran, con la anotación de clase actuando como valor por defecto y argumentosSensibles codificando la regla de 09-06 de no registrar credenciales. Y GestorPrestamos.prestar() ha quedado limpio: catorce llamadas repetidas a RegistroOperaciones han desaparecido de la lógica.

Y no funciona nada. Puedes prestar veinte libros y no se registrará una sola línea; puedes exportar el catálogo y @CampoCsv no habrá servido de nada. Las etiquetas están puestas y no hay nadie leyéndolas.

En 10-03, Reflexión, escribes ese lector. Aprenderás a obtener el objeto Class<?> de tres formas distintas, a inspeccionar campos, métodos y constructores —y la diferencia entre getFields y getDeclaredFields, que confunde a todo el mundo—, a crear instancias e invocar métodos sin conocerlos en compilación, y a saltarte el encapsulamiento con setAccessible(true). Con eso escribirás el ExportadorAnotado que recorre los campos marcados con @CampoCsv, los ordena y genera la línea CSV de cualquier entidad sin conocerla. Y darás un paso más: un proxy dinámico que intercepta cada llamada a GestorPrestamos, ve la @Auditable y registra la operación sin que el método se entere. Cuando ese proxy funcione, entenderás por dentro cómo funciona @Transactional de Spring — y los frameworks del módulo 11 dejarán de parecer magia.

Curso de Programación en Java

Módulo 1: Introducción a Java

Módulo 2: Flujo de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Programación Orientada a Objetos Avanzada

Módulo 5: Estructuras de Datos y Colecciones

Módulo 6: Manejo de Excepciones

Módulo 7: Entrada/Salida de Archivos

Módulo 8: Multihilo y Concurrencia

Módulo 9: Redes

Módulo 10: Temas Avanzados

Módulo 11: Frameworks y Librerías de Java

Módulo 12: Construcción de Aplicaciones del Mundo Real

© Copyright 2026. Todos los derechos reservados