Fins ara hem tractat els beans com a objectes que apareixen i funcionen. Aquesta lliçó obre aquesta caixa. Veurem què és exactament un bean i què és el contenidor que els custodia; quantes instàncies crea Spring de cadascun i per què aquesta decisió —l'àmbit— condiciona com has d'escriure les teves classes; què passa, pas a pas, entre que es crida el constructor i el bean queda disponible; com enganxar codi en el moment de néixer i en el de morir; i quin és el punt d'extensió, el BeanPostProcessor, sobre el qual es recolzen les transaccions, la memòria cau i pràcticament tota l'aparent màgia de Spring. A CicloUrbana construirem un CacheDisponibilitat que precarrega en arrencar i allibera recursos en tancar, i raonarem per què EstacioRepositoriEnMemoria va haver d'utilitzar un ConcurrentHashMap.

Contingut

  1. Què és un bean i què és el contenidor
  2. BeanFactory davant d'ApplicationContext
  3. Els àmbits de Spring
  4. L'àmbit singleton i el perill de l'estat mutable
  5. prototype dins d'un singleton: el problema clàssic
  6. El cicle de vida complet d'un bean
  7. @PostConstruct i @PreDestroy a CicloUrbana
  8. Inicialització mandrosa
  9. BeanPostProcessor: el punt d'extensió
  10. Errors Comuns i Consells
  11. Exercicis

  1. Què és un bean i què és el contenidor

Un bean és, senzillament, un objecte Java el cicle de vida del qual gestiona Spring: el contenidor l'instancia, li injecta les seves dependències, l'inicialitza, el manté disponible mentre calgui i el destrueix en tancar. No hi ha cap interfície per implementar ni cap classe base de la qual heretar; l'única cosa que fa bean un objecte és estar registrat al contenidor.

D'aquí se'n segueix una distinció que convé tenir ben clara:

Bean Objecte normal
Qui el crea El contenidor Tu, amb new
Quants n'hi ha Els que dicti el seu àmbit Tants com new escriguis
Rep injecció Sí No
Li funcionen @Transactional, @Cacheable, @Async Sí (via proxy) No
Exemples a CicloUrbana EstacioService, TarifaEstandard, EstacioController Estacio, Lloguer, un BigDecimal

Aquesta darrera fila explica un error molt freqüent: anotar amb @Transactional un mètode d'un objecte creat amb new i no entendre per què no s'obre transacció. Sense bean no hi ha proxy, i sense proxy no hi ha comportament afegit.

El contenidor és l'objecte que guarda el registre de beans, sap construir-los i resol les seves dependències. A Spring hi ha dos nivells.

  1. BeanFactory davant d'ApplicationContext

flowchart TD
    BF["BeanFactory<br/>getBean(), containsBean()<br/>cicle de vida bàsic"] --> AC["ApplicationContext<br/>estén BeanFactory"]
    AC --> F1["Publicació d'esdeveniments<br/>ApplicationEventPublisher"]
    AC --> F2["Resolució de missatges i18n<br/>MessageSource"]
    AC --> F3["Accés a recursos<br/>ResourceLoader"]
    AC --> F4["Environment i propietats"]
    AC --> F5["Processament automàtic de<br/>BeanPostProcessor i BeanFactoryPostProcessor"]
    AC --> F6["Precreació de singletons<br/>a l'arrencada"]
BeanFactory ApplicationContext
Funció Contenidor mínim: crear i servir beans Contenidor complet d'aplicació
Creació de singletons Mandrosa (en demanar-los) Anticipada, a l'arrencada
Esdeveniments, i18n, recursos, Environment No Sí
Registre automàtic de post-processadors Manual Automàtic
Ús a Spring Boot Intern El que utilitzes sempre

A Spring Boot el contenidor real és sempre un ApplicationContext (en concret, per a CicloUrbana, un AnnotationConfigServletWebServerApplicationContext, com vam veure al log d'arrencada de la lliçó 01-05). BeanFactory és la interfície subjacent, útil de conèixer perquè molts missatges d'error l'esmenten.

Un detall amb conseqüències pràctiques: l'ApplicationContext crea tots els singletons durant l'arrencada, no pas quan s'utilitzen per primera vegada. És el que permet que un bean mal configurat rebenti l'arrencada en lloc de la primera petició d'un client. És una decisió de disseny deliberada: fallar aviat i en veu alta.

  1. Els àmbits de Spring

L'àmbit (scope) respon a la pregunta: quantes instàncies d'aquest bean existeixen i quant viuen?

Àmbit Instàncies Vida Disponible a
singleton Una per contenidor Tot el cicle de l'aplicació Sempre (per defecte)
prototype Una per cada petició al contenidor Fins que el recol·lector d'escombraries l'alliberi Sempre
request Una per petició HTTP La petició HTTP Només aplicacions web
session Una per sessió HTTP La sessió de l'usuari Només aplicacions web
application Una per ServletContext Tota l'aplicació web Només aplicacions web
websocket Una per sessió WebSocket La sessió WebSocket Amb WebSocket

Es declara amb @Scope:

package com.ciclourbana.lloguers;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;

/**
 * Acumula els esdeveniments d'una simulació de demanda. Cada simulació
 * necessita el seu propi acumulador amb estat, així que NO pot ser singleton.
 */
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)   // equival a @Scope("prototype")
public class SessioSimulacio {

