La lliçó anterior va acabar assenyalant el deute més gran de BiblioTech: un ContenidorSimple de cent cinquanta línies que resol dependències per reflexió i al qual li falta absolutament tota la resta. Sense àmbits, sense cicle de vida, sense perfils, sense propietats externalitzades, sense resolució d'ambigüitat i sense transaccions.

Aquesta lliçó el substitueix pel contenidor que porta vint anys polint-se.

Spring és el framework dominant del back-end en Java, i el seu nucli és exactament el que vas escriure tu: un contenidor que instancia objectes, resol les seves dependències i gestiona el seu cicle de vida. Tota la resta —web, persistència, seguretat, missatgeria, observabilitat— està construïda a sobre d'aquesta base.

L'avantatge amb què arribes és enorme. No aprendràs Spring com una llista d'anotacions que cal memoritzar: reconeixeràs, una per una, les peces que ja vas construir. Quan vegis @Component, veuràs el teu @Component. Quan vegis com el contenidor resol el constructor, veuràs el teu getDeclaredConstructors(). I quan arribis a @Transactional, veuràs el teu ProxyAuditoria — amb el mateix parany de la crida interna inclòs.

En acabar sabràs què és l'ApplicationContext i com es declaren beans; per què la injecció per constructor és l'única recomanable i quins problemes concrets té la injecció per camp; què afegeix cada estereotip; com funciona el cicle de vida d'un bean i quins àmbits existeixen; com es resolen les ambigüitats quan hi ha dues implementacions; com s'externalitza la configuració amb propietats tipades i perfils; què és Spring Boot i què és l'autoconfiguració de veritat —inclòs com depurar-la—; i com funciona AOP per sota amb els proxies que ja coneixes.

I BiblioTech passarà del seu contenidor casolà a Spring, amb perfils dev i prod i configuració tipada.

Contingut

  1. Què és Spring i què és Spring Boot
  2. Els mòduls de Spring Framework
  3. El contenidor d'IoC: ApplicationContext
  4. Què és un bean i com es declara
  5. Les tres formes d'injecció
  6. Per què la injecció per constructor
  7. Comparativa amb el ContenidorSimple casolà
  8. Estereotips: @Component, @Service, @Repository, @Controller
  9. @Configuration + @Bean enfront de l'escaneig de components
  10. El cicle de vida d'un bean
  11. Àmbits d'un bean
  12. Resolució d'ambigüitat: @Qualifier, @Primary i col·leccions
  13. @Value i les propietats externalitzades
  14. @ConfigurationProperties: configuració tipada i validada
  15. Perfils: @Profile i spring.profiles.active
  16. BiblioTech: de Configuracio a la configuració de Spring
  17. Spring Boot: el pom.xml i els starters
  18. @SpringBootApplication dissecada
  19. L'autoconfiguració, explicada de veritat
  20. Depurar l'autoconfiguració
  21. CommandLineRunner i ApplicationRunner
  22. El jar executable i spring-boot-maven-plugin
  23. AOP a Spring: aspectes i punts de tall
  24. @Transactional: l'aspecte canònic
  25. Com funciona per sota: els proxies que ja vas escriure
  26. El parany de la crida interna
  27. BiblioTech sobre Spring: el resultat
  28. Què NO es veu en aquesta lliçó
  29. Errors comuns i consells
  30. Exercicis

  1. Què és Spring i què és Spring Boot

És la primera confusió de tothom, i convé aclarir-la abans d'escriure una línia.

Spring Framework (versió 6.x en aquest curs) és el framework: el contenidor d'IoC, l'abstracció de dades, la programació orientada a aspectes, el model web MVC, la gestió de transaccions. És la funcionalitat.

Spring Boot (versió 3.x) no substitueix Spring Framework: l'embolcalla. És una capa que resol tres problemes que Spring "a pèl" tenia i que feien que arrencar un projecte costés un dia sencer:

Problema Solució de Spring Boot
Triar versions compatibles de 30 dependències Starters: una dependència porta un conjunt coherent i ja provat
Escriure 300 línies d'XML o de @Bean per al de sempre Autoconfiguració: si detecta H2 al classpath, configura el DataSource sol
Desplegar un .war en un servidor d'aplicacions Servidor incrustat i jar executable: java -jar bibliotech.jar

Una analogia útil: Spring Framework és el motor; Spring Boot és el cotxe muntat, amb el motor a dins, les rodes posades i la clau al contacte. Pots fer servir el motor sol —i hi ha qui ho fa—, però pràcticament ningú comença un projecte nou així.

Un detall important per llegir documentació: tot el que aprenguis de Spring Framework serveix a Spring Boot. @Component, @Autowired, @Bean, @Transactional són de Spring Framework. @SpringBootApplication, els starters i l'autoconfiguració són de Spring Boot. En aquesta lliçó aprens primer el nucli (apartats 3-16) i després la capa de Boot (17-22), perquè aquest és l'ordre en què s'entenen.

  1. Els mòduls de Spring Framework

Spring no és un bloc monolític. Aquests són els mòduls que importen:

Mòdul Què aporta On es veu
spring-core / spring-beans El contenidor d'IoC, BeanFactory, injecció Aquesta lliçó
spring-context ApplicationContext, esdeveniments, @Configuration, planificació Aquesta lliçó
spring-aop Programació orientada a aspectes amb proxies Aquesta lliçó (§23-26)
spring-tx Gestió de transaccions, @Transactional Aquesta lliçó i 11-03
spring-jdbc / spring-orm JdbcTemplate, integració amb JPA/Hibernate 11-03
spring-web / spring-webmvc Client i servidor HTTP, controladors REST 12-04
spring-test @SpringBootTest, MockMvc, context de proves 11-04, 11-06, 12-05

Els tres primers són el nucli, i són l'objecte d'aquesta lliçó.

  1. El contenidor d'IoC: ApplicationContext

El cor de Spring és el contenidor. La seva feina és exactament la del teu ContenidorSimple:

  1. Esbrinar quins objectes cal crear.
  2. Esbrinar què necessita cadascun.
  3. Crear-los en l'ordre correcte, injectant les dependències.
  4. Desar-los i lliurar-los quan es demanen.
  5. Destruir-los ordenadament en tancar.

A Spring, aquest contenidor és un ApplicationContext. Pots crear-lo a mà per veure'l funcionar:

package com.nexussoftware.bibliotech;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.nexussoftware.bibliotech.servei.GestorPrestecs;

public class ArrencadaManual {
    public static void main(String[] args) {

        // 1. Es crea el contenidor indicant-li on buscar els components.
        var context = new AnnotationConfigApplicationContext(
                "com.nexussoftware.bibliotech");

        // 2. Es demana un bean per tipus. Spring ja l'ha construit, amb
        //    les seves dependencies resoltes recursivament.
        GestorPrestecs gestor = context.getBean(GestorPrestecs.class);

        gestor.prestar("978-0000000001", "Marta Ruiz");

        // 3. En tancar, s'executen els metodes de destruccio de tots els beans.
        context.close();
    }
}

Tres línies, i ja hi ha tot el mecanisme. Compara-ho amb el teu contenidor:

// El teu ContenidorSimple de 10-03
var contenidor = new ContenidorSimple("com.nexussoftware.bibliotech");
GestorPrestecs gestor = contenidor.obtenir(GestorPrestecs.class);

És la mateixa API. El que canvia és tot el que hi ha a sota.

Un matís de vocabulari que apareix a la documentació: BeanFactory és la interfície base del contenidor (crear beans, injectar). ApplicationContext estén BeanFactory i afegeix internacionalització, esdeveniments, càrrega de recursos i integració amb AOP. A la pràctica sempre fas servir ApplicationContext.

  1. Què és un bean i com es declara

Un bean és, senzillament, un objecte gestionat pel contenidor de Spring. Res més. No és una classe especial, no implementa cap interfície, no hereta de res. És un objecte normal que, en comptes de crear-lo tu amb new, el crea Spring.

Hi ha dues formes de declarar-los, i totes dues es fan servir:

Forma 1: anotar la classe (per a les teves pròpies classes)

package com.nexussoftware.bibliotech.servei;

import org.springframework.stereotype.Service;

@Service   // "Spring, aquest es un bean teu"
public class CalculadoraMultes {
    // ...
}

Forma 2: un mètode @Bean en una classe @Configuration (per a classes de tercers que no pots anotar)

package com.nexussoftware.bibliotech.config;

import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ConfiguracioRellotge {

    @Bean
    public Clock rellotge() {
        // Clock es del JDK: no li pots posar @Component.
        return Clock.systemDefaultZone();
    }
}

Aquest Clock és exactament el de 10-05, el que vas fer injectable per poder provar. Ara el gestiona Spring, i a 11-04 el substituiràs per un Clock.fixed a les proves sense tocar una línia de la lògica.

