La lliçó anterior va tancar amb una mancança concreta: els genèrics parlen al compilador i només al compilador, i hi ha informació sobre el codi que no cap en un sistema de tipus. "Aquest camp s'exporta al CSV a la tercera columna amb la capçalera ISBN". "Aquesta operació cal registrar-la a l'auditoria". "Aquest mètode està obsolet des de la versió 3 i desapareixerà a la 4". "Aquesta classe és un servei que cal instanciar en arrencar".

Res d'això no és un tipus. És informació sobre el codi, i el mecanisme de Java per expressar-la s'anomena anotació.

Les anotacions són, probablement, la característica del llenguatge la importància real de la qual més es menysté en aprendre-la. Vistes de prop semblen un adorn: @Override damunt d'un mètode, @Deprecated damunt d'un altre. Vistes de lluny, són el pilar sobre el qual està construït el Java empresarial modern sencer. Quan al mòdul 11 escriguis @Entity sobre una classe i Hibernate creï una taula, @Autowired sobre un camp i Spring injecti una dependència, o @Test sobre un mètode i JUnit l'executi, estaràs fent servir exactament el mecanisme que aprens aquí.

I hi ha una idea que convé fixar des de la primera línia, perquè és la font de gairebé tota la confusió: una anotació no fa res per si sola. @Test no executa res. @Entity no crea cap taula. Són etiquetes inertes. El que passa és que algú les llegeix —el compilador, una eina de construcció, o un framework en temps d'execució— i actua en conseqüència. Aquesta lliçó t'ensenya a escriure les etiquetes; la següent, a escriure el lector.

En acabar, BiblioTech tindrà @CampCsv i @Auditable, definides i col·locades. I estaran sense fer servir, esperant el motor de 10-03.

Contingut

  1. Què és una anotació: metadades sobre el codi
  2. Sintaxi d'ús: marcadora, valor únic i diversos elements
  3. Les anotacions estàndard del JDK
  4. @Override i l'error que evita
  5. @Deprecated, amb since i forRemoval
  6. @SuppressWarnings
  7. @SafeVarargs
  8. @FunctionalInterface
  9. Crear anotacions pròpies: @interface
  10. Elements: tipus permesos, default i l'element value
  11. Metaanotacions: @Retention
  12. Metaanotacions: @Target
  13. @Documented i @Inherited
  14. @Repeatable i la seva anotació contenidora
  15. Anotacions de tipus (Java 8)
  16. Com es llegeixen (I): processadors d'anotacions en compilació
  17. Com es llegeixen (II): reflexió en execució
  18. Per a què les fan servir els frameworks reals
  19. Configuració al costat del codi enfront d'XML
  20. Quan NO fer servir anotacions
  21. BiblioTech: @CampCsv
  22. BiblioTech: @Auditable
  23. Errors Comuns i Consells
  24. Exercicis

  1. Què és una anotació: metadades sobre el codi

Una anotació és una etiqueta que s'adhereix a un element del programa —una classe, un mètode, un camp, un paràmetre, una variable local, un paquet— i que transporta informació sobre aquest element sense formar part de la seva lògica.

L'analogia útil és la de les etiquetes d'un arxiu físic. Un expedient en una carpeta té el seu contingut (els documents) i té adhesius a la solapa: "urgent", "revisat per la Marta Ruiz", "destruir el 2030". Els adhesius no canvien el contingut de l'expedient. Però quan algú recorre l'arxivador buscant què destruir, els llegeix i actua.

Formalment:

  • Una anotació no canvia el comportament del codi que anota. Posar @Auditable sobre prestar() no fa que s'auditi res. El mètode continua fent exactament el mateix.
  • Una anotació és informació que algú consumeix. Aquest "algú" pot ser el compilador (@Override), una eina que s'executa durant la compilació (un processador d'anotacions), o codi que s'executa amb el teu programa (un framework, via reflexió).
  • Una anotació es compila al costat del codi i, segons el seu @Retention, pot sobreviure fins a l'execució.
graph TD
    A["Codi font<br/>amb anotacions"] --> B["Compilador javac"]
    B -->|"RETENTION SOURCE<br/>es descarten"| C["Nomes avisos i errors<br/>@Override, @SuppressWarnings"]
    B -->|"RETENTION CLASS<br/>queden al .class"| D["Eines de bytecode<br/>analitzadors estatics"]
    B -->|"RETENTION RUNTIME<br/>arriben vives"| E["JVM en execucio"]
    E --> F["Reflexio: Spring, Hibernate, JUnit<br/>i el motor de 10-03"]
    B -.->|"processador d anotacions"| G["Genera codi font nou<br/>exemple: Lombok"]
    G --> B

Aquest diagrama és, en el fons, tota la lliçó. La resta són detalls.

  1. Sintaxi d'ús: marcadora, valor únic i diversos elements

Una anotació s'escriu amb @ seguit del seu nom, davant de l'element que anota. Hi ha tres formes segons quantes dades porti:

// 1. ANOTACIO MARCADORA: sense elements. Els parentesis s ometen.
@Override
public String toString() { ... }

// 2. VALOR UNIC: si l element es diu 'value', s omet el nom.
@SuppressWarnings("unchecked")
List<Llibre> llibres = (List<Llibre>) cru;

// equival exactament a:
@SuppressWarnings(value = "unchecked")

// 3. DIVERSOS ELEMENTS: parells nom = valor, separats per comes.
@Deprecated(since = "3.2", forRemoval = true)
public void prestarLlibre(String isbn) { ... }

I quan un element és un array, es fan servir claus (i es poden ometre si hi ha un sol valor):

@SuppressWarnings({"unchecked", "rawtypes"})     // diversos valors
@SuppressWarnings("unchecked")                   // un de sol: claus opcionals

Es poden apilar diverses anotacions sobre el mateix element:

@Auditable(nivell = "ALT")
@Override
@SuppressWarnings("unchecked")
public Resultat<Prestec> prestar(String isbn, String empleat) { ... }

Convenció d'estil: les anotacions de classes, mètodes i camps van a la seva pròpia línia, damunt de l'element. Les de paràmetres i variables locals van a la mateixa línia.

  1. Les anotacions estàndard del JDK

El JDK porta un grapat d'anotacions que el compilador entén de forma especial. No són moltes, i convé conèixer-les totes perquè les faràs servir cada dia.

Anotació On es posa Què fa realment Retenció
@Override Mètodes El compilador verifica que realment se sobreescriu alguna cosa; si no, error SOURCE
@Deprecated Gairebé qualsevol element El compilador avisa en fer-lo servir; apareix al Javadoc RUNTIME
@SuppressWarnings Gairebé qualsevol element Silencia avisos concrets del compilador SOURCE
@SafeVarargs Mètodes static, final o constructors amb varargs genèrics Suprimeix l'avís de heap pollution RUNTIME
@FunctionalInterface Interfícies El compilador verifica que té exactament un mètode abstracte RUNTIME

Fixa't en la columna "què fa realment", perquè distingeix tres comportaments molt diferents: verificar (falla la compilació si no es compleix), avisar (compila amb advertiment) i silenciar (treu un advertiment).

N'hi ha algunes més, d'ús especialitzat, que només val la pena anomenar: @Native (constants referenciades des de codi natiu) i les metaanotacions de l'apartat 11 endavant.

  1. @Override i l'error que evita

Reprenem 03-05. @Override és l'anotació més usada de Java i la més útil, i el seu valor rau en un tipus de bug particularment cruel: el mètode que et penses que sobreescriu i no sobreescriu res.

public class Material {
    private final String isbn;

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

Ara mira aquest error, que és el clàssic dels clàssics:

public class Material {

    // SENSE @Override: sembla que sobreescriu equals... pero NO.
    public boolean equals(Material altre) {         // Material, no Object!
        return this.isbn.equals(altre.isbn);
    }
}

Això compila perfectament. No sobreescriu Object.equals(Object): el sobrecarrega. I l'efecte és demolidor:

Material a = new Llibre("Java Eficac", "978-0000000001");
Material b = new Llibre("Java Eficac", "978-0000000001");

System.out.println(a.equals(b));        // true: crida el teu equals(Material)

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

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

contains(), HashSet, HashMap, remove(), indexOf(): tot el framework de col·leccions crida equals(Object), que continua sent el d'Object (identitat de referència). Tens duplicats en un Set, cerques que no troben res i hores de depuració.

Amb @Override, el compilador t'ho talla al moment:

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

Regla sense excepcions: posa @Override en absolutament tots els mètodes que sobreescriguin alguna cosa. No costa res i detecta errors de signatura, de nom (toSting() en lloc de toString()) i d'aritat. Des de Java 6 també funciona per a mètodes d'interfície, així que també va a les implementacions de Comparator.compare, Runnable.run o les teves pròpies interfícies.

La seva retenció és SOURCE: compleix la seva funció al compilador i desapareix. No en queda rastre al .class.

  1. @Deprecated, amb since i forRemoval

@Deprecated marca un element com a obsolet: continua funcionant, però no l'hauries de fer servir en codi nou.

package com.nexussoftware.bibliotech.servei;

public class GestorPrestecs {

    /**
     * Presta un material identificat pel seu ISBN.
     *
     * @deprecated Des de 3.2 fes servir {@link #prestar(String, String)}, que retorna
     *             un {@code Resultat<Prestec>} en lloc de llançar excepcions
     *             per a casos esperables. S'eliminarà a la versió 4.0.
     */
    @Deprecated(since = "3.2", forRemoval = true)
    public void prestarLlibre(String isbn) throws BiblioTechException {
        prestar(isbn, "desconegut");
    }