    private final Instant inici = Instant.now();
    private final List<String> esdeveniments = new ArrayList<>();

    public void registrar(String esdeveniment) {
        esdeveniments.add(esdeveniment);
    }

    public List<String> esdeveniments() {
        return List.copyOf(esdeveniments);
    }

    public Instant inici() {
        return inici;
    }
}

Els àmbits web es declaren igual, tot i que per a ells existeixen dreceres més llegibles:

@Component
@RequestScope    // = @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ContextPeticio { /* dades de la petició HTTP en curs */ }

@Component
@SessionScope    // = @Scope(value = "session", proxyMode = ...)
public class CistellaRecarregues { /* estat per usuari, entre peticions */ }

Un advertiment important sobre aquests dos: la majoria de les API REST no els haurien d'utilitzar. Un servei REST és, per definició, sense estat; desar informació en un bean de sessió trenca aquesta propietat i complica l'escalat horitzontal (dues instàncies darrere d'un balancejador no comparteixen sessió). A CicloUrbana no utilitzarem session: l'estat de l'usuari viatjarà al token JWT que construirem al mòdul 5.

I una diferència crítica entre singleton i prototype que sorprèn molta gent:

flowchart LR
    subgraph S["singleton"]
        C1["Contenidor"] -->|crea, inicialitza,<br/>destrueix| B1["1 instància"]
    end
    subgraph P["prototype"]
        C2["Contenidor"] -->|crea i inicialitza| B2["instància 1"]
        C2 -->|crea i inicialitza| B3["instància 2"]
        C2 -.->|"NO la destrueix:<br/>no crida @PreDestroy"| B2
    end

Spring no gestiona la destrucció dels beans prototype. Els crea, els injecta i els oblida. Els seus mètodes @PreDestroy no s'executen mai. Si un bean prototype obre un recurs (un fitxer, una connexió), ets tu qui l'ha de tancar.

  1. L'àmbit singleton i el perill de l'estat mutable

L'àmbit per defecte és singleton: una sola instància compartida per tot el contenidor. I aquí hi ha la conseqüència més important de tota aquesta lliçó: en una aplicació web, aquesta única instància la utilitzen totes les peticions HTTP alhora, cadascuna en un fil diferent de Tomcat.

Per això la regla d'or és: els beans no han de tenir estat mutable.

Un exemple del que no s'ha de fer, escrit sobre el nostre EstacioService:

@Service
public class EstacioService {

    private final EstacioRepositori estacioRepositori;

    // PERILL! Camp mutable en un singleton compartit per tots els fils
    private int ultimIdConsultat;
    private List<Estacio> resultatParcial = new ArrayList<>();

    public List<Estacio> cercarPerCapacitatMinima(int minima) {
        resultatParcial.clear();                        // el fil A neteja...
        for (Estacio e : estacioRepositori.cercarTotes()) {
            if (e.capacitat() >= minima) {
                resultatParcial.add(e);                 // ...mentre el fil B afegeix
            }
        }
        return resultatParcial;                         // resultat impredictible
    }
}

Amb una sola petició funciona perfectament. Amb dues peticions simultànies retorna resultats barrejats, o llança una ConcurrentModificationException, o —el pitjor— retorna dades d'un altre usuari. I és un error que no apareix en desenvolupament: només es manifesta en producció, sota càrrega, de manera intermitent. És una de les menes de bug més cares de diagnosticar.

La versió correcta utilitza només variables locals, que viuen a la pila de cada fil i no es comparteixen:

@Service
public class EstacioService {

    private final EstacioRepositori estacioRepositori;   // final i immutable

    public EstacioService(EstacioRepositori estacioRepositori) {
        this.estacioRepositori = estacioRepositori;
    }

    public List<Estacio> cercarPerCapacitatMinima(int minima) {
        // Tot l'estat és local al fil: segur per construcció
        return estacioRepositori.cercarTotes().stream()
                .filter(e -> e.capacitat() >= minima)
                .sorted(Comparator.comparing(Estacio::nom))
                .toList();
    }
}

Les tres maneres legítimes de tenir estat en un singleton:

Manera Exemple a CicloUrbana
Camps final amb objectes immutables private final EstacioRepositori repositori;
Estructures concurrents private final Map<Long, Estacio> perId = new ConcurrentHashMap<>();
Comptadors atòmics private final AtomicLong seguentId = new AtomicLong(0);

Ara s'entén per què a la lliçó 02-01 vam escriure EstacioRepositoriEnMemoria amb ConcurrentHashMap i AtomicLong en lloc de HashMap i long: és un singleton, i el seu estat el toquen diversos fils. Amb un HashMap normal, dues insercions simultànies poden corrompre l'estructura interna i provocar bucles infinits a les lectures posteriors.

Si de debò necessites estat per usuari o per petició, les opcions són: passar-lo com a paràmetre (gairebé sempre la millor), utilitzar un bean prototype, o —en el cas d'un web amb sessió— un bean session.

  1. prototype dins d'un singleton: el problema clàssic

Aquest és l'error més citat del contenidor de Spring, i val la pena entendre'l bé.

@Service                                    // singleton
public class SimuladorDemanda {

    private final SessioSimulacio sessio;   // prototype

    public SimuladorDemanda(SessioSimulacio sessio) {
        this.sessio = sessio;               // s'injecta UNA vegada, en arrencar
    }

    public void simular() {
        sessio.registrar("simulació executada");   // sempre la MATEIXA sessió!
    }
}

La injecció passa una sola vegada, quan es construeix el singleton. La instància prototype rebuda queda congelada al camp durant tota la vida de l'aplicació. L'àmbit prototype es perd del tot: a la pràctica, aquest bean es comporta com un singleton.

Solució 1: ObjectProvider (la recomanada)

Ja el vam conèixer a la lliçó 02-02 com a manera d'expressar dependències opcionals. La seva altra virtut és la resolució mandrosa a cada crida:

package com.ciclourbana.lloguers;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
public class SimuladorDemanda {

    private final ObjectProvider<SessioSimulacio> proveidorSessions;

    public SimuladorDemanda(ObjectProvider<SessioSimulacio> proveidorSessions) {
        this.proveidorSessions = proveidorSessions;
    }

    public void simular(String nomEscenari) {
        // getObject() demana una instància NOVA al contenidor a cada crida
        SessioSimulacio sessio = proveidorSessions.getObject();
        sessio.registrar("escenari: " + nomEscenari);
        sessio.registrar("estacions avaluades: 4");
        // La sessió mor quan surti d'àmbit: Spring no la destrueix
    }
}

Solució 2: @Lookup

Spring pot sobreescriure un mètode abstracte perquè retorni una instància nova. Ho fa generant una subclasse per CGLIB:

@Service
public abstract class SimuladorDemanda {

    public void simular(String nomEscenari) {
        SessioSimulacio sessio = novaSessio();   // instància nova cada vegada
        sessio.registrar("escenari: " + nomEscenari);
    }

    /** Spring implementa aquest mètode per nosaltres. */
    @Lookup
    protected abstract SessioSimulacio novaSessio();
}

Solució 3: proxy d'àmbit

@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessioSimulacio { /* ... */ }

Amb proxyMode, el que s'injecta al singleton no és la instància sinó un proxy que, a cada invocació de mètode, resol la instància correcta. És l'opció obligatòria per als àmbits request i session (per això @RequestScope i @SessionScope el porten de sèrie): un singleton es construeix a l'arrencada, quan encara no hi ha cap petició HTTP viva de la qual obtenir el bean.

Comparades:

Solució Llegibilitat Quan utilitzar-la
ObjectProvider Explícita: es veu que es demana una instància Preferida en codi propi
@Lookup Elegant però exigeix classe abstracta Casos heretats o API que no admeten ObjectProvider
proxyMode Transparent, però amaga el que passa Obligatòria per a request/session

  1. El cicle de vida complet d'un bean

Aquest és el recorregut íntegre d'un bean singleton, des que Spring decideix crear-lo fins que l'aplicació es tanca:

flowchart TD
    A["1. Instanciació<br/>s'invoca el constructor<br/>(injecció per constructor)"] --> B["2. Injecció de setters i camps<br/>@Autowired, @Value"]
    B --> C["3. Interfícies *Aware*<br/>BeanNameAware.setBeanName()<br/>BeanFactoryAware<br/>ApplicationContextAware"]
    C --> D["4. BeanPostProcessor<br/>postProcessBeforeInitialization()"]
    D --> E["5. @PostConstruct"]
    E --> F["6. InitializingBean.afterPropertiesSet()"]
    F --> G["7. init-method<br/>@Bean(initMethod = ...)"]
    G --> H["8. BeanPostProcessor<br/>postProcessAfterInitialization()<br/>← aquí s'embolcalla en proxies AOP"]
    H --> I["BEAN LLEST PER UTILITZAR"]
    I --> J["9. @PreDestroy"]
    J --> K["10. DisposableBean.destroy()"]
    K --> L["11. destroy-method<br/>@Bean(destroyMethod = ...)"]
    L --> M["Bean destruït"]

Els tres blocs de tres passos mereixen un comentari.

Inicialització (5, 6, 7). Hi ha tres maneres d'executar codi en néixer un bean, i s'executen en aquest ordre. Són redundants; utilitza sempre @PostConstruct llevat que tinguis un motiu:

Mecanisme Acoblament a Spring Recomanació
@PostConstruct (jakarta.annotation) Cap: és un estàndard de Jakarta EE Utilitza'l
InitializingBean.afterPropertiesSet() Alt: obliga a implementar una interfície de Spring Evita'l
@Bean(initMethod = "arrencar") Cap a la classe Per a classes de tercers que no pots anotar

Destrucció (9, 10, 11). El mateix esquema amb @PreDestroy. Amb dos advertiments: només s'executen si el context es tanca de manera ordenada (recorda server.shutdown=graceful de la lliçó 01-05; un kill -9 no executa res), i no s'executen mai en beans prototype.

Post-processament (4 i 8). És on passa gairebé tot el que és interessant, i per això té apartat propi més avall.

Un detall sobre les interfícies Aware: permeten que el bean rebi infraestructura del contenidor.

@Component
public class DiagnosticBean implements BeanNameAware, ApplicationContextAware {

    private String nomBean;
    private ApplicationContext context;

    @Override
    public void setBeanName(String name) {
        this.nomBean = name;         // "diagnosticBean"
    }

    @Override
    public void setApplicationContext(ApplicationContext context) {
        this.context = context;
    }
}

Utilitza-les amb moderació: acoblen la teva classe a Spring. ApplicationContextAware en particular sol ser una olor a service locator; gairebé sempre és preferible injectar el que necessites.

  1. @PostConstruct i @PreDestroy a CicloUrbana

Construirem una peça útil: una memòria cau de disponibilitat de la xarxa que es precalcula en arrencar i allibera els seus recursos en tancar.

Deixa clara una cosa abans de començar: això és una memòria cau manual, escrita a mà per il·lustrar el cicle de vida. La memòria cau declarativa de Spring, amb @Cacheable i @CacheEvict, és una altra cosa i s'estudia a la lliçó 09-02.

package com.ciclourbana.estacions;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Memòria cau en memòria dels ancoratges lliures per estació.
 *
 * Precalcula els valors en arrencar (@PostConstruct) i bolca
 * estadístiques en tancar (@PreDestroy). És una memòria cau manual: la
 * memòria cau declarativa amb @Cacheable s'estudia a la lliçó 09-02.
 */
@Component
public class CacheDisponibilitat {

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

    private final EstacioRepositori estacioRepositori;

    // ConcurrentHashMap: el bean és singleton i el llegeixen diversos fils
    private final Map<Long, Integer> ancoratgesLliures = new ConcurrentHashMap<>();
    private final AtomicLong encerts = new AtomicLong();
    private final AtomicLong errades = new AtomicLong();

    private Instant momentCarrega;

    public CacheDisponibilitat(EstacioRepositori estacioRepositori) {
        this.estacioRepositori = estacioRepositori;
        // COMPTE: aquí NO precarreguem. El constructor només assigna dependències.
        log.debug("CacheDisponibilitat construïda (encara sense dades)");
    }

    /**
     * S'executa després que totes les dependències estiguin injectades.
     * És el lloc correcte per a feina d'inicialització pesada.
     */
    @PostConstruct
    public void precarregar() {
        Instant inici = Instant.now();

        estacioRepositori.cercarTotes().forEach(estacio ->
                // Simulació: en arrencar considerem la meitat d'ancoratges ocupats
                ancoratgesLliures.put(estacio.id(), estacio.capacitat() / 2));

        this.momentCarrega = Instant.now();
        log.info("Memòria cau de disponibilitat precarregada amb {} estacions en {} ms",
                ancoratgesLliures.size(),
                Duration.between(inici, momentCarrega).toMillis());
    }

    public int ancoratgesLliures(Long estacioId) {
        Integer valor = ancoratgesLliures.get(estacioId);
        if (valor == null) {
            errades.incrementAndGet();
            return recalcular(estacioId);
        }
        encerts.incrementAndGet();
        return valor;
    }

    public void actualitzar(Long estacioId, int lliures) {
        ancoratgesLliures.put(estacioId, lliures);
    }

    private int recalcular(Long estacioId) {
        int lliures = estacioRepositori.cercarPerId(estacioId)
                .map(estacio -> estacio.capacitat() / 2)
                .orElse(0);
        ancoratgesLliures.put(estacioId, lliures);
        return lliures;
    }

    /**
     * S'executa en tancar el context de manera ordenada.
     * Aquí s'alliberarien connexions, fils o fitxers oberts.
     */
    @PreDestroy
    public void alliberar() {
        log.info("Tancant la memòria cau de disponibilitat després de {} de servei",
                Duration.between(momentCarrega, Instant.now()));
        log.info("Estadístiques: {} encerts, {} errades ({}% d'encert)",
                encerts.get(), errades.get(), percentatgeEncert());
        ancoratgesLliures.clear();
    }

    private long percentatgeEncert() {
        long total = encerts.get() + errades.get();
        return total == 0 ? 0 : (encerts.get() * 100) / total;
    }
}

Un detall d'ordre que causa problemes reals: el @PostConstruct d'aquest bean s'executa abans que corrin els CommandLineRunner. I el nostre CarregadorEstacionsDemo és un CommandLineRunner. Resultat: quan precarregar() s'executa, el repositori encara és buit i la memòria cau neix sense dades.

sequenceDiagram
    participant SA as SpringApplication
    participant Ctx as ApplicationContext
    participant C as CacheDisponibilitat
    participant R as CarregadorEstacionsDemo

    SA->>Ctx: refresh()
    Ctx->>C: constructor
    Ctx->>C: @PostConstruct precarregar()
    Note over C: Repositori buit:<br/>0 estacions a la memòria cau
    Ctx-->>SA: context llest
    SA->>R: run() — carrega les 4 estacions
    Note over R: Ara sí que hi ha dades,<br/>però la memòria cau ja s'ha poblat

Aquesta és exactament la mena de dependència temporal que cal saber veure. Tres maneres de resoldre-la:

  1. Precarregar en un ApplicationReadyEvent en lloc de fer-ho a @PostConstruct: es publica després dels runners.
  2. Poblar el repositori abans, al mateix @PostConstruct d'un bean del qual la memòria cau depengui.
  3. Acceptar la memòria cau buida i deixar que s'ompli sota demanda amb recalcular(), que és el que ja fa la nostra implementació.

Per a CicloUrbana triem la primera, perquè és la més explícita:

@Component
public class EscalfadorCache {

    private final CacheDisponibilitat cache;
    private final EstacioRepositori repositori;

    public EscalfadorCache(CacheDisponibilitat cache, EstacioRepositori repositori) {
        this.cache = cache;
        this.repositori = repositori;
    }

    /** ApplicationReadyEvent es publica DESPRÉS dels CommandLineRunner. */
    @EventListener(ApplicationReadyEvent.class)
    public void escalfar() {
        repositori.cercarTotes().forEach(e ->
                cache.actualitzar(e.id(), e.capacitat() / 2));
    }
}

Comprovar-ho en marxa

./mvnw spring-boot:run

A l'arrencada veuràs el @PostConstruct, i en aturar amb Ctrl+C veuràs el @PreDestroy:

c.c.estacions.CacheDisponibilitat : Memòria cau de disponibilitat precarregada amb 0 estacions en 1 ms
c.c.e.CarregadorEstacionsDemo     : Carregades 4 estacions, 108 ancoratges en total
...
c.c.estacions.CacheDisponibilitat : Tancant la memòria cau de disponibilitat després de PT2M14S de servei
c.c.estacions.CacheDisponibilitat : Estadístiques: 12 encerts, 4 errades (75% d'encert)

  1. Inicialització mandrosa

Per defecte, l'ApplicationContext crea tots els singletons durant l'arrencada. Es pot canviar bean a bean:

@Component
@Lazy   // no es crea fins que algú el demani per primera vegada
public class GeneradorInformesMensuals { /* procés costós, s'utilitza un cop al mes */ }

O globalment:

# src/main/resources/application.properties
spring.main.lazy-initialization=true

Les seves contrapartides, en una taula:

Aspecte Amb creació anticipada (per defecte) Amb inicialització mandrosa
Temps d'arrencada Més gran Més petit
Detecció d'errors de configuració A l'arrencada A la primera petició
Latència de la primera petició Baixa Alta (es construeix la cadena de beans)
Memòria en arrencar Tota la necessària Només la dels beans utilitzats
Ús recomanat Producció Desenvolupament, proves puntuals

La contrapartida és seriosa: amb inicialització mandrosa, un bean mal configurat no rebenta l'arrencada; rebenta quan un usuari real toca aquest camí de codi. Es perd exactament la propietat que fa útil Spring Boot en producció.

Recomanació pràctica per a CicloUrbana: no l'activis globalment. Si la teva arrencada triga massa, el problema gairebé mai no són els beans sinó alguna cosa concreta —una connexió a base de dades, un client extern que fa una crida en arrencar— i la solució correcta és un @Lazy puntual sobre aquest bean, o moure la feina pesada a un ApplicationReadyEvent.

Un matís sobre @Lazy: si un bean mandrós és injectat per constructor en un bean no mandrós, s'haurà de crear igualment en construir aquest darrer... llevat que el punt d'injecció porti també @Lazy, cas en què Spring injecta un proxy i difereix la creació real.

  1. BeanPostProcessor: el punt d'extensió

Un BeanPostProcessor és un bean especial al qual el contenidor dona l'oportunitat d'inspeccionar o substituir cada bean just abans i just després de la seva inicialització. És el ganxo sobre el qual es recolza la meitat de Spring:

Què sembla màgia Quin BeanPostProcessor ho fa
@Autowired i @Value funcionen AutowiredAnnotationBeanPostProcessor
@PostConstruct i @PreDestroy s'executen CommonAnnotationBeanPostProcessor
@Transactional obre transaccions InfrastructureAdvisorAutoProxyCreator
@Cacheable, @Async, @Scheduled Els seus post-processadors respectius
@Repository tradueix excepcions PersistenceExceptionTranslationPostProcessor

La interfície té dos mètodes, tots dos amb implementació per defecte:

public interface BeanPostProcessor {

    default Object postProcessBeforeInitialization(Object bean, String beanName) {
        return bean;   // abans de @PostConstruct
    }

    default Object postProcessAfterInitialization(Object bean, String beanName) {
        return bean;   // després de @PostConstruct; aquí es retornen els proxies
    }
}

La clau és que tots dos retornen un objecte. Si retornes alguna cosa diferent del bean rebut, aquest serà l'objecte que quedi registrat. Així és exactament com Spring substitueix el teu EstacioService per un proxy que obre transaccions abans de cada mètode.

Un BeanPostProcessor propi per a CicloUrbana

Mesurarem quant triga a inicialitzar-se cada bean del projecte, per detectar arrencades lentes:

package com.ciclourbana.comu;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * Cronometra la inicialització dels beans de com.ciclourbana i avisa
 * dels que triguen més del llindar. Eina de diagnòstic d'arrencada.
 */
@Component
public class CronometreInicialitzacioBeans implements BeanPostProcessor {

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

    private static final long LLINDAR_MS = 50;

    // El post-processador s'executa al fil d'arrencada: n'hi ha prou amb HashMap
    private final Map<String, Long> inicis = new HashMap<>();

    @Override
    public Object postProcessBeforeInitialization(Object bean, String nomBean) {
        if (esNostre(bean)) {
            inicis.put(nomBean, System.nanoTime());
        }
        return bean;   // retornem el mateix bean: no el substituïm
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String nomBean) {
        Long inici = inicis.remove(nomBean);
        if (inici != null) {
            long ms = (System.nanoTime() - inici) / 1_000_000;
            if (ms >= LLINDAR_MS) {
                log.warn("Bean lent: '{}' ha trigat {} ms a inicialitzar-se", nomBean, ms);
            } else {
                log.debug("Bean '{}' inicialitzat en {} ms", nomBean, ms);
            }
        }
        return bean;
    }

    private boolean esNostre(Object bean) {
        return bean.getClass().getName().startsWith("com.ciclourbana");
    }
}

Activa'l en mode depuració per veure tots els temps:

logging.level.com.ciclourbana.comu.CronometreInicialitzacioBeans=DEBUG
c.c.c.CronometreInicialitzacioBeans : Bean 'estacioRepositoriEnMemoria' inicialitzat en 2 ms
c.c.c.CronometreInicialitzacioBeans : Bean 'estacioService' inicialitzat en 1 ms
c.c.c.CronometreInicialitzacioBeans : Bean 'cacheDisponibilitat' inicialitzat en 4 ms
c.c.c.CronometreInicialitzacioBeans : Bean 'tarifaEstandard' inicialitzat en 0 ms

Dos advertiments importants sobre els BeanPostProcessor:

Es creen molt aviat, abans que els beans normals. Per això no hi has d'injectar dependències que necessitin configuració complexa: podries forçar la creació prematura d'altres beans i saltar-te el seu propi post-processament (Spring avisa al log amb un missatge del tipus "is not eligible for getting processed by all BeanPostProcessors").

S'executen per a tots els beans, inclosos els interns de Spring. Són centenars. Filtra sempre per paquet o per anotació, com fa el nostre esNostre(...), o generaràs un log inservible.

El seu parent proper, el BeanFactoryPostProcessor, actua un pas abans: modifica les BeanDefinition (les receptes) abans que s'instanciï res. És el que utilitza Spring per resoldre els ${...} de les propietats. Rarament n'hauràs d'escriure un.

Errors Comuns i Consells

Estat mutable en un singleton. L'error més greu d'aquesta lliçó. Un camp no final i no concurrent en un @Service és una bomba de rellotgeria que només esclata sota càrrega. Regla mecànica: en un bean, tot camp hauria de ser final, i si el seu contingut és mutable, ha de ser una estructura concurrent.

Feina pesada al constructor. El constructor només ha d'assignar dependències. Consultes, connexions o càlculs van a @PostConstruct o, millor encara, a un ApplicationReadyEvent. Un constructor que falla deixa el context en un estat difícil de diagnosticar.

Esperar @PreDestroy en un bean prototype. No s'executa mai. Si el bean gestiona un recurs, tanca'l tu.

Utilitzar javax.annotation.PostConstruct. A Spring Boot 3 el paquet és jakarta.annotation. És el mateix canvi javax→jakarta que vam veure a la lliçó 01-01. Amb l'import antic el mètode simplement no s'executa, sense cap error: silenciós i desconcertant.

Injectar un prototype per constructor en un singleton. Es congela la primera instància i l'àmbit es perd. Utilitza ObjectProvider.

Utilitzar @SessionScope en una API REST. Trenca l'absència d'estat i complica l'escalat. L'estat de l'usuari va al token (mòdul 5).

Activar spring.main.lazy-initialization=true en producció. Canvies errors d'arrencada per errors a la cara de l'usuari. Utilitza-la, com a molt, en desenvolupament.

Consell: si dubtes de l'àmbit, és singleton. El 95 % dels beans d'una aplicació real són singletons sense estat. Quan et vegis temptat d'utilitzar prototype, pregunta't primer si l'estat no pot viatjar com a paràmetre de mètode.

Consell: utilitza @PostConstruct per validar la configuració. Comprovar en arrencar que un paràmetre crític té sentit converteix un error de producció en una fallada d'arrencada. A la lliçó 02-05 veurem que Bean Validation fa això mateix de manera declarativa.

Consell: per diagnosticar el cicle de vida, puja el nivell de log. logging.level.org.springframework.beans.factory=DEBUG mostra la creació de cada bean en ordre. És sorollós, però resol dubtes en minuts.

Exercicis

Exercici 1: demostrar els àmbits

Crea dos components que només registrin la seva identitat: ComptadorSingleton (àmbit per defecte) i ComptadorPrototype (@Scope("prototype")), tots dos amb un camp id generat al constructor. Des d'un CommandLineRunner, demana tres vegades cadascun al context i comprova per consola que el singleton sempre té el mateix id i el prototype un de diferent cada vegada. Afegeix un @PreDestroy a tots dos i observa quin s'executa en tancar.

Exercici 2: arreglar un singleton amb estat

Parteix d'aquesta classe, que té una fallada de concurrència, i corregeix-la sense canviar-ne l'API pública. Després demostra el problema llançant 100 fils simultanis contra el mètode original.

@Service
public class RegistreLloguersActius {

    private int totalIniciats = 0;
    private final List<String> matriculesActives = new ArrayList<>();

    public void iniciar(String matricula) {
        matriculesActives.add(matricula);
        totalIniciats++;
    }

    public void finalitzar(String matricula) {
        matriculesActives.remove(matricula);
    }

    public int actius() {
        return matriculesActives.size();
    }

    public int totalIniciats() {
        return totalIniciats;
    }
}

Exercici 3: un BeanPostProcessor que valida una anotació pròpia

Crea una anotació @RequereixInicialitzacio i un BeanPostProcessor que, per a cada bean del paquet com.ciclourbana marcat amb ella, comprovi a postProcessAfterInitialization que un mètode boolean estaInicialitzat() retorna true. Si no ho està, l'arrencada ha de fallar amb un missatge clar. Aplica'l a CacheDisponibilitat.


Solucions

Solució 1

package com.ciclourbana.comu;

import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
public class ComptadorSingleton {

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

    private final String id = UUID.randomUUID().toString().substring(0, 8);

    public ComptadorSingleton() {
        log.info("Construït ComptadorSingleton {}", id);
    }

    public String id() {
        return id;
    }

    @PreDestroy
    public void enDestruir() {
        log.info("Destruït ComptadorSingleton {}", id);
    }
}
package com.ciclourbana.comu;

import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
@Scope("prototype")
public class ComptadorPrototype {

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

    private final String id = UUID.randomUUID().toString().substring(0, 8);

    public String id() {
        return id;
    }

    @PreDestroy
    public void enDestruir() {
        // MAI no s'executa: Spring no gestiona la destrucció dels prototype
        log.info("Destruït ComptadorPrototype {}", id);
    }
}
package com.ciclourbana.comu;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Order(50)
public class DemostracioAmbits implements CommandLineRunner {

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

    private final ApplicationContext context;

    public DemostracioAmbits(ApplicationContext context) {
        this.context = context;
    }

    @Override
    public void run(String... args) {
        for (int i = 1; i <= 3; i++) {
            log.info("Petició {} -> singleton: {} | prototype: {}",
                    i,
                    context.getBean(ComptadorSingleton.class).id(),
                    context.getBean(ComptadorPrototype.class).id());
        }
    }
}

Sortida:

Petició 1 -> singleton: a3f91c02 | prototype: 7b2e4d81
Petició 2 -> singleton: a3f91c02 | prototype: c14a9f30
Petició 3 -> singleton: a3f91c02 | prototype: 55d0e7b2
...
Destruït ComptadorSingleton a3f91c02      <- només apareix el del singleton

Comentari: el @PreDestroy del prototype no apareix en cap de les tres instàncies, i confirma el que s'ha dit a l'apartat 3. Fixa't a més que aquí sí que utilitzem context.getBean(...): en una demostració didàctica és legítim, però recorda que en lògica de negoci és l'antipatró service locator.

Solució 2

Diagnòstic: hi ha dues condicions de cursa. matriculesActives és un ArrayList (no segur per a fils: dos add simultanis poden perdre un element o corrompre l'array intern) i totalIniciats++ no és atòmic (és llegir, sumar i escriure; dos fils poden llegir el mateix valor i perdre un increment).

package com.ciclourbana.lloguers;

import org.springframework.stereotype.Service;

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Registre de lloguers en curs. És un singleton compartit per
 * tots els fils de Tomcat, així que tot el seu estat ha de ser concurrent.
 */
@Service
public class RegistreLloguersActius {

    // Set concurrent: a més evita duplicats, que en aquest domini
    // són un error (una bicicleta no es pot llogar dues vegades)
    private final Set<String> matriculesActives = ConcurrentHashMap.newKeySet();

    // Increment atòmic: sense pèrdua d'actualitzacions
    private final AtomicInteger totalIniciats = new AtomicInteger();

    public void iniciar(String matricula) {
        if (matriculesActives.add(matricula)) {   // add retorna false si ja hi era
            totalIniciats.incrementAndGet();
        }
    }

    public void finalitzar(String matricula) {
        matriculesActives.remove(matricula);
    }

    public int actius() {
        return matriculesActives.size();
    }

    public int totalIniciats() {
        return totalIniciats.get();
    }
}

I la demostració del problema amb la versió original:

package com.ciclourbana.lloguers;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Component
public class ProvaConcurrencia implements CommandLineRunner {

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

    private final RegistreLloguersActius registre;

    public ProvaConcurrencia(RegistreLloguersActius registre) {
        this.registre = registre;
    }

    @Override
    public void run(String... args) throws InterruptedException {
        int fils = 100;
        CountDownLatch sortida = new CountDownLatch(1);
        CountDownLatch fi = new CountDownLatch(fils);

        try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < fils; i++) {
                String matricula = String.format("RB-%04d", i);
                pool.submit(() -> {
                    try {
                        sortida.await();                // tots arrenquen alhora
                        registre.iniciar(matricula);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    } finally {
                        fi.countDown();
                    }
                });
            }
            sortida.countDown();
            fi.await();
        }

        log.info("Esperats 100 | actius: {} | totalIniciats: {}",
                registre.actius(), registre.totalIniciats());
    }
}

Amb la versió original veuràs sortides com ara actius: 97 | totalIniciats: 94, i variaran a cada execució. Amb la corregida, sempre 100 | 100.

Comentari i consell: Executors.newVirtualThreadPerTaskExecutor() és de Java 21 i fa aquestes proves trivials d'escriure. El CountDownLatch de sortida és important: sense ell els fils arrenquen esglaonats i la cursa pot no manifestar-se, cosa que dona la falsa impressió que el codi original és correcte. Els bugs de concurrència són així: no reproduir-los no vol dir que no existeixin.

Solució 3

package com.ciclourbana.comu;

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

/**
 * Marca un bean que ha d'estar operatiu en acabar la seva inicialització.
 * La classe anotada ha d'exposar un mètode públic boolean estaInicialitzat().
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequereixInicialitzacio {

    /** Descripció utilitzada al missatge d'error. */
    String value() default "";
}
package com.ciclourbana.comu;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component
public class VerificadorInicialitzacio implements BeanPostProcessor {

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

