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
- Què és Spring i què és Spring Boot
- Els mòduls de Spring Framework
- El contenidor d'IoC:
ApplicationContext - Què és un bean i com es declara
- Les tres formes d'injecció
- Per què la injecció per constructor
- Comparativa amb el
ContenidorSimplecasolà - Estereotips:
@Component,@Service,@Repository,@Controller @Configuration+@Beanenfront de l'escaneig de components- El cicle de vida d'un bean
- Àmbits d'un bean
- Resolució d'ambigüitat:
@Qualifier,@Primaryi col·leccions @Valuei les propietats externalitzades@ConfigurationProperties: configuració tipada i validada- Perfils:
@Profileispring.profiles.active - BiblioTech: de
Configuracioa la configuració de Spring - Spring Boot: el
pom.xmli els starters @SpringBootApplicationdissecada- L'autoconfiguració, explicada de veritat
- Depurar l'autoconfiguració
CommandLineRunneriApplicationRunner- El jar executable i
spring-boot-maven-plugin - AOP a Spring: aspectes i punts de tall
@Transactional: l'aspecte canònic- Com funciona per sota: els proxies que ja vas escriure
- El parany de la crida interna
- BiblioTech sobre Spring: el resultat
- Què NO es veu en aquesta lliçó
- Errors comuns i consells
- Exercicis
- 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.
- 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çó.
- El contenidor d'IoC:
ApplicationContext
ApplicationContextEl cor de Spring és el contenidor. La seva feina és exactament la del teu ContenidorSimple:
- Esbrinar quins objectes cal crear.
- Esbrinar què necessita cadascun.
- Crear-los en l'ordre correcte, injectant les dependències.
- Desar-los i lliurar-los quan es demanen.
- 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.
- 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).
- 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:
- 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, ésnew GestorPrestecs(mock1, mock2, mock3). - No es pot fer servir
final. Perds la immutabilitat i la garantia que la dependència no canvia. - 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.
- 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. - Acobla la classe a Spring.
@Autowiredal 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.
- 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.
- Comparativa amb el
ContenidorSimple casolà
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.
- Estereotips:
@Component, @Service, @Repository, @Controller
@Component, @Service, @Repository, @ControllerSpring 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.
@Configuration + @Bean enfront de l'escaneig de components
@Configuration + @Bean enfront de l'escaneig de componentsHi ha dos estils de dir-li a Spring quins beans existeixen, i en un projecte real conviuen.
Escaneig de components
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
@Servicei 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:
- El nom del mètode és el nom del bean (
rellotge,clientHttp,clientMetadades). - Els paràmetres d'un mètode
@Beans'injecten igual que els d'un constructor. - 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 |
- 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è@Transactionaldeixa 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.
- À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
// ...
}
}
- Resolució d'ambigüitat:
@Qualifier, @Primary i col·leccions
@Qualifier, @Primary i col·leccionsSpring 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
- avisosPerSmsEl 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
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) |
@Value i les propietats externalitzades
@Value i les propietats externalitzadesCap 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=trueO 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: trueEs 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 aBigDecimal. Spring converteix aint,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.propertiesdins del repositori. Van a variables d'entorn o a un gestor de secrets. Es tracta a 12-07.
@ConfigurationProperties: configuració tipada i validada
@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 1Fixa'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.
- Perfils:
@Profile i spring.profiles.active
@Profile i spring.profiles.activeUn 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=devI 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.
- BiblioTech: de
Configuracio a la configuració de Spring
Configuracio a la configuració de SpringL'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:
static: no es pot substituir en una prova. És exactament el "codi que no es pot provar" que 11-04 analitzarà.- Sense validació:
prestec.dies=abcllançaNumberFormatExceptionel dia que algú demani un préstec, no en arrencar. - Sense entorns: un sol fitxer per a desenvolupament i producció.
- Conversió manual a cada accés:
Integer.parseIntinew BigDecimalcada vegada que es crida. - Un bloc
staticque pot fallar: si el fitxer no hi és, l'error apareix en unExceptionInInitializerErrorincomprensible.
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 |
- Spring Boot: el
pom.xml i els starters
pom.xml i els startersAra 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:
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.
@SpringBootApplication dissecada
@SpringBootApplication dissecadaLa 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.BiblioTechApplicationescaneja.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:
- Dedueix el tipus d'aplicació (consola, web servlet, web reactiva) segons el que hi ha al classpath.
- Crea l'
ApplicationContextadequat. - Carrega les fonts de propietats (
application.properties, perfils, entorn, arguments). - Registra les definicions de bean: escaneig +
@Bean+ autoconfiguració. - Instancia tots els singletons, injectant dependències.
- Executa
@PostConstructi crea els proxies AOP. - Arrenca el servidor incrustat si n'hi ha.
- Executa els
CommandLineRunneriApplicationRunner. - Registra un ganxo d'aturada per tancar el context de forma ordenada.
- 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:
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.
- 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:
o a application.properties:
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:
----------------------
...ConfigurationPropertiesAutoConfigurationCom 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 { }
CommandLineRunner i ApplicationRunner
CommandLineRunner i ApplicationRunnerI 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.
- El jar executable i
spring-boot-maven-plugin
spring-boot-maven-pluginEl 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.75Empaquetar 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=prodAquest 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 jarsEl 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.
- 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.
@Transactional: l'aspecte canònic
@Transactional: l'aspecte canònicL'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 (
RuntimeExceptioniError) 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.
- 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:
- Spring instancia el teu
GestorPrestecsi li injecta les seves dependències. - Un
BeanPostProcessormira si algun dels seus mètodes encaixa en algun punt de tall (per exemple, té@Transactional). - Si encaixa, crea un proxy que embolcalla el teu objecte i registra el proxy al contenidor, no el teu objecte.
- Tothom qui injecti un
GestorPrestecsrep 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.
- 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:
- El cridador extern invoca
prestarDiversosal proxy. - El proxy mira:
prestarDiversosno té@Transactional. No fa res especial. - El proxy invoca
prestarDiversosal teu objecte real. - A dins,
prestar(isbn, empleat)és en realitatthis.prestar(...), ithisés el teu objecte real, no el proxy. - 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.
- 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.ymlFixa'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:
- 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.
- 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.
- 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:
- Injecció per constructor amb camps
final. - L'estereotip correcte.
- El nombre de dies des de configuració externa i validat (
@Min(1) @Max(30)). LocalDate.now()substituït per unClockinjectat (10-05), per poder provar-ho.processarPendentsha de ser atòmic.- Afegeix el fragment d'
application.ymlcorresponent.
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:
- Al perfil
devs'ha de fer servir la de consola; aprod, la de correu. Cap classe no ha de consultar en quin entorn és. GestorPrestecsrep unServeiAvisossense saber quin.- Afegeix una tercera implementació,
AvisosPerSms, i unNotificadorMulticanalque faci servir totes les implementacions actives, sense que calgui modificar-lo en afegir-ne una quarta. - Explica què passa si al perfil
prodhi ha dues implementacions actives iGestorPrestecsen 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: 3Què 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, avisosPerSmsDues 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:
importarUnno s'executa en transacció. Cadadesares 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 sobrethis, que és l'objecte real, no el proxy. La crida no travessa el proxy i@Transactionals'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.registrarfalla, la multa ja està esborrada i no hi ha registre. - Per què: Spring Boot fa servir CGLIB, que genera una subclasse. Un mètode
finalno 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,
MaterialCorrupteExceptionpuja... 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
RuntimeExceptioniError.MaterialCorrupteExceptionesténException: é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
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
