Salta el contingut

Guia Completa de Spring Boot en Català

Taula de Continguts

  1. Introducció a Spring Boot
  2. Conceptes Fonamentals de Spring
  3. Configuració i Instal·lació
  4. Estructura d'un Projecte Spring Boot
  5. Controladors i Rutes
  6. Gestió de la Base de Dades
  7. Autenticació amb JWT
  8. Thymeleaf: Motor de Plantilles
  9. Millors Pràctiques

Introducció a Spring Boot

Què és Spring Boot?

[translate:Spring Boot] és un framework de Java que simplifica el desenvolupament d'aplicacions web i de serveis mitjançant la configuració automàtica i la reducció de la boilerplate. Està construït damunt el [translate:Spring Framework] i ofereix una forma ràpida de crear aplicacions autònomes i de producció.

Avantatges de Spring Boot

  • Configuració automàtica: Spring Boot detecta automàticament les dependències del classpath i configura els beans apropiats
  • Inicis ràpids: Permet crear aplicacions des de zero amb mínim esforç
  • Embegut: Inclou servidors com Tomcat, Jetty o Undertow, no necessites desplegar a un servidor extern
  • Gestió simplificada de dependències: Proporciona versions compatibles de les dependències
  • Mètriques i monitoratge: Inclou capacitats de monitoratge integrades
  • Àmplia comunitat: Ampli suport i abundants recursos disponibles

Conceptes Fonamentals de Spring

Injecció de Dependències (Dependency Injection)

La injecció de dependències és un patró de disseny que permet que els objectes rebin les seves dependències de l'exterior en lloc de crearles elles mateixes.

// Sense injecció de dependències
public class CaixaUsuaris {
    private RepositoriUsuaris repositori = new RepositoriUsuaris();
}

// Amb injecció de dependències
public class CaixaUsuaris {
    private final RepositoriUsuaris repositori;

    public CaixaUsuaris(RepositoriUsuaris repositori) {
        this.repositori = repositori;
    }
}

El Contenidor de Spring

El contenidor de Spring (application context) gestiona el cicle de vida dels beans (objectes) de l'aplicació. Els beans es defineixen mitjançant anotacions com @Component, @Service, @Repository, etc.

@Service
public class ServeiUsuaris {
    // Aquest bean serà gestionat per Spring

    public void crearUsuari(Usuari usuari) {
        // lògica
    }
}

Cicle de Vida de Spring Boot

[IMAGE: Cicle de vida de Spring Boot]

El cicle de vida de Spring Boot segueix aquests passos:

  1. Inicialització de l'aplicació - S'executa el mètode main()
  2. Creació del contenidor - Es crea el Spring Application Context
  3. Instanciació de beans - Es creen tots els beans registrats
  4. Injecció de dependències - S'injecten les dependències
  5. Aplicació llesta - L'aplicació està pronta per rebre peticions
  6. Processament de peticions - S'executa la lògica dels controladors

Anotacions Comunes

Anotació Ús
@SpringBootApplication Marca la classe principal de l'aplicació
@Component Marca una classe com a bean gestionat per Spring
@Service Indica que la classe és una classe de servei (capa de negoci)
@Repository Marca la classe com a capa de persistència
@Controller Marca la classe com a controlador (maneig de peticions HTTP)
@RestController Combinació de @Controller i @ResponseBody per a APIs REST
@Autowired Injecta automàticament una dependència
@Bean Defineix un bean que serà gestionat per Spring

Configuració i Instal·lació

Requisits Previs

  • Java Development Kit (JDK) versió 11 o superior
  • Maven o Gradle com a gestor de dependències
  • Un IDE com IntelliJ IDEA, Eclipse o Visual Studio Code
  • Git (opcional, però recomanat)

Crear un Projecte Spring Boot amb Maven

Pots crear un projecte fàcilment mitjançant el site [translate:Spring Initializr]:

  1. Visita https://start.spring.io
  2. Selecciona: - Project: Maven Project - Language: Java - Spring Boot Version: 3.x.x (última versió stable) - Artifact: mi-aplicacio (el nom del teu projecte)
  3. Afegeix les dependències que necessitis (Web, JPA, MySQL, etc.)
  4. Fes clic a "Generate"
  5. Descarrega el fitxer ZIP i descomprimeix-lo

