Tanquem el mòdul 1 amb una promesa: deixar d'utilitzar Spring Boot per imitació. A CicloUrbana ja hem escrit @SpringBootApplication, @RestController, @Component, @Order i @EventListener sense explicar què són ni com funcionen. Aquesta lliçó salda aquest deute. Veurem què és exactament una anotació a Java, com Spring les llegeix durant l'arrencada (spoiler: amb reflexió i una mica d'anàlisi de bytecode, no pas amb màgia), desmuntarem @SpringBootApplication peça a peça, compararem els cinc estereotips, entendrem quan cal declarar beans amb @Bean en comptes de fer-ho amb @Component i crearem la nostra pròpia anotació estereotipada. Acabarem aplicant tot això a CicloUrbana: partirem el provisional MagatzemEstacions en un EstacioService i un EstacioRepositoriEnMemoria, que és l'estructura que el projecte arrossegarà durant la resta del curs.
Contingut
- Què és una anotació a Java
- Com processa Spring les anotacions en arrencar
@SpringBootApplicationdesmuntada- Els estereotips de Spring
@Repositoryi la traducció d'excepcions@Configurationi@Bean: la via declarativa@ComponentScani els seus filtres- Crear la teva pròpia anotació estereotipada
- Refactoritzant CicloUrbana: servei i repositori
- Mapa d'anotacions del curs
- Errors Comuns i Consells
- Exercicis
- Què és una anotació a Java
Una anotació és metadada adjunta al codi: informació sobre una classe, un mètode, un camp o un paràmetre que el compilador desa al fitxer .class i que un altre programa pot llegir després. Una anotació, tota sola, no fa absolutament res. No executa codi, no canvia el comportament del mètode, no genera instàncies. És una etiqueta.
Això és una anotació completa, escrita a mà:
package com.ciclourbana.comu;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE) // es pot posar sobre classes/interfícies
@Retention(RetentionPolicy.RUNTIME) // sobreviu fins a l'execució
public @interface Provisional {
String fins() default "modul-4"; // atribut amb valor per defecte
}I així s'utilitza:
Tres detalls que convé tenir clars:
| Element | Què significa |
|---|---|
@interface |
La paraula clau que declara una anotació (no és una interfície normal). |
@Target |
On es pot col·locar: TYPE (classe), METHOD, FIELD, PARAMETER, ANNOTATION_TYPE... |
@Retention |
Fins quan viu. SOURCE (la descarta el compilador), CLASS (queda al .class però no es carrega), RUNTIME (accessible per reflexió). |
Totes les anotacions de Spring són RUNTIME. És la condició imprescindible perquè el contenidor les pugui trobar mentre l'aplicació arrenca. Si fossin SOURCE, com ara @Override, Spring no hi veuria res.
Llegir una anotació en temps d'execució és tan senzill com això:
Class<?> classe = EstacioRepositoriEnMemoria.class;
Provisional marca = classe.getAnnotation(Provisional.class);
if (marca != null) {
System.out.println("Classe provisional fins a: " + marca.fins());
// -> Classe provisional fins a: modul-4
}Aquest fragment —getAnnotation sobre un Class<?>— és, conceptualment, tot el que fa Spring. La resta és escala i enginyeria.
- Com processa Spring les anotacions en arrencar
Quan al mòdul 1 vam veure la fase refresh() de l'arrencada, vam passar per sobre del que hi passa a dins. Ara ho podem concretar. El recorregut és aquest:
flowchart TD
A["@SpringBootApplication a CicloUrbanaApplication"] --> B["@ComponentScan determina<br/>el paquet base: com.ciclourbana"]
B --> C["ClassPathScanningCandidateComponentProvider<br/>recorre els .class del classpath"]
C --> D["Llegeix cada .class amb ASM<br/>(sense carregar la classe a memòria)"]
D --> E{"Té @Component<br/>directa o meta-anotada?"}
E -- No --> F["Es descarta"]
E -- Sí --> G["Crea una BeanDefinition:<br/>nom, classe, àmbit, dependències"]
G --> H["Registre al BeanDefinitionRegistry"]
H --> I["Els BeanFactoryPostProcessor<br/>ajusten les definicions"]
I --> J["Instanciació dels singletons<br/>resolent constructors"]
J --> K["BeanPostProcessor:<br/>@Autowired, @Value, proxies AOP"]
K --> L["Beans llestos a l'ApplicationContext"]
Val la pena subratllar tres punts d'aquest flux:
L'escaneig no carrega les classes. Spring fa servir ASM, una llibreria d'anàlisi de bytecode, per inspeccionar els fitxers .class sense passar-los pel ClassLoader. Així pot descartar ràpidament els milers de classes del classpath que no l'interessen, sense el cost de carregar-les totes.
El que es registra primer és una BeanDefinition, no pas un objecte. Una BeanDefinition és una recepta: quina classe cal instanciar, amb quin constructor, amb quin àmbit, amb quin mètode d'inicialització. Els objectes reals es creen després, en un segon pas. Aquesta separació en dues fases és la que permet que Spring detecti errors de configuració (dependències que falten, ambigüitats) abans de construir res.
Les anotacions es resolen de manera transitiva. Spring no busca literalment @Component; busca @Component o qualsevol anotació que, al seu torn, estigui anotada amb @Component. Això s'anomena meta-anotació, i és el mecanisme sobre el qual es construeix tot el sistema d'estereotips. L'utilitzarem nosaltres mateixos a l'apartat 8.
La conclusió pràctica: una anotació de Spring només té efecte si la classe és allà on l'escaneig la veu, o si un altre mecanisme la registra. Una classe amb @Service fora del paquet base és text mort. Aquest és l'origen de la meitat dels "no em funciona" dels primers dies.
@SpringBootApplication desmuntada
@SpringBootApplication desmuntadaLa nostra classe principal és aquesta:
package com.ciclourbana;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CicloUrbanaApplication {
public static void main(String[] args) {
SpringApplication.run(CicloUrbanaApplication.class, args);
}
}@SpringBootApplication és una meta-anotació de conveniència. La seva declaració real, simplificada, és:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)
})
public @interface SpringBootApplication {
// atributs: exclude, excludeName, scanBasePackages, proxyBeanMethods...
}És a dir, escriure @SpringBootApplication equival exactament a escriure aquestes tres anotacions:
package com.ciclourbana;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@SpringBootConfiguration // 1. aquesta classe és una classe de configuració
@EnableAutoConfiguration // 2. activa l'autoconfiguració
@ComponentScan // 3. escaneja aquest paquet i els de sota
public class CicloUrbanaApplication {
public static void main(String[] args) {
SpringApplication.run(CicloUrbanaApplication.class, args);
}
}Pots fer aquesta substitució a CicloUrbana ara mateix, arrencar i comprovar que tot funciona igual. És un exercici excel·lent per interioritzar que no hi ha res amagat.
Què aporta cadascuna:
| Anotació | Responsabilitat |
|---|---|
@SpringBootConfiguration |
És un @Configuration especialitzat. Marca la classe com a font de definicions de beans i, a més, fa d'àncora: les proves d'integració (@SpringBootTest, mòdul 6) busquen cap amunt a l'arbre de paquets fins a trobar-la. Només n'hi ha d'haver una per aplicació. |
@EnableAutoConfiguration |
Dispara el mecanisme pel qual Spring Boot registra beans en funció del que troba al classpath. És el motor de "porto spring-boot-starter-web i apareix Tomcat". L'obrirem del tot a la lliçó 02-06. |
@ComponentScan |
Sense atributs, escaneja el paquet de la classe anotada i tots els seus subpaquets. Per això CicloUrbanaApplication viu a com.ciclourbana i no pas a com.ciclourbana.app. |
Els dos excludeFilters que porta de sèrie eviten que l'escaneig reculli classes de prova i classes d'autoconfiguració per accident. No són res que hagis de tocar.
Quan cal desmuntar la meta-anotació
En projectes reals gairebé sempre es deixa @SpringBootApplication. Les excepcions habituals:
// Escanejar un paquet addicional fora de l'arbre de com.ciclourbana
@SpringBootApplication(scanBasePackages = {"com.ciclourbana", "com.ribalta.pagaments"})
public class CicloUrbanaApplication { }
// Excloure una autoconfiguració concreta (ho veurem a 02-06)
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class CicloUrbanaApplication { }
- Els estereotips de Spring
Un estereotip és una anotació que marca una classe com a candidata a bean i indica, a més, quin paper juga a l'arquitectura. Tots deriven de @Component:
flowchart TD
C["@Component<br/>(bean genèric)"] --> S["@Service<br/>lògica de negoci"]
C --> R["@Repository<br/>accés a dades"]
C --> Ctrl["@Controller<br/>capa web MVC"]
Ctrl --> RC["@RestController<br/>= @Controller + @ResponseBody"]
C --> Cfg["@Configuration<br/>fàbrica de beans"]
Comparats en una taula:
| Anotació | Què afegeix sobre @Component |
Quan utilitzar-la a CicloUrbana |
|---|---|---|
@Component |
Res. És la base genèrica. | Peces que no encaixen en cap capa: AvisArrencada, LectorArguments, utilitats. |
@Service |
Res funcional: semàntica. Declara que la classe conté lògica de negoci. | EstacioService, LloguerService: regles de la xarxa de Ribalta. |
@Repository |
Traducció automàtica d'excepcions de persistència (apartat 5). | EstacioRepositoriEnMemoria avui; els repositoris JPA del mòdul 4. |
@Controller |
El detecta el DispatcherServlet com a gestor de peticions; els seus mètodes retornen per defecte noms de vista. |
No l'utilitzarem: CicloUrbana és una API, no pas un web amb plantilles. |
@RestController |
@Controller + @ResponseBody: el retorn de cada mètode se serialitza al cos de la resposta. |
EstacioController i tots els controladors del mòdul 3. |
Un punt que sol desconcertar: @Service no fa res tècnicament. Si substitueixes @Service per @Component a EstacioService, l'aplicació funciona igual. Aleshores, per què utilitzar-lo?
- Comunica la intenció. Qui obre el fitxer sap en quina capa és sense llegir ni una línia de codi.
- Permet filtrar. Eines d'anàlisi, aspectes AOP i filtres de
@ComponentScanpoden actuar sobre "tots els@Service". Amb@Componentarreu del projecte, aquesta distinció es perd. - És la convenció universal de l'ecosistema. Un desenvolupador Spring espera trobar-la.
L'única de la llista que sí que aporta comportament propi és @Repository.
@Repository i la traducció d'excepcions
@Repository i la traducció d'excepcionsCada tecnologia de persistència llança les seves pròpies excepcions: JDBC llança SQLException, JPA llança PersistenceException, Hibernate llança ConstraintViolationException. Si la teva capa de servei captura aquestes excepcions, queda acoblada a la tecnologia: canviar de JDBC a JPA obligaria a reescriure els catch.
Spring ho resol amb una jerarquia pròpia d'excepcions no comprovades que penja de DataAccessException:
DataAccessException ├── DataIntegrityViolationException (violació de restricció, clau duplicada) ├── DuplicateKeyException ├── EmptyResultDataAccessException (s'esperava una fila i no n'hi va haver cap) ├── OptimisticLockingFailureException └── CannotAcquireLockException (temps d'espera de bloqueig exhaurit)
@Repository activa un BeanPostProcessor anomenat PersistenceExceptionTranslationPostProcessor que embolcalla el bean en un proxy. Aquest proxy intercepta les excepcions de la tecnologia subjacent i les tradueix a la jerarquia de Spring:
sequenceDiagram
participant S as EstacioService
participant P as Proxy (@Repository)
participant R as EstacioRepositoriJpa
participant BD as PostgreSQL
S->>P: desar(estacio)
P->>R: desar(estacio)
R->>BD: INSERT ...
BD-->>R: ERROR: unique constraint
R-->>P: PersistenceException (JPA)
P-->>S: DataIntegrityViolationException (Spring)
L'avantatge pràctic: el servei escriu catch (DataIntegrityViolationException e) i aquest codi sobreviu a un canvi de tecnologia de persistència.
// A EstacioService, al mòdul 4. Avui encara no s'aplica.
try {
return estacioRepositori.desar(nova);
} catch (DataIntegrityViolationException e) {
// Val igual si a sota hi ha JDBC, JPA o MongoDB
throw new EstacioDuplicadaException(nova.nom(), e);
}Al nostre repositori en memòria no hi ha excepcions per traduir, però anotar-lo amb @Repository des d'avui deixa el contracte preparat per al mòdul 4 i comunica correctament el seu paper.
@Configuration i @Bean: la via declarativa
@Configuration i @Bean: la via declarativaL'escaneig de components només funciona amb classes que escrius tu. Com registres com a bean una classe d'una llibreria de tercers, el codi font de la qual no pots anotar? Aquí entra @Configuration + @Bean.
package com.ciclourbana.comu;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import java.time.Clock;
import java.time.ZoneId;
/**
* Beans d'infraestructura de CicloUrbana que provenen de llibreries
* externes i que, per tant, no es poden anotar amb @Component.
*/
@Configuration
public class ConfiguracioComuna {
/**
* Rellotge centralitzat. Injectar un Clock en comptes de cridar
* LocalDateTime.now() permet fixar l'hora a les proves (mòdul 6).
* El nom del bean és el del mètode: "rellotgeRibalta".
*/
@Bean
public Clock rellotgeRibalta() {
return Clock.system(ZoneId.of("Europe/Madrid"));
}
/**
* Client HTTP per cridar el servei municipal de trànsit.
* L'utilitzarem de debò al mòdul 7; aquí serveix d'exemple
* de bean construït a mà amb un builder.
*/
@Bean
public RestClient clientTransitRibalta() {
return RestClient.builder()
.baseUrl("https://transit.ribalta.example/api")
.build();
}
}Com funciona: durant l'arrencada, Spring detecta la classe (que també és un @Component per meta-anotació, de manera que la recull l'escaneig), invoca cada mètode @Bean i registra l'objecte retornat al contenidor. El nom del bean és el nom del mètode llevat que n'indiquis un altre amb @Bean("altreNom").
La diferència clau: proxyBeanMethods
Un mètode @Bean pot cridar-ne un altre:
@Configuration
public class ConfiguracioComuna {
@Bean
public Clock rellotgeRibalta() {
return Clock.system(ZoneId.of("Europe/Madrid"));
}
@Bean
public RegistreAuditoria registreAuditoria() {
return new RegistreAuditoria(rellotgeRibalta()); // crea un segon Clock?
}
}En Java pur, rellotgeRibalta() s'executaria un altre cop i hi hauria dos rellotges. Amb @Configuration, no: Spring genera una subclasse proxy CGLIB de la classe de configuració que intercepta les crides als mètodes @Bean i retorna el singleton ja registrat. Aquest comportament s'anomena mode full i és el valor per defecte (proxyBeanMethods = true).
Si els teus mètodes @Bean no es criden mai entre ells, pots desactivar-ho i estalviar la generació del proxy:
@Configuration(proxyBeanMethods = false) // mode "lite": arrencada una mica més ràpida
public class ConfiguracioComuna { }Les classes d'autoconfiguració del mateix Spring Boot utilitzen proxyBeanMethods = false de manera sistemàtica, precisament per això. Ho veurem a la lliçó 02-06.
@Component o @Bean: com decidir
| Criteri | @Component / @Service / @Repository |
@Configuration + @Bean |
|---|---|---|
| Qui escriu la classe | Tu | Tu o un tercer |
| Pots anotar el codi font? | Sí | No cal |
| Construcció | Spring crida el constructor | Tu escrius el new o el builder |
| Lògica en la creació | No hi cap | Sí: condicionals, builders, paràmetres calculats |
| Nom del bean | Nom de classe amb la inicial en minúscula (estacioService) |
Nom del mètode |
| On sol viure | Al costat de la seva funcionalitat | A com.ciclourbana.comu o un altre paquet de configuració |
Regla pràctica per a CicloUrbana: classe pròpia → estereotip; classe aliena o construcció no trivial → @Bean.
@ComponentScan i els seus filtres
@ComponentScan i els seus filtres@ComponentScan sense atributs escaneja el paquet de la classe anotada cap avall. Els seus atributs més útils:
@ComponentScan(
basePackages = {"com.ciclourbana", "com.ribalta.compartit"},
// alternativa amb seguretat de tipus: si mous la classe, el paquet s'ajusta sol
basePackageClasses = {CicloUrbanaApplication.class},
includeFilters = @ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = ServeiXarxa.class),
excludeFilters = @ComponentScan.Filter(
type = FilterType.REGEX,
pattern = "com\\.ciclourbana\\..*\\.llegat\\..*")
)Els tipus de filtre disponibles:
FilterType |
Criteri | Exemple |
|---|---|---|
ANNOTATION |
Presència d'una anotació | classes = ServeiXarxa.class |
ASSIGNABLE_TYPE |
Ser subtipus d'una classe/interfície | classes = CalculadoraTarifa.class |
ASPECTJ |
Expressió AspectJ | pattern = "com.ciclourbana..*Service" |
REGEX |
Expressió regular sobre el nom complet | pattern = ".*RepositoriMock" |
CUSTOM |
Implementació pròpia de TypeFilter |
classes = FiltrePropi.class |
Un ús realista: excloure de l'arrencada normal unes classes de simulació que només volem en desenvolupament.
@SpringBootApplication
@ComponentScan(excludeFilters = @ComponentScan.Filter(
type = FilterType.REGEX,
pattern = "com\\.ciclourbana\\.simulacio\\..*"))
public class CicloUrbanaApplication { }Dit això, per activar o desactivar conjunts de beans segons l'entorn, el mecanisme idiomàtic no és aquest sinó els perfils (@Profile), que s'estudien a la lliçó 07-02. Els filtres d'escaneig són per a casos estructurals, no pas per variar l'entorn.
I un advertiment de rendiment: @ComponentScan("com") o, encara pitjor, un paquet arrel buit, obliga Spring a recórrer tot el classpath, incloses les dependències. L'arrencada es dispara. Mantén sempre l'escaneig acotat al teu paquet base.
- Crear la teva pròpia anotació estereotipada
Com que Spring resol les anotacions de manera transitiva, pots crear estereotips propis que siguin, alhora, @Component. A CicloUrbana marcarem els serveis que formen el nucli de la xarxa (els que gestionen estacions, bicicletes i lloguers) per poder localitzar-los i, més endavant, aplicar-los polítiques transversals.
package com.ciclourbana.comu;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Service;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Estereotip propi: marca un servei del nucli operatiu de la xarxa
* de Ribalta. És un @Service a tots els efectes, més una etiqueta
* semàntica que permet localitzar-los i tractar-los en bloc.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service // <-- la meta-anotació clau
public @interface ServeiXarxa {
/** Àrea funcional a la qual pertany el servei. */
String area() default "general";
/** Permet fixar el nom del bean, igual que @Service. */
@AliasFor(annotation = Service.class, attribute = "value")
String value() default "";
}I ara:
Spring registra EstacioService com a bean exactament igual que si portés @Service, perquè @ServeiXarxa està meta-anotada amb @Service, que al seu torn ho està amb @Component.
Dos detalls tècnics:
@AliasForconnecta un atribut de la teva anotació amb el de l'anotació meta. Sense ell,@ServeiXarxa("nomBean")no tindria cap efecte sobre el nom del bean.- L'atribut
area()no l'interpreta Spring: és teu. El pots llegir per reflexió, o utilitzar-lo com a criteri en un filtre d'escaneig o en un aspecte.
Recuperar tots els beans marcats és trivial:
Map<String, Object> serveisNucli =
context.getBeansWithAnnotation(ServeiXarxa.class);
// {estacioService=..., lloguerService=...}Val la pena? En un projecte petit, no: hi afegeix una indirecció. En una base de codi gran, un estereotip propi ben triat documenta l'arquitectura i dona un punt d'enganxada únic per a mètriques, auditoria o regles d'accés. Utilitza'l amb criteri, no pas per esport.
- Refactoritzant CicloUrbana: servei i repositori
Fins ara el controlador parlava directament amb MagatzemEstacions, un @Component que ho feia tot. És hora de separar responsabilitats correctament, perquè aquesta estructura és la que sostindrà la resta del curs:
flowchart LR
C["EstacioController<br/>@RestController<br/>(HTTP)"] --> S["EstacioService<br/>@Service<br/>(regles de negoci)"]
S --> I["EstacioRepositori<br/>«interfície»"]
I -.implementa.-> R["EstacioRepositoriEnMemoria<br/>@Repository"]
I -.-> J["EstacioRepositoriJpa<br/>(mòdul 4)"]
La interfície del repositori
package com.ciclourbana.estacions;
import java.util.List;
import java.util.Optional;
/**
* Contracte d'accés a dades d'estacions. La interfície permet
* substituir la implementació en memòria per la de JPA (mòdul 4)
* sense tocar EstacioService.
*/
public interface EstacioRepositori {
List<Estacio> cercarTotes();
Optional<Estacio> cercarPerId(Long id);
Estacio desar(Estacio estacio);
long comptar();
}La implementació en memòria
package com.ciclourbana.estacions;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* Implementació provisional en memòria del repositori d'estacions.
* Se substituirà per Spring Data JPA al mòdul 4.
*
* Utilitza ConcurrentHashMap perquè el bean és singleton i diverses
* peticions HTTP concurrents el comparteixen (es raona a fons a la lliçó 02-03).
*/
@Repository
public class EstacioRepositoriEnMemoria implements EstacioRepositori {
private final Map<Long, Estacio> perId = new ConcurrentHashMap<>();
private final AtomicLong seguentId = new AtomicLong(0);
@Override
public List<Estacio> cercarTotes() {
return List.copyOf(perId.values());
}
@Override
public Optional<Estacio> cercarPerId(Long id) {
return Optional.ofNullable(perId.get(id));
}
@Override
public Estacio desar(Estacio estacio) {
Long id = (estacio.id() != null)
? estacio.id()
: seguentId.incrementAndGet();
// El record és immutable: si assignem id, en construïm un de nou
Estacio aDesar = (estacio.id() != null) ? estacio : new Estacio(
id, estacio.nom(), estacio.adreca(),
estacio.capacitat(), estacio.latitud(), estacio.longitud());
perId.put(id, aDesar);
seguentId.updateAndGet(actual -> Math.max(actual, id));
return aDesar;
}
@Override
public long comptar() {
return perId.size();
}
}El servei
package com.ciclourbana.estacions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
/**
* Lògica de negoci de les estacions de la xarxa de Ribalta.
* No coneix HTTP (això és del controlador) ni com s'emmagatzemen
* les dades (això és del repositori).
*/
@Service
public class EstacioService {
private static final Logger log = LoggerFactory.getLogger(EstacioService.class);
private final EstacioRepositori estacioRepositori;
// Injecció per constructor: s'explica a fons a la lliçó 02-02
public EstacioService(EstacioRepositori estacioRepositori) {
this.estacioRepositori = estacioRepositori;
}
public List<Estacio> llistarTotes() {
return estacioRepositori.cercarTotes().stream()
.sorted(Comparator.comparing(Estacio::nom))
.toList();
}
public Optional<Estacio> cercarPerId(Long id) {
return estacioRepositori.cercarPerId(id);
}
public Estacio registrar(Estacio estacio) {
// Regla de negoci de Ribalta: cap estació amb menys de 8 ancoratges
if (estacio.capacitat() < 8) {
throw new IllegalArgumentException(
"Una estació de Ribalta requereix com a mínim 8 ancoratges; rebuts: "
+ estacio.capacitat());
}
Estacio desada = estacioRepositori.desar(estacio);
log.info("Estació registrada: {} ({} ancoratges)",
desada.nom(), desada.capacitat());
return desada;
}
public int capacitatTotalXarxa() {
return estacioRepositori.cercarTotes().stream()
.mapToInt(Estacio::capacitat)
.sum();
}
public long comptar() {
return estacioRepositori.comptar();
}
}El controlador, ara recolzat en el servei
package com.ciclourbana.estacions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/v1/estacions")
public class EstacioController {
private final EstacioService estacioService;
public EstacioController(EstacioService estacioService) {
this.estacioService = estacioService;
}
@GetMapping
public List<Estacio> llistarEstacions() {
return estacioService.llistarTotes();
}
}Fixa't en el repartiment: el controlador només tradueix HTTP, el servei aplica regles i ordena, el repositori desa i recupera. Cada capa depèn de la de sota i cap de la de sobre.
Finalment, el CarregadorEstacionsDemo del mòdul 1 passa a utilitzar el servei i MagatzemEstacions desapareix del projecte:
package com.ciclourbana.estacions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Order(1)
public class CarregadorEstacionsDemo implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(CarregadorEstacionsDemo.class);
private final EstacioService estacioService;
public CarregadorEstacionsDemo(EstacioService estacioService) {
this.estacioService = estacioService;
}
@Override
public void run(String... args) {
log.info("Carregant estacions de demostració de Ribalta...");
estacioService.registrar(new Estacio(1L, "Plaça Major",
"Plaça Major, 1", 24, 40.4168, -3.7038));
estacioService.registrar(new Estacio(2L, "Estació Nord",
"Avinguda Estació 3", 30, 40.4290, -3.7020));
estacioService.registrar(new Estacio(3L, "Parc del Riu",
"Passeig Fluvial 12", 18, 40.4105, -3.6950));
estacioService.registrar(new Estacio(4L, "Universitat",
"Campus Sud, accés B", 36, 40.4402, -3.7255));
log.info("Carregades {} estacions, {} ancoratges en total",
estacioService.comptar(), estacioService.capacitatTotalXarxa());
}
}Recorda ajustar també AvisArrencada a com.ciclourbana.comu perquè injecti EstacioService en lloc del desaparegut MagatzemEstacions.
Comprova que el conjunt continua dret:
I observa que ara les estacions surten ordenades alfabèticament: "Estació Nord", "Parc del Riu", "Plaça Major", "Universitat". Aquesta ordenació és una decisió de negoci, i per això viu al servei i no pas al controlador ni al repositori.
- Mapa d'anotacions del curs
Aquest és el catàleg de les anotacions que aniran apareixent, amb el punt del curs on s'estudien en profunditat. Torna a aquesta taula quan et trobis amb una que no reconeguis.
| Anotació | Per a què serveix | On s'estudia |
|---|---|---|
@SpringBootApplication, @Component, @Service, @Repository, @Configuration, @Bean, @ComponentScan |
Declaració de beans | 02-01 (aquesta lliçó) |
@Autowired, @Qualifier, @Primary, @Order |
Injecció i resolució de dependències | 02-02 |
@Scope, @PostConstruct, @PreDestroy, @Lazy |
Àmbit i cicle de vida | 02-03 |
@Value, @PropertySource |
Configuració per propietats | 02-04 |
@ConfigurationProperties, @EnableConfigurationProperties, @ConfigurationPropertiesScan, @Validated |
Propietats tipades | 02-05 |
@EnableAutoConfiguration, @ConditionalOnClass, @ConditionalOnMissingBean, @ConditionalOnProperty, @AutoConfiguration |
Autoconfiguració i starters | 02-06 |
@RestController, @RequestMapping, @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PathVariable, @RequestParam, @RequestBody, @ResponseStatus |
Capa web REST | Mòdul 3 |
@Valid, @NotBlank, @Min, @Positive, @Email |
Validació d'entrada | 03-04 |
@RestControllerAdvice, @ExceptionHandler |
Gestió global d'errors | 03-06 |
@Entity, @Id, @GeneratedValue, @Column, @OneToMany, @ManyToOne, @Query, @Transactional |
Persistència amb JPA | Mòdul 4 |
@EnableWebSecurity, @PreAuthorize, @PostAuthorize, @Secured |
Seguretat | Mòdul 5 |
@SpringBootTest, @WebMvcTest, @DataJpaTest, @MockitoBean, @Testcontainers |
Proves | Mòdul 6 |
@Profile, @Scheduled, @Async, @EnableScheduling, @EnableAsync |
Perfils, tasques i asincronia | Mòdul 7 |
@Cacheable, @CacheEvict, @Timed |
Rendiment i monitoratge | Mòdul 9 |
Errors Comuns i Consells
La classe amb @Service és fora del paquet base. Si EstacioService acabés a com.altraempresa.serveis, l'escaneig no la veuria i l'arrencada fallaria amb NoSuchBeanDefinitionException. Regla: tot el codi de l'aplicació penja del paquet de CicloUrbanaApplication.
Anotar la interfície en comptes de la implementació. @Repository sobre EstacioRepositori (la interfície) no crea cap bean: Spring necessita una classe que pugui instanciar. L'anotació va a EstacioRepositoriEnMemoria. L'excepció són les interfícies de Spring Data JPA, on el mateix framework genera la implementació (mòdul 4).
Dos @SpringBootApplication al mateix projecte. Sol passar en copiar una classe principal a src/test. Provoca escanejos duplicats o errors a les proves d'integració. N'hi ha d'haver exactament una.
Esperar que @Service faci alguna cosa màgica. No afegeix transaccions, ni memòria cau, ni reintents. Cadascun d'aquests comportaments requereix la seva pròpia anotació (@Transactional, @Cacheable, @Retryable) i la corresponent activació.
Posar @Component a classes de domini. Estacio és una dada, no pas un bean. Els objectes de domini es creen amb new tantes vegades com calgui; els beans són components d'infraestructura, únics i gestionats. Confondre-ho porta a dissenys impossibles de raonar.
Consell: anomena els beans per convenció, no pas a mà. @Service("elMeuServeiDEstacions") no aporta res davant del nom per defecte estacioService. Reserva els noms explícits per quan de debò calgui desambiguar (lliçó 02-02).
Consell: fes servir basePackageClasses en lloc de cadenes. @ComponentScan(basePackageClasses = EstacioService.class) es refactoritza sol si mous el paquet; @ComponentScan("com.ciclourbana.estacions") queda obsolet en silenci.
Consell: @Configuration(proxyBeanMethods = false) si no hi ha crides entre mètodes @Bean. És gratis i estalvia la generació del proxy CGLIB durant l'arrencada.
Exercicis
Exercici 1: substituir @SpringBootApplication pels seus tres components
Reemplaça @SpringBootApplication a CicloUrbanaApplication per les tres anotacions equivalents. Després, comprova amb un CommandLineRunner temporal que el context conté els beans esperats: estacioService, estacioRepositoriEnMemoria i estacioController.
Exercici 2: un bean de llibreria externa amb @Bean
Crea com.ciclourbana.comu.ConfiguracioComuna amb dos beans que no pots anotar perquè són de llibreries: un Clock fixat a Europe/Madrid i un java.util.Random amb llavor fixa (útil per a simulacions reproduïbles). Injecta el Clock en un component nou RellotgeXarxa que exposi un mètode instantActual(), i registra al log l'hora en arrencar.
Exercici 3: estereotip propi amb detecció a l'arrencada
Crea l'anotació @ServeiXarxa de l'apartat 8, aplica-la a EstacioService amb area = "estacions" i escriu un CommandLineRunner anomenat InventariServeisXarxa que llisti per consola tots els beans marcats amb ella, mostrant el nom del bean i la seva àrea.
Solucions
Solució 1
package com.ciclourbana;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@SpringBootConfiguration // = @Configuration especialitzat, àncora de les proves
@EnableAutoConfiguration // activa l'autoconfiguració (lliçó 02-06)
@ComponentScan // escaneja com.ciclourbana i subpaquets
public class CicloUrbanaApplication {
public static void main(String[] args) {
SpringApplication.run(CicloUrbanaApplication.class, args);
}
}I el verificador temporal:
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(100) // després del carregador d'estacions, que és @Order(1)
public class VerificadorBeans implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(VerificadorBeans.class);
private final ApplicationContext context;
public VerificadorBeans(ApplicationContext context) {
this.context = context;
}
@Override
public void run(String... args) {
for (String nom : new String[]{
"estacioService", "estacioRepositoriEnMemoria", "estacioController"}) {
log.info("Bean '{}' present: {} -> {}",
nom,
context.containsBean(nom),
context.containsBean(nom)
? context.getBean(nom).getClass().getSimpleName()
: "N/D");
}
}
}Sortida esperada:
Bean 'estacioService' present: true -> EstacioService
Bean 'estacioRepositoriEnMemoria' present: true -> EstacioRepositoriEnMemoria
Bean 'estacioController' present: true -> EstacioControllerComentari: els noms per defecte es deriven del nom de classe amb la inicial en minúscula. L'aplicació es comporta de manera idèntica, cosa que demostra que @SpringBootApplication no és res més que una drecera.
Solució 2
package com.ciclourbana.comu;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Clock;
import java.time.ZoneId;
import java.util.Random;
@Configuration(proxyBeanMethods = false) // cap mètode @Bean en crida un altre
public class ConfiguracioComuna {
/** Rellotge de la xarxa. Injectar-lo permet congelar el temps a les proves. */
@Bean
public Clock rellotgeRibalta() {
return Clock.system(ZoneId.of("Europe/Madrid"));
}
/** Llavor fixa: les simulacions de demanda són reproduïbles. */
@Bean
public Random aleatoriSimulacio() {
return new Random(42L);
}
}package com.ciclourbana.comu;
import org.springframework.stereotype.Component;
import java.time.Clock;
import java.time.LocalDateTime;
@Component
public class RellotgeXarxa {
private final Clock rellotge;
public RellotgeXarxa(Clock rellotge) {
this.rellotge = rellotge;
}
public LocalDateTime instantActual() {
return LocalDateTime.now(rellotge);
}
}package com.ciclourbana.comu;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class AvisHoraArrencada implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(AvisHoraArrencada.class);
private final RellotgeXarxa rellotgeXarxa;
public AvisHoraArrencada(RellotgeXarxa rellotgeXarxa) {
this.rellotgeXarxa = rellotgeXarxa;
}
@Override
public void run(String... args) {
log.info("CicloUrbana arrenca a les {} (hora de Ribalta)",
rellotgeXarxa.instantActual());
}
}Comentari: Clock i Random són classes del JDK; no les podem anotar, així que l'única via és @Bean. El benefici real del Clock injectat apareixerà al mòdul 6: el podràs substituir per Clock.fixed(...) i provar regles de tarifa dependents de l'hora sense esperar que toquin les 22:00.
Solució 3
package com.ciclourbana.comu;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Service;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service
public @interface ServeiXarxa {
String area() default "general";
@AliasFor(annotation = Service.class, attribute = "value")
String value() default "";
}// A EstacioService, substituint @Service:
@ServeiXarxa(area = "estacions")
public class EstacioService { /* ... */ }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.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@Order(200)
public class InventariServeisXarxa implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(InventariServeisXarxa.class);
private final ApplicationContext context;
public InventariServeisXarxa(ApplicationContext context) {
this.context = context;
}
@Override
public void run(String... args) {
Map<String, Object> serveis = context.getBeansWithAnnotation(ServeiXarxa.class);
log.info("Serveis del nucli de la xarxa: {}", serveis.size());
serveis.forEach((nom, bean) -> {
// findAnnotation travessa proxies i jerarquies; getAnnotation no sempre
ServeiXarxa marca = AnnotationUtils.findAnnotation(
bean.getClass(), ServeiXarxa.class);
log.info(" - {} (àrea: {})", nom,
marca != null ? marca.area() : "desconeguda");
});
}
}Sortida esperada:
Comentari i error freqüent: si utilitzes bean.getClass().getAnnotation(ServeiXarxa.class) i el bean està embolcallat en un proxy (una cosa habitual tan bon punt apareguin @Transactional o @Cacheable), obtindràs null perquè la classe real és la del proxy generat. AnnotationUtils.findAnnotation —o AnnotatedElementUtils per a meta-anotacions— busca al llarg de la jerarquia i és l'opció robusta.
Conclusió
Les anotacions han deixat de ser màgia. Saps que són metadades amb retenció RUNTIME, que Spring les localitza escanejant el classpath amb ASM, que el resultat de l'escaneig són BeanDefinition —receptes, no pas objectes— i que els objectes es construeixen en una segona fase. Has desmuntat @SpringBootApplication en els seus tres components i has comprovat que l'aplicació es comporta igual. Coneixes els cinc estereotips, saps que només @Repository aporta comportament propi (la traducció d'excepcions de persistència) i que els altres valen pel que comuniquen. Saps quan utilitzar un estereotip i quan @Configuration + @Bean, i per què les classes de configuració s'embolcallen en un proxy CGLIB llevat que declaris proxyBeanMethods = false. I has creat el teu propi estereotip recolzant-te en la resolució transitiva de meta-anotacions.
CicloUrbana ha guanyat, a més, la seva primera arquitectura de capes de debò: EstacioController parla HTTP, EstacioService aplica les regles de la xarxa de Ribalta i EstacioRepositoriEnMemoria desa les dades darrere la interfície EstacioRepositori, a punt perquè el mòdul 4 la reemplaci per JPA sense tocar res més amunt.
Queda un serrell evident. Hem escrit public EstacioService(EstacioRepositori estacioRepositori) i Spring ha trobat, tot sol, la implementació correcta. Com? I què hauria passat si hi hagués hagut dues implementacions d'EstacioRepositori al classpath? Aquest és el tema de la lliçó següent, Injecció de Dependències a Spring Boot: els tres tipus d'injecció i per què només un és recomanable, la resolució per tipus, @Qualifier i @Primary, la injecció de col·leccions d'implementacions i què cal fer quan dos beans es necessiten mútuament. Ho aplicarem construint la primera peça del sistema de tarifes de Ribalta.
Curs de Spring Boot
Mòdul 1: Introducció a Spring Boot
- Què és Spring Boot?
- Configuració del teu entorn de desenvolupament
- Creant la teva primera aplicació Spring Boot
- Entenent l'estructura del projecte
- L'arrencada i el cicle de vida de l'aplicació
Mòdul 2: Conceptes bàsics de Spring Boot
- Anotacions de Spring Boot
- Injecció de dependències a Spring Boot
- Àmbit i cicle de vida dels beans
- Configuració de Spring Boot
- Propietats de Spring Boot
- Autoconfiguració i starters per dins
Mòdul 3: Construint serveis web RESTful
- Introducció als serveis web RESTful
- Creant controladors REST
- Gestió dels mètodes HTTP
- Validació de dades d'entrada
- DTOs i mapatge entre capes
- Gestió d'excepcions a REST
- Documentar l'API amb OpenAPI
Mòdul 4: Accés a dades amb Spring Boot
- Introducció a Spring Data JPA
- Configuració de fonts de dades
- Creació d'entitats JPA
- Relacions entre entitats
- Ús de repositoris de Spring Data
- Mètodes de consulta a Spring Data JPA
- Transaccions i gestió de la persistència
- Migracions d'esquema amb Flyway
Mòdul 5: Seguretat a Spring Boot
- Introducció a Spring Security
- Configuració de Spring Security
- Autenticació i autorització d'usuaris
- Implementació d'autenticació JWT
- Seguretat a nivell de mètode i enduriment de l'API
Mòdul 6: Proves a Spring Boot
- Introducció a les proves
- Proves unitàries amb JUnit
- Simulació amb Mockito
- Proves d'integració
- Proves amb Testcontainers
Mòdul 7: Funcions avançades de Spring Boot
- Spring Boot Actuator
- Perfils de Spring Boot
- Tasques programades i execució asíncrona
- Spring Boot amb Docker
- Spring Boot i microserveis
- Comunicació entre serveis i tolerància a fallades
Mòdul 8: Desplegament d'aplicacions Spring Boot
- Introducció al desplegament
- Desplegant a Heroku
- Desplegant a AWS
- Desplegant a Kubernetes
- Integració i lliurament continus
Mòdul 9: Rendiment i monitoratge
- Ajust de rendiment
- Memòria cau amb Spring Cache
- Monitoratge amb Spring Boot Actuator
- Ús de Prometheus i Grafana
- Gestió de registres i logs
- Traçabilitat distribuïda
