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

  1. Què és una anotació a Java
  2. Com processa Spring les anotacions en arrencar
  3. @SpringBootApplication desmuntada
  4. Els estereotips de Spring
  5. @Repository i la traducció d'excepcions
  6. @Configuration i @Bean: la via declarativa
  7. @ComponentScan i els seus filtres
  8. Crear la teva pròpia anotació estereotipada
  9. Refactoritzant CicloUrbana: servei i repositori
  10. Mapa d'anotacions del curs
  11. Errors Comuns i Consells
  12. Exercicis

  1. 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:

@Provisional(fins = "modul-4")
public class EstacioRepositoriEnMemoria { /* ... */ }

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.

  1. 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.

  1. @SpringBootApplication desmuntada

La 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 { }

  1. 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 @ComponentScan poden actuar sobre "tots els @Service". Amb @Component arreu 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.

  1. @Repository i la traducció d'excepcions

Cada 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.

  1. @Configuration i @Bean: la via declarativa

L'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.

  1. @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.

  1. 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:

@ServeiXarxa(area = "estacions")
public class EstacioService { /* ... */ }

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:

  • @AliasFor connecta 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.

  1. 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:

./mvnw spring-boot:run
curl -s http://localhost:8080/api/v1/estacions | head -5

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.

  1. 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 -> EstacioController

Comentari: 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:

Serveis del nucli de la xarxa: 1
  - estacioService (àrea: estacions)

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

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

Mòdul 3: Construint serveis web RESTful

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

Mòdul 5: Seguretat a Spring Boot

Mòdul 6: Proves a Spring Boot

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

Mòdul 8: Desplegament d'aplicacions Spring Boot

Mòdul 9: Rendiment i monitoratge

Mòdul 10: Millors pràctiques i consells

© Copyright 2026. Tots els drets reservats