Estructura de pom.xml

<?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
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>mi-aplicacio</artifactId>
    <version>1.0.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>

    <dependencies>
        <!-- Spring Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- JPA i Hibernate -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!-- Base de dades MySQL -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>

        <!-- Thymeleaf -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- Spring Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.12.3</version>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.12.3</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.12.3</version>
            <scope>runtime</scope>
        </dependency>

        <!-- Lombok (opcional, per simplificar codi) -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

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

Fitxer de Configuració application.properties

# Port del servidor
server.port=8080

# Connexió a la base de dades MySQL
spring.datasource.url=jdbc:mysql://localhost:3306/mibdbd
spring.datasource.username=root
spring.datasource.password=contrasenya
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# Configuració de JPA/Hibernate
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# Nivell de log
logging.level.root=INFO
logging.level.com.example=DEBUG

Estructura d'un Projecte Spring Boot

Organització de Paquets

mi-aplicacio/
├── src/main/java/com/example/miapp/
│   ├── MiAplicacioApplication.java       # Classe principal
│   ├── config/                           # Configuracions
│   ├── controller/                       # Controladors (REST, Web)
│   ├── service/                          # Capa de negoci
│   ├── repository/                       # Accés a dades
│   ├── model/                            # Entitats i DTOs
│   ├── security/                         # Configuració de seguretat
│   ├── exception/                        # Excepcions personalitzades
│   └── util/                             # Utilitats
├── src/main/resources/
│   ├── application.properties            # Configuració principal
│   ├── application-dev.properties        # Configuració de desenvolupament
│   ├── application-prod.properties       # Configuració de producció
│   ├── templates/                        # Plantilles Thymeleaf
│   └── static/                           # Recursos estàtics (CSS, JS)
├── pom.xml                               # Configuració Maven
└── README.md                             # Documentació del projecte