La regla pràctica: @Component (i derivats) per a les teves classes; @Bean per al que no pots anotar (classes del JDK, d'una llibreria externa, o quan la construcció requereix lògica).

  1. Les tres formes d'injecció

Spring pot injectar dependències de tres maneres. Les tres funcionen; només una és recomanable.

Injecció per constructor (la recomanada)

package com.nexussoftware.bibliotech.servei;

import org.springframework.stereotype.Service;
import com.nexussoftware.bibliotech.persistencia.RepositoriPrestecs;

@Service
public class GestorPrestecs {

    private final RepositoriPrestecs repositori;
    private final CalculadoraMultes calculadora;
    private final ServeiAvisos avisos;

    // Des de Spring 4.3: si nomes hi ha UN constructor, @Autowired es opcional.
    public GestorPrestecs(RepositoriPrestecs repositori,
                          CalculadoraMultes calculadora,
                          ServeiAvisos avisos) {
        this.repositori = repositori;
        this.calculadora = calculadora;
        this.avisos = avisos;
    }
}

Fixa't que no hi ha ni una sola anotació de Spring al constructor. Aquesta classe és Java normal: es pot instanciar amb new en una prova sense arrencar res.

Injecció per setter

@Service
public class GestorPrestecs {

    private RepositoriPrestecs repositori;   // no pot ser final

    @Autowired
    public void setRepositori(RepositoriPrestecs repositori) {
        this.repositori = repositori;
    }
}

El seu únic ús legítim són les dependències veritablement opcionals, que són rares.

Injecció per camp (la que cal evitar)

@Service
public class GestorPrestecs {

    @Autowired
    private RepositoriPrestecs repositori;   // temptadora, i mala idea!
}

És la més curta d'escriure, i per això és a tot arreu en tutorials antics. Els seus problemes són concrets, no estètics:

  1. No es pot provar sense Spring ni sense reflexió. El camp és privat i no hi ha constructor ni setter. Per a una prova unitària has d'arrencar el context sencer o fer servir ReflectionTestUtils. Amb constructor, és new GestorPrestecs(mock1, mock2, mock3).
  2. No es pot fer servir final. Perds la immutabilitat i la garantia que la dependència no canvia.
  3. Amaga l'excés de dependències. Un constructor amb nou paràmetres crida "aquesta classe fa massa". Nou camps anotats no criden l'atenció de ningú. El constructor lleig és informació útil.
  4. Permet estats inconsistents. Amb injecció per camp, l'objecte existeix un instant construït però sense dependències. Si alguna cosa s'executa aquí, hi ha NullPointerException.
  5. Acobla la classe a Spring. @Autowired al camp fa que la classe només funcioni dins d'un contenidor.

Comparades:

Aspecte Constructor Setter Camp
Permet final Sí No No
Objecte sempre vàlid després de construir-se Sí No No
Es pot instanciar en una prova amb new Sí Sí No
Detecta dependències circulars En arrencada, amb error clar Les amaga Les amaga
Fa visible l'excés de dependències Sí No No
Classe independent de Spring Sí No No
Recomanació oficial de Spring Sí Opcionals Desaconsellada

Dependències circulars

Un detall rellevant: amb injecció per constructor, si A necessita B i B necessita A, l'arrencada falla amb un missatge explícit. Això és bo: un cicle és un problema de disseny i vols saber-ho. Amb injecció per camp el cicle es resol silenciosament i el problema queda enterrat.

El teu ContenidorSimple també detectava cicles i llançava una excepció — per la mateixa raó i amb la mateixa lògica.

  1. Per què la injecció per constructor

Resumit en una frase: perquè produeix classes que són Java normal.

// Prova unitaria (ho veuras a 11-04 i 11-06). Sense Spring, sense context, en 3 ms.
var gestor = new GestorPrestecs(
        repositoriSimulat,
        new CalculadoraMultes(Clock.fixed(...)),
        avisosSimulats);

Si la classe es pot construir així, es pot provar. Si no, no. Aquest és tot l'argument, i és suficient.

  1. Comparativa amb el ContenidorSimple casolà

Val la pena veure el salt explícit, amb el teu codi al costat.

Aspecte El teu ContenidorSimple Spring
Marcar un component @Component propi @Component, @Service, @Repository, @Controller
Escaneig Recorries el paquet amb reflexió @ComponentScan (implícit a @SpringBootApplication)
Injecció Constructor únic obligatori Constructor, setter o camp
Resolució Recursiva per tipus Per tipus, amb desempat per nom, @Qualifier i @Primary
Cicles Excepció pròpia BeanCurrentlyInCreationException amb la cadena completa
Memòria cau Un HashMap<Class<?>, Object> Registre de singletons amb tres nivells de memòria cau
Àmbits Només singleton 6 àmbits
Cicle de vida Cap @PostConstruct, @PreDestroy, InitializingBean, DisposableBean, BeanPostProcessor
Configuració externa Cap @Value, @ConfigurationProperties, perfils, jerarquia de fonts
Aspectes Tres proxies aplicats a mà AOP integrat i transparent
Errors NullPointerException o excepció genèrica Missatges que diuen quin bean falta, on es demanava i quins candidats hi ha

Aquest últim punt se subestima sempre i és el que més temps estalvia a la pràctica. Un error típic de Spring:

Parameter 0 of constructor in com.nexussoftware.bibliotech.servei.GestorPrestecs
required a bean of type 'com.nexussoftware.bibliotech.persistencia.RepositoriPrestecs'
that could not be found.

Action:
Consider defining a bean of type 'RepositoriPrestecs' in your configuration.

Et diu el bean, el constructor, el paràmetre i què fer.

  1. Estereotips: @Component, @Service, @Repository, @Controller

Spring ofereix quatre anotacions per marcar components. La primera pregunta de tothom és: en què es diferencien?

Tècnicament, en molt poc. @Service, @Repository i @Controller estan anotades amb @Component. Són metaanotacions, exactament el mecanisme que vas estudiar a 10-02:

// Codi real de Spring, simplificat
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component          // <-- @Service ES un @Component
public @interface Service {
    @AliasFor(annotation = Component.class)
    String value() default "";
}

Diferències reals:

Anotació Capa Què afegeix a més de ser un bean
@Component Genèrica Res. És la base
@Service Lògica de negoci Res tècnic. Semàntica: marca la capa de servei
@Repository Accés a dades Traducció d'excepcions: converteix les excepcions pròpies de la tecnologia (SQLException, excepcions d'Hibernate) en la jerarquia DataAccessException de Spring
@Controller Presentació web El fa elegible per al mapatge de peticions de Spring MVC

La traducció d'excepcions de @Repository sí que és funcionalitat real i molt rellevant: gràcies a ella, la teva capa de servei no depèn de si a sota hi ha JDBC, Hibernate o JPA. És un cas de llibre d'aïllar la tecnologia, i connecta amb les estratègies per capes del mòdul 6.

Aplicat a BiblioTech:

@Service      // capa de negoci
public class GestorPrestecs { ... }

@Service
public class CalculadoraMultes { ... }

@Repository   // capa de dades
public class RepositoriPrestecsJpa implements RepositoriPrestecs { ... }

@Component    // infraestructura generica: no encaixa en cap capa
public class RegistreOperacions { ... }

Consell: fes servir l'estereotip correcte encara que tècnicament siguin iguals. És documentació gratuïta, i algunes eines i aspectes s'hi recolzen.

  1. @Configuration + @Bean enfront de l'escaneig de components

Hi ha dos estils de dir-li a Spring quins beans existeixen, i en un projecte real conviuen.

Escaneig de components

@ComponentScan("com.nexussoftware.bibliotech")
public class Configuracio { }

Spring recorre el classpath sota aquest paquet, busca classes amb @Component o derivats i les registra. És el que fa @SpringBootApplication implícitament sobre el seu propi paquet.

  • A favor: zero configuració. Afegeixes una classe amb @Service i ja és un bean.
  • En contra: les dependències són implícites. Per saber quins beans hi ha, cal buscar anotacions per tot el codi.

Configuració explícita amb @Bean

package com.nexussoftware.bibliotech.config;

import java.time.Clock;
import java.net.http.HttpClient;
import java.time.Duration;
import org.springframework.context.annotation.*;

@Configuration
public class ConfiguracioBiblioTech {

    @Bean
    public Clock rellotge() {
        return Clock.systemDefaultZone();
    }

    @Bean
    public HttpClient clientHttp() {
        // L'HttpClient de 09-06: car de crear, cal reutilitzar-lo.
        // Com a singleton de Spring, es crea UNA vegada.
        return HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(5))
                .build();
    }

    @Bean
    public ClientMetadades clientMetadades(HttpClient http,
                                           @Value("${bibliotech.metadades.url}") String url) {
        // Els parametres del metode @Bean son ALTRES beans: Spring els injecta.
        return new ClientMetadades(http, url);
    }
}

Tres coses per aprendre d'aquest bloc:

  1. El nom del mètode és el nom del bean (rellotge, clientHttp, clientMetadades).
  2. Els paràmetres d'un mètode @Bean s'injecten igual que els d'un constructor.
  3. Aquí es resol per fi un problema real de 09-06: l'HttpClient és car de crear i calia reutilitzar-lo. Com a singleton del contenidor, es crea una sola vegada i s'injecta on calgui.

Un detall que sorprèn: el mode full de @Configuration

@Configuration
public class Config {

    @Bean
    public Clock rellotge() { return Clock.systemDefaultZone(); }

    @Bean
    public CalculadoraMultes calculadora() {
        return new CalculadoraMultes(rellotge());   // crida directa a un altre @Bean
    }

    @Bean
    public ServeiAvisos avisos() {
        return new ServeiAvisos(rellotge());        // un altre Clock diferent?
    }
}

Intuïtivament, rellotge() es crida dues vegades i hi ha dos rellotges. No és així. Spring genera una subclasse CGLIB de la classe @Configuration que intercepta les crides a mètodes @Bean i retorna el singleton ja creat. Hi ha un sol Clock.

I aquí tens, un altre cop, el teu proxy dinàmic de 10-03 — aquesta vegada per generació de subclasse en comptes de per interfície. Aquesta és també la raó que una classe @Configuration no pugui ser final.

Estil Quan fer-lo servir
@Component + escaneig Les teves classes de domini, serveis i repositoris
@Configuration + @Bean Classes de tercers, del JDK, o construcció amb lògica condicional

  1. El cicle de vida d'un bean

Un bean no es limita a "existir". Passa per una seqüència de fases, i en diverses t'hi pots enganxar.

graph TD
    A["Arrencada del contenidor"] --> B["Lectura de definicions de bean<br/>(escaneig + @Bean)"]
    B --> C["Instanciacio<br/>(es crida el constructor)"]
    C --> D["Injeccio de dependencies<br/>(setters i camps)"]
    D --> E["Aware: BeanNameAware,<br/>ApplicationContextAware"]
    E --> F["BeanPostProcessor<br/>abans de la inicialitzacio"]
    F --> G["@PostConstruct"]
    G --> H["afterPropertiesSet<br/>(InitializingBean)"]
    H --> I["BeanPostProcessor<br/>despres de la inicialitzacio<br/>AQUI ES CREEN ELS PROXIES AOP"]
    I --> J["BEAN LLEST I EN US"]
    J --> K["context.close()"]
    K --> L["@PreDestroy"]
    L --> M["destroy (DisposableBean)"]
    M --> N["Bean destruit"]

Dues observacions que valen or:

  • La injecció per constructor passa a la instanciació; la de setter i camp, després. Per això amb injecció per camp hi ha un instant en què l'objecte existeix incomplet.
  • Els proxies AOP es creen a l'últim BeanPostProcessor. Aquest és el punt exacte en què @Transactional deixa de ser una etiqueta i es converteix en un embolcall. Recorda-ho per a l'apartat 26.

A la pràctica només faràs servir dos ganxos, i tots dos són de Jakarta, no de Spring:

package com.nexussoftware.bibliotech.persistencia;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Repository;

@Repository
public class CatalegEnMemoria {

    private final Map<String, Material> perIsbn = new ConcurrentHashMap<>();

    @PostConstruct
    public void carregarCatalegInicial() {
        // S'executa DESPRES de la injeccio: aqui totes les dependencies
        // estan disponibles. Al constructor podrien no estar-hi.
        perIsbn.putAll(importador.importarDesDeRecurs("cataleg-inicial.csv"));
        log.info("Cataleg carregat amb {} materials", perIsbn.size());
    }

    @PreDestroy
    public void desarEstat() {
        // S'executa en tancar el context de forma ordenada.
        // Substitueix l'AturadaOrdenada amb shutdown hooks de 08-05.
        escriptor.bolcar(perIsbn.values());
        log.info("Cataleg persistit abans de l'aturada");
    }
}

