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
- Qué es una anotación: metadatos sobre el código
- Sintaxis de uso: marcador, valor único y varios elementos
- Las anotaciones estándar del JDK
@Overridey el error que evita@Deprecated, consinceyforRemoval@SuppressWarnings@SafeVarargs@FunctionalInterface- Crear anotaciones propias:
@interface - Elementos: tipos permitidos,
defaulty el elementovalue - Meta-anotaciones:
@Retention - Meta-anotaciones:
@Target @Documentede@Inherited@Repeatabley su anotación contenedora- Anotaciones de tipo (Java 8)
- Cómo se leen (I): procesadores de anotaciones en compilación
- Cómo se leen (II): reflexión en ejecución
- Para qué las usan los frameworks reales
- Configuración junto al código frente a XML
- Cuándo NO usar anotaciones
- BiblioTech:
@CampoCsv - BiblioTech:
@Auditable - Errores Comunes y Consejos
- Ejercicios
- 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
@Auditablesobreprestar()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.
- 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 opcionalesSe 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.
- 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.
@Override y el error que evita
@Override y el error que evitaRetomamos 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 Setcontains(), 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:
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.
@Deprecated, con since y forRemoval
@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:
warning: [removal] prestarLibro(String) in GestorPrestamos has been deprecated and marked for removalLos 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:
- Siempre acompaña
@Deprecatedde@deprecateden 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). - 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.
@SuppressWarnings
@SuppressWarningsYa la usaste en 10-01. Silencia avisos concretos del compilador, identificados por una cadena.
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):
- Ámbito mínimo. Sobre la variable local, no sobre el método; sobre el método, no sobre la clase.
- Comentario justificando por qué la operación es segura pese al aviso.
- 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;
}
@SafeVarargs
@SafeVarargsLos 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 sí 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.
@FunctionalInterface
@FunctionalInterfaceRetomamos 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 PoliticaMultasNo 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.
- Crear anotaciones propias:
@interface
@interfaceUna 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 {
}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:
- Elementos: tipos permitidos,
default y el elemento value
default y el elemento valueTipos 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:
Esto no:
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:
@Auditable("prestar") // forma abreviada
@Auditable(value = "prestar") // forma completa: equivalentesLa abreviatura funciona también si hay más elementos, siempre que todos los demás tengan default y solo se especifique value:
@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 mezclarRegla 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.
- Meta-anotaciones:
@Retention
@RetentionUna 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 |
Sí | No | No | Anotaciones para el compilador o para procesadores: @Override, @SuppressWarnings, Lombok |
RetentionPolicy.CLASS |
Sí | Sí | No | Herramientas que analizan bytecode sin cargar clases. Es el valor por defecto |
RetentionPolicy.RUNTIME |
Sí | Sí | Sí | 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 queSi 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
- Meta-anotaciones:
@Target
@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:
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.
@Documented e @Inherited
@Documented e @InheritedDos 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 propiaSystem.out.println(Libro.class.isAnnotationPresent(Auditado.class)); // true, gracias a @Inherited
System.out.println(Libro.class.getAnnotation(Auditado.class).modulo()); // catálogoSin @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.
@Repeatable y su anotación contenedora
@Repeatable y su anotación contenedoraPor 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 { }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 contenedorPara 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() + ")");
}Regla: para anotaciones repetibles, usa siempre getAnnotationsByType. Es la única forma de no equivocarse.
Requisitos que el compilador impone a la contenedora:
- Debe tener un elemento
valueque sea un array del tipo repetible. - Su
@Retentiondebe ser igual o más amplia que la de la repetible. - Su
@Targetdebe incluir todos los destinos de la repetible.
- 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 StringCon 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.
- 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:
...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 | Sí | No (salvo proxies, 10-03) |
| Puede fallar la compilación | Sí | 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.
- 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.
- 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.
- 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 | Sí |
| 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.
- 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 |
- BiblioTech:
@CampoCsv
@CampoCsvHora 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).
- BiblioTech:
@Auditable
@AuditableEl 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 defectotrue),longitudMinima(entero, por defecto0),longitudMaxima(entero, por defectoInteger.MAX_VALUE),patron(cadena, por defecto vacía) ymensaje(cadena, por defecto vacía). - Anota con ella los campos de
Libro: el ISBN obligatorio con patrón978-\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
mainque imprima, usandogetDeclaredFields()ygetAnnotation(), 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 deClass<? extends Exception>, por defecto{Exception.class}), ysoloIdempotentes(booleano, por defectotrue). - Anota con ella tres métodos de
ClienteMetadatoscon 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));
}
}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, CLASS —el 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
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