[IMAGE: Estructura d'un projecte Spring Boot]

Classe Principal de l'Aplicació

package com.example.miapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MiAplicacioApplication {

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

L'anotació @SpringBootApplication és equivalent a utilitzar: - @Configuration - @EnableAutoConfiguration - @ComponentScan


Controladors i Rutes

Arquitectura de Tres Capes

[IMAGE: Arquitectura de tres capes de Spring Boot]

Spring Boot segueix la arquitectura de tres capes:

  1. Controller - Maneja les peticions HTTP i coordina la comunicació
  2. Service - Conté la lògica de negoci de l'aplicació
  3. Repository - Accedeix a la base de dades

Flux de Peticions HTTP

[IMAGE: Flux de peticions HTTP en Spring Boot]

Una petició HTTP segueix aquest camí:

  1. El client envia una petició HTTP
  2. DispatcherServlet rep la petició i la dirigeix al controlador apropiat
  3. El controlador processa la petició i crida el servei
  4. El servei executa la lògica de negoci i crida el repositori
  5. El repositori accedeix a la base de dades
  6. La resposta torna pels mateixos passos

Controladors REST

Els controladors REST manegen les peticions HTTP i retornen dades en format JSON.

package com.example.miapp.controller;

import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import com.example.miapp.model.Usuari;
import com.example.miapp.service.ServeiUsuaris;
import java.util.List;

@RestController
@RequestMapping("/api/usuaris")
public class ControladorUsuaris {

    private final ServeiUsuaris serveiUsuaris;

    public ControladorUsuaris(ServeiUsuaris serveiUsuaris) {
        this.serveiUsuaris = serveiUsuaris;
    }

    // GET - Obté tots els usuaris
    @GetMapping
    public ResponseEntity<List<Usuari>> obtenirTots() {
        List<Usuari> usuaris = serveiUsuaris.obtenirTots();
        return ResponseEntity.ok(usuaris);
    }

    // GET - Obté un usuari per ID
    @GetMapping("/{id}")
    public ResponseEntity<Usuari> obtenirPerID(@PathVariable Long id) {
        Usuari usuari = serveiUsuaris.obtenirPerID(id);
        return ResponseEntity.ok(usuari);
    }

    // POST - Crea un nousuari
    @PostMapping
    public ResponseEntity<Usuari> crear(@RequestBody Usuari usuari) {
        Usuari usuariCreat = serveiUsuaris.guardar(usuari);
        return ResponseEntity.status(201).body(usuariCreat);
    }

    // PUT - Actualitza un usuari
    @PutMapping("/{id}")
    public ResponseEntity<Usuari> actualitzar(
            @PathVariable Long id,
            @RequestBody Usuari usuari) {
        Usuari usuariActualitzat = serveiUsuaris.actualitzar(id, usuari);
        return ResponseEntity.ok(usuariActualitzat);
    }

    // DELETE - Elimina un usuari
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> eliminar(@PathVariable Long id) {
        serveiUsuaris.eliminar(id);
        return ResponseEntity.noContent().build();
    }
}

Controladors MVC amb Thymeleaf

Per a aplicacions que serveixen HTML:

package com.example.miapp.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.miapp.service.ServeiUsuaris;

@Controller
@RequestMapping("/usuaris")
public class ControladorUsuarisMVC {

    private final ServeiUsuaris serveiUsuaris;

    public ControladorUsuarisMVC(ServeiUsuaris serveiUsuaris) {
        this.serveiUsuaris = serveiUsuaris;
    }

    // Mostra la llista d'usuaris
    @GetMapping
    public String llistaUsuaris(Model model) {
        model.addAttribute("usuaris", serveiUsuaris.obtenirTots());
        return "usuaris/llista";
    }

    // Mostra el formulari de creació
    @GetMapping("/nou")
    public String mostrarFormulariNou(Model model) {
        model.addAttribute("usuari", new Usuari());
        return "usuaris/formulari";
    }
}

Anotacions de Mètodes HTTP

Anotació Mètode HTTP Ús
@GetMapping GET Obtenir recursos
@PostMapping POST Crear nous recursos
@PutMapping PUT Actualitzar recursos complets
@PatchMapping PATCH Actualitzar recursos parcials
@DeleteMapping DELETE Eliminar recursos

Paràmetres Comuns

@GetMapping("/usuaris")
public List<Usuari> cercar(
        @RequestParam String nom,           // Paràmetre de query
        @RequestParam(defaultValue = "0") int pagina,
        @RequestParam(required = false) String mail) {
    // ...
}

@GetMapping("/usuaris/{id}")
public Usuari obtenirPerID(@PathVariable Long id) {  // Paràmetre de ruta
    // ...
}

@PostMapping("/usuaris")
public Usuari crear(@RequestBody Usuari usuari) {   // Body de la petició
    // ...
}

@PostMapping("/usuaris/{id}/avatar")
public void subirAvatar(
        @PathVariable Long id,
        @RequestParam("fitxer") MultipartFile fitxer) {
    // ...
}

Gestió de la Base de Dades

Diagrama de Base de Dades

[IMAGE: Diagrama de base de dades en Spring Boot]

Entitats JPA

Una entitat representa una taula a la base de dades.

package com.example.miapp.model;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "usuaris")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Usuari {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true, length = 100)
    private String mail;

    @Column(nullable = false, length = 150)
    private String nom;

    @Column(nullable = false)
    private String contrasenya;

    @Column(name = "data_creacio")
    private LocalDateTime dataCreacio;

    @Enumerated(EnumType.STRING)
    private Rol rol;

    @Column(columnDefinition = "BOOLEAN DEFAULT true")
    private Boolean actiu;

    @PrePersist
    protected void onCreate() {
        dataCreacio = LocalDateTime.now();
        actiu = true;
    }
}

enum Rol {
    ADMIN, USER, MODERADOR
}

Anotacions Comunes de JPA

Anotació Ús
@Entity Marca la classe com a entitat JPA
@Table Especifica el nom de la taula
@Id Marca el camp com a clau primària
@GeneratedValue Especifica estratègia de generació de IDs
@Column Configura les propietats de la columna
@Enumerated Especifica com persistir enumeracions
@ManyToOne Relació molts-a-un
@OneToMany Relació un-a-molts
@ManyToMany Relació molts-a-molts
@JoinColumn Configura la columna de clau forana
@Transient Exclou el camp de la persistència

Repositoris (Data Access Objects)

Els repositoris extienden JpaRepository i proporcionen mètodes CRUD automàtics.

package com.example.miapp.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import com.example.miapp.model.Usuari;
import java.util.Optional;
import java.util.List;