I aquí queda saldada una altra peça: l'AturadaOrdenada que vas escriure amb shutdown hooks al mòdul 8 ja no cal. El contenidor tanca els beans en ordre invers al de creació, que és just el que vols: un servei es tanca abans que el repositori del qual depèn.

Una regla important per al constructor: no hi facis feina pesada. Res de connectar a bases de dades, llegir fitxers o cridar la xarxa. Per a això hi ha @PostConstruct.

  1. Àmbits d'un bean

Un àmbit (scope) defineix quantes instàncies crea el contenidor i quant viuen.

Àmbit Instàncies Vida Quan fer-lo servir
singleton (per defecte) Una per contenidor Tot el cicle de l'aplicació Gairebé sempre: serveis, repositoris, configuració
prototype Una de nova a cada petició del bean La gestiona qui el fa servir Objectes amb estat mutable no compartible
request Una per petició HTTP La petició Dades de la petició actual (només web)
session Una per sessió HTTP La sessió de l'usuari Cistella, preferències (només web)
application Una per ServletContext L'aplicació web Rar
websocket Una per sessió WebSocket La sessió Rar
@Service
@Scope("prototype")
public class InformeMensual {
    // Cada vegada que es demana, s'obte un objecte nou.
}

I aquí, la conseqüència més important de totes, que enllaça directament amb el mòdul 8:

Els beans singleton són compartits per tots els fils. Han de ser sense estat o segurs davant de la concurrència.

// MALAMENT: estat mutable en un singleton
@Service
public class ComptadorPrestecs {
    private int total = 0;                       // compartit per TOTS els fils
    public void registrar() { total++; }         // condicio de cursa (08-04)
}

// BE: sense estat
@Service
public class CalculadoraMultes {
    private final Clock rellotge;                // final, immutable
    public BigDecimal calcular(Prestec p) { ... }   // nomes variables locals
}

// BE: estat, pero segur davant de la concurrencia (08-06)
@Service
public class ComptadorPrestecs {
    private final AtomicLong total = new AtomicLong();
    public void registrar() { total.incrementAndGet(); }
}

Aquest és un dels errors més freqüents i més difícils de diagnosticar en aplicacions Spring: funciona perfectament en desenvolupament amb un usuari i dóna resultats incoherents en producció amb cinquanta. Tot el que vas aprendre al mòdul 8 s'aplica directament aquí.

Segon parany clàssic: injectar un prototype dins d'un singleton. Com que el singleton es construeix una sola vegada, rep una sola instància del prototip i la fa servir sempre. La solució habitual és injectar un ObjectProvider<T> i demanar una instància nova quan calgui:

@Service
public class GeneradorInformes {

    private final ObjectProvider<InformeMensual> proveidor;

    public GeneradorInformes(ObjectProvider<InformeMensual> proveidor) {
        this.proveidor = proveidor;
    }

    public void generar() {
        InformeMensual informe = proveidor.getObject();   // instancia NOVA
        // ...
    }
}

  1. Resolució d'ambigüitat: @Qualifier, @Primary i col·leccions

Spring injecta per tipus. Què passa si hi ha dues implementacions?

public interface ServeiAvisos {
    void avisar(Empleat destinatari, String missatge);
}

@Service
public class AvisosPerCorreu implements ServeiAvisos { ... }

@Service
public class AvisosPerSms implements ServeiAvisos { ... }

I algú demana un ServeiAvisos. L'arrencada falla:

Parameter 2 of constructor in ...GestorPrestecs required a single bean,
but 2 were found:
	- avisosPerCorreu
	- avisosPerSms

El teu ContenidorSimple fallava igual, però sense dir-te els candidats. Hi ha quatre solucions, i cadascuna té el seu cas.

@Primary: el candidat per defecte

@Service
@Primary
public class AvisosPerCorreu implements ServeiAvisos { ... }

Qui demani un ServeiAvisos sense més rebrà el de correu. Útil quan hi ha una implementació clarament principal.

@Qualifier: triar explícitament

@Service("avisosSms")
public class AvisosPerSms implements ServeiAvisos { ... }

@Service
public class GestorPrestecs {
    public GestorPrestecs(@Qualifier("avisosSms") ServeiAvisos avisos) { ... }
}

Un qualifier propi (més net i verificable)

Reprèn directament les metaanotacions de 10-02:

@Qualifier("sms")
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD})
public @interface PerSms { }

@Service @PerSms
public class AvisosPerSms implements ServeiAvisos { ... }

// Us: sense cadenes magiques, amb verificacio del compilador
public GestorPrestecs(@PerSms ServeiAvisos avisos) { ... }

Millor que @Qualifier("sms") perquè una cadena mal escrita falla en execució; un nom d'anotació mal escrit no compila.

Injectar totes les implementacions

De vegades no vols triar: les vols totes. Spring injecta una List amb tots els beans del tipus.

@Service
public class NotificadorMulticanal {

    private final List<ServeiAvisos> canals;

    public NotificadorMulticanal(List<ServeiAvisos> canals) {
        this.canals = canals;   // correu, SMS i els que s'afegeixin dema
    }

    public void avisarPerTotsElsCanals(Empleat destinatari, String missatge) {
        canals.forEach(canal -> canal.avisar(destinatari, missatge));
    }
}

