La lliçó anterior va acabar amb una frase incòmoda: BiblioTech té les seves anotacions posades i no funciona res. @CampCsv marca els camps exportables i no exporta res. @Auditable marca les operacions auditables i no registra res. Les etiquetes hi són, i no hi ha ningú llegint-les.

Aquesta lliçó escriu el lector.

La reflexió és la capacitat d'un programa per examinar-se i manipular-se a si mateix en temps d'execució: descobrir quines classes existeixen, quins camps i mètodes tenen, llegir-ne les anotacions, crear instàncies, invocar mètodes i modificar camps, tot això sense conèixer res d'això en temps de compilació.

Dit d'una altra manera: fins ara, per cridar llibre.getTitol() calia que la classe Llibre existís quan compilaves. Amb reflexió pots escriure codi que rebi un Object qualsevol, descobreixi que té un mètode anomenat getTitol, l'invoqui i retorni el resultat — i aquest codi continuarà funcionant d'aquí a tres anys amb classes que avui no existeixen.

Aquesta és exactament la raó per la qual Spring pot injectar els teus serveis, Hibernate pot mapar les teves entitats, JUnit pot executar les teves proves i Jackson pot convertir els teus objectes a JSON. Cap d'aquests frameworks no coneix les teves classes. Les descobreixen en execució, en llegeixen les anotacions i actuen. En acabar aquesta lliçó hauràs construït, amb les teves mans, versions reduïdes dels dos mecanismes que sostenen aquest edifici: un motor que consumeix anotacions i un proxy dinàmic que intercepta crides.

I amb això, els frameworks del mòdul 11 deixaran de ser màgia.

Contingut

  1. Què és la reflexió i què permet
  2. L'objecte Class<?>: les tres formes d'obtenir-lo
  3. Inspecció bàsica: noms, jerarquia i modificadors
  4. isEnum, isRecord, isInterface i companyia
  5. Camps: getFields enfront de getDeclaredFields
  6. Mètodes i constructors: la mateixa distinció
  7. Crear instàncies amb newInstance
  8. Invocar mètodes amb invoke
  9. Excepcions a la reflexió: InvocationTargetException
  10. Accedir al que és privat: setAccessible(true)
  11. El que trenca i el que el sistema de mòduls ja no permet
  12. Llegir anotacions en execució
  13. BiblioTech: l'ExportadorAnotat que consumeix @CampCsv
  14. Genèrics i reflexió: què sobreviu a l'esborrat
  15. Proxies dinàmics: Proxy i InvocationHandler
  16. BiblioTech: el proxy que consumeix @Auditable
  17. El preu de la reflexió
  18. Quan fer-la servir i quan no
  19. MethodHandles i VarHandle: l'alternativa moderna
  20. Errors Comuns i Consells
  21. Exercicis

  1. Què és la reflexió i què permet

El nom ve de la idea que el programa es mira en un mirall. La JVM manté, per a cada classe carregada, una descripció completa de la seva estructura: nom, superclasse, interfícies, camps, mètodes, constructors, anotacions. L'API de reflexió (java.lang.reflect més java.lang.Class) és la porta a aquesta descripció.

Compara els dos mons:

// ACCES NORMAL: tot es resol en COMPILACIO
Llibre llibre = new Llibre("Java Eficac", "978-0000000001");
String titol = llibre.getTitol();

El compilador comprova que Llibre existeix, que té un constructor amb aquesta signatura i que té un mètode getTitol() que retorna String. Si alguna cosa falla, no compila.

// ACCES PER REFLEXIO: tot es resol en EXECUCIO
Class<?> tipus = Class.forName("com.nexussoftware.bibliotech.domini.Llibre");
Object llibre = tipus.getDeclaredConstructor(String.class, String.class)
                     .newInstance("Java Eficac", "978-0000000001");
Method metode = tipus.getMethod("getTitol");
String titol = (String) metode.invoke(llibre);

Aquí Llibre és una cadena de text. El compilador no comprova res: no sap si aquesta classe existeix, ni si té aquest constructor, ni si té aquest mètode. Tot això es descobreix —o falla— en execució.

Aquest és el tracte: guanyes flexibilitat total i perds tota l'ajuda del compilador. Val la pena quan escrius alguna cosa que ha de funcionar amb classes que no coneixes; és una pèssima idea a la lògica de negoci de la teva aplicació.

Què permet exactament:

Capacitat Mètode clau
Descobrir l'estructura d'una classe getFields, getMethods, getConstructors
Llegir anotacions getAnnotation, isAnnotationPresent
Crear objectes sense conèixer el tipus getDeclaredConstructor().newInstance()
Invocar mètodes pel seu nom Method.invoke
Llegir i escriure camps, fins i tot privats Field.get, Field.set, setAccessible
Generar implementacions d'interfícies al vol Proxy.newProxyInstance

  1. L'objecte Class<?>: les tres formes d'obtenir-lo

Tot comença a Class<T>. Cada classe, interfície, enum, record, array i primitiu carregat a la JVM té exactament un objecte Class que el descriu.

// 1. LITERAL DE CLASSE: .class sobre el nom del tipus
Class<Llibre> tipus1 = Llibre.class;

// 2. DES D UNA INSTANCIA: getClass()
Llibre llibre = new Llibre("Java Eficac", "978-0000000001");
Class<? extends Llibre> tipus2 = llibre.getClass();