@Repository
public interface RepositoriUsuaris extends JpaRepository<Usuari, Long> {

    // Mètodes automàtics de Spring Data
    Optional<Usuari> findByMail(String mail);

    List<Usuari> findByNomContainingIgnoreCase(String nom);

    boolean existsByMail(String mail);

    // Consultes personalitzades amb @Query
    @Query("SELECT u FROM Usuari u WHERE u.actiu = true ORDER BY u.nom")
    List<Usuari> obtenirUsuarisActius();

    @Query(value = "SELECT * FROM usuaris WHERE rol = ?1", nativeQuery = true)
    List<Usuari> obtenirUsuarisPer(String rol);
}

Capa de Servei

La capa de servei conté la lògica de negoci:

package com.example.miapp.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.miapp.model.Usuari;
import com.example.miapp.repository.RepositoriUsuaris;
import com.example.miapp.exception.UsuariNoTrobatException;
import java.util.List;

@Service
public class ServeiUsuaris {

    private final RepositoriUsuaris repositori;

    public ServeiUsuaris(RepositoriUsuaris repositori) {
        this.repositori = repositori;
    }

    public List<Usuari> obtenirTots() {
        return repositori.findAll();
    }

    public Usuari obtenirPerID(Long id) {
        return repositori.findById(id)
                .orElseThrow(() -> new UsuariNoTrobatException("ID: " + id));
    }

    public Usuari obtenirPerMail(String mail) {
        return repositori.findByMail(mail)
                .orElseThrow(() -> new UsuariNoTrobatException("Mail: " + mail));
    }

    @Transactional
    public Usuari guardar(Usuari usuari) {
        if (repositori.existsByMail(usuari.getMail())) {
            throw new IllegalArgumentException("El mail ja existeix");
        }
        usuari.setContrasenya(encriptarContrasenya(usuari.getContrasenya()));
        return repositori.save(usuari);
    }

    @Transactional
    public Usuari actualitzar(Long id, Usuari usuari) {
        Usuari usuariExistent = obtenirPerID(id);
        usuariExistent.setNom(usuari.getNom());
        usuariExistent.setRol(usuari.getRol());
        return repositori.save(usuariExistent);
    }

    @Transactional
    public void eliminar(Long id) {
        repositori.deleteById(id);
    }

    public List<Usuari> cercar(String nom) {
        return repositori.findByNomContainingIgnoreCase(nom);
    }

    private String encriptarContrasenya(String contrasenya) {
        // En producció, utilitza BCryptPasswordEncoder
        return contrasenya; // Això és només un exemple
    }
}

Autenticació amb JWT

Flux d'Autenticació

