Salta el contingut

Spring Boot - Guia de Projecte Completa

📋 Taula de Continguts

  1. Descripció General
  2. Requisits i Instal·lació
  3. Estructura del Projecte
  4. Configuració Inicial
  5. Desenvolupament
  6. Estructura de Capes
  7. API i Documentació
  8. Testing
  9. Desplegament
  10. 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


🤝 Contribució i Suport

Per preguntes o suggeriments, contacta l'equip de desenvolupament o crea un issue al repositori.


Última actualització: November 2025