Això és potent de veritat: afegir un canal nou és crear una classe amb @Service. Ni una línia del notificador canvia. És el principi obert/tancat fet realitat, i és un patró que veuràs molt (estratègies, validadors, gestors d'esdeveniments).

També funciona amb Map<String, ServeiAvisos>, on la clau és el nom del bean, i es pot ordenar amb @Order o implementant Ordered.

Mecanisme Quan
@Primary Hi ha una implementació principal evident
@Qualifier("nom") Es tria explícitament, cas puntual
Qualifier propi Es tria explícitament i vols verificació del compilador
List<T> / Map<String,T> Vols totes les implementacions
@Profile L'elecció depèn de l'entorn (§15)

  1. @Value i les propietats externalitzades

Cap aplicació ha de portar la configuració escrita a foc. A 07-07 vas resoldre això amb Properties i les teves classes Configuracio i ReglesNegoci. Spring ho porta molt més lluny.

Un application.properties a src/main/resources:

bibliotech.nom=BiblioTech - Nexus Software
bibliotech.prestec.dies-per-defecte=15
bibliotech.prestec.maxim-per-empleat=3
bibliotech.multa.euros-per-dia=0.50
bibliotech.multa.maxima=20.00
bibliotech.metadades.url=https://api.exemple.com/llibres
bibliotech.avisos.activats=true

O el mateix contingut a application.yml, més llegible quan hi ha jerarquia:

bibliotech:
  nom: BiblioTech - Nexus Software
  prestec:
    dies-per-defecte: 15
    maxim-per-empleat: 3
  multa:
    euros-per-dia: 0.50
    maxima: 20.00
  metadades:
    url: https://api.exemple.com/llibres
  avisos:
    activats: true

Es llegeixen amb @Value:

@Service
public class CalculadoraMultes {

    private final BigDecimal eurosPerDia;
    private final BigDecimal multaMaxima;
    private final Clock rellotge;

    public CalculadoraMultes(
            @Value("${bibliotech.multa.euros-per-dia}") BigDecimal eurosPerDia,
            @Value("${bibliotech.multa.maxima:20.00}") BigDecimal multaMaxima,
            Clock rellotge) {
        this.eurosPerDia = eurosPerDia;
        this.multaMaxima = multaMaxima;
        this.rellotge = rellotge;
    }
}

Detalls importants:

  • Conversió automàtica de tipus: la cadena "0.50" arriba com a BigDecimal. Spring converteix a int, boolean, Duration, List<String>, enums i molts més.
  • Valor per defecte amb : — ${propietat:valorPerDefecte}. Sense ell, si la propietat falta, l'arrencada falla. Això sol ser el correcte: millor no arrencar que arrencar mal configurat.

D'on surten les propietats

Spring Boot busca en diverses fonts amb una precedència definida. De major a menor prioritat, simplificada:

Prioritat Font Exemple
1 Arguments de línia d'ordres --bibliotech.multa.euros-per-dia=0.75
2 Variables d'entorn BIBLIOTECH_MULTA_EUROSPERDIA=0.75
3 Propietats del sistema -Dbibliotech.multa.euros-per-dia=0.75
4 application-{perfil}.properties application-prod.properties
5 application.properties El general
6 Valors per defecte al codi ${prop:0.50}

Aquesta jerarquia és la que fa possible el desplegament modern: la mateixa imatge de contenidor en desenvolupament, preproducció i producció, canviant només variables d'entorn. Es desenvolupa a 12-06.

Nota de seguretat: les contrasenyes i claus d'API mai van a application.properties dins del repositori. Van a variables d'entorn o a un gestor de secrets. Es tracta a 12-07.

  1. @ConfigurationProperties: configuració tipada i validada

@Value funciona, però amb quinze propietats es torna sorollós i no valida res. L'alternativa moderna agrupa la configuració en un objecte.

package com.nexussoftware.bibliotech.config;

import java.math.BigDecimal;
import jakarta.validation.constraints.*;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@Validated
@ConfigurationProperties(prefix = "bibliotech")
public record PropietatsBiblioTech(

        @NotBlank String nom,
        @NotNull Prestec prestec,
        @NotNull Multa multa) {

    public record Prestec(
            @Min(1) @Max(90) int diesPerDefecte,
            @Min(1) @Max(10) int maximPerEmpleat) { }

    public record Multa(
            @DecimalMin("0.0") BigDecimal eurosPerDia,
            @DecimalMin("0.0") BigDecimal maxima) { }
}

S'activa a la classe principal:

@SpringBootApplication
@ConfigurationPropertiesScan   // detecta els @ConfigurationProperties
public class BiblioTechApplication { ... }

I s'injecta com qualsevol bean:

@Service
public class CalculadoraMultes {

    private final PropietatsBiblioTech propietats;
    private final Clock rellotge;

    public CalculadoraMultes(PropietatsBiblioTech propietats, Clock rellotge) {
        this.propietats = propietats;
        this.rellotge = rellotge;
    }

    public BigDecimal calcular(Prestec prestec) {
        LocalDate avui = LocalDate.now(rellotge);
        long diesRetard = ChronoUnit.DAYS.between(prestec.dataVenciment(), avui);
        if (diesRetard <= 0) return BigDecimal.ZERO;

        BigDecimal multa = propietats.multa().eurosPerDia()
                .multiply(BigDecimal.valueOf(diesRetard));

        return multa.min(propietats.multa().maxima());
    }
}

Avantatges enfront de @Value:

Aspecte @Value @ConfigurationProperties
Agrupació Propietat a propietat Objecte complet amb jerarquia
Tipatge Sí Sí, i estructurat
Validació No Sí, Jakarta Bean Validation
Noms relaxats No Sí (dies-per-defecte = diesPerDefecte = DIES_PER_DEFECTE)
Autocompletat a l'IDE No Sí (amb el processador de metadades)
Fallada si la configuració és invàlida Al punt d'ús A l'arrencada

Aquesta última fila és la decisiva. Si algú desplega amb maxim-per-empleat=0, vols que l'aplicació no arrenqui amb un missatge clar, no que els empleats no puguin demanar llibres i ningú sàpiga per què:

Binding to target ...PropietatsBiblioTech failed:

    Property: bibliotech.prestec.maxim-per-empleat
    Value: "0"
    Reason: ha de ser més gran o igual que 1

Fixa't a més que el bloc de validació és exactament el que feia el teu ValidadorAnotat de 10-03: anotacions llegides per reflexió que comproven el valor d'un camp. La diferència és que aquesta versió està estandarditzada, cobreix trenta casos i produeix missatges traduïts.

  1. Perfils: @Profile i spring.profiles.active

Un perfil és un entorn amb nom. Permet que existeixin beans i propietats diferents segons on s'executi l'aplicació.

Fitxers per perfil, tots a src/main/resources:

# application.properties (comu a tots)
bibliotech.nom=BiblioTech - Nexus Software
bibliotech.prestec.dies-per-defecte=15
# application-dev.properties
bibliotech.multa.euros-per-dia=0.10
bibliotech.avisos.activats=false
logging.level.com.nexussoftware=DEBUG
spring.datasource.url=jdbc:h2:mem:bibliotech
# application-prod.properties
bibliotech.multa.euros-per-dia=0.50
bibliotech.avisos.activats=true
logging.level.com.nexussoftware=INFO
spring.datasource.url=${BD_URL}
spring.datasource.username=${BD_USUARI}
spring.datasource.password=${BD_PASSWORD}

Fixa't en l'última part: en producció, les credencials no són al fitxer, es llegeixen de variables d'entorn.

S'activa un perfil de tres formes:

# 1. Argument de linia d'ordres
java -jar bibliotech.jar --spring.profiles.active=prod

# 2. Variable d'entorn (l'habitual en contenidors)
export SPRING_PROFILES_ACTIVE=prod

# 3. A application.properties (nomes per al valor per defecte de desenvolupament)
# spring.profiles.active=dev

I beans diferents segons el perfil:

@Service
@Profile("dev")
public class AvisosDeConsola implements ServeiAvisos {
    // En desenvolupament no s'envien correus: s'imprimeixen.
    public void avisar(Empleat destinatari, String missatge) {
        System.out.printf("[AVIS SIMULAT] %s -> %s%n", destinatari.correu(), missatge);
    }
}

@Service
@Profile("prod")
public class AvisosPerCorreu implements ServeiAvisos {
    // En produccio s'envien de veritat.
}

Amb això es resol un problema molt real: durant el desenvolupament, ningú envia correus de multa a companys per accident. I no hi ha cap if (esDesenvolupament) al codi.

Un avís: no abusis dels perfils. Si tens vuit perfils creuats (dev, prod, test, senseCorreu, ambCache...) la combinatòria es torna inmanejable i ningú sap quins beans hi ha actius. Dos o tres, i la resta per propietats.

  1. BiblioTech: de Configuracio a la configuració de Spring

L'abans i el després complet. Això és el que tenies a 07-07:

// ABANS (modul 7): Properties a ma
public final class ReglesNegoci {

    private static final Properties PROPIETATS = new Properties();

    static {
        try (var entrada = ReglesNegoci.class.getResourceAsStream("/regles.properties")) {
            PROPIETATS.load(entrada);
        } catch (IOException e) {
            throw new ExcepcioInicialitzacio("No s'han pogut carregar les regles", e);
        }
    }

    public static int diesPrestec() {
        return Integer.parseInt(PROPIETATS.getProperty("prestec.dies", "15"));
    }

    public static BigDecimal multaPerDia() {
        return new BigDecimal(PROPIETATS.getProperty("multa.euros.dia", "0.50"));
    }
}

Problemes, tots reals:

  1. static: no es pot substituir en una prova. És exactament el "codi que no es pot provar" que 11-04 analitzarà.
  2. Sense validació: prestec.dies=abc llança NumberFormatException el dia que algú demani un préstec, no en arrencar.
  3. Sense entorns: un sol fitxer per a desenvolupament i producció.
  4. Conversió manual a cada accés: Integer.parseInt i new BigDecimal cada vegada que es crida.
  5. Un bloc static que pot fallar: si el fitxer no hi és, l'error apareix en un ExceptionInInitializerError incomprensible.

I això és el que tens ara:

// DESPRES: un record immutable, validat, injectable i per entorn
@Validated
@ConfigurationProperties(prefix = "bibliotech")
public record PropietatsBiblioTech(
        @NotBlank String nom,
        @NotNull Prestec prestec,
        @NotNull Multa multa) { ... }
Aspecte ReglesNegoci (mòdul 7) PropietatsBiblioTech (Spring)
Accés static, global Injectat, substituïble
Tipus Conversió manual cada vegada Convertits una vegada en arrencar
Validació Cap Jakarta Validation, a l'arrencada
Entorns Un Els perfils que calguin
Sobreescriure en desplegament Impossible sense recompilar Variables d'entorn
Provable en proves No Sí, es construeix amb new
Fallada amb configuració invàlida En execució, tard En arrencar, amb missatge clar

  1. Spring Boot: el pom.xml i els starters

Ara sí, la capa de Boot. Aquest és el pom.xml mínim de BiblioTech (Maven s'explica a fons a 11-05; aquí només el necessari per executar):

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- El "pare" aporta el BOM: fixa versions compatibles de TOT -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.2</version>
        <relativePath/>
    </parent>

    <groupId>com.nexussoftware</groupId>
    <artifactId>bibliotech</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>BiblioTech</name>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- Nucli: contenidor IoC, autoconfiguracio, logging -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- Validacio: @NotNull, @Min... per a @ConfigurationProperties -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- AOP: necessari per a @Transactional i aspectes propis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- Proves: JUnit 5, AssertJ, Mockito. Es fa servir a 11-04 i 11-06 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Fixa't que cap dependència no porta <version>. Les gestiona el spring-boot-starter-parent, que garanteix que les versions de Spring, Jackson, Logback, Tomcat i tota la resta són compatibles entre si. Posar-hi una versió a mà és treure-li aquesta feina i arriscar-se a un conflicte (s'explica a 11-05).

Què és un starter

Un starter és una dependència que no conté codi: només declara un conjunt coherent de dependències. spring-boot-starter porta:

Artefacte que arrossega Per a què
spring-boot L'arrencada
spring-boot-autoconfigure Tota l'autoconfiguració
spring-core, spring-context, spring-beans El contenidor d'IoC
spring-boot-starter-logging SLF4J + Logback (ja el tens! — 11-07)
snakeyaml Lectura d'application.yml

Els starters més habituals:

Starter Què porta
spring-boot-starter Nucli, autoconfiguració, logging
spring-boot-starter-web MVC, REST, Tomcat incrustat, Jackson (12-04)
spring-boot-starter-data-jpa Hibernate, JPA, Spring Data, transaccions (11-03)
spring-boot-starter-test JUnit 5, Mockito, AssertJ, spring-test (11-04, 11-06)
spring-boot-starter-validation Jakarta Bean Validation
spring-boot-starter-aop AspectJ i proxies
spring-boot-starter-actuator Mètriques i sondes de salut (12-07)
spring-boot-starter-security Autenticació i autorització (12-07)

Aquest és un moment excel·lent per executar el de l'apartat 15 de la lliçó anterior:

mvn dependency:tree

Veuràs que aquest pom.xml de trenta línies ha portat uns quaranta jars. I veuràs amb els teus ulls que Jackson, SLF4J i Logback ja hi són, abans que 11-07 els expliqui.

  1. @SpringBootApplication dissecada

La classe principal de BiblioTech:

package com.nexussoftware.bibliotech;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication
@ConfigurationPropertiesScan
public class BiblioTechApplication {

    public static void main(String[] args) {
        SpringApplication.run(BiblioTechApplication.class, args);
    }
}

Aquestes dues línies de main arrenquen tot. @SpringBootApplication és una metaanotació que equival exactament a tres:

@Configuration              // aquesta classe pot declarar @Bean
@EnableAutoConfiguration    // activa l'autoconfiguracio
@ComponentScan              // escaneja AQUEST paquet i els seus subpaquets
public class BiblioTechApplication { }
Anotació Què fa
@SpringBootConfiguration Marca la classe com a font de definicions de beans (és un @Configuration especialitzat)
@EnableAutoConfiguration Activa el mecanisme de l'apartat 19
@ComponentScan Escaneja el paquet de la classe i tots els seus subpaquets

D'aquesta última fila surt una regla pràctica que estalvia hores de desconcert:

La classe principal ha d'estar al paquet arrel. com.nexussoftware.bibliotech.BiblioTechApplication escaneja .domini, .servei, .persistencia, .xarxa, .presentacio. Si la posessis a .presentacio, no trobaria cap servei i l'arrencada fallaria amb "no es troba el bean".

Què fa SpringApplication.run internament, en ordre:

  1. Dedueix el tipus d'aplicació (consola, web servlet, web reactiva) segons el que hi ha al classpath.
  2. Crea l'ApplicationContext adequat.
  3. Carrega les fonts de propietats (application.properties, perfils, entorn, arguments).
  4. Registra les definicions de bean: escaneig + @Bean + autoconfiguració.
  5. Instancia tots els singletons, injectant dependències.
  6. Executa @PostConstruct i crea els proxies AOP.
  7. Arrenca el servidor incrustat si n'hi ha.
  8. Executa els CommandLineRunner i ApplicationRunner.
  9. Registra un ganxo d'aturada per tancar el context de forma ordenada.

  1. L'autoconfiguració, explicada de veritat

Aquest és el punt on Spring Boot sembla màgia, i on deixa de semblar-ho.

El problema que resol. Configurar un DataSource a mà en Spring "clàssic" són unes quaranta línies: crear el pool de connexions, configurar l'EntityManagerFactory, el gestor de transaccions, el dialecte... I en el 95 % dels projectes, aquestes quaranta línies són idèntiques.

La idea. Si Spring Boot pot detectar què tens al classpath i què has configurat, pot aportar la configuració òbvia per tu — i apartar-se tan bon punt tu defineixis la teva.

El mecanisme, en tres peces.

Peça 1: un catàleg de configuracions candidates. L'artefacte spring-boot-autoconfigure conté un fitxer de text:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

amb uns 150 noms de classe, un per línia:

org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
...

@EnableAutoConfiguration llegeix aquest fitxer. És un catàleg de "coses que podrien configurar-se".

Peça 2: condicions. Cada candidata està plena d'anotacions @Conditional, que decideixen si s'aplica o no. Exemple real simplificat:

@AutoConfiguration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean          // <-- LA CLAU
    public DataSource dataSource(DataSourceProperties propietats) {
        return propietats.initializeDataSourceBuilder().build();
    }
}