    public Resultat<Prestec> prestar(String isbn, String empleat) { ... }
}

En fer-lo servir:

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

Els dos elements, afegits a Java 9, importen més del que sembla:

Element Tipus Significat
since String Versió en què es va marcar obsolet. Ajuda a estimar quant de temps porta així
forRemoval boolean true = s'eliminarà. Canvia l'avís a la categoria removal, més sorollosa

La diferència entre forRemoval = false (per defecte) i true és real: el primer significa "hi ha alguna cosa millor", el segon "això desapareix, migra ja". Els IDE els mostren diferent i -Xlint:removal permet tractar-los com a errors.

Dues regles d'ús:

  1. Acompanya sempre @Deprecated de @deprecated al Javadoc, dient què fer servir al seu lloc. Una deprecació sense alternativa és una crueltat: informes del problema i no de la solució. Fixa't que són dues coses diferents: l'anotació (per al compilador) i l'etiqueta Javadoc (per a l'humà).
  2. No depreciïs sense pla. Marcar i no eliminar mai fa que els avisos es converteixin en soroll que tothom ignora.

La seva retenció és RUNTIME, i això permet que eines d'anàlisi inspeccionin un .jar compilat i detectin usos d'API obsoleta.

  1. @SuppressWarnings

Ja la vas fer servir a 10-01. Silencia avisos concrets del compilador, identificats per una cadena.

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

Els identificadors més útils:

Valor Silencia
"unchecked" Operacions sense comprovació genèrica (les conversions de 10-01)
"rawtypes" Ús de tipus crus
"deprecation" Ús d'API obsoleta
"removal" Ús d'API marcada forRemoval = true
"serial" Classe serialitzable sense serialVersionUID (07-05)
"this-escape" Fuga de this al constructor (Java 21)
"all" Tots. Pràcticament mai no és el correcte

Els identificadors no estan estandarditzats més enllà d'uns pocs: cada compilador i IDE reconeix els seus, i un de desconegut s'ignora en silenci.

Les tres regles (les mateixes de 10-01, perquè són importants):

  1. Àmbit mínim. Sobre la variable local, no sobre el mètode; sobre el mètode, no sobre la classe.
  2. Comentari justificant per què l'operació és segura malgrat l'avís.
  3. Mai "all".
// MALAMENT: apaga tot a tota la classe per sempre
@SuppressWarnings("all")
public class ImportadorCataleg { ... }

// BE: ambit minim i justificacio
public List<Llibre> llegirLlibres(Path fitxer) throws IOException {
    // SEGUR: el fitxer nomes l escriu ExportadorCatalegCsv, que
    // sempre serialitza Llibre. Qualsevol altre contingut falla abans,
    // a la validacio de capcalera.
    @SuppressWarnings("unchecked")
    List<Llibre> llibres = (List<Llibre>) llegirEntitats(fitxer);
    return llibres;
}

  1. @SafeVarargs

Els varargs genèrics produeixen un avís incòmode. Recorda de 10-01 que no es poden crear arrays genèrics, però T... elements és exactament un T[]. El compilador ho permet creant un array de la classe esborrada, i avisa de possible contaminació del monticle (heap pollution):

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

L'avís és legítim, perquè aquest mètode que és perillós:

// PERILLOS: exposa l array de varargs
@SafeVarargs
static <T> T[] perillos(T... elements) {
    return elements;                      // retorna l array creat pel compilador
}

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

@SafeVarargs és la teva promesa al compilador que el mètode no guarda ni exposa l'array de varargs, només hi llegeix. Si la promesa és certa, l'avís desapareix:

@SafeVarargs
public static <T> List<T> llistaDe(T... elements) {
    return new ArrayList<>(Arrays.asList(elements));   // nomes LLEGEIX de l array
}

Restricció: només es pot posar en mètodes que ningú no pugui sobreescriure —static, final, private (Java 9+)— i en constructors. És lògic: no pots prometre res sobre una implementació que encara no existeix.

  1. @FunctionalInterface

Reprenem 04-06. Marca una interfície com a funcional, i el compilador verifica que té exactament un mètode abstracte:

package com.nexussoftware.bibliotech.servei;

/**
 * Calcula la multa d un prestec amb retard.
 * En ser funcional, es pot implementar amb una lambda.
 */
@FunctionalInterface
public interface PoliticaMultes {

    double calcular(int diesDeRetard);

    // Els default NO compten com a abstractes: la interficie continua sent funcional
    default PoliticaMultes ambSostreDe(double maxim) {
        return dies -> Math.min(calcular(dies), maxim);
    }

    // Els static tampoc no compten
    static PoliticaMultes fixa(double perDia) {
        return dies -> dies * perDia;
    }
}
PoliticaMultes estandard = dies -> dies * 0.25;
PoliticaMultes acotada   = estandard.ambSostreDe(10.0);

System.out.printf("30 dies de retard: %.2f €%n", estandard.calcular(30));   // 7,50 €
System.out.printf("100 dies acotat:   %.2f €%n", acotada.calcular(100));    // 10,00 €

Si algú afegeix un segon mètode abstracte, el compilador ho impedeix:

@FunctionalInterface
public interface PoliticaMultes {
    double calcular(int dies);
    double calcularAmbDescompte(int dies, double descompte);   // segon abstracte
}
error: Unexpected @FunctionalInterface annotation
    PoliticaMultes is not a functional interface
      multiple non-overriding abstract methods found in interface PoliticaMultes

No és obligatòria. Qualsevol interfície amb un sol mètode abstracte es pot fer servir amb lambdes, la porti o no. El que fa @FunctionalInterface és protegir el contracte: declara la intenció i evita que un company trenqui tots els usuaris de la interfície afegint-hi un mètode. És documentació que el compilador verifica, que és la millor mena de documentació.

  1. Crear anotacions pròpies: @interface

Una anotació es declara amb @interface. Formalment és una interfície (estén implícitament java.lang.annotation.Annotation), encara que no es faci servir com a tal.

La més simple és una anotació marcadora, sense elements:

package com.nexussoftware.bibliotech.anotacions;

/**
 * Marca un element que encara no esta acabat.
 * Per si sola no fa absolutament res.
 */
public @interface EnConstruccio {
}
@EnConstruccio
public class ImportadorMarc21 {
    // ...
}

Compila i no passa res. Literalment res: l'anotació és SOURCE per defecte... no, i aquest és el primer detall que cal fixar: per defecte la retenció és CLASS, no SOURCE ni RUNTIME. Queda al .class però no és visible per reflexió. És el valor per defecte menys útil dels tres, i la causa número u de la pregunta "per què la meva anotació no apareix a getAnnotations()?". Hi tornarem a l'apartat 11.

Una anotació amb elements:

package com.nexussoftware.bibliotech.anotacions;

/**
 * Documenta qui es responsable d un component i des de quan.
 */
public @interface Responsable {

    /** Nom del responsable. Obligatori: no te default. */
    String nom();

    /** Equip. Opcional. */
    String equip() default "Plataforma";

    /** Data en format ISO. Opcional. */
    String desDe() default "";

    /** Persones de suport. Opcional, array buit per defecte. */
    String[] suport() default {};
}
@Responsable(nom = "Marta Ruiz", equip = "Cataleg", desDe = "2026-01-15",
             suport = {"Diego Alonso", "Nuria Vidal"})
public class CatalegService { }

@Responsable(nom = "Nuria Vidal")        // els altres prenen el seu default
public class ServeiAvisos { }

Fixa't en la sintaxi dels elements: es declaren com a mètodes sense cos (String nom();), no com a camps. Això és perquè una anotació és una interfície, i en llegir-la per reflexió cridaràs anotacio.nom().

Un element sense default és obligatori. Ometre'l és un error de compilació:

@Responsable(equip = "Cataleg")
error: annotation @Responsable is missing a default value for the element 'nom'

  1. Elements: tipus permesos, default i l'element value

Tipus permesos

Els tipus que pot tenir un element d'anotació estan molt restringits, i no és arbitrari: el seu valor ha de poder guardar-se al fitxer .class com una constant i ser conegut en compilació.

Permès Exemple
Primitius int ordre(); boolean obligatori(); double factor();
String String nom();
Class o Class<?> Class<?> conversor();
Tipus enum Gravetat nivell();
Altres anotacions Responsable propietari();
Arrays de tot l'anterior String[] alies(); Class<?>[] grups();

No permès: qualsevol altra cosa. Res de List<String>, Object, LocalDate, les teves pròpies classes o tipus genèrics.

public @interface Dolent {
    List<String> etiquetes();     // ERROR: invalid type for annotation member
    LocalDate creat();            // ERROR
    Llibre llibre();              // ERROR
}

Per això les dates en anotacions s'escriuen com a String en format ISO ("2026-01-15") i s'analitzen en llegir-les (veuràs com a 10-05), i les llistes es declaren com a arrays.

Els valors han de ser constants de temps de compilació. Això compila:

@Responsable(nom = "Marta " + "Ruiz")              // concatenacio de literals: constant

Això no:

@Responsable(nom = obtenirResponsable())           // ERROR: no es una constant

Cap element no pot ser null. No hi ha manera d'expressar-ho, ni com a valor ni com a default. La convenció és fer servir la cadena buida o un valor sentinella:

public @interface CampCsv {
    String nom() default "";         // "" significa "fes servir el nom del camp"
    int ordre() default Integer.MAX_VALUE;   // sentinella: "al final"
}

L'element value i la seva sintaxi abreujada

Si una anotació té un element anomenat exactament value, qui la fa servir pot ometre el nom:

public @interface Auditable {
    String value();     // el nom magic
}
@Auditable("prestar")            // forma abreujada
@Auditable(value = "prestar")    // forma completa: equivalents

L'abreviatura funciona també si hi ha més elements, sempre que tots els altres tinguin default i només s'especifiqui value:

public @interface Auditable {
    String value();
    Gravetat nivell() default Gravetat.MITJANA;
}
@Auditable("prestar")                                  // OK: nivell pren el seu default
@Auditable(value = "prestar", nivell = Gravetat.ALTA)  // OK: forma completa obligatoria
// @Auditable("prestar", nivell = Gravetat.ALTA)       // ERROR: no es poden barrejar

Regla de disseny: si la teva anotació té un element clarament principal, anomena'l value. @SuppressWarnings("unchecked") i @RequestMapping("/llibres") d'Spring es llegeixen bé precisament per això.

  1. Metaanotacions: @Retention

Una metaanotació és una anotació que anota una altra anotació. Són cinc, i les dues primeres són les que de debò defineixen el comportament de la teva.

@Retention decideix fins on viu la teva anotació. És la decisió més important que prendràs en dissenyar-la.

Política Present al .java Present al .class Visible per reflexió Per a què
RetentionPolicy.SOURCE No No Anotacions per al compilador o per a processadors: @Override, @SuppressWarnings, Lombok
RetentionPolicy.CLASS No Eines que analitzen bytecode sense carregar classes. És el valor per defecte
RetentionPolicy.RUNTIME Frameworks que actuen en execució: Spring, Hibernate, JUnit, i el motor de 10-03
graph LR
    S["Codi font<br/>.java"] --> C["Compilacio"]
    C --> B["Bytecode<br/>.class"]
    B --> R["Execucio<br/>JVM"]

    S -.->|"SOURCE arriba fins aqui"| C
    B -.->|"CLASS arriba fins aqui"| B2["fi"]
    R -.->|"RUNTIME arriba fins al final"| R2["getAnnotation funciona"]

El parany del valor per defecte. Si no poses @Retention, la teva anotació és CLASS i isAnnotationPresent() retornarà false sense cap explicació:

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

// en una altra banda
boolean te = metode.isAnnotationPresent(Auditable.class);
System.out.println(te);              // false, i no entens per que

Si la teva anotació l'ha de llegir codi en execució —que és el 90 % dels casos d'una anotació pròpia— posa-hi @Retention(RetentionPolicy.RUNTIME) sempre. És l'error de principiant més freqüent amb anotacions i costa mitja tarda trobar-lo.

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

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

  1. Metaanotacions: @Target

@Target restringeix on es pot posar la teva anotació. Rep un array d'ElementType:

ElementType Es pot posar a
TYPE Classe, interfície, enum, record, anotació
FIELD Camp (incloses constants d'enum)
METHOD Mètode
PARAMETER Paràmetre d'un mètode o constructor
CONSTRUCTOR Constructor
LOCAL_VARIABLE Variable local
ANNOTATION_TYPE Una altra anotació (per a metaanotacions)
PACKAGE Paquet (a package-info.java)
TYPE_PARAMETER Paràmetre de tipus genèric (Java 8)
TYPE_USE Qualsevol ús d'un tipus (Java 8)
MODULE Declaració de mòdul (Java 9)
RECORD_COMPONENT Component d'un record (Java 16)
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.RECORD_COMPONENT})
public @interface CampCsv {
    String nom() default "";
    int ordre();
}

Amb això, posar-la on no toca és un error de compilació:

@CampCsv(ordre = 1)       // ERROR: no aplicable a classes
public class Llibre { }
error: annotation type not applicable to this kind of declaration

Sense @Target, l'anotació es pot posar gairebé a qualsevol lloc, cosa que sol ser un descuit i no una decisió. Posar @Target és documentar la intenció i evitar usos absurds.

Un detall sobre RECORD_COMPONENT: quan anotes el component d'un record, l'anotació es propaga al camp, al paràmetre del constructor canònic i al mètode d'accés, segons a quin ElementType sigui aplicable. És una comoditat important perquè les anotacions funcionin amb records.

  1. @Documented i @Inherited

Dues metaanotacions menors però amb efectes concrets.

@Documented: fa que l'anotació aparegui al Javadoc generat de l'element anotat. Sense ella, qui llegeixi la documentació de la teva classe no veurà que porta @Responsable.

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

Regla simple: si l'anotació forma part del contracte públic del que anota, posa-la. Si és un detall intern, no.

@Inherited: fa que l'anotació s'heretin per les subclasses. Només funciona en anotacions de TYPE, i només s'hereta de classes, no d'interfícies.

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

@Auditat(modul = "cataleg")
public abstract class Material { }

public class Llibre extends Material { }   // sense anotacio propia
System.out.println(Llibre.class.isAnnotationPresent(Auditat.class));    // true, gracies a @Inherited
System.out.println(Llibre.class.getAnnotation(Auditat.class).modul());  // cataleg

Sense @Inherited, aquest primer println donaria false.

Compte amb l'asimetria: getAnnotations() inclou les heretades; getDeclaredAnnotations() només les posades directament. És la mateixa distinció entre getFields i getDeclaredFields que veuràs a 10-03, i confon igual.

  1. @Repeatable i la seva anotació contenidora

Per defecte, una anotació no es pot repetir sobre el mateix element:

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

Abans de Java 8 la solució era manual i lletja: definir una segona anotació que contingués un array. Java 8 va automatitzar el patró amb @Repeatable, però continua havent-hi dues anotacions: la repetible i la seva contenidora.

package com.nexussoftware.bibliotech.anotacions;

import java.lang.annotation.*;

/** L anotacio CONTENIDORA: guarda un array de les repetibles. */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Responsables {
    Responsable[] value();     // l element HA de dir-se value i ser un array
}
package com.nexussoftware.bibliotech.anotacions;

import java.lang.annotation.*;

/** L anotacio REPETIBLE: declara qui la conte. */
@Repeatable(Responsables.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Responsable {
    String nom();
    String equip() default "Plataforma";
}

Ara sí:

@Responsable(nom = "Marta Ruiz",   equip = "Cataleg")
@Responsable(nom = "Diego Alonso", equip = "Prestecs")
public class CatalegService { }

Què fa el compilador per sota: quan veu dues o més @Responsable, les embolcalla automàticament en un @Responsables({...}). I això té una conseqüència pràctica que sorprèn en llegir-les per reflexió:

// Amb UNA sola @Responsable
CatalegService.class.getAnnotation(Responsable.class);     // retorna l anotacio
CatalegService.class.getAnnotation(Responsables.class);    // null

// Amb DUES @Responsable
CatalegService.class.getAnnotation(Responsable.class);     // null! son dins del contenidor
CatalegService.class.getAnnotation(Responsables.class);    // retorna el contenidor

Per no haver de distingir els dos casos, Java 8 va afegir getAnnotationsByType, que funciona igual amb una o amb diverses:

Responsable[] tots = CatalegService.class.getAnnotationsByType(Responsable.class);
for (Responsable r : tots) {
    System.out.println(r.nom() + " (" + r.equip() + ")");
}
Marta Ruiz (Cataleg)
Diego Alonso (Prestecs)

Regla: per a anotacions repetibles, fes servir sempre getAnnotationsByType. És l'única manera de no equivocar-se.

Requisits que el compilador imposa a la contenidora:

  1. Ha de tenir un element value que sigui un array del tipus repetible.
  2. El seu @Retention ha de ser igual o més ampli que el de la repetible.
  3. El seu @Target ha d'incloure tots els destins de la repetible.

  1. Anotacions de tipus (Java 8)

Fins a Java 8, una anotació es posava sobre declaracions. Java 8 va afegir dos ElementType que permeten posar-les sobre qualsevol ús d'un tipus:

  • TYPE_USE: a qualsevol lloc on aparegui un tipus.
  • TYPE_PARAMETER: sobre un paràmetre de tipus genèric (el <T> de 10-01).
import java.lang.annotation.*;

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

I ara es pot escriure:

// En una declaracio de variable
@NoNul String titol = "Java Eficac";

// Al tipus d un parametre generic
List<@NoNul Llibre> cataleg = new ArrayList<>();

// En una conversio
String s = (@NoNul String) objecte;

// En un new
Llibre llibre = new @NoNul Llibre("Refactoritzacio", "978-0000000003");

// En un throws i en un implements
public class Cataleg implements @NoNul Identificable { }

// En arrays: anota el TIPUS D ELEMENT
@NoNul String[] noms;             // array de String no nuls
String @NoNul [] noms2;           // array no nul de String

Amb TYPE_PARAMETER:

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

public class Repositori<@Entitat T extends Identificable> { }

Per a què serveix això? No per al compilador de Java, que les ignora. Serveix per a verificadors de tipus externs (pluggable type checkers) que analitzen el codi buscant errors que el sistema de tipus de Java no pot detectar. El cas emblemàtic és el Checker Framework, capaç de demostrar en compilació que un programa no pot llançar NullPointerException gràcies a anotacions @Nullable/@NonNull sobre els usos de tipus.

En el dia a dia del desenvolupament empresarial les veuràs sobretot en forma de @NotNull de Bean Validation o de l'IDE. No les necessitaràs per escriure les teves pròpies anotacions de negoci, però convé reconèixer-les quan apareixen en signatures alienes.

  1. Com es llegeixen (I): processadors d'anotacions en compilació

Aquí hi ha la primera de les dues formes que una anotació "faci alguna cosa". Un processador d'anotacions és un programa que javac executa durant la compilació, al qual lliura els elements anotats i que pot generar codi font nou, que al seu torn es compila.

El mecanisme viu a javax.annotation.processing i funciona per rondes:

graph TD
    A["Fonts .java amb anotacions"] --> B["javac ronda 1"]
    B --> C{"Hi ha processadors<br/>registrats?"}
    C -->|"si"| D["El processador rep els elements anotats"]
    D --> E["Genera nous .java"]
    E --> F["javac ronda 2 compila el generat"]
    F --> C
    C -->|"no en queden"| G["Bytecode final .class"]

Un processador, en esquelet:

package com.nexussoftware.bibliotech.processador;

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

@SupportedAnnotationTypes("com.nexussoftware.bibliotech.anotacions.CampCsv")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class ProcessadorCampCsv extends AbstractProcessor {

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

        for (Element element : entorn.getElementsAnnotatedWith(CampCsv.class)) {

            // Comprovacio en TEMPS DE COMPILACIO
            if (element.getModifiers().contains(Modifier.STATIC)) {
                processingEnv.getMessager().printMessage(
                        Diagnostic.Kind.ERROR,
                        "@CampCsv no es pot posar en un camp estatic",
                        element);
            }
            // Aqui es podria generar codi amb
            // processingEnv.getFiler().createSourceFile(...)
        }
        return true;   // true = aquestes anotacions ja estan processades
    }
}

