Validación de Spring Bean
La validación de datos no es un tema nuevo en el desarrollo de aplicaciones web.
Echamos un breve vistazo a la validación de datos en el ecosistema Java en general y Spring Framework específicamente. La plataforma Java ha sido el estándar de facto para implementar la validación de datos esa es la especificación de Validación de Bean. La especificación Bean Validation tiene varias versiones:
- 1.0 (JSR-303),
- 1.1 (JSR-349),
- 2.0 (JSR 380):la última versión
Esta especificación define un conjunto de componentes, interfaces y anotaciones. Esto proporciona una forma estándar de poner restricciones a los parámetros y devolver valores de métodos y parámetros de constructores, proporciona API para validar objetos y gráficos de objetos.
Un modelo declarativo se utiliza para poner restricciones en forma de anotaciones en los objetos y sus campos. Hay anotaciones predefinidas como @NotNull
, @Digits
, @Pattern
, @Email
, @CreditCard
. Existe la posibilidad de crear nuevas restricciones personalizadas.
La validación puede ejecutarse manualmente o de forma más natural, cuando otras especificaciones y marcos validan los datos en el momento adecuado, por ejemplo, la entrada del usuario, la inserción o la actualización en JPA.
Ejemplo de validación en Java
Echemos un vistazo a cómo se puede hacer en la práctica en este ejemplo simple de Validación de Bean dentro de una aplicación Java normal.
Tenemos un objeto que queremos validar con todos los campos anotados con restricciones.
public class SimpleDto {
@Min(value = 1, message = "Id can't be less than 1 or bigger than 999999")
@Max(999999)
private int id;
@Size(max = 100)
private String name;
@NotNull
private Boolean active;
@NotNull
private Date createdDatetime;
@Pattern(regexp = "^asc|desc$")
private String order = "asc";
@ValidCategory(categoryType="simpleDto")
private String category;
…
Constructor, getters and setters
Ahora podemos usarlo en una aplicación Java simple y validar manualmente el objeto.
public class SimpleApplication {
public static void main(String[] args) {
final SimpleDto simpleDto = new SimpleDto();
simpleDto.setId(-1);
simpleDto.setName("Test Name");
simpleDto.setCategory("simple");
simpleDto.setActive(true);
simpleDto.setOrder("asc");
simpleDto.setCreatedDatetime(new Date());
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.usingContext().getValidator();
Set constrains = validator.validate(simpleDto);
for (ConstraintViolation constrain : constrains) {
System.out.println(
"[" + constrain.getPropertyPath() + "][" + constrain.getMessage() + "]"
);
}
}
}
Y el resultado en Consola será:
“[id] [Id can't be less than 1 or bigger than 999999]”
Restricción de validación y anotación
Cree una restricción de validación y una anotación personalizadas.
@Retention(RUNTIME)
@Target(FIELD)
@Constraint(validatedBy = {ValidCategoryValidator.class})
public @interface ValidCategory {
String categoryType();
String message() default "Category is not valid";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And constraint validation implementation:
public class ValidCategoryValidator implements ConstraintValidator<ValidCategory, String> {
private static final Map<String, List> availableCategories;
static {
availableCategories = new HashMap<>();
availableCategories.put("simpleDto", Arrays.asList("simple", "advanced"));
}
private String categoryType;
@Override
public void initialize(ValidCategory constraintAnnotation) {
this.setCategoryType(constraintAnnotation.categoryType());
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
List categories = ValidCategoryValidator.availableCategories.get(categoryType);
if (categories == null || categories.isEmpty()) {
return false;
}
for (String category : categories) {
if (category.equals(value)) {
return true;
}
}
return false;
}
}
En el ejemplo anterior, las categorías disponibles provienen de un mapa hash simple. En casos de uso de aplicaciones reales, se pueden recuperar de la base de datos o de cualquier otro servicio.
Tenga en cuenta que las restricciones y validaciones se pueden especificar y realizar no solo a nivel de campo, sino también en un objeto completo.
Cuando necesitamos validar varias dependencias de campo, por ejemplo, fecha de inicio, no puede ser posterior a la fecha de finalización.
Las implementaciones más utilizadas de Bean Validation las especificaciones son Hibernate Validator y Apache BVal .
Validación con Spring
Spring Framework ofrece varias características para la validación.
- La compatibilidad con Bean Validation API versiones 1.0, 1.1 (JSR-303, JSR-349) se introdujo en Spring Framework a partir de la versión 3.
- Spring tiene su propia interfaz Validator que es muy básica y se puede configurar en una instancia específica de DataBinder. Esto podría ser útil para implementar la lógica de validación sin anotaciones.
Validación de Bean con Spring
Spring Boot proporciona una validación iniciada que se puede incluir en el proyecto:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Este iniciador proporciona una versión de Hibernate Validator compatible con el Spring Boot actual.
Usando Bean Validation, podríamos validar un cuerpo de solicitud, parámetros de consulta, variables dentro de la ruta (por ejemplo, / /simpledto/{id} ), o cualquier parámetro de método o constructor.
Solicitudes POST o PUT
En las solicitudes POST o PUT, por ejemplo, pasamos la carga útil de JSON, Spring lo convierte automáticamente en un objeto Java y ahora queremos validar el objeto resultante. Usemos SimpleDto
objeto del ejemplo 1:
@RestController
@RequestMapping("/simpledto")
public class SimpleDtoController {
@Autowired
private SimpleDtoService simpleDtoService;
@RequestMapping(path = "", method = RequestMethod.POST, produces =
"application/json")
public SimpleDto createSimpleDto(
@Valid @RequestBody SimpleDto simpleDto) {
SimpleDto result = simpleDtoService.save(simpleDto);
return result;
}
}
Acabamos de agregar @Valid
anotación a SimpleDto
parámetro anotado con @RequestBody
. Esto le indicará a Spring que procese la validación antes de realizar una llamada de método real. En caso de que falle la validación, Spring lanzará una MethodArgument NotValidException
que, de forma predeterminada, devolverá una respuesta 400 (Solicitud incorrecta).
Validación de variables de ruta
La validación de variables de ruta funciona un poco diferente. El problema es que ahora tenemos que agregar anotaciones de restricciones directamente a los parámetros del método en lugar de dentro de los objetos.
Para que esto funcione, hay 2 opciones posibles:
Opción 1:@Anotación validada
Agregar @Validated
anotación al controlador a nivel de clase para evaluar las anotaciones de restricción en los parámetros del método.
Opción 2:variable de ruta
Use un objeto que represente la variable de ruta como se ve en el siguiente ejemplo:
@RestController
@RequestMapping("/simpledto")
public class SimpleDtoController {
@Autowired
private SimpleDtoService simpleDtoService;
@RequestMapping(path = "/{simpleDtoId}", method = RequestMethod.GET, produces =
"application/json")
public SimpleDto getSimpleDto(
@Valid SimpleDtoIdParam simpleDtoIdParam) {
SimpleDto result = simpleDtoService.findById(simpleDtoIdParam.getSimpleDtoId());
if (result == null) {
throw new NotFoundException();
}
return result;
}
}
En este caso, tenemos SimpleDtoIdParam
clase que contiene el simpleDtoId
campo que será validado contra una anotación de restricción Bean estándar o personalizada. Ruta Nombre de variable (/{simpleDtoId} ) debe ser el mismo que el nombre del campo (para que Spring pueda encontrar el setter para este campo).
private static final long serialVersionUID = -8165488655725668928L;
@Min(value = 1)
@Max(999999)
private int simpleDtoId;
public int getSimpleDtoId() {
return simpleDtoId;
}
public void setSimpleDtoId(int simpleDtoId) {
this.simpleDtoId = simpleDtoId;
}
}
A diferencia de Cuerpo de solicitud validación, Variable de ruta la validación arroja ConstraintViolationException
en lugar de MethodArgumentNotValidException
. Por lo tanto, necesitaremos crear un controlador de excepciones personalizado.
El marco de Java Spring también permite validar parámetros en un nivel de servicio con @Validated
anotación (nivel de clase) y @Valid
(nivel de parámetro).
Teniendo en cuenta que Spring JPA usa Hibernate debajo, también es compatible con Bean Validation para clases de entidad. Tenga en cuenta que probablemente no sea una buena idea en muchos casos confiar en este nivel de validación, ya que significa que toda la lógica anterior estaba tratando con objetos no válidos.
Interfaz de validación de primavera
Spring define su propia interfaz para la validación Validator (org.springframework.validation.Validator). Se puede configurar para una instancia específica de DataBinder e implementar la validación sin anotaciones (enfoque no declarativo).
Para implementar este enfoque necesitaríamos:
- Implementar la interfaz del validador
- Agregar validador
Implementar interfaz de validación
Implemente la interfaz Validator, por ejemplo, trabajemos con nuestro SimpleDto
clase:
@Component
public class SpringSimpleDtoValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return SimpleDto.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
if (errors.getErrorCount() == 0) {
SimpleDto param = (SimpleDto) target;
Date now = new Date();
if (param.getCreatedDatetime() == null) {
errors.reject("100",
"Create Date Time can't be null");
} else if (now.before(param.getCreatedDatetime())) {
errors.reject("101",
"Create Date Time can't be after current date time");
}
}
}
}
Compruebe aquí si el Datetime
creado la marca de tiempo está en el futuro.
Añadir Validador
Agregue la implementación de Validator a DataBinder
:
@RestController
@RequestMapping("/simpledto")
public class SimpleDtoController {
@Autowired
private SimpleDtoService simpleDtoService;
@Autowired
private SpringSimpleDtoValidator springSimpleDtoValidator;
@InitBinder("simpleDto")
public void initMerchantOnlyBinder(WebDataBinder binder) {
binder.addValidators(springSimpleDtoValidator);
}
@RequestMapping(path = "", method = RequestMethod.POST, produces =
"application/json")
public SimpleDto createSimpleDto(
@Valid @RequestBody SimpleDto simpleDto) {
SimpleDto result = simpleDtoService.save(simpleDto);
return result;
}
}
Ahora tenemos SimpleDto
validado usando anotaciones de restricción y nuestra implementación personalizada de Spring Validation.