Convalida Spring Bean
La convalida dei dati non è un argomento nuovo nello sviluppo di applicazioni web.
Diamo una breve occhiata alla convalida dei dati nell'ecosistema Java in generale e nello Spring Framework in particolare. La piattaforma Java è stata di fatto lo standard per implementare la convalida dei dati questa è la specifica di convalida del bean. La specifica Bean Validation ha diverse versioni:
- 1.0 (JSR-303),
- 1.1 (JSR-349),
- 2.0 (JSR 380) – l'ultima versione
Questa specifica definisce un insieme di componenti, interfacce e annotazioni. Ciò fornisce un modo standard per porre vincoli ai parametri e restituire valori di metodi e parametri di costruttori, fornire API per convalidare oggetti e grafici di oggetti.
Un modello dichiarativo viene utilizzato per inserire vincoli sotto forma di annotazioni sugli oggetti e sui relativi campi. Ci sono annotazioni predefinite come @NotNull
, @Digits
, @Pattern
, @Email
, @CreditCard
. C'è la possibilità di creare nuovi vincoli personalizzati.
La convalida può essere eseguita manualmente o in modo più naturale, quando altre specifiche e framework convalidano i dati al momento giusto, ad esempio l'input dell'utente, l'inserimento o l'aggiornamento in JPA.
Esempio di convalida in Java
Diamo un'occhiata a come può essere fatto in pratica in questo semplice esempio di Bean Validation all'interno di una normale applicazione Java.
Abbiamo un oggetto che vogliamo convalidare con tutti i campi annotati con vincoli.
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
Ora possiamo usarlo in una semplice applicazione Java e convalidare manualmente l'oggetto.
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() + "]"
);
}
}
}
E il risultato in Console sarà:
“[id] [Id can't be less than 1 or bigger than 999999]”
Vincolo di convalida e annotazione
Crea un vincolo di convalida e un'annotazione personalizzati.
@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;
}
}
Nell'esempio sopra, le categorie disponibili provengono da una semplice mappa hash. Nei casi d'uso di applicazioni reali, possono essere recuperati dal database o da qualsiasi altro servizio.
Tieni presente che i vincoli e le convalide possono essere specificati ed eseguiti non solo a livello di campo, ma anche su un intero oggetto.
Quando è necessario convalidare varie dipendenze di campo, ad esempio la data di inizio, non può essere successiva alla data di fine.
Le implementazioni più utilizzate di Bean Validation le specifiche sono Hibernate Validator e Apache BVal .
Convalida con Primavera
Il framework Spring fornisce diverse funzionalità per la convalida.
- Il supporto per Bean Validation API versioni 1.0, 1.1 (JSR-303, JSR-349) è stato introdotto in Spring Framework a partire dalla versione 3.
- Spring ha la sua interfaccia Validator che è molto semplice e può essere impostata in un'istanza DataBinder specifica. Questo potrebbe essere utile per implementare la logica di convalida senza annotazioni.
Convalida del bean con Spring
Spring Boot fornisce la convalida avviata che può essere inclusa nel progetto:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Questo starter fornisce una versione di Hibernate Validator compatibile con l'attuale Spring Boot.
Utilizzando Bean Validation, potremmo convalidare un corpo di richiesta, parametri di query, variabili all'interno del percorso (ad es. / /simpledto/{id} ), o qualsiasi metodo o parametro del costruttore.
Richieste POST o PUT
Nelle richieste POST o PUT, ad esempio, passiamo il payload JSON, Spring lo converte automaticamente in oggetto Java e ora vogliamo convalidare l'oggetto risultante. Usiamo SimpleDto
oggetto dal 1 esempio:
@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;
}
}
Abbiamo appena aggiunto @Valid
annotazione al SimpleDto
parametro annotato con @RequestBody
. Questo dirà a Spring di elaborare la convalida prima di effettuare una vera e propria chiamata al metodo. Nel caso in cui la convalida fallisca, Spring genererà una MethodArgument NotValidException
che, per impostazione predefinita, restituirà una risposta 400 (Richiesta errata).
Convalida delle variabili di percorso
La convalida delle variabili di percorso funziona in modo leggermente diverso. Il problema è che ora dobbiamo aggiungere annotazioni di vincolo direttamente ai parametri del metodo invece che all'interno degli oggetti.
Per farlo funzionare ci sono 2 possibili opzioni:
Opzione 1:@Annotazione convalidata
Aggiungi @Validated
annotazione al controller a livello di classe per valutare le annotazioni di vincolo sui parametri del metodo.
Opzione 2:variabile di percorso
Utilizzare un oggetto che rappresenti la variabile di percorso come mostrato nell'esempio seguente:
@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;
}
}
In questo caso, abbiamo SimpleDtoIdParam
classe che contiene il simpleDtoId
campo che verrà convalidato rispetto a un'annotazione di vincolo Bean standard o personalizzata. Nome della variabile di percorso (/{simpleDtoId} ) deve essere uguale al nome del campo (in modo che Spring possa trovare il setter per questo 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;
}
}
In contrasto con Organismo di richiesta convalida, Variabile di percorso la convalida genera ConstraintViolationException
invece di MethodArgumentNotValidException
. Pertanto, dovremo creare un gestore di eccezioni personalizzato.
Il framework Java Spring consente anche di convalidare i parametri a livello di servizio con @Validated
annotazione (livello di classe) e @Valid
(livello di parametro).
Considerando che Spring JPA sta usando Hibernate sotto, supporta anche la convalida del bean per le classi di entità. Tieni presente che probabilmente non è una buona idea in molti casi fare affidamento su questo livello di convalida poiché significa che tutta la logica prima aveva a che fare con oggetti non validi.
Interfaccia di convalida della primavera
Spring definisce la propria interfaccia per la validazione Validator (org.springframework.validation.Validator). Può essere impostato per un'istanza DataBinder specifica e implementare la convalida senza annotazioni (approccio non dichiarativo).
Per implementare questo approccio avremmo bisogno di:
- Implementare l'interfaccia del validatore
- Aggiungi validatore
Implementare l'interfaccia di convalida
Implementare l'interfaccia Validator, ad esempio, lavorare con il nostro SimpleDto
classe:
@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");
}
}
}
}
Controlla qui se il Datetime
creato il timestamp è nel futuro.
Aggiungi validatore
Aggiungi l'implementazione di 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;
}
}
Ora abbiamo SimpleDto
convalidato utilizzando le annotazioni di vincolo e la nostra implementazione personalizzata di Spring Validation.