    @Override
    public Object postProcessAfterInitialization(Object bean, String nomBean) {
        // AnnotationUtils travessa proxies i jerarquies de classes
        RequereixInicialitzacio marca =
                AnnotationUtils.findAnnotation(bean.getClass(), RequereixInicialitzacio.class);

        if (marca == null) {
            return bean;
        }

        try {
            Method metode = bean.getClass().getMethod("estaInicialitzat");
            boolean llest = (boolean) metode.invoke(bean);

            if (!llest) {
                throw new BeanInitializationException(
                        "El bean '" + nomBean + "' no ha quedat inicialitzat. "
                                + marca.value());
            }
            log.info("Bean '{}' verificat: inicialització correcta", nomBean);

        } catch (NoSuchMethodException e) {
            throw new BeanInitializationException(
                    "El bean '" + nomBean + "' porta @RequereixInicialitzacio "
                            + "però no exposa un mètode públic boolean estaInicialitzat()", e);
        } catch (ReflectiveOperationException e) {
            throw new BeanInitializationException(
                    "No s'ha pogut verificar la inicialització de '" + nomBean + "'", e);
        }

        return bean;   // no substituïm el bean, només el validem
    }
}

I l'aplicació sobre la memòria cau:

@Component
@RequereixInicialitzacio("La memòria cau de disponibilitat s'ha de precarregar abans d'acceptar trànsit.")
public class CacheDisponibilitat {