Es registra a META-INF/services/javax.annotation.processing.Processor i s'activa amb -processorpath.

L'exemple canònic és Lombok. Quan escrius:

@Data
public class Llibre {
    private String isbn;
    private String titol;
}

...el processador de Lombok genera durant la compilació els getIsbn(), getTitol(), setIsbn(), setTitol(), equals(), hashCode() i toString(). El .class resultant els conté tots, encara que a la teva font no n'aparegui cap. Per això les anotacions de Lombok són SOURCE: compleixen la seva funció al compilador i desapareixen. Veuràs Lombok a 11-07.

Aspecte Processador (compilació) Reflexió (execució)
Quan actua Durant javac Amb el programa en marxa
Retenció necessària SOURCE n'hi ha prou RUNTIME obligatòria
Pot generar codi No (llevat de proxies, 10-03)
Pot fer fallar la compilació No
Cost en execució Zero Sí, mesurable
Exemples Lombok, MapStruct, Dagger Spring, Hibernate, JUnit

Aquesta taula explica una tendència real de l'ecosistema: cada vegada més frameworks mouen feina de l'execució a la compilació (Micronaut, Quarkus, Spring AOT), perquè arrencar més ràpid i consumir menys memòria importa molt en contenidors.

  1. Com es llegeixen (II): reflexió en execució

La segona forma, i la que desenvoluparàs per complet a la lliçó següent. En execució, qualsevol element anotat respon a aquests mètodes:

import java.lang.reflect.Method;

Method metode = GestorPrestecs.class.getMethod("prestar", String.class, String.class);

// Te l anotacio?
if (metode.isAnnotationPresent(Auditable.class)) {

    // Obtenir-la i llegir-ne els elements
    Auditable auditable = metode.getAnnotation(Auditable.class);
    System.out.println("Operacio: " + auditable.value());
    System.out.println("Nivell:   " + auditable.nivell());
}

// Totes les anotacions (incloses les heretades)
for (Annotation a : metode.getAnnotations()) {
    System.out.println(a);
}

// Nomes les declarades directament
for (Annotation a : metode.getDeclaredAnnotations()) { }

// Repetibles
Responsable[] responsables = CatalegService.class.getAnnotationsByType(Responsable.class);
Operacio: prestar
Nivell:   ALTA
@com.nexussoftware.bibliotech.anotacions.Auditable(value="prestar", nivell=ALTA)

Requisit absolut: @Retention(RetentionPolicy.RUNTIME). Sense ella, isAnnotationPresent retorna false i no hi ha cap missatge que t'expliqui per què.

Això és exactament el que fan Spring, Hibernate i JUnit en arrencar: recorren les classes, en busquen les anotacions i construeixen a partir d'elles la configuració de l'aplicació. A 10-03 n'escriuràs un.

  1. Per a què les fan servir els frameworks reals

Un recorregut ràpid, només perquè reconeguis el patró. Tot això és el mòdul 11; aquí només interessa veure que són anotacions corrents llegides per reflexió.

Hibernate / JPA (11-03) — mapen classes a taules:

@Entity
@Table(name = "materials")
public class Llibre {

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

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

En arrencar, Hibernate llegeix aquestes anotacions per reflexió i construeix el mapatge objecte-relacional: sap quina taula, quines columnes, quina és la clau primària i com generar l'SQL.

Spring (11-02) — declaren components i injecció de dependències:

@Service
public class GestorPrestecs {

    private final Repositori<Material> cataleg;

    @Autowired
    public GestorPrestecs(Repositori<Material> cataleg) {
        this.cataleg = cataleg;
    }

    @Transactional
    public Resultat<Prestec> prestar(String isbn, String empleat) { ... }
}

Spring escaneja el classpath, troba les classes amb @Service, les instancia, resol les seves dependències per tipus i les injecta. I @Transactional fa una cosa que reconeixeràs a 10-03: embolcalla l'objecte en un proxy dinàmic que obre una transacció abans de cada crida i la confirma després.

JUnit (11-04) — marquen què executar:

class GestorPrestecsTest {

    @BeforeEach
    void prepararCataleg() { ... }

    @Test
    @DisplayName("Prestar un material disponible retorna exit")
    void prestarDisponible() { ... }
}

JUnit recorre les classes de prova, busca els mètodes amb @Test i els invoca per reflexió.

El patró és sempre el mateix:

graph LR
    A["Tu escrius una anotacio<br/>sobre la teva classe"] --> B["El framework escaneja<br/>en arrencar"]
    B --> C["Llegeix l anotacio<br/>per reflexio"]
    C --> D["Construeix configuracio<br/>o comportament"]
    D --> E["Instancia, injecta,<br/>embolcalla en proxy, invoca"]

Quan a 10-03 escriguis l'ExportadorAnotat de BiblioTech, estaràs fent exactament els passos 2, 3 i 4 amb les teves pròpies mans. Després d'això, Spring deixarà de ser màgia.

  1. Configuració al costat del codi enfront d'XML

Mereix un apartat propi perquè explica per què l'ecosistema es va moure cap a les anotacions.

Abans de les anotacions (Java 5, 2004), tota la configuració dels frameworks vivia en XML separat del codi:

<bean id="gestorPrestecs" class="com.nexussoftware.bibliotech.servei.GestorPrestecs">
    <constructor-arg ref="repositoriCataleg"/>
</bean>
<bean id="repositoriCataleg" class="com.nexussoftware.bibliotech.servei.Repositori">
    <constructor-arg value="cataleg"/>
</bean>

Enfront de:

@Service
public class GestorPrestecs {
    public GestorPrestecs(Repositori<Material> cataleg) { ... }
}
Criteri Anotacions XML extern
Proximitat La configuració és on és el codi Cal saltar entre dos fitxers
Verbositat Mínima Molt cerimonial
Refactorització L'IDE reanomena classe i anotació alhora L'XML es queda amb el nom vell i falla en execució
Verificació El compilador comprova tipus i noms Cadenes de text sense comprovar
Canviar sense recompilar Impossible
Configuració diferent per entorn Difícil Natural
Veure tota la configuració d'un cop d'ull Impossible, està repartida Sí, en un fitxer

Cap de les dues no guanya en tot, i per això els frameworks moderns fan servir les dues: anotacions per a l'estructural (què és un servei, què s'injecta, què mapa a quina taula) i fitxers externs per al que canvia entre entorns (URL de la base de dades, credencials, mides de pool). És exactament el que ja vas fer a 07-07 amb Configuracio i ReglesNegoci sobre Properties, i el que veuràs formalitzat a 11-02 amb application.properties.

  1. Quan NO fer servir anotacions

Les anotacions són atractives i se n'abusa. Quatre casos clars on són l'eina equivocada:

1. Lògica de negoci. Una anotació no pot contenir codi. Intentar expressar regles complexes en elements d'anotació acaba en cadenes que cal analitzar:

// MALAMENT: has inventat un llenguatge dins d una cadena
@Regla("si dies > 15 && empleat.categoria != 'DIRECCIO' llavors multa = dies * 0.25")
public double calcularMulta(Prestec p) { ... }

Això és codi disfressat, sense comprovació del compilador, sense depurador, sense autocompletat. Escriu-ho en Java.

2. Configuració que canvia per entorn. Una anotació es compila dins del .class. Canviar-la exigeix recompilar i tornar a desplegar:

// MALAMENT: canviar de servidor obliga a recompilar
@ConnexioBd(url = "jdbc:postgresql://produccio.nexus:5432/bibliotech")
public class MagatzemPrestecs { }

Això va en un .properties o en una variable d'entorn.

3. Valors que canvien sovint. Llindars, límits, terminis. @LimitPrestecs(5) sembla còmode fins que direcció en vol 7 un dimarts a la tarda.

4. Quan una interfície ho expressa millor. Si vols dir "aquesta classe es pot exportar", una interfície Exportable et dona comprovació del compilador, autocompletat i polimorfisme. Una anotació @Exportable només et dona una etiqueta que cal llegir per reflexió.

Fes servir una anotació quan... Fes servir una altra cosa quan...
La informació és estructural i estable El valor canvia per entorn → Properties
És transversal a moltes classes (auditoria, transaccions) És lògica → escriu-la en Java
L'ha de consumir una eina o framework El comportament és polimòrfic → interfície
Va lligada a l'element del codi És una llista llarga i jeràrquica → fitxer extern

  1. BiblioTech: @CampCsv