Les condicions més importants:

Condició Es compleix quan
@ConditionalOnClass La classe és al classpath (o sigui: tens la dependència)
@ConditionalOnMissingClass La classe no hi és
@ConditionalOnBean Ja existeix un bean d'aquest tipus
@ConditionalOnMissingBean NO existeix un bean d'aquest tipus: "només si tu no l'has fet"
@ConditionalOnProperty Una propietat té cert valor
@ConditionalOnWebApplication És una aplicació web
@ConditionalOnMissingFilterBean No hi ha un filtre d'aquest tipus registrat

Peça 3: l'ordre. Les autoconfiguracions s'avaluen després dels teus beans. Per això @ConditionalOnMissingBean funciona: quan s'avalua, les teves definicions ja estan registrades.

I d'aquí surt la regla d'or de Spring Boot:

L'autoconfiguració mai no et trepitja. Si tu defineixes un bean, el seu no es crea.

Exemple concret amb H2, el que faràs servir a 11-03:

graph TD
    A["Arrenca Spring Boot"] --> B{"Hi es la classe<br/>DataSource al classpath?"}
    B -->|No| Z["No fa res"]
    B -->|Si| C{"Has definit tu<br/>un bean DataSource?"}
    C -->|Si| Y["Es fa servir el TEU.<br/>L'autoconfiguracio s'aparta"]
    C -->|No| D{"Hi ha una BD incrustada<br/>(H2, HSQL, Derby)?"}
    D -->|Si| E["Crea un DataSource H2<br/>en memoria, sense configuracio"]
    D -->|No| F["Fa servir spring.datasource.url<br/>i el pool HikariCP"]

Aquest diagrama explica per què, a 11-03, n'hi haurà prou amb afegir la dependència d'H2 per tenir una base de dades funcionant sense escriure una línia de configuració. No és màgia: són dues condicions.

  1. Depurar l'autoconfiguració

Quan alguna cosa es configura sola i no era el que volies, o quan no es configura i no saps per què, Spring Boot et dóna l'informe complet:

java -jar target/bibliotech-1.0.0-SNAPSHOT.jar --debug

o a application.properties:

debug=true

Sortida (retallada):

============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:
-----------------
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'javax.sql.DataSource',
        'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType'

   DataSourceAutoConfiguration#dataSource matched:
      - @ConditionalOnMissingBean (types: javax.sql.DataSource;
        SearchStrategy: all) did not find any beans

Negative matches:
-----------------
   MongoAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class
           'com.mongodb.client.MongoClient'

Exclusions:
-----------
   None

Unconditional classes:
----------------------
   ...ConfigurationPropertiesAutoConfiguration

Com llegir-ho:

  • Positive matches: què s'ha autoconfigurat i per què.
  • Negative matches: què no, i quina condició va fallar. Aquí hi ha la resposta a la majoria dels «per què no funciona?».

I per veure el resultat final, el mateix contenidor:

@Bean
CommandLineRunner llistarBeans(ApplicationContext context) {
    return args -> Arrays.stream(context.getBeanDefinitionNames())
            .sorted()
            .filter(nom -> nom.startsWith("bibliotech") || nom.contains("nexus"))
            .forEach(System.out::println);
}

Si necessites desactivar una autoconfiguració concreta:

@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class BiblioTechApplication { }

  1. CommandLineRunner i ApplicationRunner

I on va el codi que vols executar en arrencar? No al main: quan SpringApplication.run retorna, el context ja està muntat, però el lloc idiomàtic és un runner.

package com.nexussoftware.bibliotech.presentacio;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class ArrencadaBiblioTech implements CommandLineRunner {

    private final GestorPrestecs gestor;
    private final PropietatsBiblioTech propietats;

    public ArrencadaBiblioTech(GestorPrestecs gestor, PropietatsBiblioTech propietats) {
        this.gestor = gestor;
        this.propietats = propietats;
    }

    @Override
    public void run(String... args) {
        System.out.println("=== " + propietats.nom() + " ===");
        System.out.println("Dies de prestec: " + propietats.prestec().diesPerDefecte());

        gestor.prestar("978-0000000001", "Marta Ruiz");
        gestor.prestar("978-0000000002", "Diego Alonso");

        gestor.prestecsActius()
              .forEach(p -> System.out.printf("  %s -> %s (venc %s)%n",
                      p.isbn(), p.empleat(), p.dataVenciment()));
    }
}

S'executa després que el context estigui completament llest. Diferència amb ApplicationRunner:

Interfície Signatura Quan
CommandLineRunner run(String... args) Arguments en cru
ApplicationRunner run(ApplicationArguments args) Arguments ja analitzats (--clau=valor, opcions)

Si n'hi ha diversos, s'ordenen amb @Order. I una nota: si un runner llança una excepció, l'aplicació no arrenca. Sol ser el desitjable.

  1. El jar executable i spring-boot-maven-plugin

El plugin declarat al pom.xml fa dues coses.

Executar en desenvolupament sense empaquetar:

mvn spring-boot:run
mvn spring-boot:run -Dspring-boot.run.profiles=dev
mvn spring-boot:run -Dspring-boot.run.arguments=--bibliotech.multa.euros-per-dia=0.75

Empaquetar un jar executable:

mvn clean package
java -jar target/bibliotech-1.0.0-SNAPSHOT.jar
java -jar target/bibliotech-1.0.0-SNAPSHOT.jar --spring.profiles.active=prod

Aquest jar és un fat jar amb una estructura especial:

bibliotech-1.0.0-SNAPSHOT.jar
├── META-INF/MANIFEST.MF          <- Main-Class: JarLauncher
├── org/springframework/boot/loader/   <- el carregador de Spring Boot
└── BOOT-INF/
    ├── classes/                  <- LES TEVES classes i recursos
    └── lib/                      <- TOTES les dependencies, com a jars

El truc és que Java no sap executar jars imbricats, així que Spring Boot inclou el seu propi carregador de classes. El resultat és un artefacte únic i autocontingut: es copia a qualsevol màquina amb Java 17 i funciona. És la base del desplegament en contenidors de 12-06.

  1. AOP a Spring: aspectes i punts de tall

Última peça del nucli, i la que més directament connecta amb el que vas escriure.

La programació orientada a aspectes (AOP) resol un problema concret: hi ha preocupacions que travessen tota l'aplicació —transaccions, seguretat, traces, mètriques, reintents, memòria cau— i que, si s'escriuen a mà, embruten tots els mètodes amb codi que no és lògica de negoci.

És exactament el que va motivar els teus tres proxies de 10-03. El vocabulari:

Terme Què és El teu equivalent
Aspecte (aspect) La classe que agrupa la funcionalitat transversal El teu ProxyAuditoria
Punt d'unió (join point) Un punt on es pot intervenir. A Spring, sempre una crida a mètode L'invoke de l'InvocationHandler
Punt de tall (pointcut) L'expressió que selecciona quins punts d'unió El teu if (metode.isAnnotationPresent(...))
Consell (advice) El codi que s'executa El cos de l'invoke
Teixit (weaving) Com s'aplica. A Spring, proxies en execució Proxy.newProxyInstance

Un aspecte de BiblioTech, que substitueix el teu ProxyCronometre de 10-03:

package com.nexussoftware.bibliotech.infraestructura;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class AspecteCronometre {

    private static final Logger log = LoggerFactory.getLogger(AspecteCronometre.class);

    // Punt de tall: TOTS els metodes publics del paquet .servei
    @Around("execution(public * com.nexussoftware.bibliotech.servei..*(..))")
    public Object mesurar(ProceedingJoinPoint punt) throws Throwable {
        long inici = System.nanoTime();
        try {
            return punt.proceed();          // <- la crida real al metode
        } finally {
            long ms = (System.nanoTime() - inici) / 1_000_000;
            if (ms > 100) {
                log.warn("Metode lent: {} ha trigat {} ms",
                        punt.getSignature().toShortString(), ms);
            }
        }
    }
}

Compara-ho amb el que vas escriure:

// El teu ProxyCronometre de 10-03
(proxy, metode, args) -> {
    long inici = System.nanoTime();
    try {
        return metode.invoke(objectiu, args);
    } finally {
        long ms = (System.nanoTime() - inici) / 1_000_000;
        // ...
    }
};

És el mateix codi. El que aporta Spring és el @Around("execution(...)"): en comptes d'aplicar el proxy a mà objecte per objecte, declares una expressió i el contenidor l'aplica a tot el que hi encaixi.

Els tipus de consell:

Anotació Quan s'executa Ús típic
@Before Abans del mètode Comprovacions de seguretat, traces
@AfterReturning Després d'acabar bé Auditoria d'èxit
@AfterThrowing Després de llançar excepció Registre d'errors, mètriques
@After Sempre (com finally) Alliberament
@Around Embolcalla la crida El més potent: transaccions, memòria cau, reintents, cronometratge

I punts de tall per anotació, que és el que més es fa servir i el que connecta amb el teu @Auditable:

@Around("@annotation(com.nexussoftware.bibliotech.anotacions.Auditable)")
public Object auditar(ProceedingJoinPoint punt) throws Throwable {
    // S'aplica NOMES als metodes anotats amb @Auditable.
    // La teva anotacio de 10-02, ara consumida per Spring en comptes del teu proxy.
    registre.anotarInici(punt.getSignature().getName());
    Object resultat = punt.proceed();
    registre.anotarFi(punt.getSignature().getName());
    return resultat;
}

El teu @Auditable continua viu. El que ha canviat és qui el llegeix.

  1. @Transactional: l'aspecte canònic

L'aspecte més usat del món Java no l'escrius tu: ve amb Spring.

@Service
public class GestorPrestecs {

    @Transactional
    public Prestec prestar(String isbn, String empleat) {
        Material material = repositoriMaterials.cercarPerIsbn(isbn)
                .orElseThrow(() -> new MaterialNoTrobatException(isbn));

        material.decrementarDisponibles();      // UPDATE 1
        Prestec prestec = new Prestec(material, empleat, LocalDate.now(rellotge));
        repositoriPrestecs.desar(prestec);      // INSERT
        registreAuditoria.anotar(prestec);      // INSERT

        return prestec;
    }
}