    // ... la resta igual que a l'apartat 7 ...

    /** Contracte exigit per @RequereixInicialitzacio. */
    public boolean estaInicialitzat() {
        return momentCarrega != null;
    }
}

Sortida en una arrencada correcta:

c.c.comu.VerificadorInicialitzacio : Bean 'cacheDisponibilitat' verificat: inicialització correcta

I si comentes la línia this.momentCarrega = Instant.now() del @PostConstruct:

***************************
APPLICATION FAILED TO START
***************************

org.springframework.beans.factory.BeanInitializationException: El bean
'cacheDisponibilitat' no ha quedat inicialitzat. La memòria cau de disponibilitat
s'ha de precarregar abans d'acceptar trànsit.

Comentari: fixa't en l'ordre. El BeanPostProcessor actua a postProcessAfterInitialization, és a dir, després del @PostConstruct, que és precisament on té sentit comprovar que la inicialització ha funcionat. Si ho haguéssim posat a postProcessBeforeInitialization, momentCarrega seria sempre null i tots els beans fallarien.

Aquest exercici, en petit, és exactament el mecanisme pel qual @Transactional o @Cacheable funcionen: una anotació que tota sola no fa res, més un post-processador que la detecta i actua. L'única diferència és que aquells, en lloc de retornar el mateix bean, retornen un proxy que l'embolcalla.

