Spring Boot - Guia de Projecte Completa¶
📋 Taula de Continguts¶
- Descripció General
- Requisits i Instal·lació
- Estructura del Projecte
- Configuració Inicial
- Desenvolupament
- Estructura de Capes
- API i Documentació
- Testing
- Desplegament
- Millors Pràctiques
📚 Descripció General¶
Spring Boot és un framework per a la creació d'aplicacions Java autònomes, de producció i basades en Spring que s'executen amb la mínima configuració. Aquesta guia proporciona les millors pràctiques per estructurar, desenvolupar i documentar un projecte Spring Boot de manera professional.
Comparació amb Angular¶
| Aspecte | Angular | Spring Boot |
|---|---|---|
| Tipus | Framework Frontend | Framework Backend |
| Estructura | Modular per features | Capes o features |
| Dependències | npm packages | Maven/Gradle dependencies |
| Configuració | angular.json | application.yml/properties |
| Server | Dev server (ng serve) | Tomcat integrat |
| Build | ng build | mvn package / gradle build |
🛠️ Requisits i Instal·lació¶
Requisits Mínims¶
- Java: JDK 17 o superior (recomanat JDK 21)
- Maven: 3.8.1+ o Gradle: 8.0+
- Git: Per control de versions
- IDE: IntelliJ IDEA, Eclipse o Visual Studio Code
Instal·lació Inicial¶
Crear un Projecte amb Spring Initializr¶
# Opció 1: Usar Spring Boot CLI
spring boot new --from web --name my-app --type maven
# Opció 2: Descarregar de https://start.spring.io amb les dependencies desitjades
# Opció 3: Usar l'IDE (New Project → Spring Boot)
Dependencies Bàsiques Recomanades¶
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.0.2</version>
</dependency>
📁 Estructura del Projecte¶
Estructura Estàndard Maven/Spring Boot¶
my-app/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── myapp/
│ │ │ ├── MyAppApplication.java
│ │ │ ├── config/
│ │ │ ├── controller/
│ │ │ ├── service/
│ │ │ ├── repository/
│ │ │ ├── entity/
│ │ │ ├── dto/
│ │ │ ├── exception/
│ │ │ └── util/
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── application-dev.yml
│ │ ├── application-prod.yml
│ │ └── static/
│ └── test/
│ ├── java/
│ │ └── com/example/myapp/
│ │ ├── controller/
│ │ ├── service/
│ │ └── integration/
│ └── resources/
│ └── application-test.yml
├── pom.xml
├── README.md
├── .gitignore
└── docker-compose.yml
Estructura Alternativa per Features (Recomanada per Projectes Grans)¶
my-app/
├── src/main/java/com/example/myapp/
│ ├── MyAppApplication.java
│ ├── common/
│ │ ├── config/
│ │ ├── exception/
│ │ ├── util/
│ │ └── dto/
│ ├── user/
│ │ ├── UserController.java
│ │ ├── UserService.java
│ │ ├── UserRepository.java
│ │ ├── User.java
│ │ └── UserDTO.java
│ ├── order/
│ │ ├── OrderController.java
│ │ ├── OrderService.java
│ │ ├── OrderRepository.java
│ │ ├── Order.java
│ │ └── OrderDTO.java
│ └── product/
│ ├── ProductController.java
│ ├── ProductService.java
│ ├── ProductRepository.java
│ ├── Product.java
│ └── ProductDTO.java
⚙️ Configuració Inicial¶
Fitxer application.yml¶
spring:
application:
name: my-app
# Base de Dades
datasource:
url: jdbc:mysql://localhost:3306/myapp_db
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
# Configuració de Servidor
servlet:
context-path: /api
# Configuració de Logging
logging:
level:
root: INFO
cat.xaviersastre.daw.dwes.codisapunts: DEBUG
# Configuració del Servidor
server:
port: 8080
servlet:
context-path: /api
# Swagger/OpenAPI
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
enabled: true
Configuracions per Entorn¶
# application-dev.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/myapp_dev
jpa:
show-sql: true
# application-prod.yml
spring:
datasource:
url: jdbc:mysql://prod-db:3306/myapp_prod
jpa:
show-sql: false
💻 Desenvolupament¶
Classe Principal¶
package cat.xaviersastre.daw.dwes.codisapunts;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyAppApplication {
public static void main(String[] args) {
SpringApplication.run(MyAppApplication.class, args);
}
}
Exemples de Capes¶
Entity (Model de Dades)¶
package cat.xaviersastre.daw.dwes.codisapunts.user;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String firstName;
@Column(nullable = false)
private String lastName;
}
DTO (Data Transfer Object)¶
package cat.xaviersastre.daw.dwes.codisapunts.user;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
private Long id;
@NotBlank(message = "Email és obligatori")
@Email(message = "Email ha de ser vàlid")
private String email;
@NotBlank(message = "Nom és obligatori")
private String firstName;
@NotBlank(message = "Cognom és obligatori")
private String lastName;
}
Repository¶
package cat.xaviersastre.daw.dwes.codisapunts.user;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
Service¶
package cat.xaviersastre.daw.dwes.codisapunts.user;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class UserService {
private final UserRepository userRepository;
public UserDTO createUser(UserDTO userDTO) {
log.info("Creating user with email: {}", userDTO.getEmail());
User user = new User();
user.setEmail(userDTO.getEmail());
user.setFirstName(userDTO.getFirstName());
user.setLastName(userDTO.getLastName());
User savedUser = userRepository.save(user);
return mapToDTO(savedUser);
}
public UserDTO getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
return mapToDTO(user);
}
public List<UserDTO> getAllUsers() {
return userRepository.findAll()
.stream()
.map(this::mapToDTO)
.toList();
}
private UserDTO mapToDTO(User user) {
return new UserDTO(user.getId(), user.getEmail(),
user.getFirstName(), user.getLastName());
}
}
Controller¶
package cat.xaviersastre.daw.dwes.codisapunts.user;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity<UserDTO> createUser(@Valid @RequestBody UserDTO userDTO) {
UserDTO created = userService.createUser(userDTO);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
@GetMapping
public ResponseEntity<List<UserDTO>> getAllUsers() {
return ResponseEntity.ok(userService.getAllUsers());
}
}
Configuration Class¶
package cat.xaviersastre.daw.dwes.codisapunts.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:4200")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true);
}
}
Global Exception Handler¶
package cat.xaviersastre.daw.dwes.codisapunts.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity<ErrorResponse> handleNotFoundException(RuntimeException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
🏗️ Estructura de Capes¶
Arquitectura en Capes¶
┌─────────────────────────────────────┐
│ REST Controller (API) │
├─────────────────────────────────────┤
│ Service Layer │
│ (Lògica de negoci) │
├─────────────────────────────────────┤
│ Repository Layer │
│ (Accés a dades) │
├─────────────────────────────────────┤
│ Entity/Model Layer │
│ (Mappeig de BD) │
└─────────────────────────────────────┘
Responsabilitats de Cada Capa¶
| Capa | Responsabilitat | Exemple |
|---|---|---|
| Controller | Gestionar HTTP requests | Validar input, cridar service |
| Service | Lògica de negoci | Processar dades, transaccions |
| Repository | Accés a dades | Consultes a BD |
| Entity | Mappeig a BD | Taules, relacions |
📖 API i Documentació¶
Swagger/OpenAPI Setup¶
// Dependency ja inclòs a pom.xml
// springdoc-openapi-starter-webmvc-ui
// Accés automàtic a:
// http://localhost:8080/api/swagger-ui.html
// http://localhost:8080/api/v3/api-docs
Documentar Endpoints amb Annotations¶
package cat.xaviersastre.daw.dwes.codisapunts.user;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/users")
@Tag(name = "Users", description = "API de Gestió d'Usuaris")
public class UserController {
@PostMapping
@Operation(
summary = "Crear usuari",
description = "Crea un nou usuari al sistema"
)
@ApiResponse(responseCode = "201", description = "Usuari creat")
@ApiResponse(responseCode = "400", description = "Validació fallada")
public ResponseEntity<UserDTO> createUser(@RequestBody UserDTO userDTO) {
// ...
}
@GetMapping("/{id}")
@Operation(summary = "Obtenir usuari per ID")
@ApiResponse(responseCode = "200", description = "Usuari trobat")
@ApiResponse(responseCode = "404", description = "Usuari no trobat")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
// ...
}
}
README.md - Documentació del Projecte¶
# My Spring Boot Application
## Descripció
Aplicació REST per gestionar usuaris i comandes.
## Tecnologies
- Spring Boot 3.x
- MySQL 8.0
- Maven 3.8+
- Docker
## Setup Local
### Requisits
- JDK 17+
- MySQL 8.0+
- Maven 3.8+
### Instal·lació
1. Clonar el repositori
\`\`\`bash
git clone https://github.com/example/my-app.git
cd my-app
\`\`\`
2. Configurar BD
\`\`\`bash
mysql -u root -p < database/schema.sql
\`\`\`
3. Configurar application.yml
\`\`\`yaml
spring.datasource.url: jdbc:mysql://localhost:3306/myapp_db
spring.datasource.username: root
spring.datasource.password: your_password
\`\`\`
4. Executar l'aplicació
\`\`\`bash
mvn spring-boot:run
\`\`\`
L'aplicació estarà disponible a `http://localhost:8080/api`
## API Endpoints
### Users
- POST /api/v1/users - Crear usuari
- GET /api/v1/users/{id} - Obtenir usuari
- GET /api/v1/users - Llistar usuaris
- PUT /api/v1/users/{id} - Actualitzar usuari
- DELETE /api/v1/users/{id} - Eliminar usuari
## Documentació API
Accedeix a la documentació Swagger en: http://localhost:8080/api/swagger-ui.html
## Docker
### Build i Run
\`\`\`bash
docker-compose up -d
\`\`\`
## Testing
\`\`\`bash
mvn test
\`\`\`
## Contribució
Si vols contribuir, siusplau fes un fork i envía un pull request.
🧪 Testing¶
Unit Testing¶
package cat.xaviersastre.daw.dwes.codisapunts.user;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
private User testUser;
private UserDTO testUserDTO;
@BeforeEach
void setUp() {
testUser = new User(1L, "test@example.com", "John", "Doe");
testUserDTO = new UserDTO(1L, "test@example.com", "John", "Doe");
}
@Test
void testCreateUser() {
when(userRepository.save(any(User.class))).thenReturn(testUser);
UserDTO result = userService.createUser(testUserDTO);
assertNotNull(result);
assertEquals("test@example.com", result.getEmail());
verify(userRepository, times(1)).save(any(User.class));
}
@Test
void testGetUserById() {
when(userRepository.findById(1L)).thenReturn(java.util.Optional.of(testUser));
UserDTO result = userService.getUserById(1L);
assertNotNull(result);
assertEquals(1L, result.getId());
}
}
Integration Testing¶
package cat.xaviersastre.daw.dwes.codisapunts.user;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void testCreateUserEndpoint() throws Exception {
String userJson = """
{
"email": "test@example.com",
"firstName": "John",
"lastName": "Doe"
}
""";
mockMvc.perform(post("/api/v1/users")
.contentType("application/json")
.content(userJson))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.email").value("test@example.com"));
}
}
🚀 Desplegament¶
Build per Producció¶
# Maven
mvn clean package -DskipTests
# Resultat: target/my-app-1.0.0.jar
Docker Setup¶
# Dockerfile
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY target/my-app-1.0.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/myapp_db
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: password
depends_on:
- db
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: myapp_db
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:
Executar amb Docker¶
docker-compose up -d
docker-compose logs -f app
✅ Millors Pràctiques¶
1. Estructura de Codi¶
- ✅ Organitza per features o per capes de forma consistent
- ✅ Utilitza paquet de root amb @SpringBootApplication
- ✅ Separa controllers, services i repositories
2. Dependències¶
- ✅ Usa constructors amb @RequiredArgsConstructor (Lombok)
- ✅ Evita injecció de dependències en setters
- ✅ Usa interfícies per services quan sigui possible
3. Logging¶
- ✅ Utilitza Slf4j amb Lombok @Slf4j
- ✅ Registra nivells apropats (INFO, DEBUG, ERROR)
- ✅ No utilitzis System.out.print()
@Slf4j
public class UserService {
public void processUser(Long userId) {
log.info("Processing user: {}", userId);
try {
// ...
} catch (Exception e) {
log.error("Error processing user: {}", userId, e);
}
}
}
4. Validació¶
- ✅ Utilitza Bean Validation (@Valid, @NotNull, etc.)
- ✅ Valida a nivel de DTO
- ✅ Retorna errors apropats (400 Bad Request)
5. DTOs vs Entities¶
- ✅ Usa DTOs per API responses
- ✅ No exposis entitats directament
- ✅ Mapeja dades correctament
6. Transaccions¶
- ✅ Marca serveis amb @Transactional
- ✅ Utilitza readOnly = true quan apropat
- ✅ Maneja excepcions correctament
@Service
@RequiredArgsConstructor
public class OrderService {
@Transactional
public OrderDTO createOrder(OrderDTO dto) {
// Lògica de negoci
}
@Transactional(readOnly = true)
public OrderDTO getOrder(Long id) {
// Lectura
}
}
7. Configuració per Entorn¶
- ✅ Utilitza application-{profile}.yml
- ✅ Externalitza secrets (vars d'entorn)
- ✅ Utilitza @Profile si necessari
8. Documentació¶
- ✅ Documenta endpoints amb Swagger
- ✅ Manté README.md actualitzat
- ✅ Comenta codi complex
9. Security¶
- ✅ Hash passwords (BCrypt)
- ✅ Valida input
- ✅ Utilitza HTTPS en producció
- ✅ Implementa JWT per autenticació si necessari
10. Performance¶
- ✅ Utilitza pagination
- ✅ Implementa caching on apropat
- ✅ Evita N+1 queries (eager loading)
- ✅ Indexa taules de BD
@Service
public class UserService {
@Transactional(readOnly = true)
public Page<UserDTO> getAllUsers(Pageable pageable) {
return userRepository.findAll(pageable)
.map(this::mapToDTO);
}
}
11. Naming Conventions¶
- Classes:
PascalCase(UserController, UserService) - Mètodes/variables:
camelCase(getUserById, firstName) - Constants:
SCREAMING_SNAKE_CASE(MAX_RETRY_ATTEMPTS) - BD fields:
snake_case(first_name, created_at)
12. Error Handling¶
- ✅ Centralitza exceptions amb @ControllerAdvice
- ✅ Retorna HTTP status apropats
- ✅ Proporciona missatges d'error útils
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(EntityNotFoundException ex) {
return new ErrorResponse(404, ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
return new ErrorResponse(400, "Validació fallada");
}
}
📚 Recursos Addicionals¶
- Spring Boot Official Docs
- Spring Data JPA Guide
- Swagger/OpenAPI Docs
- Lombok Project
- Maven Repository
🤝 Contribució i Suport¶
Per preguntes o suggeriments, contacta l'equip de desenvolupament o crea un issue al repositori.
Última actualització: November 2025