Sense @Transactional, si el tercer INSERT falla, els dos primers ja estan confirmats: hi ha un exemplar menys disponible i un préstec sense auditar. Dades incoherents.

Amb @Transactional, les tres operacions són atòmiques: o les tres o cap.

El que fa el proxy, exactament:

// Pseudocodi de l'aspecte transaccional de Spring
public Object invoke(Method metode, Object[] args) throws Throwable {
    Transaccio tx = gestorTransaccions.iniciar();        // BEGIN
    try {
        Object resultat = metode.invoke(objectiu, args);
        gestorTransaccions.confirmar(tx);                // COMMIT
        return resultat;
    } catch (RuntimeException | Error e) {
        gestorTransaccions.revertir(tx);                 // ROLLBACK
        throw e;
    } catch (Exception excepcioComprovada) {
        gestorTransaccions.confirmar(tx);                // CONFIRMA! (per defecte)
        throw excepcioComprovada;
    }
}

Fixa't en l'últim catch, perquè és el comportament per defecte que més sorprèn:

Per defecte, Spring reverteix davant d'excepcions no comprovades (RuntimeException i Error) i confirma davant d'excepcions comprovades.

Si la teva BiblioTechException del mòdul 6 és comprovada (estén Exception), llançar-la no reverteix la transacció. Es corregeix de dues formes: fer-la no comprovada (recomanable i coherent amb el que es va decidir a 06-04), o declarar-ho:

@Transactional(rollbackFor = BiblioTechException.class)
public Prestec prestar(String isbn, String empleat) { ... }

Els atributs que es fan servir de veritat:

Atribut Per a què Valor habitual
readOnly Optimitza consultes; Hibernate desactiva la comprovació de canvis true en mètodes de només lectura
rollbackFor Força rollback davant d'excepcions comprovades La teva excepció base
propagation Què fer si ja hi ha una transacció REQUIRED (per defecte)
isolation Nivell d'aïllament de la base de dades El del gestor (per defecte)
timeout Segons màxims Segons el cas

La propagació i l'aïllament es desenvolupen a 11-03, on tenen sentit amb una base de dades real al davant. Aquí n'hi ha prou de saber que @Transactional és un aspecte i que l'aspecte és un proxy.

  1. Com funciona per sota: els proxies que ja vas escriure

Recupera el diagrama del cicle de vida (apartat 10) i fixa't en el penúltim pas: "BeanPostProcessor després de la inicialització: AQUÍ ES CREEN ELS PROXIES AOP".

El que passa exactament:

  1. Spring instancia el teu GestorPrestecs i li injecta les seves dependències.
  2. Un BeanPostProcessor mira si algun dels seus mètodes encaixa en algun punt de tall (per exemple, té @Transactional).
  3. Si encaixa, crea un proxy que embolcalla el teu objecte i registra el proxy al contenidor, no el teu objecte.
  4. Tothom qui injecti un GestorPrestecs rep el proxy.
graph LR
    A["Cridador<br/>(un altre bean)"] -->|prestar| B["Proxy<br/>(generat per Spring)"]
    B -->|1. BEGIN| D[("Base de dades")]
    B -->|2. proceed| C["GestorPrestecs<br/>(EL TEU objecte real)"]
    C -->|3. SQL| D
    B -->|4. COMMIT o ROLLBACK| D

Dues formes de crear aquest proxy, i això té conseqüències pràctiques:

Mecanisme Quan el fa servir Spring Limitació
Proxy dinàmic del JDK La classe implementa alguna interfície Només intercepta mètodes de la interfície
CGLIB (subclasse generada) La classe no implementa interfícies La classe i els mètodes no poden ser final

Spring Boot fa servir CGLIB per defecte (spring.aop.proxy-target-class=true), cosa que evita la primera limitació però introdueix la segona. D'aquí una fallada silenciosa i molt real:

@Service
public class GestorPrestecs {

    @Transactional
    public final Prestec prestar(...) {   // <- final: CGLIB no pot sobreescriure'l
        // La transaccio NO s'aplica. I no hi ha cap error.
    }
}

El comportament és idèntic al del Proxy.newProxyInstance que vas escriure, amb la diferència que aquell només funcionava sobre interfícies.

Una conseqüència curiosa que veuràs a les traces de pila: apareixeran classes com GestorPrestecs$$SpringCGLIB$$0. No t'espantis: és la teva classe, embolcallada.

  1. El parany de la crida interna

Aquest apartat explica probablement el problema número u del món Spring. I ja l'has vist: és exactament el forat del teu proxy de 10-03.

@Service
public class GestorPrestecs {

    public void prestarDiversos(List<String> isbns, String empleat) {
        for (String isbn : isbns) {
            prestar(isbn, empleat);      // <- CRIDA INTERNA: this.prestar(...)
        }
    }

    @Transactional
    public Prestec prestar(String isbn, String empleat) {
        // Cridat des de prestarDiversos: NO HI HA TRANSACCIO.
    }
}

Per què falla, pas a pas:

  1. El cridador extern invoca prestarDiversos al proxy.
  2. El proxy mira: prestarDiversos no té @Transactional. No fa res especial.
  3. El proxy invoca prestarDiversos al teu objecte real.
  4. A dins, prestar(isbn, empleat) és en realitat this.prestar(...), i this és el teu objecte real, no el proxy.
  5. La crida no passa pel proxy. No hi ha transacció. I no hi ha cap error.
graph TD
    A["Cridador extern"] -->|prestarDiversos| P["PROXY"]
    P -->|sense transaccio, correcte| G["GestorPrestecs real"]
    G -->|"this.prestar(...)<br/>NO PASSA PEL PROXY"| G
    G -.->|"@Transactional IGNORADA"| X["Sense transaccio"]

Vas escriure aquest mateix error, amb aquest mateix diagnòstic, quan vas provar el teu ProxyAuditoria: els mètodes interns no s'auditaven. És el mateix mecanisme, i és inherent als proxies. No és una fallada de Spring: és una conseqüència que un proxy només pot interceptar el que passa per ell.

Solucions, de millor a pitjor:

1. Posar l'anotació al mètode extern (el correcte gairebé sempre):

@Transactional          // tota l'operacio en una transaccio
public void prestarDiversos(List<String> isbns, String empleat) {
    for (String isbn : isbns) { prestar(isbn, empleat); }
}

2. Separar en dos beans (correcte quan la semàntica transaccional ha de ser diferent):

@Service
public class GestorPrestecsMassiu {
    private final GestorPrestecs gestor;   // ALTRE bean: la crida SI passa pel seu proxy
    public GestorPrestecsMassiu(GestorPrestecs gestor) { this.gestor = gestor; }

    public void prestarDiversos(List<String> isbns, String empleat) {
        isbns.forEach(isbn -> gestor.prestar(isbn, empleat));  // una transaccio per prestec
    }
}

3. Autoinjecció (funciona, però és un símptoma de disseny millorable):

@Service
public class GestorPrestecs {
    @Autowired @Lazy
    private GestorPrestecs self;    // el PROXY de si mateix

    public void prestarDiversos(List<String> isbns, String empleat) {
        isbns.forEach(isbn -> self.prestar(isbn, empleat));
    }
}

El mateix parany afecta @Cacheable, @Async, @Retryable, @PreAuthorize i qualsevol aspecte propi. Ara que saps per què, el reconeixeràs en tots ells.

  1. BiblioTech sobre Spring: el resultat

L'estructura del projecte després de la migració:

bibliotech/
├── pom.xml
└── src/main/
    ├── java/com/nexussoftware/bibliotech/
    │   ├── BiblioTechApplication.java        <- @SpringBootApplication
    │   ├── config/
    │   │   ├── PropietatsBiblioTech.java     <- @ConfigurationProperties + @Validated
    │   │   └── ConfiguracioBiblioTech.java   <- @Bean Clock, @Bean HttpClient
    │   ├── domini/                           <- SENSE anotacions de Spring
    │   │   ├── Material.java (sealed), Llibre, Revista, Dvd
    │   │   ├── Empleat.java, Prestec.java, Reserva.java
    │   │   └── Gravetat, TipusMaterial, EstatPrestec, Fitxa, ResumSessio
    │   ├── servei/
    │   │   ├── GestorPrestecs.java           <- @Service, @Transactional
    │   │   ├── CalculadoraMultes.java        <- @Service, Clock injectat
    │   │   ├── ServeiAvisos.java             <- interficie
    │   │   ├── AvisosDeConsola.java          <- @Service @Profile("dev")
    │   │   ├── AvisosPerCorreu.java          <- @Service @Profile("prod")
    │   │   └── EstadistiquesBiblioTech.java  <- @Service
    │   ├── persistencia/                     <- @Repository (JPA a 11-03)
    │   ├── xarxa/                            <- ClientMetadades, ServidorCataleg
    │   ├── infraestructura/
    │   │   └── AspecteCronometre.java        <- @Aspect @Component
    │   └── presentacio/
    │       └── ArrencadaBiblioTech.java      <- CommandLineRunner
    └── resources/
        ├── application.yml
        ├── application-dev.yml
        └── application-prod.yml

Fixa't en un detall deliberat: el paquet domini no té ni una anotació de Spring. Material, Prestec i Empleat són Java pur. Es poden instanciar amb new, provar sense context i reutilitzar en un altre framework. Aquesta separació és un principi de disseny que s'aprofundeix a 12-02, i és una de les decisions que més agrairàs d'aquí a dos anys.

I el que ha desaparegut:

S'elimina Substituït per
ContenidorSimple (150 línies) ApplicationContext
@Component i @Injectar propis @Service, @Repository, @Component
ProxyAuditoria, ProxyCronometre (aplicats a mà) @Aspect amb punts de tall declaratius
Configuracio / ReglesNegoci estàtiques @ConfigurationProperties validat
AturadaOrdenada amb shutdown hooks Cicle de vida del contenidor i @PreDestroy
Un main de 60 línies muntant objectes a mà SpringApplication.run i un CommandLineRunner

Per executar-ho:

mvn spring-boot:run -Dspring-boot.run.profiles=dev

  1. Què NO es veu en aquesta lliçó

Perquè el mapa quedi clar:

Tema On
Controladors REST, @RestController, @GetMapping 12-04
Persistència amb JPA i Spring Data 11-03 (introducció completa)
@SpringBootTest, MockMvc, proves d'integració 11-04, 11-06 i 12-05
Actuator: sondes de salut, mètriques, /actuator 12-07
Spring Security 12-07
Els patrons de disseny al darrere (Injecció de Dependències, Fàbrica, Proxy, Singleton) 12-02
Empaquetar en contenidor i desplegar 12-06