Conclusió

El contenidor ha deixat de ser una caixa negra. Saps que un bean és un objecte el cicle de vida del qual gestiona Spring, i que aquesta gestió és l'única cosa que separa EstacioService d'un objecte creat amb new —i també la raó per la qual les anotacions de comportament només funcionen sobre beans. Coneixes la diferència entre BeanFactory i ApplicationContext, i per què aquest darrer crea els singletons per endavant: per fallar a l'arrencada i no davant de l'usuari. Domines els cinc àmbits, saps que Spring no destrueix els prototype i saps resoldre amb ObjectProvider el clàssic problema d'injectar un prototype en un singleton. Sobretot, has interioritzat la regla que més disgustos evita: un singleton no pot tenir estat mutable, i per això EstacioRepositoriEnMemoria utilitza ConcurrentHashMap i AtomicLong. Coneixes els onze passos del cicle de vida, has enganxat codi al naixement i la mort d'un bean amb @PostConstruct i @PreDestroy, has descobert la dependència temporal entre aquesta precàrrega i els CommandLineRunner, i has escrit el teu primer BeanPostProcessor, el mateix mecanisme sobre el qual es recolzen les transaccions i la memòria cau declarativa.

CicloUrbana suma ara CacheDisponibilitat, amb precàrrega en arrencar i bolcat d'estadístiques en tancar, i CronometreInicialitzacioBeans com a eina de diagnòstic de l'arrencada.