Hora d'aplicar-ho. A 07-07 vas escriure ExportadorCatalegCsv a mà, i té un problema real:

public class ExportadorCatalegCsv {

    public String aLinia(Llibre llibre) {
        return String.join(";",
                llibre.getIsbn(),
                llibre.getTitol(),
                llibre.getAutor(),
                String.valueOf(llibre.getPagines()));
    }

    public String capcalera() {
        return "ISBN;Titol;Autor;Pagines";
    }
}

Tres defectes: cal escriure un exportador per cada classe, l'ordre de la capçalera i el dels valors es poden desincronitzar sense que ningú avisi, i afegir un camp a Llibre obliga a tocar dos mètodes en un altre fitxer (i si s'oblida, el CSV surt malament en silenci).

La solució és marcar els camps a la mateixa classe:

package com.nexussoftware.bibliotech.anotacions;

import java.lang.annotation.*;

/**
 * Marca un camp com a exportable a CSV.
 *
 * La llegeix l ExportadorAnotat de 10-03, que recorre per reflexio
 * els camps de qualsevol entitat, es queda amb els anotats,
 * els ordena per 'ordre' i construeix capcalera i linia.
 *
 * RUNTIME es OBLIGATORI: sense ell, la reflexio no la veura.
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.RECORD_COMPONENT})
public @interface CampCsv {

    /**
     * Capcalera de la columna.
     * Buit = fer servir el nom del camp (no s hi pot posar null: apartat 10).
     */
    String nom() default "";

    /** Posicio de la columna. Els camps s ordenen per aquest valor. */
    int ordre();

    /** Format opcional per a String.format. Buit = String.valueOf. */
    String format() default "";

    /** Si es true, el valor s emmascara en exportar (dades personals). */
    boolean sensible() default false;
}

I s'aplica:

package com.nexussoftware.bibliotech.domini;

import com.nexussoftware.bibliotech.anotacions.CampCsv;

public class Llibre extends Material {

    @CampCsv(nom = "ISBN", ordre = 1)
    private final String isbn;

    @CampCsv(nom = "Titol", ordre = 2)
    private final String titol;

    @CampCsv(nom = "Autor", ordre = 3)
    private final String autor;

    @CampCsv(nom = "Pagines", ordre = 4)
    private final int pagines;

    @CampCsv(nom = "Valoracio", ordre = 5, format = "%.2f")
    private final double valoracio;

    // SENSE anotacio: es estat intern, no s exporta
    private boolean prestat;

    // SENSE anotacio: memoria cau interna
    private transient String resumCalculat;

    public Llibre(String isbn, String titol, String autor, int pagines, double valoracio) {
        super(isbn, titol);
        this.isbn = isbn;
        this.titol = titol;
        this.autor = autor;
        this.pagines = pagines;
        this.valoracio = valoracio;
    }

    // getters...
}

I al record Fitxa de 04-07, aprofitant RECORD_COMPONENT:

package com.nexussoftware.bibliotech.domini;

import com.nexussoftware.bibliotech.anotacions.CampCsv;

public record Fitxa(
        @CampCsv(nom = "ISBN", ordre = 1) String isbn,
        @CampCsv(nom = "Titol", ordre = 2) String titol,
        @CampCsv(nom = "Disponible", ordre = 3) boolean disponible) {
}

I a Prestec, amb un camp sensible:

public class Prestec implements Identificable {

    @CampCsv(nom = "Referencia", ordre = 1)
    private final String referencia;

    @CampCsv(nom = "ISBN", ordre = 2)
    private final String isbn;

    @CampCsv(nom = "Empleat", ordre = 3, sensible = true)
    private final String empleat;

    @CampCsv(nom = "Dia prestec", ordre = 4)
    private final int diaPrestec;       // continuara sent int fins a 10-05

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

Fixa't en el que ha canviat conceptualment. La informació "aquest camp s'exporta i en aquesta posició" ha passat d'estar a ExportadorCatalegCsv a estar al costat del camp que descriu. Quan algú afegeixi un camp a Llibre, la decisió d'exportar-lo o no està a la vista, a la mateixa línia. I l'exportador —que encara no existeix— servirà per a Llibre, Prestec, Fitxa, SalaReunions i qualsevol cosa que vingui després, sense conèixer-les.

Això és exactament el que fa Jackson amb @JsonProperty (11-07) i el que fa Hibernate amb @Column (11-03).

  1. BiblioTech: @Auditable

El segon cas. Nexus Software exigeix registrar qui fa què amb els materials. A 06-07 ho vas resoldre cridant RegistreOperacions a mà a cada mètode:

public Resultat<Prestec> prestar(String isbn, String empleat) {
    registre.registrar("prestar", empleat, isbn);       // repetit en 14 metodes
    // ... logica real
}

Catorze crides idèntiques que cal recordar de posar, que ningú no comprova i que embruten la lògica. És un assumpte transversal (cross-cutting concern): no pertany a la lògica de préstecs, però travessa tots els seus mètodes. I és justament el cas on una anotació brilla.

package com.nexussoftware.bibliotech.anotacions;

import com.nexussoftware.bibliotech.domini.Gravetat;
import java.lang.annotation.*;

/**
 * Marca una operacio que ha de quedar registrada a l auditoria.
 *
 * Per si sola NO registra res: es una etiqueta. El proxy dinamic
 * de 10-03 la llegeix i intercepta les crides als metodes marcats.
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Auditable {

    /**
     * Nom de l operacio al registre.
     * Es diu 'value' per permetre la forma abreujada @Auditable("prestar").
     * Buit = fer servir el nom del metode.
     */
    String value() default "";

    /** Importancia de l operacio. */
    Gravetat nivell() default Gravetat.MITJANA;

    /** Si es true, tambe es registren els arguments de la crida. */
    boolean registrarArguments() default false;

    /** Index dels arguments que NO han d apareixer al registre. */
    int[] argumentsSensibles() default {};
}

Aplicada al servei:

package com.nexussoftware.bibliotech.servei;

import com.nexussoftware.bibliotech.anotacions.Auditable;
import com.nexussoftware.bibliotech.domini.*;

/**
 * @Auditable a nivell de TYPE: valor per defecte per a tota la classe.
 * Els metodes poden afinar-lo amb la seva propia anotacio.
 */
@Auditable(nivell = Gravetat.BAIXA)
public class GestorPrestecs implements ServeiPrestecs {

    private final Repositori<Material> cataleg;
    private final Repositori<Prestec> prestecs;

    public GestorPrestecs(Repositori<Material> cataleg, Repositori<Prestec> prestecs) {
        this.cataleg = cataleg;
        this.prestecs = prestecs;
    }

    @Override
    @Auditable(value = "prestar", nivell = Gravetat.ALTA, registrarArguments = true)
    public Resultat<Prestec> prestar(String isbn, String empleat) {
        Material material = cataleg.cercarPerId(isbn).orElse(null);
        if (material == null) {
            return Resultat.fallada("No existeix cap material amb ISBN " + isbn);
        }
        if (material.estaPrestat()) {
            return Resultat.fallada("El material '" + material.getTitol() + "' ja esta prestat");
        }
        material.marcarPrestat(empleat);
        Prestec prestec = new Prestec(seguentReferencia(), isbn, empleat);
        prestecs.desar(prestec);
        return Resultat.exit(prestec);
        // Ni una sola linia d auditoria: la logica esta NETA
    }

    @Override
    @Auditable(value = "retornar", nivell = Gravetat.ALTA, registrarArguments = true)
    public Resultat<Prestec> retornar(String referencia) { ... }

    @Override
    @Auditable(value = "canviarClau", registrarArguments = true, argumentsSensibles = {1})
    public Resultat<Void> canviarClauEmpleat(String empleat, String clauNova) { ... }

    // SENSE @Auditable: consulta de nomes lectura, no s audita
    @Override
    public List<Prestec> llistarActius() { ... }
}

Tres detalls de disseny que mereixen atenció:

L'anotació a nivell de classe actua com a valor per defecte. El motor de 10-03 mirarà primer el mètode i, si no la troba, la classe. És exactament el mecanisme de @Transactional d'Spring.

argumentsSensibles = {1} resol elegantment el que a 09-06 era una regla escrita en un comentari: "no registris mai tokens ni credencials". Ara la regla està codificada al costat del mètode al qual s'aplica, i el motor no la pot oblidar.

La lògica del mètode està neta. prestar() parla de préstecs i de res més. L'auditoria és una decisió declarada en una etiqueta.

I ara, la frase important: tot això no fa absolutament res encara. Pots executar BiblioTechApp, prestar vint llibres i no es registrarà ni una línia. @CampCsv no exporta res i @Auditable no audita res, perquè són etiquetes i ningú no les està llegint.

Escriure aquest lector és la lliçó següent.

Errors Comuns i Consells

1. Oblidar @Retention(RetentionPolicy.RUNTIME). L'error número u, sense discussió. La retenció per defecte és CLASS: l'anotació queda al .class però no és visible per reflexió, i isAnnotationPresent() retorna false sense cap pista. Si la teva anotació l'ha de llegir codi en execució, la metaanotació és obligatòria.

2. Creure que una anotació fa alguna cosa. @Auditable no audita. @Transactional no obre transaccions — ho fa el proxy que Spring crea en llegir-la. Si poses la teva anotació i no passa res, no està trencada: és que falta el lector. Corol·lari: @Transactional sobre un mètode privat o cridat des de dins de la mateixa classe no funciona, perquè la crida no passa pel proxy.