Spring Actuator mereix una menció aquí perquè s'activa amb un sol starter: exposa /actuator/health, /actuator/metrics i /actuator/env sense escriure codi. És la resposta directa a "com sé si la meva aplicació és viva en producció". Es desenvolupa a 12-07.

  1. Errors comuns i consells

Error: injecció per camp amb @Autowired. És la forma més curta i la pitjor. Impedeix final, impedeix instanciar la classe en una prova, amaga l'excés de dependències i acobla la classe a Spring. Fes servir sempre el constructor.

Error: la classe principal fora del paquet arrel. @ComponentScan només mira el paquet de la classe anotada i els seus subpaquets. Si BiblioTechApplication és a com.nexussoftware.bibliotech.presentacio, no veurà com.nexussoftware.bibliotech.servei i l'arrencada fallarà amb "no es troba el bean".

Error: estat mutable en un bean singleton. El singleton el comparteixen tots els fils. Un private int comptador és una condició de cursa de manual (08-04). Sense estat, o Atomic* i col·leccions concurrents (08-06).

Error: crida interna a un mètode @Transactional. No passa pel proxy i l'anotació s'ignora sense cap error. Apartat 26.

Error: mètode o classe final amb anotacions AOP. CGLIB no pot sobreescriure'l i l'aspecte no s'aplica, també en silenci.

Error: esperar rollback davant d'una excepció comprovada. Per defecte Spring confirma. O fas l'excepció no comprovada, o fas servir rollbackFor.

Error: feina pesada al constructor. Connectar a la base de dades, llegir fitxers o cridar la xarxa al constructor trenca l'ordre del contenidor i fa la classe impossible d'instanciar en una prova. Fes servir @PostConstruct.

Error: posar <version> a dependències que gestiona l'starter parent. Li treus la seva feina i provoques conflictes de versions difícils de diagnosticar.

Error: javax.* en comptes de jakarta.*. Spring Boot 3 va migrar tots els paquets. Un import javax.persistence.Entity copiat d'un tutorial del 2020 no compila.

Consell: mantén el domini lliure d'anotacions del framework. Material i Prestec han de poder existir sense Spring. Anota els serveis i els repositoris, no les entitats de negoci.

Consell: prefereix @ConfigurationProperties a @Value. Agrupa, valida, autocompleta i falla a l'arrencada en comptes de en execució.

Consell: fes servir --debug quan alguna cosa no es configuri com esperes. L'informe de condicions respon a gairebé tots els "per què".

Consell: @Profile("dev") en comptes d'if (esDesenvolupament). La lògica condicional per entorn dins del codi de negoci envelleix molt malament.

Consell: quan dubtis de què està passant, imprimeix els beans. Un CommandLineRunner que recorri getBeanDefinitionNames() és una eina de diagnòstic excel·lent.

  1. Exercicis

Exercici 1: migrar un servei al contenidor

Parteixes d'aquesta classe de BiblioTech, escrita a l'estil del mòdul 7:

public class ProcessadorReserves {

    private final RepositoriReserves repositori = new RepositoriReservesCsv(
            Path.of("dades/reserves.csv"));
    private final GestorPrestecs gestor = new GestorPrestecs();
    private final int diesCaducitatReserva =
            Integer.parseInt(ReglesNegoci.get("reserva.dies.caducitat", "3"));

    public void processarPendents() {
        for (Reserva reserva : repositori.pendents()) {
            if (reserva.data().plusDays(diesCaducitatReserva).isBefore(LocalDate.now())) {
                repositori.caducar(reserva);
            } else if (gestor.hiHaDisponible(reserva.isbn())) {
                gestor.prestar(reserva.isbn(), reserva.empleat());
                repositori.completar(reserva);
            }
        }
    }
}

Converteix-la a Spring. Ha de complir:

  1. Injecció per constructor amb camps final.
  2. L'estereotip correcte.
  3. El nombre de dies des de configuració externa i validat (@Min(1) @Max(30)).
  4. LocalDate.now() substituït per un Clock injectat (10-05), per poder provar-ho.
  5. processarPendents ha de ser atòmic.
  6. Afegeix el fragment d'application.yml corresponent.

Exercici 2: dues implementacions i un entorn

BiblioTech necessita enviar avisos de venciment. Hi ha dues implementacions de ServeiAvisos: AvisosPerCorreu (SMTP real) i AvisosDeConsola (imprimeix per pantalla).

Requisits:

  1. Al perfil dev s'ha de fer servir la de consola; a prod, la de correu. Cap classe no ha de consultar en quin entorn és.
  2. GestorPrestecs rep un ServeiAvisos sense saber quin.
  3. Afegeix una tercera implementació, AvisosPerSms, i un NotificadorMulticanal que faci servir totes les implementacions actives, sense que calgui modificar-lo en afegir-ne una quarta.
  4. Explica què passa si al perfil prod hi ha dues implementacions actives i GestorPrestecs en demana una de sola, i dóna dues solucions diferents.

Exercici 3: diagnosticar tres fallades de proxy

Aquests tres fragments compilen, arrenquen sense errors i no fan el que sembla. Per a cadascun: explica què passa, per què, i corregeix-lo.

// CAS A
@Service
public class ServeiCataleg {

    public void importarLot(List<Material> materials) {
        materials.forEach(this::importarUn);
    }

    @Transactional
    public void importarUn(Material material) {
        repositori.desar(material);
        index.actualitzar(material);
    }
}
// CAS B
@Service
public class GestorMultes {

    @Transactional
    public final void condonar(Long prestecId) {
        repositori.esborrarMulta(prestecId);
        auditoria.registrar("Multa condonada: " + prestecId);
    }
}
// CAS C
@Service
public class ImportadorCataleg {

    @Transactional
    public void importar(Path fitxer) throws MaterialCorrupteException {
        for (String linia : Files.readAllLines(fitxer)) {
            repositori.desar(analitzar(linia));   // llanca MaterialCorrupteException
        }
    }
}
// on: public class MaterialCorrupteException extends Exception { ... }

Solucions

Solució 1

package com.nexussoftware.bibliotech.servei;

import java.time.Clock;
import java.time.LocalDate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service   // (2) capa de negoci: @Service, no @Component ni @Repository
public class ProcessadorReserves {

    // (1) tot final, injectat per constructor
    private final RepositoriReserves repositori;
    private final GestorPrestecs gestor;
    private final PropietatsBiblioTech propietats;
    private final Clock rellotge;                   // (4) rellotge injectat

    public ProcessadorReserves(RepositoriReserves repositori,
                               GestorPrestecs gestor,
                               PropietatsBiblioTech propietats,
                               Clock rellotge) {
        this.repositori = repositori;
        this.gestor = gestor;
        this.propietats = propietats;
        this.rellotge = rellotge;
    }

    // (5) atomic: o es processen totes les reserves o cap
    @Transactional
    public void processarPendents() {
        LocalDate avui = LocalDate.now(rellotge);       // (4) determinista en proves
        int dies = propietats.reserva().diesCaducitat();   // (3) configurat i validat

        for (Reserva reserva : repositori.pendents()) {
            if (reserva.data().plusDays(dies).isBefore(avui)) {
                repositori.caducar(reserva);
            } else if (gestor.hiHaDisponible(reserva.isbn())) {
                gestor.prestar(reserva.isbn(), reserva.empleat());
                repositori.completar(reserva);
            }
        }
    }
}

Ampliació de les propietats (punt 3):

@Validated
@ConfigurationProperties(prefix = "bibliotech")
public record PropietatsBiblioTech(
        @NotBlank String nom,
        @NotNull Prestec prestec,
        @NotNull Multa multa,
        @NotNull Reserva reserva) {

    public record Reserva(@Min(1) @Max(30) int diesCaducitat) { }
    // ... Prestec i Multa com abans
}
# (6) application.yml
bibliotech:
  nom: BiblioTech - Nexus Software
  prestec:
    dies-per-defecte: 15
    maxim-per-empleat: 3
  multa:
    euros-per-dia: 0.50
    maxima: 20.00
  reserva:
    dies-caducitat: 3

Què s'ha guanyat, punt per punt:

Abans Després Benefici
new RepositoriReservesCsv(...) Injectat Substituïble per JPA a 11-03 sense tocar aquesta classe
new GestorPrestecs() Injectat Substituïble per un simulat a 11-06
ReglesNegoci.get(...) estàtic PropietatsBiblioTech Validat en arrencada, diferent per entorn
LocalDate.now() LocalDate.now(rellotge) Provable: Clock.fixed a la prova
Sense transacció @Transactional Sense estats a mitges si alguna cosa falla

Nota: processarPendents és un mètode públic del mateix bean cridat des de fora, així que sí que passa pel proxy i la transacció s'aplica. Si el cridés un altre mètode d'aquesta mateixa classe, no (apartat 26).

Solució 2

Punts 1 i 2: perfils.

public interface ServeiAvisos {
    void avisar(Empleat destinatari, String missatge);
}

@Service
@Profile("dev")
public class AvisosDeConsola implements ServeiAvisos {
    private static final Logger log = LoggerFactory.getLogger(AvisosDeConsola.class);
    @Override public void avisar(Empleat destinatari, String missatge) {
        log.info("[AVIS SIMULAT] {} -> {}", destinatari.correu(), missatge);
    }
}

@Service
@Profile("prod")
public class AvisosPerCorreu implements ServeiAvisos {
    @Override public void avisar(Empleat destinatari, String missatge) { /* SMTP */ }
}

GestorPrestecs no canvia gens:

@Service
public class GestorPrestecs {
    private final ServeiAvisos avisos;        // no sap quin rep
    public GestorPrestecs(ServeiAvisos avisos, ...) { this.avisos = avisos; }
}

Cap classe pregunta per l'entorn: la decisió és a les anotacions i a spring.profiles.active.

Punt 3: totes les implementacions.

@Service
@Profile("prod")
public class AvisosPerSms implements ServeiAvisos { ... }

@Service
public class NotificadorMulticanal {

    private static final Logger log = LoggerFactory.getLogger(NotificadorMulticanal.class);
    private final List<ServeiAvisos> canals;

    public NotificadorMulticanal(List<ServeiAvisos> canals) {
        this.canals = canals;   // Spring injecta TOTS els beans ACTIUS del tipus
        log.info("Notificador amb {} canals actius", canals.size());
    }

    public void avisarPerTots(Empleat destinatari, String missatge) {
        canals.forEach(canal -> {
            try {
                canal.avisar(destinatari, missatge);
            } catch (Exception e) {
                // Un canal caigut no ha d'impedir els altres (estrategia per capes, 06-07)
                log.warn("Ha fallat el canal {}", canal.getClass().getSimpleName(), e);
            }
        });
    }
}

Afegir un quart canal és crear una classe amb @Service. NotificadorMulticanal no es toca: és el principi obert/tancat, i Spring ho fa gratis.