Queda una cosa que hem anat esquivant lliçó rere lliçó: els números escrits a foc. El preu de desbloqueig 0.50, el preu per minut 0.12, els 15 minuts gratuïts de la tarifa d'estudiant, el llindar de 50 ms del cronòmetre, la capacitat mínima de 8 ancoratges. Tot això són constants compilades dins del codi, i canviar qualsevol d'elles obliga avui a recompilar i tornar a desplegar. La lliçó següent, Configuració de Spring Boot, ataca aquest problema d'arrel: l'Environment i les seves fonts de propietats, l'ordre exacte de precedència entre arguments de línia d'ordres, variables d'entorn i fitxers, les diferències entre .properties i .yaml, la relaxació de noms, @Value amb SpEL i —molt important— per què les credencials no poden estar mai al repositori.

Curs de Spring Boot

Mòdul 1: Introducció a Spring Boot

Mòdul 2: Conceptes bàsics de Spring Boot

Mòdul 3: Construint serveis web RESTful

Mòdul 4: Accés a dades amb Spring Boot

Mòdul 5: Seguretat a Spring Boot

Mòdul 6: Proves a Spring Boot

Mòdul 7: Funcions avançades de Spring Boot

Mòdul 8: Desplegament d'aplicacions Spring Boot

Mòdul 9: Rendiment i monitoratge

Mòdul 10: Millors pràctiques i consells

© Copyright 2026. Tots els drets reservats