3. Posar @SuppressWarnings("all"). Silencia tot, inclosos els avisos que encara no existeixen. Fes servir l'identificador concret i l'àmbit mínim.

4. Ometre @Override. Un equals(Material) en lloc d'equals(Object) compila, sembla correcte i trenca totes les col·leccions. Posa-la sempre.

5. Intentar fer servir null en un element. No es pot, ni com a valor ni com a default. Fes servir "", un array buit {} o una constant sentinella.

6. Intentar tipus no permesos. List<String>, LocalDate o les teves pròpies classes no valen. Només primitius, String, Class, enum, anotacions i arrays d'això.

7. No fer servir getAnnotationsByType amb repetibles. Amb una sola aparició, getAnnotation funciona; amb dues, retorna null perquè el compilador les va ficar al contenidor. getAnnotationsByType funciona en tots dos casos.

8. Ficar configuració d'entorn en anotacions. Una URL de base de dades anotada obliga a recompilar per canviar de servidor. Això va en Properties.

9. Anotacions sense @Target. Es poden posar a qualsevol lloc, inclosos els que no tenen sentit, i el teu motor rebrà elements que no espera. Declarar el destí és documentació verificada.

Consell 1: dissenya l'anotació pensant en qui l'escriu. Anomena value l'element principal per permetre la forma abreujada, i posa default a tota la resta. Comparar @CampCsv(ordre = 3) amb @CampCsv(nom = "", ordre = 3, format = "", sensible = false) deixa clar per què.

Consell 2: fes servir enum en lloc de String per a valors tancats. Gravetat nivell() dona autocompletat i comprovació del compilador; String nivell() dona errades que fallen en execució.

Consell 3: documenta el que fa el lector, no el que fa l'anotació. El Javadoc de @CampCsv ha de dir "la llegeix ExportadorAnotat, que ordena per ordre i fa servir nom com a capçalera". Sense això, qui la posi no sap què esperar.

Consell 4: mantén un paquet anotacions propi. Tenir-les juntes a com.nexussoftware.bibliotech.anotacions fa evident l'inventari de metadades del projecte i evita duplicats.

Consell 5: davant del dubte entre anotació i interfície, tria la interfície. El compilador comprova les interfícies. Reserva les anotacions per al que una interfície no pot expressar: informació amb paràmetres (ordre = 3) o comportament transversal.

Exercicis

Exercici 1: @Validar amb regles declaratives

Defineix una anotació @Validar per a BiblioTech que permeti declarar restriccions sobre els camps d'una entitat:

  • Aplicable a camps i components de record, visible en execució.
  • Elements: obligatori (booleà, per defecte true), longitudMinima (enter, per defecte 0), longitudMaxima (enter, per defecte Integer.MAX_VALUE), patro (cadena, per defecte buida) i missatge (cadena, per defecte buida).
  • Anota-hi els camps de Llibre: l'ISBN obligatori amb patró 978-\d{10}, el títol obligatori d'entre 1 i 200 caràcters, l'autor opcional de màxim 120.
  • Escriu a més una anotació repetible @Exemple(String) amb la seva contenidora, per documentar valors vàlids de cada camp, i posa-la dues vegades sobre l'ISBN.
  • No escriguis el validador: és 10-03. Sí que escriu un main que imprimeixi, fent servir getDeclaredFields() i getAnnotation(), quins camps estan anotats i amb quins valors.

Exercici 2: Anotacions estàndard ben posades

Et donen aquesta classe de BiblioTech, escrita sense cap anotació i amb diversos problemes latents. Corregeix-la afegint-hi les anotacions estàndard del JDK que corresponguin i arreglant el que les anotacions destapin.

public class CatalegLlegat {

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

    public boolean equals(CatalegLlegat altre) {
        return materials.equals(altre.materials);
    }

    public String toString() {
        return "CatalegLlegat amb " + materials.size() + " materials";
    }

    public void afegirTots(List... llistes) {
        for (List llista : llistes) {
            for (Object o : llista) {
                Material m = (Material) o;
                materials.put(m.getId(), m);
            }
        }
    }

    public void agregarLlibre(String isbn, String titol) {
        materials.put(isbn, new Llibre(isbn, titol));
    }

    public interface FiltreMaterial {
        boolean accepta(Material m);
    }
}

Requisits: agregarLlibre ha de quedar obsoleta des de la versió 3.1, marcada per a eliminació, amb alternativa afegir(Material). afegirTots ha d'acceptar varargs genèrics sense avisos. FiltreMaterial ha de quedar protegida com a interfície funcional. I equals ha de funcionar de debò amb HashMap.

Exercici 3: @Reintentable amb política declarativa

A 09-06 vas escriure ClientHttpResistent amb la política de reintents codificada dins del mètode. Dissenya una anotació que la declari:

  • @Reintentable, aplicable a mètodes, visible en execució.
  • Elements: intents (per defecte 3), esperaInicialMs (per defecte 200), factorDeCreixement (double, per defecte 2.0), excepcions (array de Class<? extends Exception>, per defecte {Exception.class}), i nomesIdempotents (booleà, per defecte true).
  • Anota-hi tres mètodes de ClientMetadades amb polítiques diferents.
  • Escriu un mètode descriurePolitica(Class<?>) que, per reflexió, imprimeixi una taula llegible de la política de cada mètode anotat, calculant i mostrant l'espera acumulada màxima.

Afegeix a més una metaanotació pròpia @Transversal (aplicable només a altres anotacions) que marqui @Auditable i @Reintentable com a aspectes transversals, i demostra que es pot llegir des d'Auditable.class.

Solucions

Solució 1

package com.nexussoftware.bibliotech.anotacions;

import java.lang.annotation.*;

/**
 * Restriccions declaratives sobre el valor d un camp.
 * El validador que les consumeix s escriu a 10-03.
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)          // OBLIGATORI per llegir-la per reflexio
@Target({ElementType.FIELD, ElementType.RECORD_COMPONENT})
public @interface Validar {

    boolean obligatori() default true;

    int longitudMinima() default 0;

    int longitudMaxima() default Integer.MAX_VALUE;

    /** Expressio regular. Buit = sense patro (no s hi pot posar null). */
    String patro() default "";

    /** Missatge personalitzat. Buit = el motor en genera un. */
    String missatge() default "";
}
package com.nexussoftware.bibliotech.anotacions;

import java.lang.annotation.*;

/** CONTENIDORA de @Exemple. El seu value es un array del tipus repetible. */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.RECORD_COMPONENT})
public @interface Exemples {
    Exemple[] value();
}
package com.nexussoftware.bibliotech.anotacions;

import java.lang.annotation.*;

/** Documenta un valor valid d exemple. Repetible. */
@Documented
@Repeatable(Exemples.class)                  // declara qui la conte
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.RECORD_COMPONENT})
public @interface Exemple {
    String value();                          // 'value' permet @Exemple("978-0000000001")
}

L'entitat anotada:

package com.nexussoftware.bibliotech.domini;

import com.nexussoftware.bibliotech.anotacions.*;

public class Llibre extends Material {

    @Validar(obligatori = true, patro = "978-\\d{10}",
             missatge = "L ISBN ha de seguir el format 978-XXXXXXXXXX")
    @Exemple("978-0000000001")
    @Exemple("978-0000000002")
    private final String isbn;

    @Validar(obligatori = true, longitudMinima = 1, longitudMaxima = 200)
    @Exemple("Java Eficac")
    private final String titol;

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

    // Sense anotar: no es valida
    private boolean prestat;

    public Llibre(String isbn, String titol, String autor) {
        super(isbn, titol);
        this.isbn = isbn;
        this.titol = titol;
        this.autor = autor;
    }
}

L'inspector:

package com.nexussoftware.bibliotech;

import com.nexussoftware.bibliotech.anotacions.*;
import com.nexussoftware.bibliotech.domini.Llibre;

import java.lang.reflect.Field;

public class InspectorDeRegles {

    public static void main(String[] args) {

        System.out.printf("%-10s %-6s %-5s %-6s %-20s%n",
                "CAMP", "OBLIG", "MIN", "MAX", "PATRO");
        System.out.println("-".repeat(56));

        for (Field camp : Llibre.class.getDeclaredFields()) {

            // Sense l anotacio, aquest camp no es valida
            if (!camp.isAnnotationPresent(Validar.class)) {
                continue;
            }

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

            System.out.printf("%-10s %-6s %-5d %-6s %-20s%n",
                    camp.getName(),
                    regla.obligatori() ? "si" : "no",
                    regla.longitudMinima(),
                    regla.longitudMaxima() == Integer.MAX_VALUE ? "-" : regla.longitudMaxima(),
                    regla.patro().isEmpty() ? "-" : regla.patro());

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

            // REPETIBLES: getAnnotationsByType funciona amb 0, 1 o N aparicions.
            // getAnnotation(Exemple.class) retornaria null quan n hi ha dues.
            Exemple[] exemples = camp.getAnnotationsByType(Exemple.class);
            if (exemples.length > 0) {
                StringBuilder sb = new StringBuilder("           exemples: ");
                for (Exemple e : exemples) {
                    sb.append(e.value()).append("  ");
                }
                System.out.println(sb);
            }
        }

        // Demostracio del parany de les repetibles
        System.out.println();
        try {
            Field isbn = Llibre.class.getDeclaredField("isbn");
            System.out.println("isbn: getAnnotation(Exemple)      -> " + isbn.getAnnotation(Exemple.class));
            System.out.println("isbn: getAnnotation(Exemples)     -> present="
                    + isbn.isAnnotationPresent(Exemples.class));
            System.out.println("isbn: getAnnotationsByType(Exemple) -> "
                    + isbn.getAnnotationsByType(Exemple.class).length + " elements");

            Field titol = Llibre.class.getDeclaredField("titol");
            System.out.println("titol: getAnnotation(Exemple)     -> " + titol.getAnnotation(Exemple.class));
        } catch (NoSuchFieldException e) {
            throw new IllegalStateException(e);
        }
    }
}
CAMP       OBLIG  MIN   MAX    PATRO
--------------------------------------------------------
isbn       si     0     -      978-\d{10}
           missatge: L ISBN ha de seguir el format 978-XXXXXXXXXX
           exemples: 978-0000000001  978-0000000002
