Guia Completa de Spring Boot en Català¶
Taula de Continguts¶
- Introducció a Spring Boot
- Conceptes Fonamentals de Spring
- Configuració i Instal·lació
- Estructura d'un Projecte Spring Boot
- Controladors i Rutes
- Gestió de la Base de Dades
- Autenticació amb JWT
- Thymeleaf: Motor de Plantilles
- 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:
- Inicialització de l'aplicació - S'executa el mètode main()
- Creació del contenidor - Es crea el Spring Application Context
- Instanciació de beans - Es creen tots els beans registrats
- Injecció de dependències - S'injecten les dependències
- Aplicació llesta - L'aplicació està pronta per rebre peticions
- 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]:
- Visita https://start.spring.io
- Selecciona:
- Project: Maven Project
- Language: Java
- Spring Boot Version: 3.x.x (última versió stable)
- Artifact:
mi-aplicacio(el nom del teu projecte) - Afegeix les dependències que necessitis (Web, JPA, MySQL, etc.)
- Fes clic a "Generate"
- 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:
- Controller - Maneja les peticions HTTP i coordina la comunicació
- Service - Conté la lògica de negoci de l'aplicació
- 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í:
- El client envia una petició HTTP
- DispatcherServlet rep la petició i la dirigeix al controlador apropiat
- El controlador processa la petició i crida el servei
- El servei executa la lògica de negoci i crida el repositori
- El repositori accedeix a la base de dades
- 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:
- Documentació oficial de Spring Boot: https://spring.io/projects/spring-boot
- Documentació de Spring Data JPA: https://spring.io/projects/spring-data-jpa
- Documentació de Thymeleaf: https://www.thymeleaf.org/
- Documentació de Spring Security: https://spring.io/projects/spring-security
- JWT.io: https://jwt.io/
Document actualitzat amb diagrames el 23 de novembre de 2025