· Jose Antonio López · 4 min lectura
Com crear validadors personalitzats amb Hibernate i Spring Boot
Aprèn a implementar validacions personalitzades en Java amb Hibernate Validator, Bean Validation i Spring Boot.

Objectiu
La validació de dades és una part crítica de qualsevol aplicació. Tot i que Java Bean Validation (JSR 303/JSR 380) proporciona anotacions útils com @NotNull, @Size o @Email, sovint es necessiten regles de negoci més específiques.
En aquest article es descriu com crear un validador personalitzat per assegurar que les contrasenyes compleixin amb criteris de seguretat específics (longitud mínima, majúscules, números i caràcters especials). Per aprendre a gestionar de forma professional les excepcions llançades per aquests validadors, es recomana consultar la guia de maneig global d’excepcions.
Prerequisits
Per seguir aquest article es requereix tenir instal·lat:
Configuració del Projecte (build.gradle)
Per a aquest article s’utilitza Gradle. A continuació es mostren les línies més rellevants per construir un projecte amb suport de validació i Hibernate Validator:
plugins {
id 'org.springframework.boot' version '3.5.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.safeboxes'
version = '1.0.0'
sourceCompatibility = '21'
repositories {
mavenCentral()
}
dependencies {
// Spring Boot
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// Testing
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// Third-party libraries
implementation 'org.hibernate.validator:hibernate-validator:9.1.0.Final'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
tasks.named('test') {
useJUnitPlatform()
}
test {
testLogging {
events "PASSED", "SKIPPED", "FAILED"
}
}Implementació
Directori i estructura de fitxers
Seguint una estructura de projecte neta, s’organitzen els validadors i controladors de la següent manera:
src
├── main
│ └── java
│ └── com/safeboxes
│ ├── config
│ │ └── ApiConfig.java
│ ├── domain
│ │ ├── exceptions
│ │ │ ├── SafeBoxException.java
│ │ │ └── enums
│ │ │ └── SafeBoxErrorMessage.java
│ │ ├── services
│ │ │ └── SafeBoxService.java
│ │ └── validators
│ │ ├── SecurePassword.java
│ │ └── SecurePasswordValidator.java
│ ├── dtos
│ │ └── safes
│ │ └── CreateSafeBoxDTO.java
│ └── infrastructure
│ └── controllers
│ └── SafeBoxController.java
└── test
└── java
└── com/safeboxes
└── domain/validators
└── SecurePasswordValidatorTest.java1. Definició de l’Anotació
Primer es crea la interfície de l’anotació. Aquesta defineix on es pot aplicar i quina classe s’encarregarà de la lògica de validació.
package com.safeboxes.domain.validators;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {SecurePasswordValidator.class})
public @interface SecurePassword {
String message() default "Invalid Password";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}2. Implementació del Validador
La classe SecurePasswordValidator implementa ConstraintValidator, on resideix la lògica per verificar si la contrasenya és segura.
package com.safeboxes.domain.validators;
import com.safeboxes.domain.exceptions.SafeBoxException;
import com.safeboxes.domain.exceptions.enums.SafeBoxErrorMessage;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
public class SecurePasswordValidator implements ConstraintValidator<SecurePassword, String> {
@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
if (!isSecurePassword(password)) {
SafeBoxErrorMessage error = SafeBoxErrorMessage.INVALID_PASSWORD_FORMAT;
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(SafeBoxErrorMessage.getMessage(error));
throw new SafeBoxException(error);
}
return true;
}
private boolean isSecurePassword(String password) {
final int MINIMUM_PASSWORD_LENGTH = 12;
if (password == null || password.length() < MINIMUM_PASSWORD_LENGTH) {
return false;
}
final String UPPER_CASE_REG_EX = ".*[A-Z].*";
final String LOWER_CASE_REG_EX = ".*[a-z].*";
final String DIGIT_REG_EX = ".*\\d.*";
final String SPECIAL_CHAR_REG_EX = ".*[! @#$%^&*()].*";
return Pattern.matches(UPPER_CASE_REG_EX, password) &&
Pattern.matches(LOWER_CASE_REG_EX, password) &&
Pattern.matches(DIGIT_REG_EX, password) &&
Pattern.matches(SPECIAL_CHAR_REG_EX, password);
}
}3. Ús del Validador en un DTO
S’aplica la nova anotació @SecurePassword en un DTO (Data Transfer Object) utilitzant un record de Java.
package com.safeboxes.dtos.safes;
import com.safeboxes.domain.validators.SecurePassword;
import jakarta.validation.constraints.NotNull;
public record CreateSafeBoxDTO(
@NotNull String name,
@NotNull @SecurePassword String password
) {
}4. Ús en el Controlador
Perquè la validació s’executi en rebre una petició, s’utilitza l’anotació @Valid al controlador de Spring Boot. S’empra Lombok amb @RequiredArgsConstructor per simplificar la injecció de dependències.
package com.safeboxes.infrastructure.controllers;
import com.safeboxes.config.ApiConfig;
import com.safeboxes.domain.services.SafeBoxService;
import com.safeboxes.dtos.safes.CreateSafeBoxDTO;
import com.safeboxes.dtos.safes.SafeBoxDTO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
import jakarta.validation.Valid;
@RestController
@RequestMapping(value = ApiConfig.BASE_PATH + "/safeboxes")
@RequiredArgsConstructor
public class SafeBoxController {
private final SafeBoxService safeBoxService;
@PostMapping
public SafeBoxDTO createSafeBox(@Valid @RequestBody CreateSafeBoxDTO request) {
return safeBoxService.createSafeBox(request);
}
}Tests Unitaris
És fonamental provar que el validador funcioni correctament per a tots els casos:
package com.safeboxes.domain.validators;
import com.safeboxes.domain.exceptions.SafeBoxException;
import com.safeboxes.domain.exceptions.enums.SafeBoxErrorMessage;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import jakarta.validation.ConstraintValidatorContext;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(MockitoExtension.class)
class SecurePasswordValidatorTest {
@InjectMocks
private SecurePasswordValidator passwordValidator;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private ConstraintValidatorContext context;
@ParameterizedTest
@ValueSource(strings = {
"short", // Too short
"123456789012", // No letters
"abcdefghijkl", // No upper case
"ABCDEFGHIJKL", // No lower case
"Abcdefghijkl", // No digits
"Abcdefghijk1", // No special char
"Abcdefghijk!", // No digit
" ", // Spaces
})
@NullSource
@DisplayName("Invalid passwords should throw SafeBoxException")
void invalid_passwords_should_throw_exception(String password) {
SafeBoxException exception = assertThrows(SafeBoxException.class, () -> {
passwordValidator.isValid(password, context);
});
assertEquals(SafeBoxErrorMessage.INVALID_PASSWORD_FORMAT, exception.getErrorMessage());
}
@ParameterizedTest
@ValueSource(strings = {
"ValidPass123!",
"Secure#2026Password",
"A1b2C3d4E5f!",
"PasswordWithSpace 1!"
})
@DisplayName("Valid passwords should return true")
void valid_passwords_should_return_true(String password) {
assertTrue(passwordValidator.isValid(password, context));
}
@Test
@DisplayName("should return true for a secure password")
void secure_password_ok() {
final String testPassword = "ValidPassword123!";
assertTrue(passwordValidator.isValid(testPassword, context));
}
}Referències
- Hibernate
- Spring Boot