titol      si     1     200    -
           exemples: Java Eficac
autor      no     0     120    -

isbn: getAnnotation(Exemple)      -> null
isbn: getAnnotation(Exemples)     -> present=true
isbn: getAnnotationsByType(Exemple) -> 2 elements
titol: getAnnotation(Exemple)     -> @...Exemple(value="Java Eficac")

Comentaris. La sortida final és la part instructiva.

Amb dues @Exemple, getAnnotation(Exemple.class) retorna null. El compilador les va embolcallar en @Exemples({...}), així que el camp ja no porta directament cap @Exemple. Amb una de sola (el títol) sí que funciona. Aquesta asimetria és exactament per la qual cal fer servir sempre getAnnotationsByType amb anotacions repetibles.

El patró "978-\\d{10}" porta doble barra perquè és una cadena Java que conté una expressió regular: \\d a la font és \d a la cadena.

El camp prestat no apareix perquè no està anotat, i el continue se'l salta. Aquest és el mecanisme bàsic de tot motor d'anotacions: recórrer-ho tot i quedar-se amb el marcat.

Solució 2

package com.nexussoftware.bibliotech.servei;

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

public class CatalegLlegat {

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

    /**
     * CORREGIT: la signatura original era equals(CatalegLlegat), que SOBRECARREGA
     * en lloc de sobreescriure. Amb @Override el compilador ho hauria tallat.
     * HashMap, HashSet i contains() criden equals(Object).
     */
    @Override
    public boolean equals(Object altre) {
        if (this == altre) return true;
        if (!(altre instanceof CatalegLlegat)) return false;
        return materials.equals(((CatalegLlegat) altre).materials);
    }

    /**
     * Sobreescriure equals OBLIGA a sobreescriure hashCode (03-09).
     * L @Override sobre equals ens ha recordat que faltava.
     */
    @Override
    public int hashCode() {
        return Objects.hash(materials);
    }

    @Override
    public String toString() {
        return "CatalegLlegat amb " + materials.size() + " materials";
    }

    /**
     * CORREGIT: era List... (tipus cru) amb conversio a Material a dins.
     * Ara es generic i tipat. @SafeVarargs perque el metode
     * NOMES LLEGEIX de l array de varargs: no el guarda ni el retorna.
     * Requereix ser final (o static/private) per poder anotar-lo.
     */
    @SafeVarargs
    public final void afegirTots(List<? extends Material>... llistes) {
        for (List<? extends Material> llista : llistes) {   // ? extends: nomes llegim (PECS, 10-01)
            for (Material m : llista) {
                materials.put(m.getId(), m);
            }
        }
    }

    /** Substitut d agregarLlibre. */
    public void afegir(Material material) {
        Objects.requireNonNull(material, "material");
        materials.put(material.getId(), material);
    }

    /**
     * @deprecated Des de 3.1 fes servir {@link #afegir(Material)}, que accepta
     *             qualsevol material i no només llibres. S'eliminarà a la 4.0.
     */
    @Deprecated(since = "3.1", forRemoval = true)
    public void agregarLlibre(String isbn, String titol) {
        afegir(new Llibre(isbn, titol));
    }

    /**
     * @FunctionalInterface protegeix el contracte: si algu afegeix
     * un segon metode abstracte, la compilacio falla aqui
     * en lloc de trencar tots els qui la fan servir amb lambdes.
     */
    @FunctionalInterface
    public interface FiltreMaterial {
        boolean accepta(Material m);

        // Els default NO compten com a abstractes
        default FiltreMaterial i(FiltreMaterial altre) {
            return m -> this.accepta(m) && altre.accepta(m);
        }
    }
}

Demostració que ara funciona:

public class ProvaCatalegLlegat {

    @SuppressWarnings("removal")   // fem servir a proposit el metode obsolet, i ho justifiquem
    public static void main(String[] args) {

        CatalegLlegat a = new CatalegLlegat();
        CatalegLlegat b = new CatalegLlegat();

        List<Llibre> llibres = List.of(
                new Llibre("978-0000000001", "Java Eficac"),
                new Llibre("978-0000000002", "Patrons de Disseny"));

        a.afegirTots(llibres);   // sense avisos: varargs generics segurs
        b.afegirTots(llibres);

        // Amb l equals correcte, aixo ARA funciona
        System.out.println("a.equals(b): " + a.equals(b));

        Set<CatalegLlegat> conjunt = new HashSet<>();
        conjunt.add(a);
        conjunt.add(b);
        System.out.println("Mida del Set: " + conjunt.size() + " (abans hauria estat 2)");

        // El metode obsolet continua funcionant, amb avis en compilacio
        a.agregarLlibre("978-0000000003", "Refactoritzacio");

        // La interficie funcional es fa servir amb lambda
        CatalegLlegat.FiltreMaterial disponibles = m -> !m.estaPrestat();
        CatalegLlegat.FiltreMaterial ambTitol    = m -> m.getTitol() != null;
        CatalegLlegat.FiltreMaterial ambdos = disponibles.i(ambTitol);
        System.out.println("Filtre compost creat: " + (ambdos != null));
    }
}
a.equals(b): true
Mida del Set: 1 (abans hauria estat 2)
Filtre compost creat: true

Comentaris.

@Override sobre equals va destapar dos bugs, no un. El primer, la signatura incorrecta. El segon, en cascada: en arreglar-la, el contracte d'Object (03-09) exigeix hashCode() coherent, que faltava. Un HashSet amb equals correcte i hashCode heretat continuaria donant mida 2.

@SafeVarargs exigeix final. El mètode va passar de public void a public final void. Sense final, static o private, el compilador rebutja l'anotació: no pots prometre que una implementació futura serà segura.

El @SuppressWarnings("removal") del main està justificat al comentari, que és l'única forma legítima de fer-lo servir: aquí cridem a propòsit el mètode obsolet per demostrar que continua funcionant.

Solució 3

package com.nexussoftware.bibliotech.anotacions;

import java.lang.annotation.*;

/**
 * METAANOTACIO: nomes es pot posar sobre altres anotacions.
 * Marca un aspecte que travessa l aplicacio en lloc de
 * pertanyer a una capa concreta.
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)     // NOMES sobre anotacions
public @interface Transversal {

    /** Ordre d aplicacio quan diversos aspectes s apilen. */
    int ordre() default 0;

    String descripcio() default "";
}
package com.nexussoftware.bibliotech.anotacions;

import java.lang.annotation.*;

/**
 * Declara la politica de reintents d una operacio de xarxa.
 * Substitueix la politica codificada a ma a ClientHttpResistent (09-06).
 */
@Documented
@Transversal(ordre = 20, descripcio = "Reintents amb espera creixent")
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Reintentable {

    /** Nombre total d intents, inclos el primer. */
    int intents() default 3;

    /** Espera abans del segon intent, en millisegons. */
    long esperaInicialMs() default 200;

    /** Multiplicador de l espera a cada reintent. */
    double factorDeCreixement() default 2.0;

    /**
     * Excepcions que activen el reintent.
     * Class<? extends Exception>[] es un tipus PERMES en anotacions
     * (Class i arrays de Class ho son), i a mes esta acotat (10-01).
     */
    Class<? extends Exception>[] excepcions() default { Exception.class };

    /** Si es true, nomes es reintenta si l operacio es idempotent (09-06). */
    boolean nomesIdempotents() default true;
}

Aplicada amb tres polítiques diferents:

package com.nexussoftware.bibliotech.xarxa;

import com.nexussoftware.bibliotech.anotacions.Reintentable;

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

public class ClientMetadades {

    /** Consulta de nomes lectura: es pot reintentar amb confianca. */
    @Reintentable(intents = 5, esperaInicialMs = 100, factorDeCreixement = 2.0,
                  excepcions = { SocketTimeoutException.class, ConnectException.class })
    public String consultarPerIsbn(String isbn) throws IOException { ... }

    /** Descarrega pesada: menys intents i esperes mes llargues. */
    @Reintentable(intents = 2, esperaInicialMs = 1000, factorDeCreixement = 3.0,
                  excepcions = { IOException.class })
    public byte[] descarregarPortada(String isbn) throws IOException { ... }

    /** POST no idempotent: MAI no es reintenta a cegues (09-06). */
    @Reintentable(intents = 1, nomesIdempotents = true)
    public void publicarValoracio(String isbn, int estrelles) throws IOException { ... }

    /** Sense anotacio: sense politica de reintents. */
    public boolean estaDisponible() { ... }
}

El descriptor:

package com.nexussoftware.bibliotech;

import com.nexussoftware.bibliotech.anotacions.*;
import com.nexussoftware.bibliotech.xarxa.ClientMetadades;

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

public class DescriptorDePolitiques {