[IMAGE: Flux d'autenticació amb JWT]

Configuració de Spring Security amb JWT

JWT (JSON Web Tokens) és un estàndard per autenticar usuaris en aplicacions web modernes.

Estructura d'un JWT

Un JWT consta de tres parts separades per punts:

header.payload.signature

Exemple:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibm9tIjoiSm9hbiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Configuració de Seguretat

package com.example.miapp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.example.miapp.security.FiltreJWT;

@Configuration
@EnableWebSecurity
public class ConfiguracioSeguretat {

    private final FiltreJWT filtreJWT;
    private final UserDetailsService userDetailsService;

    public ConfiguracioSeguretat(FiltreJWT filtreJWT, UserDetailsService userDetailsService) {
        this.filtreJWT = filtreJWT;
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public PasswordEncoder codificadorContrasenya() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public DaoAuthenticationProvider proveïdorAutenticacio() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(codificadorContrasenya());
        return provider;
    }

    @Bean
    public AuthenticationManager managerAutenticacio(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain cadenaFiltreSeguretat(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/actuator/**").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(filtreJWT, UsernamePasswordAuthenticationFilter.class)
            .authenticationProvider(proveïdorAutenticacio());

        return http.build();
    }
}

Generador de JWT

package com.example.miapp.security;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Map;
import javax.crypto.SecretKey;

@Component
public class GeneradorJWT {

    @Value("${jwt.secret:clau-secreta-super-segura-de-minim-32-caracters}")
    private String clauSecreta;

    @Value("${jwt.expiracio:86400000}") // 24 hores per defecte
    private long tempsExpiracio;

    public String generarToken(String usuari, Map<String, Object> claims) {
        Date ara = new Date();
        Date data_expiracio = new Date(ara.getTime() + tempsExpiracio);

        SecretKey clau = Keys.hmacShaKeyFor(clauSecreta.getBytes());

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(usuari)
                .setIssuedAt(ara)
                .setExpiration(data_expiracio)
                .signWith(clau, SignatureAlgorithm.HS256)
                .compact();
    }

    public String obtenirUsuariDelToken(String token) {
        SecretKey clau = Keys.hmacShaKeyFor(clauSecreta.getBytes());

        return Jwts.parserBuilder()
                .setSigningKey(clau)
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }

    public boolean esTokenValid(String token) {
        try {
            SecretKey clau = Keys.hmacShaKeyFor(clauSecreta.getBytes());

            Jwts.parserBuilder()
                    .setSigningKey(clau)
                    .build()
                    .parseClaimsJws(token);

            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

Filtre JWT

package com.example.miapp.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;

@Component
public class FiltreJWT extends OncePerRequestFilter {

    private final GeneradorJWT generadorJWT;
    private final UserDetailsService userDetailsService;

    public FiltreJWT(GeneradorJWT generadorJWT, UserDetailsService userDetailsService) {
        this.generadorJWT = generadorJWT;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {

        try {
            String token = extraureToken(request);

            if (token != null && generadorJWT.esTokenValid(token)) {
                String usuari = generadorJWT.obtenirUsuariDelToken(token);
                UserDetails userDetails = userDetailsService.loadUserByUsername(usuari);

                UsernamePasswordAuthenticationToken autenticacio =
                        new UsernamePasswordAuthenticationToken(
                                userDetails,
                                null,
                                userDetails.getAuthorities()
                        );

                SecurityContextHolder.getContext().setAuthentication(autenticacio);
            }
        } catch (Exception e) {
            logger.error("No es pot establir autenticació de l'usuari", e);
        }

        filterChain.doFilter(request, response);
    }

    private String extraureToken(HttpServletRequest request) {
        String capçalera = request.getHeader("Authorization");

        if (capçalera != null && capçalera.startsWith("Bearer ")) {
            return capçalera.substring(7);
        }

        return null;
    }
}

Controlador de Autenticació

package com.example.miapp.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.*;
import com.example.miapp.dto.CredencialLogin;
import com.example.miapp.dto.RespostaLogin;
import com.example.miapp.security.GeneradorJWT;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/auth")
public class ControladorAutenticacio {

    private final AuthenticationManager authenticationManager;
    private final GeneradorJWT generadorJWT;

    public ControladorAutenticacio(AuthenticationManager authenticationManager,
                                    GeneradorJWT generadorJWT) {
        this.authenticationManager = authenticationManager;
        this.generadorJWT = generadorJWT;
    }

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody CredencialLogin credencials) {
        try {
            Authentication autenticacio = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            credencials.getMail(),
                            credencials.getContrasenya()
                    )
            );

            Map<String, Object> claims = new HashMap<>();
            claims.put("roles", autenticacio.getAuthorities());

            String token = generadorJWT.generarToken(
                    credencials.getMail(),
                    claims
            );

            return ResponseEntity.ok(new RespostaLogin(token));

        } catch (AuthenticationException e) {
            return ResponseEntity
                    .status(401)
                    .body(Map.of("error", "Mail o contrasenya invàlids"));
        }
    }
}

DTOs per a Autenticació

package com.example.miapp.dto;

import lombok.Data;

@Data
public class CredencialLogin {
    private String mail;
    private String contrasenya;
}
package com.example.miapp.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class RespostaLogin {
    private String token;
}

Thymeleaf: Motor de Plantilles

Flux de Renderització de Thymeleaf

[IMAGE: Flux de renderització de Thymeleaf]

Introducció a Thymeleaf

[translate:Thymeleaf] és un motor de plantilles Java que permet crear HTML dinàmic. Integra bé amb Spring Boot i suporta tant HTML com XML.

Estructura Bàsica d'una Plantilla

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Llista d'Usuaris</title>
    <link rel="stylesheet" href="/css/estil.css" th:href="@{/css/estil.css}">
</head>
<body>
    <h1>Usuaris del Sistema</h1>

    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>Nom</th>
                <th>Mail</th>
                <th>Accions</th>
            </tr>
        </thead>
        <tbody>
            <tr th:each="usuari : ${usuaris}">
                <td th:text="${usuari.id}"></td>
                <td th:text="${usuari.nom}"></td>
                <td th:text="${usuari.mail}"></td>
                <td>
                    <a th:href="@{/usuaris/{id}/editar(id=${usuari.id})}">Editar</a>
                    <a th:href="@{/usuaris/{id}/eliminar(id=${usuari.id})}">Eliminar</a>
                </td>
            </tr>
        </tbody>
    </table>
</body>
</html>

Anotacions Comunes de Thymeleaf

Anotació Ús Exemple
th:text Estableix el text del node <p th:text="${nom}"></p>
th:utext Estableix HTML cru <p th:utext="${html}"></p>
th:if Condicional <p th:if="${esMajor}">Major d'edat</p>
th:unless Negació de condicional <p th:unless="${esMajor}">Menor d'edat</p>
th:each Itera sobre col·leccions <li th:each="item : ${items}" th:text="${item}"></li>
th:switch/th:case Switch-case <div th:switch="${rol}"><p th:case="ADMIN">Administrador</p></div>
th:object Selecciona objecte <form th:object="${usuari}">
th:field Vincula propietats <input th:field="*{nom}">
th:action URL d'acció de formulari <form th:action="@{/usuaris/guardar}">
th:href URL de link <a th:href="@{/usuaris/{id}(id=${usuari.id})}">Veure</a>
th:src Font d'imatge <img th:src="@{/imatges/{nom}(nom=${imatge})}">
th:class Classes CSS dinàmiques <div th:class="${actiu} ? 'actiu' : 'inactiu'"></div>
th:style Estils inline dinàmics <div th:style="'color: ' + ${color}">
th:attr Estableix atributs genèrics <div th:attr="data-id=${usuari.id}">

Exemple: Formulari de Creació

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Crear Nou Usuari</title>
</head>
<body>
    <h1>Crear Nou Usuari</h1>

    <form th:action="@{/usuaris/guardar}" th:object="${usuari}" method="post">

        <div>
            <label for="nom">Nom:</label>
            <input type="text" id="nom" th:field="*{nom}" required>
            <span th:if="${#fields.hasErrors('nom')}" th:errors="*{nom}"></span>
        </div>

        <div>
            <label for="mail">Mail:</label>
            <input type="email" id="mail" th:field="*{mail}" required>
            <span th:if="${#fields.hasErrors('mail')}" th:errors="*{mail}"></span>
        </div>

        <div>
            <label for="contrasenya">Contrasenya:</label>
            <input type="password" id="contrasenya" th:field="*{contrasenya}" required>
            <span th:if="${#fields.hasErrors('contrasenya')}" th:errors="*{contrasenya}"></span>
        </div>

        <div>
            <label for="rol">Rol:</label>
            <select id="rol" th:field="*{rol}">
                <option value="">-- Selecciona un rol --</option>
                <option value="USER">Usuari</option>
                <option value="ADMIN">Administrador</option>
                <option value="MODERADOR">Moderador</option>
            </select>
        </div>

        <button type="submit">Guardar</button>
        <a href="/usuaris">Cancelar</a>
    </form>
</body>
</html>

Funcions d'Utilitat de Thymeleaf

<!-- Iteració amb índex -->
<div th:each="usuari, iter : ${usuaris}">
    <p>Posició: <span th:text="${iter.index}"></span></p>
    <p>Comptador: <span th:text="${iter.count}"></span></p>
    <p>És parell: <span th:text="${iter.even}"></span></p>
    <p>És senar: <span th:text="${iter.odd}"></span></p>
    <p>Nom: <span th:text="${usuari.nom}"></span></p>
</div>

<!-- Operacions de string -->
<p th:text="${#strings.toUpperCase(nom)}"></p>
<p th:text="${#strings.toLowerCase(nom)}"></p>
<p th:text="${#strings.length(nom)}"></p>
<p th:text="${#strings.substring(nom, 0, 3)}"></p>
<p th:text="${#strings.contains(nom, 'a')}"></p>
<p th:text="${#strings.startsWith(nom, 'Jo')}"></p>

<!-- Operacions de dates -->
<p th:text="${#dates.format(dataCreacio, 'dd/MM/yyyy')}"></p>
<p th:text="${#dates.format(dataCreacio, 'dd/MM/yyyy HH:mm:ss')}"></p>
<p th:text="${#dates.year(dataCreacio)}"></p>

<!-- Operacions de llistes -->
<p th:text="${#lists.size(usuaris)}"></p>
<p th:text="${#lists.isEmpty(usuaris)}"></p>
<p th:text="${#lists.contains(usuaris, usuari)}"></p>

Millors Pràctiques

Gestió d'Excepcions Personalitzades

[IMAGE: Procés de validació de dades]

package com.example.miapp.exception;

public class UsuariNoTrobatException extends RuntimeException {
    public UsuariNoTrobatException(String message) {
        super(message);
    }
}
package com.example.miapp.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.example.miapp.exception.UsuariNoTrobatException;
import java.util.Map;

@RestControllerAdvice
public class ManejadorExcepcions {

    @ExceptionHandler(UsuariNoTrobatException.class)
    public ResponseEntity<?> manejadorUsuariNoTrobat(UsuariNoTrobatException e) {
        return ResponseEntity
                .status(404)
                .body(Map.of("error", e.getMessage()));
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<?> manejadorArgumentInvalid(IllegalArgumentException e) {
        return ResponseEntity
                .status(400)
                .body(Map.of("error", e.getMessage()));
    }
}

Validació de Dades

package com.example.miapp.model;

import jakarta.validation.constraints.*;

public class UsuariDTO {

    @NotBlank(message = "El nom no pot estar buit")
    @Size(min = 2, max = 100, message = "El nom ha de tindre entre 2 i 100 caràcters")
    private String nom;

    @Email(message = "El format del mail és invàlid")
    @NotBlank(message = "El mail no pot estar buit")
    private String mail;

    @NotBlank(message = "La contrasenya no pot estar buida")
    @Size(min = 8, message = "La contrasenya ha de tindre almenys 8 caràcters")
    private String contrasenya;

    @NotNull(message = "El rol és obligatori")
    private Rol rol;
}
@PostMapping
public ResponseEntity<Usuari> crear(@Valid @RequestBody UsuariDTO usuarioDTO) {
    Usuari usuari = convertirDTO(usuarioDTO);
    Usuari usuariCreat = serveiUsuaris.guardar(usuari);
    return ResponseEntity.status(201).body(usuariCreat);
}

Logging Adequat

package com.example.miapp.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class ServeiUsuaris {

    private static final Logger logger = LoggerFactory.getLogger(ServeiUsuaris.class);

    public Usuari guardar(Usuari usuari) {
        logger.info("Intentant guardar usuari amb mail: {}", usuari.getMail());

        try {
            Usuari usuariGuardat = repositori.save(usuari);
            logger.info("Usuari guardat correctament amb ID: {}", usuariGuardat.getId());
            return usuariGuardat;
        } catch (Exception e) {
            logger.error("Error al guardar l'usuari: {}", usuari.getMail(), e);
            throw e;
        }
    }
}

Configuració per Entorns

application-dev.properties:

spring.datasource.url=jdbc:mysql://localhost:3306/mibd_dev
spring.jpa.hibernate.ddl-auto=create-drop
logging.level.root=DEBUG

application-prod.properties:

spring.datasource.url=jdbc:mysql://localhost:3306/mibd_prod
spring.jpa.hibernate.ddl-auto=validate
logging.level.root=INFO
server.ssl.enabled=true

Iniciar amb un perfil específic:

java -jar aplicacio.jar --spring.profiles.active=prod

DTOs per a Transferència de Dades

Els DTOs (Data Transfer Objects) aïllen la teva API dels canvis en les entitats:

package com.example.miapp.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UsuariDTO {
    private Long id;
    private String nom;
    private String mail;
    private String rol;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UsuariCreacioDTO {
    private String nom;
    private String mail;
    private String contrasenya;
    private String rol;
}

Mapeig de DTOs amb MapStruct

Afegeix la dependència:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version>
</dependency>

Crea el mapeador:

package com.example.miapp.mapper;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import com.example.miapp.model.Usuari;
import com.example.miapp.dto.UsuariDTO;

@Mapper(componentModel = "spring")
public interface MapejadorUsuari {

    @Mapping(target = "contrasenya", ignore = true)
    UsuariDTO toDTO(Usuari usuari);

    @Mapping(target = "id", ignore = true)
    @Mapping(target = "dataCreacio", ignore = true)
    Usuari toEntity(UsuariDTO usuariDTO);
}

Utilitza-ho al servei:

@Service
public class ServeiUsuaris {
    private final MapejadorUsuari mapejador;

    public UsuariDTO obtenirDTOPerID(Long id) {
        Usuari usuari = repositori.findById(id)
                .orElseThrow(() -> new UsuariNoTrobatException("ID: " + id));
        return mapejador.toDTO(usuari);
    }
}

Proves Unitàries

package com.example.miapp.service;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import com.example.miapp.model.Usuari;
import com.example.miapp.repository.RepositoriUsuaris;

@DisplayName("Proves del Servei d'Usuaris")
class TestServeiUsuaris {

    @Mock
    private RepositoriUsuaris repositori;

    @InjectMocks
    private ServeiUsuaris servei;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    @DisplayName("Guardar un usuari correctament")
    void testGuardarUsuari() {
        // Arrange (Preparació)
        Usuari usuari = new Usuari();
        usuari.setNom("Joan Doe");
        usuari.setMail("joan@example.com");

        when(repositori.save(usuari)).thenReturn(usuari);

        // Act (Acció)
        Usuari usuariGuardat = servei.guardar(usuari);

        // Assert (Verificació)
        assertNotNull(usuariGuardat);
        assertEquals("Joan Doe", usuariGuardat.getNom());
        verify(repositori, times(1)).save(usuari);
    }
}

Documentació amb Swagger/SpringDoc

Afegeix la dependència:

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.0.4</version>
</dependency>

Anotacions de Swagger:

@RestController
@RequestMapping("/api/usuaris")
@Tag(name = "Usuaris", description = "API de gestió d'usuaris")
public class ControladorUsuaris {

    @GetMapping("/{id}")
    @Operation(
        summary = "Obtenir usuari per ID",
        description = "Retorna un usuari específic pel seu identificador"
    )
    @ApiResponse(
        responseCode = "200",
        description = "Usuari trobat correctament"
    )
    @ApiResponse(
        responseCode = "404",
        description = "Usuari no trobat"
    )
    public ResponseEntity<Usuari> obtenirPerID(@PathVariable Long id) {
        // ...
    }
}

Accedeix a la documentació a: http://localhost:8080/swagger-ui.html

Configuració CORS

@Configuration
public class ConfiguracióCORS {

    @Bean
    public WebMvcConfigurer configuradorCORS() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**")
                    .allowedOrigins("http://localhost:3000", "https://exemple.com")
                    .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
                    .allowedHeaders("*")
                    .allowCredentials(true)
                    .maxAge(3600);
            }
        };
    }
}

Configuració de Properties de l'Aplicació

application.properties personalitzada:

# Configuració personalitzada
app.nom=Aplicació d'Usuaris
app.versio=1.0.0
app.descripció=Gestió centralitzada d'usuaris

# JWT
jwt.secret=clau-molt-secreta-amb-molts-caracters-aleatoris-1234567890
jwt.expiracio=86400000

# Email (opcional)
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=${MAIL_USERNAME}
spring.mail.password=${MAIL_PASSWORD}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true


Resum de Conceptes Clau

Spring Boot és un framework potent que simplifica el desenvolupament d'aplicacions web en Java. Els conceptes clau a retenir són:

Arquitectura: Segueix l'arquitectura de tres capes (controlador-servei-repositori) per mantenir la separació de responsabilitats.

Autenticació i Seguretat: Utilitza Spring Security amb JWT per a una seguretat robusta i stateless.

Persistència de Dades: JPA i Hibernate permeten mappeig objecte-relacional amb poc codi.

Plantilles: Thymeleaf proporciona una forma neta i intuïtiva de generar HTML dinàmic.

Buones Pràctiques: Sempre valida dades, maneja excepcions correctament, logueja adequadament i escriu proves unitàries.

Escalabilitat: Configura els perfils d'entorn per a desenvolupament i producció.


Recursos Addicionals

Per a més informació i exemples avançats, consulta:


Document actualitzat amb diagrames el 23 de novembre de 2025