// 3. DES DEL SEU NOM COMPLET: Class.forName
Class<?> tipus3 = Class.forName("com.nexussoftware.bibliotech.domini.Llibre");
Forma Sintaxi Requereix la classe en compilació Tipus retornat Carrega la classe Excepció
Literal Llibre.class Class<Llibre> (tipat) No la inicialitza Cap
Instància objecte.getClass() No (n'hi ha prou amb un Object) Class<? extends X> Ja està carregada Cap
Per nom Class.forName("...") No Class<?> (sense tipar) Sí, i la inicialitza ClassNotFoundException

Tres detalls que importen:

El literal està tipat; forName no. Llibre.class és un Class<Llibre>, i per això tipus.cast(x) retorna un Llibre sense conversió manual (el testimoni de tipus de 10-01). Class.forName(...) retorna Class<?>, i tot el que en surti serà Object.

getClass() retorna el tipus REAL, no el declarat. Aquesta distinció és el polimorfisme de 03-06 vist des de la reflexió:

Material m = new Llibre("Java Eficac", "978-0000000001");
System.out.println(m.getClass().getSimpleName());   // Llibre, no Material

Class.forName inicialitza la classe. Executa els seus blocs static i els seus inicialitzadors de camps estàtics. És el mecanisme pel qual els antics controladors JDBC es registraven sols amb Class.forName("com.mysql.jdbc.Driver"). Si no vols aquesta inicialització, existeix la variant de tres arguments:

Class<?> senseInicialitzar = Class.forName(
        "com.nexussoftware.bibliotech.domini.Llibre",
        false,                                  // no inicialitzar
        Thread.currentThread().getContextClassLoader());

Casos especials que convé conèixer:

System.out.println(int.class.getName());          // int
System.out.println(Integer.class.getName());      // java.lang.Integer  (diferents)
System.out.println(String[].class.getName());     // [Ljava.lang.String;
System.out.println(int[][].class.getName());      // [[I
System.out.println(void.class.getName());         // void

Aquesta notació amb claudàtors és la interna de la JVM per als arrays: [ per cada dimensió, després L<nom>; per a objectes o una lletra per a primitius (I int, D double, Z boolean, J long...). Apareix en traces de pila i en missatges d'error, i ara la reconeixeràs.

  1. Inspecció bàsica: noms, jerarquia i modificadors

Amb el Class a la mà, es pot preguntar de tot.

package com.nexussoftware.bibliotech;

import java.lang.reflect.Modifier;
import java.util.Arrays;

public class InspectorDeClasses {

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

        System.out.println("=== " + tipus.getSimpleName() + " ===");

        // --- NOMS ---
        System.out.println("getName():          " + tipus.getName());
        System.out.println("getSimpleName():    " + tipus.getSimpleName());
        System.out.println("getCanonicalName(): " + tipus.getCanonicalName());
        System.out.println("getPackageName():   " + tipus.getPackageName());

        // --- JERARQUIA ---
        Class<?> pare = tipus.getSuperclass();
        System.out.println("superclasse:        " + (pare == null ? "cap" : pare.getSimpleName()));

        System.out.println("interficies:        " + Arrays.toString(
                Arrays.stream(tipus.getInterfaces()).map(Class::getSimpleName).toArray()));

        // Cadena completa d herencia fins a Object
        StringBuilder cadena = new StringBuilder(tipus.getSimpleName());
        for (Class<?> c = tipus.getSuperclass(); c != null; c = c.getSuperclass()) {
            cadena.append(" -> ").append(c.getSimpleName());
        }
        System.out.println("cadena:             " + cadena);

        // --- MODIFICADORS ---
        int mods = tipus.getModifiers();
        System.out.println("modificadors:       " + Modifier.toString(mods));
        System.out.println("  es public:        " + Modifier.isPublic(mods));
        System.out.println("  es abstract:      " + Modifier.isAbstract(mods));
        System.out.println("  es final:         " + Modifier.isFinal(mods));
        System.out.println();
    }

    public static void main(String[] args) {
        descriure(Llibre.class);
        descriure(Material.class);
        descriure(Gravetat.class);
    }
}
=== Llibre ===
getName():          com.nexussoftware.bibliotech.domini.Llibre
getSimpleName():    Llibre
getCanonicalName(): com.nexussoftware.bibliotech.domini.Llibre
getPackageName():   com.nexussoftware.bibliotech.domini
superclasse:        Material
interficies:        []
cadena:             Llibre -> Material -> Object
modificadors:       public
  es public:        true
  es abstract:      false
  es final:         false

=== Material ===
getName():          com.nexussoftware.bibliotech.domini.Material
getSimpleName():    Material
getCanonicalName(): com.nexussoftware.bibliotech.domini.Material
getPackageName():   com.nexussoftware.bibliotech.domini
superclasse:        Object
interficies:        [Identificable]
cadena:             Material -> Object
modificadors:       public abstract
  es public:        true
  es abstract:      true
  es final:         false

Els tres noms es diferencien sobretot en casos rars, i aquests casos rars són justament on un s'equivoca:

Tipus getName() getSimpleName() getCanonicalName()
Llibre com...Llibre Llibre com...Llibre
Map.Entry (imbricada) java.util.Map$Entry Entry java.util.Map.Entry
String[] [Ljava.lang.String; String[] java.lang.String[]
Classe anònima Exemple$1 "" (buit) null
Classe local Exemple$1Local Local null

Regla pràctica: fes servir getSimpleName() per a missatges a l'usuari, getName() per identificar la classe de forma única (és el que accepta Class.forName), i getCanonicalName() només si vols el nom tal com s'escriuria al codi font.

Modifier és una màscara de bits. getModifiers() retorna un int els bits del qual codifiquen public, private, static, final, abstract, synchronized, volatile, transient, native. Els mètodes estàtics de Modifier els interpreten, i Modifier.toString(int) els imprimeix.

  1. isEnum, isRecord, isInterface i companyia

Un grup de mètodes booleans permet classificar el tipus:

public static String classificar(Class<?> tipus) {
    if (tipus.isPrimitive())   return "primitiu";
    if (tipus.isArray())       return "array de " + tipus.getComponentType().getSimpleName();
    if (tipus.isEnum())        return "enum amb " + tipus.getEnumConstants().length + " constants";
    if (tipus.isRecord())      return "record amb " + tipus.getRecordComponents().length + " components";
    if (tipus.isAnnotation())  return "anotacio";
    if (tipus.isInterface())   return "interficie";
    if (tipus.isSealed())      return "classe segellada";   // Java 17, veure 10-06
    if (Modifier.isAbstract(tipus.getModifiers())) return "classe abstracta";
    return "classe concreta";
}
System.out.println(Llibre.class         + " -> " + classificar(Llibre.class));
System.out.println(Material.class       + " -> " + classificar(Material.class));
System.out.println(Gravetat.class       + " -> " + classificar(Gravetat.class));
System.out.println(Fitxa.class          + " -> " + classificar(Fitxa.class));
System.out.println(Identificable.class  + " -> " + classificar(Identificable.class));
System.out.println(int[].class          + " -> " + classificar(int[].class));
System.out.println(CampCsv.class        + " -> " + classificar(CampCsv.class));
class ...Llibre -> classe concreta
class ...Material -> classe abstracta
class ...Gravetat -> enum amb 3 constants
class ...Fitxa -> record amb 3 components
interface ...Identificable -> interficie
class [I -> array de int
interface ...CampCsv -> anotacio

Dos mètodes especialment útils per a BiblioTech:

getEnumConstants() retorna un array amb totes les constants d'un enum, en ordre de declaració. És el que permet construir un menú desplegable sense conèixer l'enum:

for (Object constant : Gravetat.class.getEnumConstants()) {
    System.out.println("  - " + constant);
}

getRecordComponents() (Java 16) dona els components d'un record amb el seu nom, tipus, anotacions i mètode d'accés. És el que fa servir Jackson per serialitzar records sense configuració:

for (RecordComponent comp : Fitxa.class.getRecordComponents()) {
    System.out.printf("  %s %s (accessor: %s)%n",
            comp.getType().getSimpleName(), comp.getName(), comp.getAccessor().getName());
}
  String isbn (accessor: isbn)
  String titol (accessor: titol)
  boolean disponible (accessor: disponible)

I també hi ha comprovacions de relació entre tipus, que són l'equivalent reflexiu d'instanceof:

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

// isInstance: equival a  (m instanceof Llibre)
System.out.println(Llibre.class.isInstance(m));                    // true

// isAssignableFrom: es pot assignar un Llibre a una variable Material?
System.out.println(Material.class.isAssignableFrom(Llibre.class)); // true
System.out.println(Llibre.class.isAssignableFrom(Material.class)); // false

L'ordre d'isAssignableFrom es confon sempre. Llegeix-lo així: Pare.class.isAssignableFrom(Fill.class) és true, igual que Pare p = new Fill() compila.

  1. Camps: getFields enfront de getDeclaredFields

Aquí hi ha la distinció que confon tothom, i que convé fixar d'una vegada perquè es repeteix idèntica per a mètodes i constructors.

Mètode Retorna Inclou heretats Inclou privats
getFields() Camps públics, propis i heretats No
getDeclaredFields() Tots els camps declarats en aquesta classe No

És a dir: una llista mira cap amunt (herència) i l'altra mira cap endins (visibilitat). Cap de les dues no dona "tots els camps de tot".

graph TD
    A["Camps d una classe"] --> B["getFields<br/>publics<br/>+ heretats publics"]
    A --> C["getDeclaredFields<br/>tots els declarats aqui<br/>private, protected, public, package"]
    B --> D["NO veu els privats"]
    C --> E["NO veu els heretats"]

Amb les classes de BiblioTech:

package com.nexussoftware.bibliotech;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class ComparadorDeCamps {

    public static void main(String[] args) {

        System.out.println("--- getFields() sobre Llibre ---");
        for (Field f : Llibre.class.getFields()) {
            System.out.printf("  %-12s %-10s declarat a %s%n",
                    f.getName(), Modifier.toString(f.getModifiers()),
                    f.getDeclaringClass().getSimpleName());
        }

        System.out.println("--- getDeclaredFields() sobre Llibre ---");
        for (Field f : Llibre.class.getDeclaredFields()) {
            System.out.printf("  %-12s %-20s %s%n",
                    f.getName(), Modifier.toString(f.getModifiers()),
                    f.getType().getSimpleName());
        }
    }
}

Suposant que Materialpublic static final String CATEGORIA_PER_DEFECTE i camps privats, i Llibre té els seus camps privats:

--- getFields() sobre Llibre ---
  CATEGORIA_PER_DEFECTE public static final declarat a Material
--- getDeclaredFields() sobre Llibre ---
  isbn         private final        String
  titol        private final        String
  autor        private final        String
  pagines      private final        int
  valoracio    private final        double
  prestat      private              boolean

Conclusió pràctica: com que en Java ben escrit tots els camps són privats (encapsulament, 03-07), getFields() gairebé sempre retorna un array buit o només constants. El mètode que es fa servir de debò és getDeclaredFields().

I si necessites tots els camps incloent-hi els heretats, cal recórrer la jerarquia a mà:

/**
 * Tots els camps declarats a la classe i a totes les seves superclasses,
 * de la mes derivada a la mes general.
 */
public static List<Field> totsElsCamps(Class<?> tipus) {
    List<Field> camps = new ArrayList<>();
    for (Class<?> c = tipus; c != null && c != Object.class; c = c.getSuperclass()) {
        camps.addAll(Arrays.asList(c.getDeclaredFields()));
    }
    return camps;
}

Aquest bucle és un patró que veuràs en qualsevol framework i que faràs servir a l'ExportadorAnotat de l'apartat 13.

Llegir i escriure un camp:

Llibre llibre = new Llibre("Java Eficac", "978-0000000001", "Joshua Bloch", 412, 4.8);

Field campTitol = Llibre.class.getDeclaredField("titol");
campTitol.setAccessible(true);                    // es private (apartat 10)

String valor = (String) campTitol.get(llibre);    // LLEGIR
System.out.println(valor);                        // Java Eficac

campTitol.set(llibre, "Java Eficac 3a ed.");      // ESCRIURE
System.out.println(llibre.getTitol());            // Java Eficac 3a ed.

Per a camps primitius hi ha accessos tipats que eviten l'autoboxing: getInt, getDouble, getBoolean, setInt...

Field campPagines = Llibre.class.getDeclaredField("pagines");
campPagines.setAccessible(true);
int pagines = campPagines.getInt(llibre);      // sense boxing

Per a camps static, es passa null com a instància:

Field constant = Material.class.getDeclaredField("CATEGORIA_PER_DEFECTE");
String valor = (String) constant.get(null);    // null: no hi ha instancia

  1. Mètodes i constructors: la mateixa distinció

La simetria és exacta, així que n'hi ha prou amb la taula:

Mètode Retorna
getMethods() Mètodes públics, propis i heretats (inclosos els d'Object)
getDeclaredMethods() Tots els declarats en aquesta classe, de qualsevol visibilitat
getMethod(nom, tipus...) Un mètode públic concret, propi o heretat
getDeclaredMethod(nom, tipus...) Un mètode concret declarat aquí, de qualsevol visibilitat
getConstructors() Constructors públics
getDeclaredConstructors() Tots els constructors

Un detall que sorprèn: getMethods() sobre qualsevol classe retorna almenys nou mètodes heretats d'Object (equals, hashCode, toString, getClass, notify, notifyAll i tres wait). Gairebé mai no és el que vols.

package com.nexussoftware.bibliotech;

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

public class InspectorDeMetodes {

    public static void main(String[] args) {

        System.out.println("getMethods():         " + Llibre.class.getMethods().length + " metodes");
        System.out.println("getDeclaredMethods(): " + Llibre.class.getDeclaredMethods().length + " metodes");
        System.out.println();

        Method[] metodes = Llibre.class.getDeclaredMethods();
        Arrays.sort(metodes, Comparator.comparing(Method::getName));   // ordre estable

        for (Method m : metodes) {

            // Filtrar els metodes PONT i SINTETICS generats pel compilador (10-01)
            if (m.isBridge() || m.isSynthetic()) {
                System.out.println("  [pont/sintetic] " + m.getName() + " -- ignorat");
                continue;
            }

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

            System.out.printf("  %-10s %s %s(%s)%n",
                    Modifier.toString(m.getModifiers()),
                    m.getReturnType().getSimpleName(),
                    m.getName(),
                    parametres);
        }
    }
}
getMethods():         15 metodes
getDeclaredMethods(): 8 metodes

  public     boolean equals(Object)
  public     String getAutor()
  public     String getId()
  public     String getIsbn()
  public     int getPagines()
  public     String getTitol()
  public     int hashCode()
  public     String toString()

El filtre d'isBridge() i isSynthetic() no és opcional. Recorda de 10-01 que el compilador genera mètodes pont en implementar interfícies genèriques. Si el teu motor recorre mètodes i no els filtra, processarà duplicats amb paràmetres Object i produirà resultats estranys.

Obtenir un mètode concret requereix donar els tipus dels seus paràmetres, perquè hi pot haver sobrecàrregues:

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

// Un metode sense parametres
Method llistar = GestorPrestecs.class.getMethod("llistarActius");

// Un metode privat: getDeclaredMethod
Method intern = GestorPrestecs.class.getDeclaredMethod("seguentReferencia");

Si el mètode no existeix, salta NoSuchMethodExceptionen execució, no en compilació. Aquest és el preu del qual parlarem a l'apartat 17.

Informació de paràmetres:

for (Parameter p : prestar.getParameters()) {
    System.out.println(p.getType().getSimpleName() + " " + p.getName());
}
String arg0
String arg1

Els noms reals dels paràmetres no són al .class per defecte: el compilador els descarta. Per conservar-los cal compilar amb -parameters:

javac -parameters com/nexussoftware/bibliotech/servei/GestorPrestecs.java
String isbn
String empleat

És una opció que els frameworks necessiten (Spring la fa servir per injectar per nom de paràmetre) i que convé activar al projecte. A 11-05 la veuràs configurada a Maven.

  1. Crear instàncies amb newInstance

La forma correcta és obtenir el Constructor i cridar newInstance:

// Constructor sense arguments
Class<?> tipus = Class.forName("com.nexussoftware.bibliotech.domini.Llibre");
Object objecte = tipus.getDeclaredConstructor().newInstance();

// Constructor amb arguments: es donen els TIPUS en cercar-lo
// i els VALORS en invocar-lo
Constructor<?> ctor = tipus.getDeclaredConstructor(
        String.class, String.class, String.class, int.class, double.class);

Object llibre = ctor.newInstance("Java Eficac", "978-0000000001", "Joshua Bloch", 412, 4.8);

Fixa't en int.class i double.class, no Integer.class ni Double.class. La cerca del constructor compara tipus exactes, i un paràmetre int es cerca amb int.class. És un error freqüentíssim que produeix un NoSuchMethodException desconcertant.

Amb el testimoni de tipus de 10-01, el resultat ve ja tipat:

Class<Llibre> tipus = Llibre.class;                    // Class<Llibre>, no Class<?>
Constructor<Llibre> ctor = tipus.getDeclaredConstructor(String.class, String.class);
Llibre llibre = ctor.newInstance("Java Eficac", "978-0000000001");   // sense conversio

El mètode obsolet que veuràs en codi antic:

Object o = tipus.newInstance();    // @Deprecated(since = "9")

Class.newInstance() està obsolet des de Java 9 per una raó concreta: propaga les excepcions comprovades del constructor sense declarar-les, saltant-se el sistema d'excepcions comprovades de 06-01. getDeclaredConstructor().newInstance() les embolcalla correctament en InvocationTargetException.

Un avís important sobre encapsulament: la reflexió pot invocar constructors privats.

// Un singleton "protegit" per constructor privat
Constructor<Configuracio> ctor = Configuracio.class.getDeclaredConstructor();
ctor.setAccessible(true);
Configuracio segona = ctor.newInstance();   // una segona instancia!

El patró singleton fet amb constructor privat no resisteix la reflexió. L'única forma que sí que resisteix és el singleton amb enum, perquè la JVM prohibeix explícitament instanciar constants d'enum per reflexió (IllegalArgumentException: Cannot reflectively create enum objects). És un dels arguments de Joshua Bloch per preferir enum, i el reprendràs a 12-02.

  1. Invocar mètodes amb invoke

Method metode = Llibre.class.getMethod("getTitol");

// invoke(instancia, arguments...)
String titol = (String) metode.invoke(llibre);

La signatura és Object invoke(Object obj, Object... args). Tres regles:

  1. El primer argument és la instància sobre la qual s'invoca. Per a un mètode static, es passa null.
  2. Els següents són els arguments, en ordre. Els primitius s'autoboxegen.
  3. Retorna Object, sempre. Si el mètode retorna void, retorna null. Si retorna int, retorna un Integer.
// Metode amb arguments
Method prestar = GestorPrestecs.class.getMethod("prestar", String.class, String.class);
Object resultat = prestar.invoke(gestor, "978-0000000001", "Marta Ruiz");

// Metode estatic: instancia null
Method fabrica = Resultat.class.getMethod("exit", Object.class);
Object exit = fabrica.invoke(null, "valor");

// Metode que retorna primitiu: arriba autoboxejat
Method pagines = Llibre.class.getMethod("getPagines");
int n = (Integer) pagines.invoke(llibre);

Un exemple complet que demostra la potència: un petit "inspector genèric" que imprimeix l'estat de qualsevol objecte fent servir els seus getters, sense conèixer-ne la classe.

package com.nexussoftware.bibliotech.servei;

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

public final class InspectorDeEstat {

    private InspectorDeEstat() { }

    /** Imprimeix el resultat de tots els getters sense arguments de qualsevol objecte. */
    public static void bolcar(Object objecte) {

        Class<?> tipus = objecte.getClass();
        System.out.println("Estat de " + tipus.getSimpleName() + ":");

        Method[] metodes = tipus.getMethods();
        Arrays.sort(metodes, Comparator.comparing(Method::getName));

        for (Method m : metodes) {

            boolean esGetter = (m.getName().startsWith("get") || m.getName().startsWith("is"))
                    && m.getParameterCount() == 0
                    && m.getReturnType() != void.class
                    && !m.getName().equals("getClass")          // heretat d Object
                    && !Modifier.isStatic(m.getModifiers())
                    && !m.isSynthetic();

            if (!esGetter) {
                continue;
            }

            try {
                Object valor = m.invoke(objecte);
                System.out.printf("  %-16s = %s%n", nomDePropietat(m.getName()), valor);
            } catch (ReflectiveOperationException e) {
                System.out.printf("  %-16s = <error: %s>%n",
                        nomDePropietat(m.getName()), e.getCause());
            }
        }
    }

    /** getTitol -> titol, isPrestat -> prestat */
    private static String nomDePropietat(String nomMetode) {
        String sensePrefix = nomMetode.startsWith("is")
                ? nomMetode.substring(2)
                : nomMetode.substring(3);
        return Character.toLowerCase(sensePrefix.charAt(0)) + sensePrefix.substring(1);
    }
}
InspectorDeEstat.bolcar(new Llibre("Java Eficac", "978-0000000001", "Joshua Bloch", 412, 4.8));
InspectorDeEstat.bolcar(new Prestec("PR-2026-0041", "978-0000000001", "Marta Ruiz"));
Estat de Llibre:
  autor            = Joshua Bloch
  id               = 978-0000000001
  isbn             = 978-0000000001
  pagines          = 412
  prestat          = false
  titol            = Java Eficac
  valoracio        = 4.8
Estat de Prestec:
  diaPrestec       = 120
  empleat          = Marta Ruiz
  id               = PR-2026-0041
  isbn             = 978-0000000001
  referencia       = PR-2026-0041

Quaranta línies que funcionen amb qualsevol classe que s'escrigui en el futur. Aquest és l'argument de venda de la reflexió, i també el seu perill: res d'això no ho comprova el compilador.

  1. Excepcions a la reflexió: InvocationTargetException

La reflexió té la seva pròpia família d'excepcions, totes comprovades, i una en particular mereix atenció especial.

Excepció Quan salta
ClassNotFoundException Class.forName amb un nom que no existeix
NoSuchMethodException getMethod/getDeclaredMethod amb nom o tipus que no encaixen
NoSuchFieldException getField/getDeclaredField amb un nom que no existeix
IllegalAccessException Accés a un membre no accessible sense setAccessible(true)
IllegalArgumentException Arguments del tipus o nombre equivocat a invoke
InstantiationException newInstance sobre una classe abstracta o una interfície
InvocationTargetException El mètode invocat va llançar una excepció
InaccessibleObjectException setAccessible bloquejat pel sistema de mòduls (10-06)

Totes menys les dues últimes hereten de ReflectiveOperationException, cosa que permet capturar-les juntes:

try {
    Object o = tipus.getDeclaredConstructor().newInstance();
} catch (ReflectiveOperationException e) {      // captura les cinc de cop
    throw new BiblioTechException("No s ha pogut instanciar " + tipus.getName(), e);
}

InvocationTargetException és la important. Quan el mètode que invoques llança una excepció, la reflexió no la propaga: l'embolcalla. Això reprèn directament l'encadenament de causes de 06-03.

public class DemostracioInvocationTarget {

    public static void main(String[] args) throws Exception {

        GestorPrestecs gestor = new GestorPrestecs(cataleg, prestecs);
        Method retornar = GestorPrestecs.class.getMethod("retornar", String.class);

        try {
            retornar.invoke(gestor, "REFERENCIA-INEXISTENT");

        } catch (InvocationTargetException e) {
            // e NO es l excepcio real: es l embolcall
            System.out.println("Embolcall: " + e.getClass().getSimpleName());

            // getCause() dona l excepcio QUE DE DEBO va llancar el metode
            Throwable causa = e.getCause();
            System.out.println("Causa real: " + causa.getClass().getSimpleName()
                                              + ": " + causa.getMessage());

            // El correcte: rellancar la causa, no l embolcall
            if (causa instanceof BiblioTechException bte) {
                throw bte;
            }
            throw new BiblioTechException("Fallada en retornar per reflexio", causa);
        }
    }
}
Embolcall: InvocationTargetException
Causa real: BiblioTechException: No existeix el prestec REFERENCIA-INEXISTENT

Per què existeix aquest embolcall. Sense ell seria impossible distingir "la reflexió ha fallat" de "el mètode ha fallat". Si invoke propagués una IllegalArgumentException llançada pel mètode, no sabries si el problema és que vas passar malament els arguments o que el mètode va rebutjar les dades.

La regla en escriure un motor: captura InvocationTargetException i rellança getCause(), no l'embolcall. Si no ho fas, els teus usuaris veuen traces de pila plenes de sun.reflect.NativeMethodAccessorImpl i no troben la seva pròpia excepció. Aquesta és la raó que les traces d'Spring i JUnit siguin tan llargues: cada capa de reflexió afegeix soroll.

  1. Accedir al que és privat: setAccessible(true)

Tots els membres reflexius (Field, Method, Constructor) hereten d'AccessibleObject, que ofereix:

Field camp = Llibre.class.getDeclaredField("titol");
camp.setAccessible(true);        // desactiva la comprovacio d acces
String titol = (String) camp.get(llibre);

Sense aquesta crida:

Exception in thread "main" java.lang.IllegalAccessException:
    class Exemple cannot access a member of class Llibre with modifiers "private final"

Què fa exactament setAccessible(true): no canvia el modificador del camp ni el fa públic per a la resta del programa. El que fa és desactivar la comprovació d'accés per a aquest objecte reflexiu concret. El camp continua sent private per al compilador i per a qualsevol altre codi.

Què trenca de l'encapsulament. Tot. L'encapsulament de 03-07 es basa en el fet que ningú de fora no pot tocar l'estat intern; la reflexió amb setAccessible el travessa per complet:

public class TrencantElEncapsulament {

    public static void main(String[] args) throws Exception {

        Llibre llibre = new Llibre("Java Eficac", "978-0000000001", "Joshua Bloch", 412, 4.8);
        System.out.println("Abans:   " + llibre.getPagines() + " pagines");

        // El camp es private FINAL: ni tan sols la classe el pot canviar
        Field pagines = Llibre.class.getDeclaredField("pagines");
        pagines.setAccessible(true);
        pagines.setInt(llibre, -999);

        System.out.println("Despres: " + llibre.getPagines() + " pagines");
    }
}
Abans:   412 pagines
Despres: -999 pagines

Un objecte amb un estat que els seus propis invariants prohibeixen. El constructor de Llibre valida que les pàgines siguin positives; la reflexió se'l salta. És exactament l'escenari que l'encapsulament existeix per impedir.

Amb camps static final de tipus primitiu o String és encara pitjor: el compilador els incrusta com a constants, així que canviar el camp per reflexió no canvia el codi que ja els feia servir. I des de Java 17, modificar un static final per reflexió llança directament IllegalAccessException llevat de casos molt concrets.

Quan és legítim:

  • Un framework de serialització que ha de llegir els camps de les teves entitats (Jackson, Hibernate).
  • Un framework d'injecció de dependències que ha d'escriure als teus camps (Spring amb @Autowired en camps).
  • Un framework de proves que ha d'invocar mètodes privats o injectar dobles.
  • Eines de diagnòstic.

Quan no ho és: quan vols saltar-te una API que algú va dissenyar a propòsit. Si necessites setAccessible per fer servir una biblioteca, o l'estàs fent servir malament, o la biblioteca té un defecte de disseny que cal reportar.

  1. El que trenca i el que el sistema de mòduls ja no permet

Durant vint anys, setAccessible(true) va funcionar sobre qualsevol cosa, incloses les classes internes del JDK. Biblioteques senceres es van construir accedint a camps privats d'String, ArrayList o sun.misc.Unsafe. I quan l'equip de Java volia canviar aquestes classes internes, trencava mig ecosistema.

Java 9 va introduir el sistema de mòduls (JPMS, que veuràs a fons a 10-06) i amb ell l'encapsulament fort: un mòdul declara quins paquets exporta i, sobretot, quins obre a la reflexió. El que no està obert, no es pot obrir amb setAccessible.

public class ReflexioBloquejada {

    public static void main(String[] args) throws Exception {
        Field valor = String.class.getDeclaredField("value");
        valor.setAccessible(true);      // ja no funciona
    }
}
Exception in thread "main" java.lang.reflect.InaccessibleObjectException:
    Unable to make field private final byte[] java.lang.String.value accessible:
    module java.base does not "opens java.lang" to unnamed module @0x1b6d3586

InaccessibleObjectException és l'excepció que veuràs cada vegada que una biblioteca antiga intenti furgar a les entranyes del JDK modern.

La sortida d'emergència és una opció de la JVM:

java --add-opens java.base/java.lang=ALL-UNNAMED -cp . ElMeuPrograma

Que es llegeix: "obre el paquet java.lang del mòdul java.base a tot el codi sense mòdul".

I per al teu propi codi modularitzat, la declaració corresponent a module-info.java:

module com.nexussoftware.bibliotech {
    requires java.logging;
    requires java.net.http;

    exports com.nexussoftware.bibliotech.domini;       // us normal en compilacio
    opens com.nexussoftware.bibliotech.domini;         // a mes, reflexio profunda
}

La diferència entre exports i opens és exactament aquesta: exports permet fer servir els tipus públics normalment; opens permet a més que la reflexió accedeixi al que és privat. Un framework de persistència necessita el segon.

Tot això es desenvolupa a 10-06. Aquí n'hi ha prou de reconèixer el missatge d'error i saber que --add-opens existeix, perquè te'l trobaràs.

  1. Llegir anotacions en execució

I arribem al motiu pel qual aquesta lliçó ve després de l'anterior. Les anotacions de 10-02 es llegeixen així:

// La te?
boolean te = camp.isAnnotationPresent(CampCsv.class);

// Obtenir-la (null si no la te)
CampCsv anotacio = camp.getAnnotation(CampCsv.class);
if (anotacio != null) {
    System.out.println(anotacio.nom() + " a la posicio " + anotacio.ordre());
}

// Totes (incloses les heretades amb @Inherited)
Annotation[] totes = tipus.getAnnotations();

// Nomes les declarades directament
Annotation[] propies = tipus.getDeclaredAnnotations();

// Repetibles: funciona amb 0, 1 o N
Responsable[] responsables = tipus.getAnnotationsByType(Responsable.class);

Aquests mètodes estan disponibles a Class, Field, Method, Constructor, Parameter i RecordComponent, perquè tots implementen AnnotatedElement.

Recordatori que costa mitja tarda a qui l'oblida: tot això només funciona si l'anotació té @Retention(RetentionPolicy.RUNTIME). Amb la retenció per defecte (CLASS), isAnnotationPresent retorna false sense explicació.

També es poden llegir les anotacions dels paràmetres, encara que l'API és incòmoda perquè retorna un array d'arrays:

Annotation[][] perParametre = metode.getParameterAnnotations();
for (int i = 0; i < perParametre.length; i++) {
    System.out.println("Parametre " + i + ": " + Arrays.toString(perParametre[i]));
}

// Alternativa mes llegible des de Java 8
for (Parameter p : metode.getParameters()) {
    if (p.isAnnotationPresent(NoNul.class)) {
        System.out.println(p.getName() + " no admet null");
    }
}

  1. BiblioTech: l'ExportadorAnotat que consumeix @CampCsv

Aquest és el moment de la lliçó. Escriurem un exportador que genera la línia CSV de qualsevol entitat de BiblioTech —present o futura— sense conèixer-la, llegint les seves @CampCsv.

package com.nexussoftware.bibliotech.persistencia;

import com.nexussoftware.bibliotech.anotacions.CampCsv;

import java.lang.reflect.Field;
import java.util.*;
import java.util.logging.Logger;

/**
 * Exportador CSV universal basat en anotacions.
 *
 * No coneix Llibre, ni Prestec, ni Fitxa. Descobreix en execucio
 * quins camps porten @CampCsv, els ordena per 'ordre' i construeix
 * capcalera i linia.
 *
 * Substitueix ExportadorCatalegCsv (07-07) i tots els
 * exportadors per-classe que caldria escriure sense aixo.
 */
public class ExportadorAnotat {

    private static final Logger LOG = Logger.getLogger(ExportadorAnotat.class.getName());

    private static final char SEPARADOR = ';';
    private static final String EMMASCARAT = "***";

    /**
     * Cache de la introspeccio: getDeclaredFields() es car i el
     * resultat no canvia mai per a una classe donada (apartat 17).
     */
    private final Map<Class<?>, List<Field>> cache = new HashMap<>();

    /** Si es true, els camps marcats sensible = true s emmascaren. */
    private final boolean emmascararSensibles;

    public ExportadorAnotat(boolean emmascararSensibles) {
        this.emmascararSensibles = emmascararSensibles;
    }

    // ------------------------------------------------------------------
    // API publica
    // ------------------------------------------------------------------

    /** Capcalera CSV d una classe, sense necessitat de tenir una instancia. */
    public String capcalera(Class<?> tipus) {
        StringJoiner sj = new StringJoiner(String.valueOf(SEPARADOR));
        for (Field camp : campsExportables(tipus)) {
            CampCsv meta = camp.getAnnotation(CampCsv.class);
            // nom() buit significa "fes servir el nom del camp" (10-02, apartat 10)
            String titolColumna = meta.nom().isEmpty() ? camp.getName() : meta.nom();
            sj.add(escapar(titolColumna));
        }
        return sj.toString();
    }

    /** Linia CSV d una entitat qualsevol. */
    public String linia(Object entitat) {
        Objects.requireNonNull(entitat, "entitat");

        StringJoiner sj = new StringJoiner(String.valueOf(SEPARADOR));

        for (Field camp : campsExportables(entitat.getClass())) {
            CampCsv meta = camp.getAnnotation(CampCsv.class);
            sj.add(escapar(valorDe(camp, meta, entitat)));
        }
        return sj.toString();
    }

    /** Exporta una colleccio completa: capcalera + una linia per element. */
    public List<String> exportar(Class<?> tipus, Collection<?> entitats) {
        List<String> linies = new ArrayList<>(entitats.size() + 1);
        linies.add(capcalera(tipus));
        for (Object entitat : entitats) {
            linies.add(linia(entitat));
        }
        return linies;
    }

    // ------------------------------------------------------------------
    // Introspeccio
    // ------------------------------------------------------------------

    /**
     * Camps anotats amb @CampCsv, de la classe i de TOTES les seves superclasses,
     * ordenats per l element 'ordre'.
     */
    private List<Field> campsExportables(Class<?> tipus) {

        // computeIfAbsent: la introspeccio es fa UNA vegada per classe (05-05)
        return cache.computeIfAbsent(tipus, t -> {

            List<Field> trobats = new ArrayList<>();

            // Recorregut de la jerarquia: getDeclaredFields NO veu els heretats
            for (Class<?> c = t; c != null && c != Object.class; c = c.getSuperclass()) {
                for (Field camp : c.getDeclaredFields()) {

                    if (camp.isSynthetic()) {
                        continue;            // this$0 de classes internes, etc.
                    }
                    if (!camp.isAnnotationPresent(CampCsv.class)) {
                        continue;            // sense anotacio: no s exporta
                    }

                    camp.setAccessible(true);    // son private: cal obrir-los
                    trobats.add(camp);
                }
            }

            // Ordre declarat. getDeclaredFields NO garanteix cap ordre,
            // aixi que sense aixo la sortida seria impredictible entre execucions.
            trobats.sort(Comparator.comparingInt(
                    f -> f.getAnnotation(CampCsv.class).ordre()));

            LOG.fine(() -> "Introspeccio de " + t.getSimpleName()
                    + ": " + trobats.size() + " camps exportables");

            return List.copyOf(trobats);
        });
    }

    /** Llegeix el camp i el formata segons l anotacio. */
    private String valorDe(Field camp, CampCsv meta, Object entitat) {
        try {
            Object valor = camp.get(entitat);

            if (valor == null) {
                return "";
            }
            if (emmascararSensibles && meta.sensible()) {
                return EMMASCARAT;
            }
            if (!meta.format().isEmpty()) {
                return String.format(meta.format(), valor);
            }
            return String.valueOf(valor);

        } catch (IllegalAccessException e) {
            // Nomes pot passar si setAccessible ha fallat (moduls, apartat 11)
            throw new IllegalStateException(
                    "No s ha pogut llegir el camp " + camp.getName()
                    + " de " + entitat.getClass().getSimpleName(), e);
        }
    }

    /** Escapat CSV segons RFC 4180 (07-07): cometes si hi ha separador, cometes o salt. */
    private String escapar(String valor) {
        boolean necessita = valor.indexOf(SEPARADOR) >= 0
                || valor.indexOf('"') >= 0
                || valor.indexOf('\n') >= 0
                || valor.indexOf('\r') >= 0;

        if (!necessita) {
            return valor;
        }
        return '"' + valor.replace("\"", "\"\"") + '"';
    }
}

I el programa que el fa servir:

package com.nexussoftware.bibliotech;

import com.nexussoftware.bibliotech.domini.*;
import com.nexussoftware.bibliotech.persistencia.ExportadorAnotat;

import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;

public class ExportacioUniversal {

    public static void main(String[] args) throws Exception {

        ExportadorAnotat exportador = new ExportadorAnotat(true);   // emmascarar sensibles

        // --- Llibres ---
        List<Llibre> llibres = List.of(
                new Llibre("978-0000000001", "Java Eficac",        "Joshua Bloch",  412, 4.85),
                new Llibre("978-0000000002", "Patrons de Disseny", "GoF",           395, 4.60),
                new Llibre("978-0000000003", "Refactoritzacio",    "Martin Fowler", 448, 4.72));

        System.out.println("--- cataleg.csv ---");
        exportador.exportar(Llibre.class, llibres).forEach(System.out::println);

        // --- Prestecs: UNA ALTRA classe, MATEIX exportador ---
        List<Prestec> prestecs = List.of(
                new Prestec("PR-2026-0041", "978-0000000001", "Marta Ruiz",   120),
                new Prestec("PR-2026-0042", "978-0000000003", "Diego Alonso", 124));

        System.out.println();
        System.out.println("--- prestecs.csv ---");
        exportador.exportar(Prestec.class, prestecs).forEach(System.out::println);

        // --- Un record: TAMBE funciona ---
        List<Fitxa> fitxes = List.of(
                new Fitxa("978-0000000001", "Java Eficac", false),
                new Fitxa("978-0000000002", "Patrons de Disseny", true));

        System.out.println();
        System.out.println("--- fitxes.csv ---");
        exportador.exportar(Fitxa.class, fitxes).forEach(System.out::println);

        // I escriure-ho a disc amb NIO.2 i UTF-8 explicit (07-06)
        Files.write(Path.of("cataleg.csv"),
                    exportador.exportar(Llibre.class, llibres),
                    StandardCharsets.UTF_8);
    }
}
--- cataleg.csv ---
ISBN;Titol;Autor;Pagines;Valoracio
978-0000000001;Java Eficac;Joshua Bloch;412;4,85
978-0000000002;Patrons de Disseny;GoF;395;4,60
978-0000000003;Refactoritzacio;Martin Fowler;448;4,72

--- prestecs.csv ---
Referencia;ISBN;Empleat;Dia prestec
PR-2026-0041;978-0000000001;***;120
PR-2026-0042;978-0000000003;***;124

--- fitxes.csv ---
ISBN;Titol;Disponible
978-0000000001;Java Eficac;false
978-0000000002;Patrons de Disseny;true

Atura't aquí un moment, perquè això és important.

ExportadorAnotat no té ni una sola referència a Llibre, Prestec o Fitxa. No els importa. No sap que existeixen. I exporta els tres correctament, respectant l'ordre de les columnes, aplicant el format %.2f a la valoració i emmascarant el nom de l'empleat perquè el seu camp està marcat sensible = true.

Compara-ho amb la situació de 07-07: un exportador per classe, la capçalera i els valors en dos mètodes diferents que es poden desincronitzar, i un camp nou que calia recordar d'afegir en dos llocs. Ara, afegir una columna a Llibre és una línia:

@CampCsv(nom = "Editorial", ordre = 6)
private final String editorial;

I apareix automàticament a la capçalera i a totes les línies, en la posició correcta.

Això és exactament el que fa Jackson quan serialitza els teus objectes a JSON llegint @JsonProperty, i el que fa Hibernate quan construeix l'INSERT llegint @Column. La diferència entre el teu exportador de cent línies i aquests frameworks és quantitat de casos coberts, no naturalesa. El mecanisme és el mateix i l'acabes d'escriure.

Tres decisions del codi que mereixen comentari:

La memòria cau no és una optimització prematura. getDeclaredFields() construeix un array nou a cada crida (perquè ningú no pugui modificar l'original) i a més ordenar costa. Exportar deu mil préstecs faria deu mil introspeccions idèntiques. Amb computeIfAbsent, se'n fa una per classe. Tots els frameworks reals guarden la introspecció en memòria cau; és la primera optimització que apliquen.

L'ordre explícit és obligatori. La documentació de getDeclaredFields() diu literalment que no garanteix cap ordre. A la pràctica, HotSpot sol retornar-los en ordre de declaració, però això no està especificat i ha canviat entre versions. Un CSV les columnes del qual canvien d'ordre en actualitzar la JVM és un incident de producció. Per això @CampCsvordre.

El recorregut de la jerarquia importa. Si Llibre heretés els camps isbn i titol de Material en lloc de declarar-los, getDeclaredFields() sobre Llibre no els veuria. El bucle for (Class<?> c = t; c != Object.class; c = c.getSuperclass()) és el que ho resol.

  1. Genèrics i reflexió: què sobreviu a l'esborrat

Reprenem 10-01. Vam dir que l'esborrat de tipus elimina la informació genèrica en execució, però amb un matís: les signatures declarades sí que es conserven a les metadades del .class. La reflexió les pot llegir.

El que NO es pot saber:

List<Llibre> llibres = new ArrayList<>();
List<String> textos = new ArrayList<>();

System.out.println(llibres.getClass());                        // class java.util.ArrayList
System.out.println(llibres.getClass() == textos.getClass());   // true

Una instància no sap amb quin tipus es va parametritzar. Això es va perdre per sempre.

El que que es pot saber:

package com.nexussoftware.bibliotech.servei;

public class CatalegService {
    private Repositori<Material> cataleg;            // declaracio generica
    private Map<String, List<Prestec>> perEmpleat;

    public List<Llibre> cercarLlibres(Predicate<Material> criteri) { ... }
}
package com.nexussoftware.bibliotech;

import java.lang.reflect.*;
import java.util.Arrays;

public class InspectorDeGenerics {

    public static void main(String[] args) throws Exception {

        // --- CAMPS ---
        for (Field camp : CatalegService.class.getDeclaredFields()) {

            System.out.println("camp: " + camp.getName());
            System.out.println("  getType():        " + camp.getType().getSimpleName());
            System.out.println("  getGenericType(): " + camp.getGenericType());

            Type generic = camp.getGenericType();
            if (generic instanceof ParameterizedType pt) {
                System.out.println("  tipus cru:        "
                        + ((Class<?>) pt.getRawType()).getSimpleName());
                System.out.println("  arguments:        "
                        + Arrays.toString(pt.getActualTypeArguments()));
            }
            System.out.println();
        }

        // --- METODES ---
        Method cercar = CatalegService.class.getMethod("cercarLlibres", java.util.function.Predicate.class);
        System.out.println("metode: " + cercar.getName());
        System.out.println("  getReturnType():         " + cercar.getReturnType().getSimpleName());
        System.out.println("  getGenericReturnType():  " + cercar.getGenericReturnType());
        System.out.println("  getGenericParameterTypes(): "
                + Arrays.toString(cercar.getGenericParameterTypes()));
    }
}
camp: cataleg
  getType():        Repositori
  getGenericType(): com...Repositori<com...Material>
  tipus cru:        Repositori
  arguments:        [class com...Material]

camp: perEmpleat
  getType():        Map
  getGenericType(): java.util.Map<java.lang.String, java.util.List<com...Prestec>>
  tipus cru:        Map
  arguments:        [class java.lang.String, java.util.List<com...Prestec>]

metode: cercarLlibres
  getReturnType():         List
  getGenericReturnType():  java.util.List<com...Llibre>
  getGenericParameterTypes(): [java.util.function.Predicate<com...Material>]
Mètode Retorna Veu els genèrics
Field.getType() Class<?> No
Field.getGenericType() Type
Method.getReturnType() Class<?> No
Method.getGenericReturnType() Type
Method.getParameterTypes() Class<?>[] No
Method.getGenericParameterTypes() Type[]

La jerarquia de java.lang.reflect.Type té aquests implementadors, i convé reconèixer-los:

graph TD
    T["Type (interficie)"] --> C["Class<br/>String, Llibre, int"]
    T --> P["ParameterizedType<br/>List de Llibre"]
    T --> G["GenericArrayType<br/>T array"]
    T --> W["WildcardType<br/>? extends Material"]
    T --> V["TypeVariable<br/>la T de Repositori de T"]

Per què això importa de debò. Gràcies a getGenericType(), un framework de persistència pot veure que el camp és List<Prestec> i saber que ha de crear una relació un-a-molts amb la taula de préstecs. I un deserialitzador pot veure que el paràmetre és Map<String, List<Prestec>> i construir l'estructura correcta. Sense això, Hibernate i Jackson no podrien funcionar.

És també l'explicació de la tècnica del super type token: si crees una subclasse anònima d'una classe genèrica, la seva signatura declarada conserva el tipus:

// Classe anonima: la signatura de la seva superclasse SI que conserva <List<Llibre>>
var testimoni = new TestimoniDeTipus<List<Llibre>>() { };
System.out.println(testimoni.tipus());
public abstract class TestimoniDeTipus<T> {

    public Type tipus() {
        // getGenericSuperclass() de la classe anonima dona TestimoniDeTipus<List<Llibre>>
        ParameterizedType superclasse = (ParameterizedType) getClass().getGenericSuperclass();
        return superclasse.getActualTypeArguments()[0];
    }
}
java.util.List<com.nexussoftware.bibliotech.domini.Llibre>

Això és, exactament, el TypeReference de Jackson que veuràs a 11-07 i que resol el problema que List<Llibre>.class no existeixi.

  1. Proxies dinàmics: Proxy i InvocationHandler

I ara el segon mecanisme gran, el que explica d'on surten @Transactional, els repositoris d'Spring Data i la meitat de la "màgia" dels frameworks.

Un proxy dinàmic és una classe que la JVM genera en temps d'execució i que implementa les interfícies que li indiquis. No l'escrius tu: es fabrica en memòria. Cada vegada que algú crida qualsevol mètode del proxy, la crida es desvia a un únic mètode teu: invoke.

graph LR
    A["Client"] -->|"gestor.prestar(...)"| B["PROXY<br/>generat en memoria"]
    B -->|"invoke(proxy, metode, args)"| C["El teu InvocationHandler"]
    C -->|"abans: auditar, mesurar, obrir transaccio"| C
    C -->|"metode.invoke(objecteReal, args)"| D["Objecte REAL<br/>GestorPrestecs"]
    D -->|"resultat"| C
    C -->|"despres: registrar, tancar transaccio"| C
    C -->|"resultat"| A

Les dues peces:

// 1. La interficie que s implementa
public interface ServeiPrestecs {
    Resultat<Prestec> prestar(String isbn, String empleat);
    Resultat<Prestec> retornar(String referencia);
    List<Prestec> llistarActius();
}

// 2. El gestor que intercepta
InvocationHandler gestor = (proxy, metode, args) -> {
    System.out.println("Interceptat: " + metode.getName());
    return metode.invoke(objecteReal, args);
};

// 3. La fabricacio del proxy
ServeiPrestecs proxy = (ServeiPrestecs) Proxy.newProxyInstance(
        ServeiPrestecs.class.getClassLoader(),       // qui carrega la classe generada
        new Class<?>[] { ServeiPrestecs.class },     // interficies a implementar
        gestor);                                     // qui aten les crides

Un exemple mínim i complet, un proxy que mesura temps:

package com.nexussoftware.bibliotech.servei;

import java.lang.reflect.*;

public final class ProxyCronometre {

    private ProxyCronometre() { }

    @SuppressWarnings("unchecked")
    public static <T> T embolcallar(T objecte, Class<T> interficie) {
        return (T) Proxy.newProxyInstance(
                interficie.getClassLoader(),
                new Class<?>[] { interficie },
                (proxy, metode, args) -> {

                    long inici = System.nanoTime();
                    try {
                        return metode.invoke(objecte, args);

                    } catch (InvocationTargetException e) {
                        throw e.getCause();      // rellancar la causa, no l embolcall
                    } finally {
                        long ms = (System.nanoTime() - inici) / 1_000_000;
                        System.out.printf("[TEMPS] %s() -> %d ms%n", metode.getName(), ms);
                    }
                });
    }
}
ServeiPrestecs real = new GestorPrestecs(cataleg, prestecs);
ServeiPrestecs mesurat = ProxyCronometre.embolcallar(real, ServeiPrestecs.class);

mesurat.prestar("978-0000000001", "Marta Ruiz");
mesurat.llistarActius();
[TEMPS] prestar() -> 3 ms
[TEMPS] llistarActius() -> 0 ms

El client no se n'assabenta de res. mesurat és un ServeiPrestecs com qualsevol altre; es pot passar a qualsevol mètode que esperi aquesta interfície. I GestorPrestecs no sap que està sent mesurat.

La limitació fonamental de Proxy: només funciona amb interfícies. No es pot fer un proxy dinàmic d'una classe concreta amb aquesta API. Per això Spring, quan la classe no implementa cap interfície, recorre a CGLIB o ByteBuddy, que generen una subclasse en bytecode. I per això @Transactional sobre un mètode final no funciona: la subclasse no el pot sobreescriure.

Tres regles en escriure un InvocationHandler:

  1. Rellança sempre e.getCause() en capturar InvocationTargetException. Si no, el client veu un embolcall inútil en lloc de la seva excepció.
  2. Compte amb equals, hashCode i toString: també passen pel gestor. Si delegues equals a l'objecte real, comparar el proxy amb el real donarà true en un sentit i false en l'altre.
  3. Els mètodes default de la interfície també s'intercepten. Si vols que s'executi la implementació per defecte, cal fer servir InvocationHandler.invokeDefault (Java 16+).

  1. BiblioTech: el proxy que consumeix @Auditable

Ajuntem tot: el proxy dinàmic llegeix l'@Auditable de 10-02 i registra les operacions sense que GestorPrestecs se n'assabenti.

package com.nexussoftware.bibliotech.servei;

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

import java.lang.reflect.*;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Proxy d auditoria dirigit per anotacions.
 *
 * Intercepta cada crida, busca @Auditable al metode i, si no
 * la troba, a la classe (herencia de configuracio, com el
 * @Transactional d Spring). Registra inici, fi, durada i fallada.
 *
 * GestorPrestecs NO conte ni una linia d auditoria.
 */
public final class ProxyAuditoria implements InvocationHandler {

    private static final Logger LOG = Logger.getLogger("bibliotech.auditoria");

    private final Object objectiu;
    private final Class<?> classeObjectiu;

    private ProxyAuditoria(Object objectiu) {
        this.objectiu = objectiu;
        this.classeObjectiu = objectiu.getClass();
    }

    /** Fabrica tipada: retorna alguna cosa que es pot fer servir com la interficie original. */
    @SuppressWarnings("unchecked")
    public static <T> T auditar(T objectiu, Class<T> interficie) {
        if (!interficie.isInterface()) {
            throw new IllegalArgumentException(
                    interficie.getSimpleName() + " no es una interficie; Proxy nomes admet interficies");
        }
        return (T) Proxy.newProxyInstance(
                interficie.getClassLoader(),
                new Class<?>[] { interficie },
                new ProxyAuditoria(objectiu));
    }

    @Override
    public Object invoke(Object proxy, Method metode, Object[] args) throws Throwable {

        Auditable politica = resoldrePolitica(metode);

        if (politica == null) {
            // Sense anotacio: passar la crida sense fer res mes
            return invocar(metode, args);
        }

        String operacio = politica.value().isEmpty() ? metode.getName() : politica.value();
        Level nivell = aNivellDeLog(politica.nivell());

        String descripcioArgs = politica.registrarArguments()
                ? descriureArguments(args, politica.argumentsSensibles())
                : "(no registrats)";

        LOG.log(nivell, () -> String.format("INICI %s %s", operacio, descripcioArgs));

        long inici = System.nanoTime();
        try {
            Object resultat = invocar(metode, args);

            long ms = (System.nanoTime() - inici) / 1_000_000;
            LOG.log(nivell, () -> String.format("FI    %s en %d ms -> %s", operacio, ms, resultat));
            return resultat;

        } catch (Throwable fallada) {
            long ms = (System.nanoTime() - inici) / 1_000_000;
            // Les fallades SEMPRE es registren com a SEVERE, sigui quin sigui el nivell
            LOG.log(Level.SEVERE, fallada,
                    () -> String.format("ERROR %s despres de %d ms: %s", operacio, ms, fallada));
            throw fallada;
        }
    }

    // ------------------------------------------------------------------

    /**
     * Busca l anotacio al metode de la CLASSE REAL (no a la interficie,
     * on podria no ser-hi) i, si no n hi ha, a la classe.
     */
    private Auditable resoldrePolitica(Method metodeDeInterficie) {
        try {
            Method metodeReal = classeObjectiu.getMethod(
                    metodeDeInterficie.getName(), metodeDeInterficie.getParameterTypes());

            Auditable delMetode = metodeReal.getAnnotation(Auditable.class);
            if (delMetode != null) {
                return delMetode;
            }
        } catch (NoSuchMethodException e) {
            // No hauria de passar: el proxy implementa la interficie que la classe implementa
        }
        return classeObjectiu.getAnnotation(Auditable.class);   // valor per defecte de classe
    }

    /** Invoca el metode real desembolcallant l excepcio. */
    private Object invocar(Method metode, Object[] args) throws Throwable {
        try {
            return metode.invoke(objectiu, args);
        } catch (InvocationTargetException e) {
            throw e.getCause();          // REGLA: rellancar la causa, no l embolcall
        }
    }

    /** Emmascara els arguments marcats com a sensibles (09-06: no registrar mai credencials). */
    private String descriureArguments(Object[] args, int[] sensibles) {
        if (args == null || args.length == 0) {
            return "()";
        }
        Object[] copia = Arrays.copyOf(args, args.length);
        for (int index : sensibles) {
            if (index >= 0 && index < copia.length) {
                copia[index] = "***";
            }
        }
        return Arrays.toString(copia);
    }

    private Level aNivellDeLog(Gravetat gravetat) {
        return switch (gravetat) {
            case ALTA    -> Level.WARNING;
            case MITJANA -> Level.INFO;
            case BAIXA   -> Level.FINE;
        };
    }
}

El programa:

package com.nexussoftware.bibliotech;

import com.nexussoftware.bibliotech.domini.*;
import com.nexussoftware.bibliotech.servei.*;

public class AuditoriaTransparent {

    public static void main(String[] args) {

        ConfiguracioLog.inicialitzar();     // del modul 6

        Repositori<Material> cataleg  = new Repositori<>("cataleg");
        Repositori<Prestec> prestecs  = new Repositori<>("prestecs");
        cataleg.desar(new Llibre("978-0000000001", "Java Eficac"));
        cataleg.desar(new Llibre("978-0000000002", "Patrons de Disseny"));

        // L objecte REAL, sense res d auditoria a dins
        ServeiPrestecs real = new GestorPrestecs(cataleg, prestecs);

        // L objecte que fa servir l aplicacio
        ServeiPrestecs servei = ProxyAuditoria.auditar(real, ServeiPrestecs.class);

        servei.prestar("978-0000000001", "Marta Ruiz");
        servei.prestar("978-0000000001", "Diego Alonso");   // ja prestat: retorna fallada
        servei.llistarActius();                             // sense @Auditable: no es registra
        servei.canviarClauEmpleat("Nuria Vidal", "clau-secreta-real");

        try {
            servei.retornar("PR-INEXISTENT");
        } catch (Exception e) {
            System.out.println("Excepcio rebuda pel client: "
                    + e.getClass().getSimpleName() + " -- " + e.getMessage());
        }
    }
}
[WARNING] bibliotech.auditoria: INICI prestar [978-0000000001, Marta Ruiz]
[WARNING] bibliotech.auditoria: FI    prestar en 4 ms -> Exit[Prestec[PR-2026-0001]]
[WARNING] bibliotech.auditoria: INICI prestar [978-0000000001, Diego Alonso]
[WARNING] bibliotech.auditoria: FI    prestar en 0 ms -> Fallada[El material 'Java Eficac' ja esta prestat]
[INFO]    bibliotech.auditoria: INICI canviarClau [Nuria Vidal, ***]
[INFO]    bibliotech.auditoria: FI    canviarClau en 1 ms -> Exit[null]
[WARNING] bibliotech.auditoria: INICI retornar [PR-INEXISTENT]
[SEVERE]  bibliotech.auditoria: ERROR retornar despres de 0 ms: BiblioTechException: No existeix el prestec PR-INEXISTENT
    com.nexussoftware.bibliotech.BiblioTechException: No existeix el prestec PR-INEXISTENT
        at com.nexussoftware.bibliotech.servei.GestorPrestecs.retornar(GestorPrestecs.java:78)
        ...
Excepcio rebuda pel client: BiblioTechException -- No existeix el prestec PR-INEXISTENT

Cinc observacions que resumeixen tota la lliçó:

GestorPrestecs no conté ni una línia d'auditoria. Les catorze crides repetides a RegistreOperacions de 06-07 han desaparegut. La lògica de préstecs parla de préstecs.

llistarActius() no apareix a la sortida. No porta @Auditable, i el gestor la deixa passar sense registrar. La decisió d'auditar o no és on ha de ser: al costat del mètode.

La clau de la Nuria Vidal surt com a ***. argumentsSensibles = {1} va fer la seva feina. La regla de 09-06 —no registrar mai credencials— ha passat de ser un comentari que algú ha de recordar a ser una declaració que el motor aplica.

El client rep la seva BiblioTechException, no una InvocationTargetException. El throw e.getCause() del mètode invocar és el que ho aconsegueix. Sense aquesta línia, el catch (BiblioTechException e) del client no funcionaria i l'aplicació es comportaria de forma inexplicable.

I això és, literalment, com funciona @Transactional. Spring crea un proxy del teu servei, intercepta cada crida, veu l'anotació, obre una transacció, invoca el mètode real, i confirma o desfà segons hi hagi excepció. Canvia LOG.log(...) per transaccio.begin() i transaccio.commit() i tens el mecanisme complet. També explica les seves dues limitacions famoses: no funciona en mètodes privats (el proxy no els veu) ni en crides internes de la classe a si mateixa (no passen pel proxy). Quan ho vegis a 11-02, ja sabràs per què.

  1. El preu de la reflexió

La reflexió no és gratis, i convé tenir les magnituds clares.

Cost en rendiment

Aquestes xifres són il·lustratives i varien enormement segons JVM, versió i escalfament, però l'ordre de magnitud és representatiu:

Operació Cost relatiu aproximat
Crida directa a un mètode 1× (referència)
Crida per Method.invoke cacheat i amb setAccessible(true) 2× – 5×
Crida per Method.invoke sense cachejar el Method 20× – 100×
Class.forName + getMethod + invoke a cada crida 100× – 1000×
Accés directe a un camp
Field.get cacheat 2× – 10×

La conclusió pràctica és una de sola: el car és la cerca, no la invocació. getMethod, getDeclaredFields i Class.forName recorren estructures internes, construeixen arrays defensius nous a cada crida i comproven accessos. La invocació en si, amb el Method ja a la mà i accessible, la JIT l'optimitza força bé.

Per això el patró universal en frameworks és fer la introspecció una vegada i guardar-la en memòria cau, exactament com fa l'ExportadorAnotat amb el seu Map<Class<?>, List<Field>>. Spring construeix les seves metadades en arrencar; Hibernate mapa les entitats en iniciar la sessió; Jackson guarda els seus serialitzadors per classe.

I el mesurament honest: mesurar això amb un bucle casolà dona resultats falsos, per l'escalfament de la JIT i l'eliminació de codi mort. Això ho veuràs a 10-07 amb JMH.

Els altres costos, que solen ser pitjors

Pèrdua de comprovació en compilació. És el cost més car i no apareix en cap perfil de rendiment:

// Si algu reanomena getTitol() a titol(), aixo compila igual
// i falla en execucio, potser mesos despres, potser en produccio.
Method m = tipus.getMethod("getTitol");

Codi fràgil davant de refactoritzacions. El "Reanomenar" de l'IDE canvia totes les crides... excepte les cadenes de text. Un getDeclaredField("titol") sobreviu intacte a un reanomenament i falla en execució. És el mateix problema de l'XML de l'apartat 19 de 10-02, i per això els frameworks moderns prefereixen anotacions (que sí que es reanomenen amb la classe) a noms en cadenes.

Problemes amb la compilació nativa. GraalVM Native Image compila a binari i necessita saber en compilació quines classes es faran servir. La reflexió trenca aquesta anàlisi: si el nom de la classe es construeix en execució, el compilador no pot saber que cal incloure-la, i l'elimina. Cal declarar-la en un reflect-config.json. Aquesta és una de les grans raons per les quals Spring Boot 3 i Quarkus mouen feina de la reflexió en execució al processament d'anotacions en compilació.

Seguretat. setAccessible(true) travessa l'encapsulament. Un connector carregat dinàmicament pot llegir camps privats amb credencials.

Traces de pila illegibles. Cada capa de reflexió afegeix línies de jdk.internal.reflect. Les traces quilomètriques d'Spring vénen d'aquí.

  1. Quan fer-la servir i quan no

La regla, en una frase: la reflexió la fan servir les biblioteques, no la lògica de negoci.

Si estàs escrivint codi que resol un problema de Nexus Software —prestar llibres, calcular multes, avisar de venciments— i apareix un Class.forName, alguna cosa va malament. Si estàs escrivint una infraestructura que ha de funcionar amb classes que encara no existeixen, la reflexió és l'eina correcta.

Fes servir reflexió quan... No la facis servir quan...
Escrius un framework o una biblioteca genèrica Escrius lògica de negoci
Has de treballar amb classes desconegudes en compilació Coneixes el tipus: fes servir polimorfisme (03-06)
Implementes serialització, mapatge o injecció Vols "estalviar-te" un switch o una interfície
Carregues connectors descoberts en execució Vols saltar-te una API pública aliena
Escrius eines de diagnòstic És en un bucle calent
Implementes un framework de proves Hi ha una alternativa amb comprovació del compilador

Abans de recórrer a la reflexió, comprova aquestes alternatives:

  1. Polimorfisme. Si tens tres tipus i vols comportament diferent, una interfície amb tres implementacions és millor que descobrir mètodes per reflexió.
  2. ServiceLoader. Per descobrir implementacions d'una interfície al classpath, el JDK porta un mecanisme estàndard que és més net que cercar classes a mà.
  3. Supplier<T> o Function<T, R>. Si només necessites "crear un objecte d'un tipus que decideix qui em crida", un Supplier<T> (10-01) dona el mateix amb comprovació de tipus.
  4. Processador d'anotacions. Si la informació es coneix en compilació, generar codi costa zero en execució (10-02, apartat 16).

  1. MethodHandles i VarHandle: l'alternativa moderna

Java 7 va introduir java.lang.invoke amb una alternativa més ràpida a la reflexió clàssica, i Java 9 la va completar amb VarHandle.

Un MethodHandle és una referència executable i tipada a un mètode. La diferència clau amb Method.invoke és que la comprovació d'accés es fa una vegada, en obtenir-lo, i no a cada invocació, i que la JIT el pot optimitzar gairebé com una crida directa quan el handle és una constant static final.

package com.nexussoftware.bibliotech.servei;

import java.lang.invoke.*;

public class ExempleMethodHandle {

    public static void main(String[] args) throws Throwable {

        MethodHandles.Lookup lookup = MethodHandles.lookup();

        // El TIPUS del metode es declara explicitament: (retorn, parametres...)
        MethodType tipus = MethodType.methodType(String.class);
        MethodHandle getTitol = lookup.findVirtual(Llibre.class, "getTitol", tipus);

        Llibre llibre = new Llibre("978-0000000001", "Java Eficac");

        // invokeExact exigeix que els tipus coincideixin EXACTAMENT
        String titol = (String) getTitol.invokeExact(llibre);
        System.out.println(titol);

        // VarHandle per a camps, amb semantica de memoria explicita (repren 08-04)
        VarHandle campPrestat = MethodHandles
                .privateLookupIn(Llibre.class, lookup)
                .findVarHandle(Llibre.class, "prestat", boolean.class);

        System.out.println("prestat: " + campPrestat.get(llibre));
        campPrestat.setVolatile(llibre, true);               // escriptura volatil
        System.out.println("prestat: " + campPrestat.get(llibre));

        // I operacions atomiques sense AtomicBoolean (08-06)
        boolean canviat = campPrestat.compareAndSet(llibre, true, false);
        System.out.println("compareAndSet: " + canviat);
    }
}
Java Eficac
prestat: false
prestat: true
compareAndSet: true
Aspecte Reflexió clàssica MethodHandle / VarHandle
Comprovació d'accés A cada invoke (llevat de setAccessible) Una vegada, en obtenir el handle
Optimització de la JIT Limitada Bona, com a crida directa si és static final
Tipatge de la invocació Object invoke(Object, Object...) MethodType explícit, invokeExact estricte
Excepcions Comprovades, amb embolcall Throwable, sense embolcall
Semàntica de memòria No la controla VarHandle: volatile, acquire, release, CAS
Facilitat d'ús Més gran Menor: més cerimonial
Introspecció (llistar membres) No: només invocació

Quan fer servir cadascun:

  • Per descobrir l'estructura (llistar camps, llegir anotacions): reflexió clàssica, que és l'única que ho fa.
  • Per invocar repetidament alguna cosa ja descoberta en un camí crític: MethodHandle.
  • Per accés atòmic o amb semàntica de memòria a camps: VarHandle, que substitueix els antics AtomicReferenceFieldUpdater i el sun.misc.Unsafe de 08-06.

Els VarHandle són, de fet, la base de les classes atòmiques modernes del JDK. I les lambdes de 04-05 s'implementen amb invokedynamic i MethodHandles per sota: quan escrius llibre -> llibre.getTitol(), el compilador genera una crida invokedynamic que en el primer ús construeix la implementació amb LambdaMetafactory, fent servir handles. Per això les lambdes són gairebé tan ràpides com una crida directa i no tenen el cost de la reflexió.

Errors Comuns i Consells

1. Oblidar @Retention(RetentionPolicy.RUNTIME) a l'anotació. L'error es manifesta aquí, a la reflexió, encara que la seva causa sigui a 10-02: isAnnotationPresent retorna false sense cap explicació. És el primer que cal comprovar quan un motor d'anotacions "no veu" res.

2. Fer servir getFields() esperant veure els privats. getFields() dona només els públics (propis i heretats); getDeclaredFields() dona tots els d'aquesta classe però cap heretat. Per a tots els camps de la jerarquia cal recórrer les superclasses a mà.

3. No cridar setAccessible(true) abans de tocar un membre privat. Salta IllegalAccessException amb un missatge que menciona els modificadors.

4. Capturar InvocationTargetException i no rellançar getCause(). El client veu un embolcall inútil en lloc de la seva pròpia excepció, i els seus catch deixen de funcionar. És el bug més freqüent en escriure un InvocationHandler.

5. Cercar constructors o mètodes amb Integer.class on el paràmetre és int. La cerca compara tipus exactes. getDeclaredConstructor(int.class) i getDeclaredConstructor(Integer.class) són cerques diferents, i l'equivocada produeix un NoSuchMethodException desconcertant.

6. No guardar la introspecció en memòria cau. getDeclaredFields() construeix un array nou cada vegada. Cridar-lo dins d'un bucle sobre deu mil entitats multiplica el cost per deu mil sense cap necessitat.

7. Refiar-se de l'ordre de getDeclaredFields() o getDeclaredMethods(). L'especificació diu explícitament que no hi ha ordre garantit. Ordena tu, per això @CampCsvordre.

8. No filtrar mètodes pont i sintètics. Els genèrics (10-01) generen mètodes que tu no vas escriure. Sense isBridge() i isSynthetic(), el teu motor els processa com si fossin reals.

9. Fer servir Class.newInstance(). Obsolet des de Java 9 perquè propaga excepcions comprovades sense declarar-les. Fes servir getDeclaredConstructor().newInstance().

10. Esperar que un proxy dinàmic funcioni sobre una classe. java.lang.reflect.Proxy només implementa interfícies. Si el teu servei no té interfície, aquest mecanisme no serveix, i d'aquí vénen la meitat de les preguntes sobre per què @Transactional "no fa res".

Consell 1: encapsula la reflexió en una capa i no la deixis sortir. Que ExportadorAnotat sigui l'únic lloc del projecte on apareix getDeclaredFields. La resta de BiblioTech no ha de saber que existeix la reflexió.

Consell 2: falla aviat i amb missatges clars. Si el teu motor no troba un mètode o una anotació, llança una excepció amb el nom de la classe, el del membre i què esperaves. Els errors de reflexió són opacs per naturalesa; compensa-ho amb missatges generosos.

Consell 3: guarda-ho sempre en memòria cau, des del primer dia. Un Map<Class<?>, X> amb computeIfAbsent són deu línies i elimina el 90 % del cost. Si el motor es fa servir des de diversos fils, ConcurrentHashMap (08-06).

Consell 4: prefereix anotacions a cadenes. @CampCsv sobre el camp es reanomena amb ell; getDeclaredField("titol") no. Cada cadena amb un nom de membre és una refactorització futura que fallarà en silenci.

Consell 5: compila amb -parameters. Sense aquesta opció, els noms dels paràmetres són arg0, arg1, i qualsevol motor que els vulgui fer servir queda inutilitzat.

Exercicis

Exercici 1: el validador que consumeix @Validar

A l'exercici 1 de 10-02 vas definir l'anotació @Validar amb obligatori, longitudMinima, longitudMaxima, patro i missatge. Escriu ara el motor.

  • Classe ValidadorAnotat amb un mètode Resultat<T> validar(T entitat) (fes servir el Resultat<T> genèric de 10-01).
  • Ha de recórrer tots els camps anotats de la classe i de les seves superclasses, guardar la introspecció en memòria cau, i acumular totes les fallades.
  • Ha de respectar cada element de l'anotació: obligatorietat, longituds, patró (amb Pattern precompilat i cacheat) i missatge personalitzat.
  • Els missatges generats per defecte han d'incloure el nom del camp i el valor rebut.
  • Afegeix un mètode descriure(Class<?>) que imprimeixi les regles d'una classe sense necessitat d'instanciar-la.

Prova'l amb tres Llibre: un de vàlid, un amb l'ISBN mal format i un amb el títol buit i l'autor massa llarg.

Exercici 2: mini contenidor d'injecció de dependències

Construeix un contenidor de dependències mínim per a BiblioTech, amb el qual entendràs què fa Spring en arrencar.

  • Defineix @Component (sobre classes, RUNTIME) i @Injectar (sobre constructors i camps, RUNTIME).
  • Escriu ContenidorSimple amb:
    • registrar(Class<?>... classes): registra els tipus disponibles.
    • <T> T obtenir(Class<T> tipus): retorna la instància, creant-la si cal.
    • Resolució per constructor: si hi ha un constructor amb @Injectar, resol recursivament cada paràmetre pel seu tipus.
    • Resolució per camp: després de crear la instància, injecta els camps amb @Injectar.
    • Singleton: cada tipus s'instancia una sola vegada.
    • Detecció de dependències circulars amb un missatge útil que mostri el cicle.
  • Demostra-ho muntant GestorPrestecs amb els seus dos Repositori i una CalculadoraMultes, sense escriure ni un new.

Exercici 3: proxy de reintents que consumeix @Reintentable

A l'exercici 3 de 10-02 vas definir @Reintentable(intents, esperaInicialMs, factorDeCreixement, excepcions, nomesIdempotents). Escriu el proxy que l'aplica.

  • ProxyReintents implementant InvocationHandler.
  • Ha de llegir l'anotació del mètode real, reintentar només davant de les excepcions declarades, esperar amb creixement exponencial i aleatorització (jitter, reprèn 09-06) i respectar la interrupció del fil (08-02).
  • Si s'exhaureixen els intents, ha de llançar l'última excepció amb les anteriors com a excepcions suprimides (addSuppressed, reprèn 06-06).
  • Ha de registrar cada intent amb la seva espera.
  • Prova'l amb un ClientMetadades fals que falli les dues primeres vegades i funcioni a la tercera, i amb un altre que falli sempre.

Solucions

Solució 1

package com.nexussoftware.bibliotech.servei;

import com.nexussoftware.bibliotech.anotacions.Validar;

import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

/**
 * Motor de validacio dirigit per @Validar.
 * No coneix Llibre, Prestec ni cap entitat concreta.
 */
public class ValidadorAnotat {

    /** Introspeccio cacheada per classe (ConcurrentHashMap: es pot fer servir des de diversos fils). */
    private final Map<Class<?>, List<Field>> campsPerClasse = new ConcurrentHashMap<>();

    /** Patrons compilats: Pattern.compile es car i el patro no canvia. */
    private final Map<String, Pattern> patrons = new ConcurrentHashMap<>();

    // ------------------------------------------------------------------

    public <T> Resultat<T> validar(T entitat) {
        if (entitat == null) {
            return Resultat.fallada("L'entitat es nulla");
        }

        List<String> fallades = new ArrayList<>();

        for (Field camp : campsValidables(entitat.getClass())) {
            Validar regla = camp.getAnnotation(Validar.class);
            Object valor = llegir(camp, entitat);
            comprovar(camp, regla, valor, fallades);
        }

        if (fallades.isEmpty()) {
            return Resultat.exit(entitat);
        }
        return Resultat.fallada(entitat.getClass().getSimpleName()
                + " no valid: " + String.join("; ", fallades));
    }

    /** Imprimeix les regles d una classe sense instanciar-la. */
    public void descriure(Class<?> tipus) {
        System.out.println("Regles de " + tipus.getSimpleName() + ":");
        for (Field camp : campsValidables(tipus)) {
            Validar r = camp.getAnnotation(Validar.class);
            System.out.printf("  %-12s obligatori=%-5s long=[%d..%s] patro=%s%n",
                    camp.getName(),
                    r.obligatori(),
                    r.longitudMinima(),
                    r.longitudMaxima() == Integer.MAX_VALUE ? "∞" : r.longitudMaxima(),
                    r.patro().isEmpty() ? "-" : r.patro());
        }
    }

    // ------------------------------------------------------------------

    /** Camps anotats de la classe i de totes les seves superclasses. */
    private List<Field> campsValidables(Class<?> tipus) {
        return campsPerClasse.computeIfAbsent(tipus, t -> {
            List<Field> trobats = new ArrayList<>();
            for (Class<?> c = t; c != null && c != Object.class; c = c.getSuperclass()) {
                for (Field camp : c.getDeclaredFields()) {
                    if (camp.isSynthetic() || !camp.isAnnotationPresent(Validar.class)) {
                        continue;
                    }
                    camp.setAccessible(true);
                    trobats.add(camp);
                }
            }
            // Ordre estable per nom: getDeclaredFields no el garanteix
            trobats.sort(Comparator.comparing(Field::getName));
            return List.copyOf(trobats);
        });
    }

    private Object llegir(Field camp, Object entitat) {
        try {
            return camp.get(entitat);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException("No s ha pogut llegir " + camp.getName(), e);
        }
    }

    private void comprovar(Field camp, Validar regla, Object valor, List<String> fallades) {

        String nom = camp.getName();

        // 1. Obligatorietat
        if (valor == null) {
            if (regla.obligatori()) {
                fallades.add(missatge(regla, "'" + nom + "' es obligatori i esta buit"));
            }
            return;                                  // sense valor no hi ha res mes a comprovar
        }

        // Les altres regles nomes apliquen a text
        if (!(valor instanceof String text)) {
            return;
        }

        if (regla.obligatori() && text.isBlank()) {
            fallades.add(missatge(regla, "'" + nom + "' es obligatori i esta en blanc"));
            return;
        }

        // 2. Longituds
        if (text.length() < regla.longitudMinima()) {
            fallades.add(missatge(regla, String.format(
                    "'%s' necessita almenys %d caracters i en te %d",
                    nom, regla.longitudMinima(), text.length())));
        }
        if (text.length() > regla.longitudMaxima()) {
            fallades.add(missatge(regla, String.format(
                    "'%s' admet com a maxim %d caracters i en te %d",
                    nom, regla.longitudMaxima(), text.length())));
        }

        // 3. Patro
        if (!regla.patro().isEmpty()) {
            Pattern patro = patrons.computeIfAbsent(regla.patro(), Pattern::compile);
            if (!patro.matcher(text).matches()) {
                fallades.add(missatge(regla, String.format(
                        "'%s' no compleix el format %s (valor: \"%s\")",
                        nom, regla.patro(), text)));
            }
        }
    }

    /** El missatge personalitzat de l anotacio te prioritat sobre el generat. */
    private String missatge(Validar regla, String generat) {
        return regla.missatge().isEmpty() ? generat : regla.missatge();
    }
}

Prova:

package com.nexussoftware.bibliotech;

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

import java.util.List;

public class ProvaValidadorAnotat {

    public static void main(String[] args) {

        ValidadorAnotat validador = new ValidadorAnotat();

        validador.descriure(Llibre.class);
        System.out.println();

        List<Llibre> candidats = List.of(
                new Llibre("978-0000000001", "Java Eficac",     "Joshua Bloch"),
                new Llibre("978-1",          "Refactoritzacio", "Martin Fowler"),
                new Llibre("978-0000000003", "",                "A".repeat(150)));

        for (Llibre llibre : candidats) {
            Resultat<Llibre> r = validador.validar(llibre);
            System.out.println(r.esExit()
                    ? "OK       -> " + r.valor().getTitol()
                    : "REBUTJAT -> " + r.error());
        }
    }
}
Regles de Llibre:
  autor        obligatori=false long=[0..120] patro=-
  isbn         obligatori=true  long=[0..∞] patro=978-\d{10}
  titol        obligatori=true  long=[1..200] patro=-

OK       -> Java Eficac
REBUTJAT -> Llibre no valid: L ISBN ha de seguir el format 978-XXXXXXXXXX
REBUTJAT -> Llibre no valid: 'autor' admet com a maxim 120 caracters i en te 150; 'titol' es obligatori i esta en blanc

Comentaris.

Els dos nivells de memòria cau són el patró real. Un per a la introspecció per classe i un altre per als Pattern compilats. Pattern.compile construeix un autòmat: fer-ho a cada validació de cada camp de cada entitat és un malbaratament enorme i una font clàssica de lentitud.

El missatge personalitzat guanya al generat. L'ISBN mal format produeix el missatge que va posar el desenvolupador a l'anotació, no el tècnic. És la diferència entre un missatge per a l'usuari i un per al log.

ConcurrentHashMap i no HashMap. Aquest validador és un objecte que es comparteix —a Spring seria un @Component singleton— i diversos fils el cridaran alhora. Un HashMap sense sincronitzar en aquesta situació es pot corrompre (08-06).

I fixa't en el que aquest validador té en comú amb ExportadorAnotat: recórrer la jerarquia, filtrar per anotació, setAccessible, ordenar, cachejar. És el mateix esquelet. Quan l'hagis escrit dues vegades, reconeixeràs aquest esquelet dins de qualsevol framework.

Solució 2

package com.nexussoftware.bibliotech.anotacions;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
    String nom() default "";
}
package com.nexussoftware.bibliotech.anotacions;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD})
public @interface Injectar {
}
package com.nexussoftware.bibliotech.servei;

import com.nexussoftware.bibliotech.anotacions.*;

import java.lang.reflect.*;
import java.util.*;

/**
 * Contenidor d injeccio de dependencies minim.
 * Es, en essencia, el que fa l ApplicationContext d Spring
 * reduit a l imprescindible.
 */
public class ContenidorSimple {

    /** Tipus registrats com a disponibles. */
    private final Set<Class<?>> registrats = new LinkedHashSet<>();

    /** Instancies ja creades: cada component es un singleton. */
    private final Map<Class<?>, Object> instancies = new LinkedHashMap<>();

    /** Tipus que s estan creant ara mateix: serveix per detectar cicles. */
    private final Deque<Class<?>> enConstruccio = new ArrayDeque<>();

    // ------------------------------------------------------------------

    public ContenidorSimple registrar(Class<?>... classes) {
        for (Class<?> classe : classes) {
            if (!classe.isAnnotationPresent(Component.class)) {
                throw new IllegalArgumentException(
                        classe.getSimpleName() + " no esta anotada amb @Component");
            }
            registrats.add(classe);
        }
        return this;
    }

    /** Registra una instancia ja construida (per a configuracio, valors, etc.). */
    public <T> ContenidorSimple registrarInstancia(Class<T> tipus, T instancia) {
        instancies.put(tipus, instancia);
        registrats.add(tipus);
        return this;
    }

    @SuppressWarnings("unchecked")
    public <T> T obtenir(Class<T> tipus) {
        // Conversio segura: nomes desem instancies del tipus amb el qual s indexen
        return (T) resoldre(tipus);
    }

    public void llistar() {
        System.out.println("Components instanciats (" + instancies.size() + "):");
        instancies.forEach((tipus, obj) ->
                System.out.printf("  %-24s -> %s%n", tipus.getSimpleName(), obj));
    }

    // ------------------------------------------------------------------

    private Object resoldre(Class<?> tipus) {

        // 1. Ja existeix? Singleton.
        Object existent = instancies.get(tipus);
        if (existent != null) {
            return existent;
        }

        // 2. Cercar una implementacio registrada compatible
        Class<?> implementacio = cercarImplementacio(tipus);

        // 3. Detectar cicles ABANS d entrar en recursio
        if (enConstruccio.contains(implementacio)) {
            List<String> cicle = new ArrayList<>();
            for (Class<?> c : enConstruccio) {
                cicle.add(0, c.getSimpleName());
            }
            cicle.add(implementacio.getSimpleName());
            throw new IllegalStateException("Dependencia circular: " + String.join(" -> ", cicle));
        }

        enConstruccio.push(implementacio);
        try {
            Object instancia = construir(implementacio);
            injectarCamps(instancia);
            instancies.put(tipus, instancia);
            if (tipus != implementacio) {
                instancies.put(implementacio, instancia);
            }
            return instancia;
        } finally {
            enConstruccio.pop();
        }
    }

    /** Si demanen una interficie, busca l unica implementacio registrada. */
    private Class<?> cercarImplementacio(Class<?> tipus) {
        if (registrats.contains(tipus) && !tipus.isInterface()) {
            return tipus;
        }
        List<Class<?>> candidats = new ArrayList<>();
        for (Class<?> c : registrats) {
            if (tipus.isAssignableFrom(c) && !c.isInterface()
                    && !Modifier.isAbstract(c.getModifiers())) {
                candidats.add(c);
            }
        }
        if (candidats.isEmpty()) {
            throw new IllegalStateException(
                    "No hi ha cap component registrat que satisfaci " + tipus.getSimpleName());
        }
        if (candidats.size() > 1) {
            throw new IllegalStateException("Hi ha " + candidats.size()
                    + " candidats per a " + tipus.getSimpleName() + ": " + candidats);
        }
        return candidats.get(0);
    }

    /** Instancia resolent recursivament els parametres del constructor. */
    private Object construir(Class<?> classe) {

        Constructor<?> triat = triarConstructor(classe);
        triat.setAccessible(true);

        Class<?>[] tipusParametre = triat.getParameterTypes();
        Object[] arguments = new Object[tipusParametre.length];

        for (int i = 0; i < tipusParametre.length; i++) {
            arguments[i] = resoldre(tipusParametre[i]);       // RECURSIO
        }

        try {
            return triat.newInstance(arguments);
        } catch (InvocationTargetException e) {
            throw new IllegalStateException(
                    "El constructor de " + classe.getSimpleName() + " ha llancat una excepcio",
                    e.getCause());                            // la CAUSA, no l embolcall
        } catch (ReflectiveOperationException e) {
            throw new IllegalStateException("No s ha pogut instanciar " + classe.getSimpleName(), e);
        }
    }

    private Constructor<?> triarConstructor(Class<?> classe) {
        Constructor<?> senseArguments = null;

        for (Constructor<?> c : classe.getDeclaredConstructors()) {
            if (c.isAnnotationPresent(Injectar.class)) {
                return c;                                     // prioritat a l anotat
            }
            if (c.getParameterCount() == 0) {
                senseArguments = c;
            }
        }
        if (senseArguments != null) {
            return senseArguments;
        }
        // Convencio: si nomes n hi ha un, es fa servir (com fa Spring des de 4.3)
        Constructor<?>[] tots = classe.getDeclaredConstructors();
        if (tots.length == 1) {
            return tots[0];
        }
        throw new IllegalStateException(classe.getSimpleName()
                + " te diversos constructors i cap no porta @Injectar");
    }

    /** Injeccio per camp, DESPRES de construir. */
    private void injectarCamps(Object instancia) {
        for (Class<?> c = instancia.getClass(); c != null && c != Object.class; c = c.getSuperclass()) {
            for (Field camp : c.getDeclaredFields()) {
                if (!camp.isAnnotationPresent(Injectar.class)) {
                    continue;
                }
                camp.setAccessible(true);
                try {
                    camp.set(instancia, resoldre(camp.getType()));
                } catch (IllegalAccessException e) {
                    throw new IllegalStateException(
                            "No s ha pogut injectar el camp " + camp.getName(), e);
                }
            }
        }
    }
}

Els components:

@Component
public class CalculadoraMultes {
    public double calcular(int diesDeRetard) { return diesDeRetard * 0.25; }
    @Override public String toString() { return "[email protected]€/dia"; }
}

@Component
public class RepositoriMaterials extends Repositori<Material> {
    public RepositoriMaterials() { super("cataleg"); }
}

@Component
public class RepositoriPrestecs extends Repositori<Prestec> {
    public RepositoriPrestecs() { super("prestecs"); }
}

@Component
public class GestorPrestecs implements ServeiPrestecs {

    private final RepositoriMaterials cataleg;
    private final RepositoriPrestecs prestecs;

    @Injectar                                  // el contenidor fara servir AQUEST constructor
    public GestorPrestecs(RepositoriMaterials cataleg, RepositoriPrestecs prestecs) {
        this.cataleg = cataleg;
        this.prestecs = prestecs;
    }

    @Injectar                                  // injeccio per camp
    private CalculadoraMultes calculadora;

    @Override public String toString() {
        return "GestorPrestecs[cataleg=" + cataleg + ", multes=" + calculadora + "]";
    }
}

L'arrencada:

public class ArrencadaAmbContenidor {

    public static void main(String[] args) {

        ContenidorSimple contenidor = new ContenidorSimple()
                .registrar(GestorPrestecs.class,
                           RepositoriMaterials.class,
                           RepositoriPrestecs.class,
                           CalculadoraMultes.class);

        // NI UN SOL new: el contenidor resol tot el graf
        ServeiPrestecs servei = contenidor.obtenir(ServeiPrestecs.class);

        System.out.println("Obtingut: " + servei);
        System.out.println();
        contenidor.llistar();

        // Singleton: dues peticions donen el MATEIX objecte
        System.out.println();
        System.out.println("Singleton? "
                + (contenidor.obtenir(CalculadoraMultes.class)
                   == contenidor.obtenir(CalculadoraMultes.class)));
    }
}
Obtingut: GestorPrestecs[cataleg=Repositori[cataleg, 0 entitats], [email protected]€/dia]

Components instanciats (5):
  RepositoriMaterials      -> Repositori[cataleg, 0 entitats]
  RepositoriPrestecs       -> Repositori[prestecs, 0 entitats]
  CalculadoraMultes        -> [email protected]€/dia
  GestorPrestecs           -> GestorPrestecs[...]
  ServeiPrestecs           -> GestorPrestecs[...]

Singleton? true

I la detecció de cicles, amb dos components que es necessiten mútuament:

Exception in thread "main" java.lang.IllegalStateException:
    Dependencia circular: ServeiA -> ServeiB -> ServeiA

Comentaris. Aquest exercici és el més revelador de la lliçó.

Cent cinquanta línies i ja tens injecció de dependències. Resolució recursiva per constructor, injecció per camp, singletons, resolució d'interfície a implementació i detecció de cicles. Spring fa el mateix, amb moltíssims més casos: àmbits, cicles de vida, configuració externa, proxies, perfils, esdeveniments. Però el nucli és aquest, i ja no és un misteri.

La detecció de cicles fa servir una pila. enConstruccio guarda què s'està creant ara; si la recursió torna a demanar alguna cosa que ja és a la pila, hi ha cicle. Sense aquesta comprovació, el programa entraria en recursió infinita i moriria amb StackOverflowError sense dir per què — que és exactament el que feien els contenidors primitius.

La injecció per camp va després de construir, i no pot ser d'altra manera: no es pot escriure en un camp d'un objecte que encara no existeix. Això explica una limitació real d'Spring: un camp @Autowired no està disponible dins del constructor. És la raó principal per la qual es recomana la injecció per constructor: garanteix que l'objecte neix complet i permet camps final.

El throw new IllegalStateException(..., e.getCause()) del constructor és la regla de l'apartat 9 aplicada. Si el constructor del teu component llança una excepció, vols veure-la a ella, no una InvocationTargetException.

Solució 3

package com.nexussoftware.bibliotech.xarxa;

import com.nexussoftware.bibliotech.anotacions.Reintentable;

import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Proxy que aplica la politica de @Reintentable.
 * Substitueix la logica de reintents codificada a ma
 * dins de ClientHttpResistent (09-06).
 */
public final class ProxyReintents implements InvocationHandler {

    private static final Logger LOG = Logger.getLogger("bibliotech.reintents");

    private final Object objectiu;

    private ProxyReintents(Object objectiu) {
        this.objectiu = objectiu;
    }

    @SuppressWarnings("unchecked")
    public static <T> T ambReintents(T objectiu, Class<T> interficie) {
        return (T) Proxy.newProxyInstance(
                interficie.getClassLoader(),
                new Class<?>[] { interficie },
                new ProxyReintents(objectiu));
    }

    @Override
    public Object invoke(Object proxy, Method metode, Object[] args) throws Throwable {

        Reintentable politica = resoldrePolitica(metode);

        if (politica == null || politica.intents() <= 1) {
            return invocar(metode, args);          // sense politica: una sola vegada
        }

        List<Throwable> falladesPrevies = new ArrayList<>();
        long espera = politica.esperaInicialMs();

        for (int intent = 1; intent <= politica.intents(); intent++) {
            try {
                Object resultat = invocar(metode, args);

                if (intent > 1) {
                    int n = intent;
                    LOG.info(() -> String.format("%s() ha reeixit a l'intent %d",
                            metode.getName(), n));
                }
                return resultat;

            } catch (Throwable fallada) {

                if (!esReintentable(fallada, politica)) {
                    LOG.fine(() -> "No es reintenta " + fallada.getClass().getSimpleName()
                            + ": no es a la politica");
                    throw fallada;                  // error permanent: no insistir
                }

                falladesPrevies.add(fallada);

                if (intent == politica.intents()) {
                    // Ultim intent: adjuntar els anteriors com a suprimits (06-06)
                    for (int i = 0; i < falladesPrevies.size() - 1; i++) {
                        fallada.addSuppressed(falladesPrevies.get(i));
                    }
                    LOG.log(Level.SEVERE, () -> String.format(
                            "%s() ha exhaurit els seus %d intents", metode.getName(), politica.intents()));
                    throw fallada;
                }

                // Aleatoritzacio (jitter): evita que N clients reintentin alhora (09-06)
                long ambJitter = espera + ThreadLocalRandom.current().nextLong(espera / 2 + 1);

                int n = intent;
                LOG.warning(() -> String.format("%s() ha fallat a l'intent %d (%s); espera %d ms",
                        metode.getName(), n, fallada.getClass().getSimpleName(), ambJitter));

                dormir(ambJitter);

                espera = (long) (espera * politica.factorDeCreixement());
            }
        }
        throw new IllegalStateException("inabastable");
    }

    // ------------------------------------------------------------------

    private Reintentable resoldrePolitica(Method metodeDeInterficie) {
        try {
            Method real = objectiu.getClass().getMethod(
                    metodeDeInterficie.getName(), metodeDeInterficie.getParameterTypes());
            return real.getAnnotation(Reintentable.class);
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

    private boolean esReintentable(Throwable fallada, Reintentable politica) {
        for (Class<? extends Exception> tipus : politica.excepcions()) {
            if (tipus.isInstance(fallada)) {    // isInstance: l instanceof reflexiu
                return true;
            }
        }
        return false;
    }

    private Object invocar(Method metode, Object[] args) throws Throwable {
        try {
            return metode.invoke(objectiu, args);
        } catch (InvocationTargetException e) {
            throw e.getCause();
        }
    }

    /** Respecta la interrupcio: restaura el marcador i avorta (08-02). */
    private void dormir(long ms) throws InterruptedException {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw e;
        }
    }
}

Prova amb un client fals:

package com.nexussoftware.bibliotech;

import com.nexussoftware.bibliotech.anotacions.Reintentable;
import com.nexussoftware.bibliotech.xarxa.ProxyReintents;

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

public class ProvaProxyReintents {

    interface ServeiMetadades {
        String consultarPerIsbn(String isbn) throws IOException;
        String sempreFalla(String isbn) throws IOException;
    }

    /** Doble de prova: falla les dues primeres vegades i despres funciona. */
    static class ClientInestable implements ServeiMetadades {

        private int crides = 0;

        @Override
        @Reintentable(intents = 4, esperaInicialMs = 100, factorDeCreixement = 2.0,
                      excepcions = { SocketTimeoutException.class })
        public String consultarPerIsbn(String isbn) throws IOException {
            crides++;
            if (crides < 3) {
                throw new SocketTimeoutException("temps exhaurit (crida " + crides + ")");
            }
            return "{\"isbn\":\"" + isbn + "\",\"titol\":\"Java Eficac\"}";
        }

        @Override
        @Reintentable(intents = 3, esperaInicialMs = 50, factorDeCreixement = 3.0,
                      excepcions = { IOException.class })
        public String sempreFalla(String isbn) throws IOException {
            throw new IOException("el servei de metadades no respon");
        }
    }

    public static void main(String[] args) throws Exception {

        ConfiguracioLog.inicialitzar();

        ServeiMetadades servei =
                ProxyReintents.ambReintents(new ClientInestable(), ServeiMetadades.class);

        System.out.println("--- cas 1: es recupera al tercer intent ---");
        System.out.println("Resultat: " + servei.consultarPerIsbn("978-0000000001"));

        System.out.println();
        System.out.println("--- cas 2: exhaureix els intents ---");
        try {
            servei.sempreFalla("978-0000000002");
        } catch (IOException e) {
            System.out.println("Excepcio final: " + e.getMessage());
            System.out.println("Fallades suprimides: " + e.getSuppressed().length);
            for (Throwable s : e.getSuppressed()) {
                System.out.println("  suprimida: " + s.getMessage());
            }
        }
    }
}
--- cas 1: es recupera al tercer intent ---
[WARNING] consultarPerIsbn() ha fallat a l'intent 1 (SocketTimeoutException); espera 137 ms
[WARNING] consultarPerIsbn() ha fallat a l'intent 2 (SocketTimeoutException); espera 265 ms
[INFO]    consultarPerIsbn() ha reeixit a l'intent 3
Resultat: {"isbn":"978-0000000001","titol":"Java Eficac"}

--- cas 2: exhaureix els intents ---
[WARNING] sempreFalla() ha fallat a l'intent 1 (IOException); espera 71 ms
[WARNING] sempreFalla() ha fallat a l'intent 2 (IOException); espera 194 ms
[SEVERE]  sempreFalla() ha exhaurit els seus 3 intents
Excepcio final: el servei de metadades no respon
Fallades suprimides: 2
  suprimida: el servei de metadades no respon
  suprimida: el servei de metadades no respon

Comentaris.

La política ja no és al mètode: està declarada damunt d'ell. consultarPerIsbn no conté ni un bucle de reintents, ni un Thread.sleep, ni un comptador. Compara amb la versió de 09-06, on aquelles vint línies eren dins de cada mètode de xarxa i calia copiar-les en afegir el següent.

Les excepcions suprimides conserven l'historial complet. Amb addSuppressed (06-06), qui rep la fallada final pot veure tots els intents, no només l'últim. En un incident real, saber que les tres fallades van ser la mateixa IOException o que la primera va ser un timeout i la segona un ConnectException canvia el diagnòstic.

tipus.isInstance(fallada) és l'instanceof reflexiu, i és l'única forma de comprovar contra un Class<?> que ve d'una anotació. Un fallada instanceof tipus no compilaria: tipus és una variable, no un tipus.

La interrupció es respecta. Si algú interromp el fil durant l'espera, el proxy restaura el marcador i propaga la InterruptedException en lloc d'empassar-se-la i continuar reintentant. És la regla de 08-02, i en un proxy que dorm és especialment important: sense ella, un ExecutorService no es podria apagar mentre algú reintenta.

I amb això, BiblioTech té els seus tres motors. Un llegeix anotacions de camps i exporta; un altre llegeix anotacions de classes i munta el graf d'objectes; un altre llegeix anotacions de mètodes i intercepta crides. Els tres desconeixen per complet el domini de la biblioteca. Això és un framework.

Conclusió

Has escrit el lector que faltava, i amb ell has vist per dins com funcionen els frameworks.

Saps què és la reflexió: inspeccionar i manipular classes, camps i mètodes en temps d'execució sense conèixer-los en compilació. I saps quin és el tracte: guanyes flexibilitat total i perds tota l'ajuda del compilador.

Obtens l'objecte Class<?> de tres formes —el literal .class tipat, getClass() que dona el tipus real i no el declarat, i Class.forName que accepta una cadena, no requereix la classe en compilació i inicialitza la classe—. L'inspecciones amb getName/getSimpleName/getCanonicalName (i saps en què es diferencien amb classes imbricades, arrays i anònimes), en recorres la jerarquia amb getSuperclass i getInterfaces, i en llegeixes els modificadors amb la màscara de bits que interpreta Modifier. El classifiques amb isEnum, isRecord, isInterface, isArray, i n'extreus les constants amb getEnumConstants o els components amb getRecordComponents.

Domines la distinció que confon tothom: getFields dona els públics, propis i heretats; getDeclaredFields dona tots els d'aquesta classe però cap heretat. Una mira cap amunt i l'altra cap endins, i cap no dona tot, per la qual cosa cal recórrer la jerarquia amb un bucle sobre getSuperclass(). El mateix, exactament igual, per a mètodes i constructors. I filtres els ponts i sintètics que l'esborrat de genèrics de 10-01 va generar a la teva esquena.

Crees instàncies amb getDeclaredConstructor(tipus).newInstance(valors) —recordant que un paràmetre int es cerca amb int.class i no amb Integer.class, i que Class.newInstance() està obsolet perquè se salta les excepcions comprovades—. Invoques mètodes amb invoke(instancia, args...), passant null per als estàtics i rebent sempre Object. I saps que les excepcions del mètode invocat arriben embolcallades en InvocationTargetException, amb la regla d'or corresponent: captura i rellança getCause(), mai l'embolcall, o els catch dels teus usuaris deixaran de funcionar.

Accedeixes al que és privat amb setAccessible(true), entens que travessa per complet l'encapsulament de 03-07 —fins al punt de poder posar -999 pàgines en un private final que el constructor valida—, i saps que des de Java 9 el sistema de mòduls ho pot bloquejar amb InaccessibleObjectException, que --add-opens és la sortida d'emergència i que opens és diferent d'exports. Tot això es desenvolupa a 10-06.

I sobretot: vas escriure els motors. L'ExportadorAnotat recorre els camps amb @CampCsv de qualsevol entitat —present o futura—, els ordena, aplica formats, emmascara els sensibles i genera capçalera i línies sense conèixer Llibre, Prestec ni Fitxa. El ProxyAuditoria, construït sobre Proxy.newProxyInstance i InvocationHandler, intercepta cada crida a GestorPrestecs, en llegeix l'@Auditable —del mètode, i si no, de la classe—, registra inici, fi, durada i fallada, i emmascara els arguments marcats com a sensibles, mentre GestorPrestecs no conté ni una línia d'auditoria. I als exercicis has escrit un validador dirigit per anotacions, un contenidor d'injecció de dependències complet amb singletons, resolució recursiva i detecció de cicles, i un proxy de reintents amb espera creixent i excepcions suprimides.

Això ja no és aprendre reflexió: és haver escrit un framework en miniatura. Quan a 11-02 vegis que Spring instancia els teus serveis i els injecta, sabràs que recorre el classpath, busca @Component, resol constructors i crida newInstance. Quan @Transactional obri transaccions, sabràs que hi ha un proxy interceptant — i sabràs també per què no funciona en mètodes privats ni en crides internes. Quan Hibernate mapi les teves entitats llegint @Column, i Jackson serialitzi el teu record llegint-ne els components, i JUnit executi els teus @Test, reconeixeràs el mateix esquelet que has escrit tres vegades: recórrer, filtrar per anotació, setAccessible, ordenar, cachejar, invocar.

Saps també el preu. El car no és invocar, és cercar: getMethod i getDeclaredFields construeixen arrays nous a cada crida, i per això tots els frameworks —i el teu exportador— guarden la introspecció en memòria cau. Però els costos pitjors no són de rendiment: perds la comprovació del compilador, el codi es torna fràgil davant de refactoritzacions perquè l'IDE no reanomena les cadenes de text, i la compilació nativa deixa de poder analitzar el teu programa, que és la raó real per la qual Spring Boot 3 i Quarkus estan movent feina de la reflexió cap al processament d'anotacions en compilació. D'aquí la regla: la reflexió la fan servir les biblioteques, no la lògica de negoci. I coneixes l'alternativa moderna: MethodHandles per invocar ràpid el que ja s'ha descobert, i VarHandle per a accés amb semàntica de memòria i operacions atòmiques — la maquinària sobre la qual s'implementen les lambdes i les classes atòmiques del mateix JDK.

BiblioTech ha canviat de categoria. Ja no és un programa amb classes: és un programa amb infraestructura pròpia. Un exportador universal, un motor de validació, un contenidor de dependències i dos proxies que afegeixen auditoria i reintents sense tocar la lògica. GestorPrestecs parla només de préstecs; tota la resta ho declara amb etiquetes.

I tanmateix, el codi de dins continua igual de verbós. Per saber quants llibres de més de 400 pàgines estan prestats i ordenar-los per títol, EstadistiquesBiblioTech continua fent el mateix que al mòdul 5: declarar una llista buida, recórrer amb un for, comprovar amb un if, acumular, ordenar al final. Per agrupar els préstecs per empleat hi ha bucles imbricats amb un HashMap i un computeIfAbsent. I quan cercarPerMateria no troba res retorna null, i hi ha setze llocs on algú s'ha de recordar de comprovar-ho — i un d'ells se n'oblidarà.

A 10-04, Característiques de Java 8: Streams i Optional, s'acaben les dues coses. Veuràs l'API de Streams, que converteix aquests bucles en una descripció declarativa del que vols —filter, map, flatMap, sorted, collect, groupingBy— i fa que l'informe per tipus de material i empleat que ocupava trenta línies de bucles imbricats n'ocupi tres. Veuràs l'avaluació mandrosa, que fa que l'ordre real d'execució no sigui el que esperes. Veuràs els streams paral·lels i quan ajuden de debò i quan perjudiquen. I veuràs Optional, que fa impossible oblidar-se de comprovar l'absència, amb els seus antipatrons inclosos.

És la lliçó que el curs porta esperant des del mòdul 4, quan vas aprendre les lambdes i et vam dir que el seu veritable destí era més endavant. Ha arribat.

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