    public static void main(String[] args) {
        descriurePolitica(ClientMetadades.class);

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

    public static void descriurePolitica(Class<?> tipus) {

        System.out.println("Politiques de reintent a " + tipus.getSimpleName());
        System.out.printf("%-22s %-8s %-9s %-7s %-11s %s%n",
                "METODE", "INTENTS", "ESPERA_1a", "FACTOR", "ESPERA_MAX", "EXCEPCIONS");
        System.out.println("-".repeat(96));

        // getDeclaredMethods no garanteix ordre: el fixem perque la sortida sigui estable
        Method[] metodes = tipus.getDeclaredMethods();
        Arrays.sort(metodes, Comparator.comparing(Method::getName));

        for (Method metode : metodes) {

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

            // Espera acumulada: e, e*f, e*f^2, ... per a (intents - 1) reintents
            double esperaTotal = 0;
            double espera = politica.esperaInicialMs();
            for (int i = 1; i < politica.intents(); i++) {
                esperaTotal += espera;
                espera *= politica.factorDeCreixement();
            }

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

            System.out.printf("%-22s %-8d %-9d %-7.1f %-11.0f %s%n",
                    metode.getName(),
                    politica.intents(),
                    politica.esperaInicialMs(),
                    politica.factorDeCreixement(),
                    esperaTotal,
                    excepcions);

            if (politica.nomesIdempotents() && politica.intents() > 1) {
                System.out.println("    avis: exigeix idempotencia; verifica que el metode ho sigui");
            }
        }
    }

    /** Llegeix les META-anotacions d una anotacio: es reflexio sobre reflexio. */
    public static void descriureMetaAnotacions(Class<? extends java.lang.annotation.Annotation> anotacio) {

        System.out.println("Metaanotacions de @" + anotacio.getSimpleName() + ":");

        Transversal t = anotacio.getAnnotation(Transversal.class);
        if (t != null) {
            System.out.printf("  @Transversal(ordre=%d) %s%n", t.ordre(), t.descripcio());
        } else {
            System.out.println("  no es un aspecte transversal");
        }

        Retention r = anotacio.getAnnotation(Retention.class);
        Target    d = anotacio.getAnnotation(Target.class);
        System.out.println("  retencio: " + (r == null ? "CLASS (per defecte)" : r.value()));
        System.out.println("  destins:  " + (d == null ? "qualsevol" : Arrays.toString(d.value())));
    }
}

Afegint @Transversal(ordre = 10, descripcio = "Auditoria d operacions") sobre @Auditable, la sortida és:

Politiques de reintent a ClientMetadades
METODE                 INTENTS  ESPERA_1a FACTOR  ESPERA_MAX  EXCEPCIONS
------------------------------------------------------------------------------------------------
consultarPerIsbn       5        100       2,0     1500        SocketTimeoutException, ConnectException
descarregarPortada     2        1000      3,0     1000        IOException
    avis: exigeix idempotencia; verifica que el metode ho sigui
publicarValoracio      1        200       2,0     0           Exception

Metaanotacions de @Reintentable:
  @Transversal(ordre=20) Reintents amb espera creixent
  retencio: RUNTIME
  destins:  [METHOD]
Metaanotacions de @Auditable:
  @Transversal(ordre=10) Auditoria d operacions
  retencio: RUNTIME
  destins:  [METHOD, TYPE]

Comentaris. Quatre observacions.

Class<? extends Exception>[] funciona perquè Class és un tipus permès, i a més està acotat amb genèrics (10-01): no hi pots posar String.class, el compilador ho impedeix. És un bon exemple de les dues característiques cooperant.

publicarValoracio amb intents = 1 té espera màxima 0, i és exactament el que es busca: un POST no idempotent que no es reintenta. L'anotació documenta la decisió en lloc que sigui un if perdut dins del mètode.

@Transversal és una metaanotació de debò, amb @Target(ANNOTATION_TYPE). Posar-la sobre una classe no compila. I llegir-la des d'Auditable.class demostra una cosa conceptualment important: una anotació és una classe, i per tant es pot inspeccionar com qualsevol altra. Retention i Target es llegeixen igual que les teves, perquè no tenen res d'especial.

I falta l'essencial: res d'això no reintenta res. descriurePolitica només descriu. Perquè @Reintentable funcioni de debò cal interceptar la crida, executar-la, capturar l'excepció, esperar i repetir — és a dir, un proxy dinàmic. Això és la lliçó següent.

Conclusió

Ja saps què és una anotació i, sobretot, què no és.

Una anotació és informació sobre el codi: una etiqueta adherida a una classe, un mètode, un camp o un paràmetre que no en canvia el comportament. @Auditable no audita. @Entity no crea taules. @Test no executa res. Algú les llegeix i actua, i aquesta és l'única forma en què una anotació produeix efectes.

Coneixes les estàndard del JDK i què fa realment cadascuna: @Override verifica que sobreescrius alguna cosa de debò i et salva de l'equals(Material) que sobrecarrega en lloc de sobreescriure i trenca en silenci totes les col·leccions; @Deprecated avisa, amb since per saber des de quan i forRemoval per distingir "hi ha alguna cosa millor" de "això desapareix"; @SuppressWarnings silencia avisos concrets, amb àmbit mínim i justificació escrita; @SafeVarargs és la teva promesa que no exposes l'array de varargs, i exigeix final, static o private per poder fer-la; @FunctionalInterface protegeix el contracte d'una interfície amb un sol mètode abstracte.

Escrius les teves amb @interface, els elements de les quals es declaren com a mètodes sense cos, amb default per fer-los opcionals i amb value com a nom màgic que habilita la forma abreujada. Saps quins tipus es permeten —primitius, String, Class, enum, anotacions i arrays d'això— i que null no és un valor possible, per la qual cosa la convenció és "", {} o un sentinella.

Domines les metaanotacions, començant per la que decideix si la teva anotació existirà tan sols: @Retention. SOURCE mor al compilador, CLASSel valor per defecte— queda al .class però és invisible per a la reflexió, i RUNTIME és l'única que arriba viva a l'execució. Aquest defecte és l'error número u amb anotacions pròpies i ara no t'enxamparà. Amb @Target declares on es pot posar, amb @Documented que formi part del contracte públic, amb @Inherited que les subclasses l'heretin, i amb @Repeatable que pugui aparèixer diverses vegades — sabent que el compilador les embolcalla a la contenidora i que per això cal fer servir sempre getAnnotationsByType. I reconeixes les anotacions de tipus de Java 8 (TYPE_USE, TYPE_PARAMETER), que existeixen per a verificadors externs com el Checker Framework.

Saps que hi ha exactament dues formes de llegir-les. En compilació, amb un processador d'anotacions que rep els elements marcats, pot fer fallar la compilació i pot generar codi nou — que és literalment el que fa Lombok quan @Data produeix els getters, els setters, equals, hashCode i toString que no vas escriure (11-07). I en execució, amb reflexió: isAnnotationPresent, getAnnotation, getAnnotationsByType. La primera costa zero en execució; la segona és el que fan Spring, Hibernate i JUnit en arrencar, i és la lliçó següent.

Entens per què l'ecosistema es va moure de l'XML extern a la configuració al costat del codi —proximitat, menys cerimonial, refactorització segura, verificació del compilador— i per què no va guanyar del tot: el que canvia per entorn continua vivint fora, a Properties. I saps quan no fer servir anotacions: per a lògica de negoci (acabes inventant un llenguatge dins d'una cadena), per a configuració d'entorn (obliga a recompilar), per a valors que canvien sovint, i quan una interfície expressaria el mateix amb comprovació del compilador.

BiblioTech té ara un paquet com.nexussoftware.bibliotech.anotacions. @CampCsv(nom, ordre, format, sensible) marca quins camps s'exporten i en quina columna, i està posada sobre Llibre, Prestec i el record Fitxa —aprofitant RECORD_COMPONENT—, movent aquesta decisió des d'ExportadorCatalegCsv fins a la línia del camp que descriu. @Auditable(value, nivell, registrarArguments, argumentsSensibles) marca quines operacions es registren, amb l'anotació de classe actuant com a valor per defecte i argumentsSensibles codificant la regla de 09-06 de no registrar credencials. I GestorPrestecs.prestar() ha quedat net: catorze crides repetides a RegistreOperacions han desaparegut de la lògica.

I no funciona res. Pots prestar vint llibres i no es registrarà ni una sola línia; pots exportar el catàleg i @CampCsv no haurà servit de res. Les etiquetes estan posades i no hi ha ningú llegint-les.

A 10-03, Reflexió, escrius aquest lector. Aprendràs a obtenir l'objecte Class<?> de tres formes diferents, a inspeccionar camps, mètodes i constructors —i la diferència entre getFields i getDeclaredFields, que confon tothom—, a crear instàncies i invocar mètodes sense conèixer-los en compilació, i a saltar-te l'encapsulament amb setAccessible(true). Amb això escriuràs l'ExportadorAnotat que recorre els camps marcats amb @CampCsv, els ordena i genera la línia CSV de qualsevol entitat sense conèixer-la. I faràs un pas més: un proxy dinàmic que intercepta cada crida a GestorPrestecs, veu l'@Auditable i registra l'operació sense que el mètode se n'assabenti. Quan aquest proxy funcioni, entendràs per dins com funciona @Transactional d'Spring — i els frameworks del mòdul 11 deixaran de semblar màgia.

Curs de Programació en Java

Mòdul 1: Introducció a Java

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

Mòdul 4: Programació orientada a objectes avançada

Mòdul 5: Estructures de dades i col·leccions

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

Mòdul 12: Construcció d'aplicacions del món real

© Copyright 2026. Tots els drets reservats