Punt 4: l'ambigüitat. A prod hi ha dos beans de tipus ServeiAvisos (AvisosPerCorreu i AvisosPerSms). Quan GestorPrestecs en demana un, l'arrencada falla:

Parameter 0 of constructor in ...GestorPrestecs required a single bean,
but 2 were found: avisosPerCorreu, avisosPerSms

Dues solucions:

// Solucio A: marcar el principal
@Service @Profile("prod") @Primary
public class AvisosPerCorreu implements ServeiAvisos { ... }
// Solucio B: triar explicitament amb un qualifier propi (10-02)
@Qualifier("correu")
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.PARAMETER})
public @interface PerCorreu { }

@Service @Profile("prod") @PerCorreu
public class AvisosPerCorreu implements ServeiAvisos { ... }

@Service
public class GestorPrestecs {
    public GestorPrestecs(@PerCorreu ServeiAvisos avisos, ...) { ... }
}

L'A és més còmoda; la B és explícita i la verifica el compilador. I NotificadorMulticanal, que demana una List<ServeiAvisos>, no es veu afectat per cap ambigüitat: les vol totes.

Solució 3

CAS A — crida interna.

  • Què passa: importarUn no s'executa en transacció. Cada desar es confirma pel seu compte; si el material número 40 falla, queden 39 importats a mitges, amb l'índex possiblement descompensat.
  • Per què: materials.forEach(this::importarUn) és una referència a mètode sobre this, que és l'objecte real, no el proxy. La crida no travessa el proxy i @Transactional s'ignora.
  • Correcció (depèn de la semàntica desitjada):
// Opcio 1: tot el lot en UNA transaccio (l'habitual en una importacio)
@Service
public class ServeiCataleg {

    @Transactional
    public void importarLot(List<Material> materials) {
        materials.forEach(this::importarUn);   // ara ja hi ha transaccio oberta
    }

    // sense @Transactional: hereta la del metode extern
    void importarUn(Material material) {
        repositori.desar(material);
        index.actualitzar(material);
    }
}
// Opcio 2: una transaccio PER material (si una fallada no ha de tombar el lot)
@Service
public class ImportadorLot {
    private final ServeiCataleg cataleg;   // altre bean -> passa pel SEU proxy
    public ImportadorLot(ServeiCataleg cataleg) { this.cataleg = cataleg; }

    public void importarLot(List<Material> materials) {
        materials.forEach(cataleg::importarUn);
    }
}

CAS B — mètode final.

  • Què passa: no hi ha transacció. Si auditoria.registrar falla, la multa ja està esborrada i no hi ha registre.
  • Per què: Spring Boot fa servir CGLIB, que genera una subclasse. Un mètode final no es pot sobreescriure, així que el proxy no l'intercepta. No s'emet cap error; la fallada és completament silenciosa.
  • Correcció: treure final.
@Service
public class GestorMultes {
    @Transactional
    public void condonar(Long prestecId) {   // sense final
        repositori.esborrarMulta(prestecId);
        auditoria.registrar("Multa condonada: " + prestecId);
    }
}

Regla general: una classe o un mètode destinats a portar aspectes no poden ser final. El mateix val per a @Cacheable, @Async i @PreAuthorize.

CAS C — excepció comprovada.

  • Què passa: si la línia 300 és corrupta, MaterialCorrupteException puja... i Spring confirma la transacció. Queden 299 materials importats i el fitxer es dóna per fallit. El següent reintent duplica aquests 299.
  • Per què: per defecte només es reverteix davant de RuntimeException i Error. MaterialCorrupteException estén Exception: és comprovada, i davant d'ella Spring confirma.
  • Correcció, dues opcions:
// Opcio 1 (recomanada): declarar-ho explicitament
@Transactional(rollbackFor = MaterialCorrupteException.class)
public void importar(Path fitxer) throws MaterialCorrupteException { ... }
// Opcio 2 (millor a llarg termini): que la jerarquia del modul 6 sigui NO comprovada
public class BiblioTechException extends RuntimeException { ... }
public class MaterialCorrupteException extends BiblioTechException { ... }

@Transactional   // ara reverteix per defecte
public void importar(Path fitxer) { ... }

L'opció 2 és coherent amb el que es va decidir a 06-04: les excepcions de les quals el cridador no es pot recuperar de forma raonable han de ser no comprovades, i així a més no contaminen totes les signatures del codi amb throws.

Els tres casos comparteixen una arrel: AOP funciona per proxies, i un proxy només intercepta el que passa a través d'ell. Amb això clar, els tres es diagnostiquen en segons.

Conclusió

El ContenidorSimple ha desaparegut, i amb ell la meitat del deute tècnic de BiblioTech.

Saps distingir Spring Framework de Spring Boot: el primer és la funcionalitat —contenidor, AOP, transaccions, web—; el segon és la capa que resol les versions amb starters, la configuració repetitiva amb autoconfiguració i el desplegament amb jar executable i servidor incrustat. I saps que tot el que aprens del nucli serveix tal qual dins de Boot.

Domines el contenidor d'IoC: què és un ApplicationContext, què és un bean —simplement un objecte que gestiona Spring— i les dues formes de declarar-los, @Component per a les teves classes i @Bean per a les que no pots anotar, com aquell Clock de 10-05 i aquell HttpClient de 09-06 que per fi es crea una sola vegada. Coneixes les tres formes d'injecció i per què només una val: la del constructor, perquè permet final, deixa l'objecte sempre vàlid, fa visible l'excés de dependències, detecta cicles a l'arrencada i —el decisiu— produeix classes que s'instancien amb new en una prova. La injecció per camp és curta d'escriure i cara de mantenir.

Saps què afegeix cada estereotip —i que @Repository sí que aporta alguna cosa real, la traducció d'excepcions a DataAccessException—, quan fer servir @Configuration amb @Bean i per què una classe @Configuration no pot ser final: perquè Spring genera una subclasse CGLIB perquè cridar dues vegades rellotge() retorni el mateix rellotge.

Coneixes el cicle de vida d'un bean complet, amb els seus dos punts que importen: que la injecció per constructor passa a la instanciació i la de camp després, i que els proxies AOP es creen a l'últim BeanPostProcessor — la dada que explica tot l'apartat d'AOP. Fas servir @PostConstruct per a la feina pesada que no ha d'anar al constructor i @PreDestroy per a l'aturada ordenada que al mòdul 8 feies amb shutdown hooks. I coneixes els àmbits, amb la conseqüència que més incidents de producció provoca: un singleton el comparteixen tots els fils, així que o no té estat o fa servir el que vas aprendre a 08-04 i 08-06.

Saps resoldre ambigüitats amb @Primary, @Qualifier i qualifiers propis que el compilador verifica; i coneixes el patró d'injectar una List<T> amb totes les implementacions, que converteix "afegir un canal d'avís" en "crear una classe".

Has externalitzat la configuració de veritat. @Value amb conversió de tipus i valors per defecte, la jerarquia de fonts que permet desplegar la mateixa imatge en tres entorns canviant variables d'entorn, i sobretot @ConfigurationProperties amb validació, que converteix aquell ReglesNegoci estàtic, sense validar i sense entorns de 07-07 en un record immutable, tipat, injectable i que impedeix arrencar si la configuració és invàlida — amb un missatge que diu quina propietat, quin valor i per què. I tens perfils: dev que imprimeix els avisos per consola i prod que els envia, sense un sol if (esDesenvolupament) al codi.

Del costat de Boot, tens el pom.xml amb spring-boot-starter-parent que fixa versions compatibles —i per això no hi poses <version>—, saps què és un starter i què arrossega cadascun, i has dissecat @SpringBootApplication en les seves tres anotacions, amb la regla pràctica que se'n deriva: la classe principal va al paquet arrel. I sobretot, entens l'autoconfiguració com el que és: un catàleg de candidates en un fitxer .imports, un conjunt de condicions —@ConditionalOnClass, @ConditionalOnProperty i, la clau, @ConditionalOnMissingBean— i un ordre d'avaluació que garanteix la regla d'or: si tu defineixes un bean, el seu no es crea. Amb --debug pots llegir l'informe de condicions i veure, per a cada peça, si es va aplicar i per què.

I has tancat el cercle amb AOP. Saps què és un aspecte, un punt de tall i un consell, i has vist que el teu ProxyCronometre de 10-03 i un @Around("execution(...)") de Spring són el mateix codi, amb la diferència que Spring l'aplica per expressió en comptes d'objecte a objecte. Coneixes @Transactional com l'aspecte canònic, amb el seu comportament per defecte que sorprèn tothom —reverteix davant d'excepcions no comprovades i confirma davant de les comprovades—, i saps que per sota hi ha un proxy JDK sobre interfícies o una subclasse CGLIB, amb les seves dues conseqüències silencioses: el que és final no es pot proxiar i la crida interna no passa pel proxy. Aquest segon parany, que provoca més incidents a Spring que cap altre, el vas reconèixer a l'instant perquè tenia exactament el mateix forat el proxy que vas escriure tu.


BiblioTech és ara una aplicació Spring Boot: arrenca amb mvn spring-boot:run, té el seu domini net d'anotacions del framework, els seus serveis injectats per constructor, la seva configuració tipada i validada, els seus perfils dev i prod, els seus aspectes declaratius i el seu cicle de vida gestionat.

I la seva persistència continua sent fitxers CSV.

Aquest @Transactional que acabes de posar sobre prestar no està fent res, perquè no hi ha cap base de dades a sota que pugui confirmar o revertir. RepositoriPrestecsJpa és encara un nom sense contingut. El spring.datasource.url=jdbc:h2:mem:bibliotech del perfil dev apunta a una base de dades que no existeix. I aquell DataSourceAutoConfiguration que apareixia a l'informe de condicions està esperant que algú afegeixi una dependència.

A la propera lliçó s'hi afegeix. Veuràs què és el desajust objecte-relacional i què automatitza exactament un ORM; veuràs JDBC en vint línies com a línia base i comprendràs quanta feina desapareix; mapejaràs Material, Empleat i Prestec a taules amb @Entity, @Id i @Column —anotacions llegides per reflexió, com el @CampCsv que vas escriure—; modelaràs les relacions entre elles i et trobaràs amb la LazyInitializationException i amb el problema N+1, els dos clàssics que tothom pateix abans d'entendre'ls; descobriràs la comprovació de canvis automàtica, que desa les teves modificacions sense que cridis save; i @Transactional cobrarà per fi sentit, amb la seva propagació, el seu aïllament i el seu bloqueig optimista per al cas que ja coneixes del mòdul 8: dos bibliotecaris prestant el mateix exemplar alhora.

Els CSV de BiblioTech tenen els dies comptats.

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