Dans ce ticket nous allons voir comment utiliser les validations natives avec Spring Boot. Les technologies utilisées sont les suivantes :
Prérequis : Il est nécessaire de générer un projet Spring Boot via le site https://start.spring.io . Ajouter Spring web, JPA, h2 pour la persistance des données puis validation pour la validation des données.
Nous créerons une entité voiture, contenant des informations de validation qui seront testées lors de l'appel au controller. Nous en profiterons pour stocker les informations des voitures dans une base de données h2 pour pouvoir être récupérées.
@Entity
public class Car {
@Id
@GeneratedValue
private Long id;
@NotEmpty(message = "Please provide a model")
private String model;
@NotEmpty(message = "Please provide a registration")
private String registration;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public String getRegistration() {
return registration;
}
public void setRegistration(String registration) {
this.registration = registration;
}
}
Remarque : Des annotations @NotEmpty sont présentes. Elles seront utiles pour la validation des champs. Elles décrivent un champ comme ne devant pas être vide.
Repository de l'entité Car :
public interface CarRepository extends JpaRepository<Car, Long>{}
Service appelant le repository de l'entité Car :
@Service
public class CarService {
@Autowired
private CarRepository carRepository;
public List<Car> findAll(){
return this.carRepository.findAll();
}
public Car save(Car theCar){
return this.carRepository.save(theCar);
}
public Optional<Car> findById(Long theId){
return this.carRepository.findById(theId);
}
}
@Controller
public class CarController {
@Autowired
private CarService carService;
@GetMapping("/cars")
public ResponseEntity<List<Car>> cars() {
return ResponseEntity.ok(this.carService.findAll());
}
@PostMapping("/car")
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<Car> car(@Valid @RequestBody Car car) {
return ResponseEntity.ok(this.carService.save(car));
}
}
Si nous tentons de vérifier le bon fonctionnement des validations, nous observerons une erreur 400 en cas d'absence de valeur lors de l'enregistrement d'une voiture.
$ curl -v -X POST localhost:8080/car -H "Content-type:application/json" -d "{\"model\":\"ABC\"}"
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /car HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> Content-type:application/json
> Content-Length: 15
>
* upload completely sent off: 15 out of 15 bytes
< HTTP/1.1 400
< Content-Length: 0
< Date: Fri, 08 Jan 2021 17:47:10 GMT
< Connection: close
<
* Closing connection 0
Afin d'avoir un résultat JSON sur les problèmes rencontrés lors de l'enregistrement, nous allons nous aider d'un controller qui sera appelé dans le cas d'une exception du type MethodeArgumentNotValidException :
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", new Date());
body.put("status", status.value());
// Get all errors
List<String> errors = ex.getBindingResult().getFieldErrors().stream().map(FieldError::getDefaultMessage)
.collect(Collectors.toList());
body.put("errors", errors);
return new ResponseEntity<>(body, headers, status);
}
}
Si nous réessayons d'insérer une voiture dans la base de données, avec des paramètres erronés, nous obtenons un JSON en sortie avec les erreurs associées :
$ curl -v -X POST localhost:8080/car -H "Content-type:application/json" -d "{\"model\":\"ABC\"}"
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /car HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> Content-type:application/json
> Content-Length: 15
>
* upload completely sent off: 15 out of 15 bytes
< HTTP/1.1 400
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Fri, 08 Jan 2021 17:58:28 GMT
< Connection: close
<
* Closing connection 0
{"timestamp":"2021-01-08T17:58:27.990+00:00","status":400,"errors":["Please provide a registration"]}%
Il est possible de créer une validation directement en paramètre du controller :
@GetMapping("/car/{id}")
public ResponseEntity<Car> car(@PathVariable @Min(1) Long id) {
return ResponseEntity.ok(this.carService.findById(id).orElse(null));
}
Remarque : Il est nécessaire de metre l'annotation @Validated au controller.
Dans le cas d'une récupération d'une voiture, si le paramètre id est inférieur à 1, alors une erreur 500 est levée (et non 400). Une exception est également disponible dans la console lorsque le paramètre ne valide pas la validation.
Pour modifier ce comportement, nous allons ajouter une méthode pour capturer les exceptions du type ConstraintViolationException dans la classe CustomGlobalExceptionHandler :
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, Object>> constraintViolationException(
ConstraintViolationException constraintViolationException) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", new Date());
body.put("status", HttpStatus.BAD_REQUEST.value());
// Get all errors
List<String> errors = constraintViolationException.getConstraintViolations().stream()
.map(x -> x.getPropertyPath() + " " + x.getMessage()).collect(Collectors.toList());
body.put("errors", errors);
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
}
Désormais, dans le cas d'un échec à cause du paramètre, le code d'erreur retourné sera 400 et non 500.
Test avec TestRestTemplate :
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class CarControllerRestTemplateTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void save_emptyModel_emptyRegistration_400() throws JSONException {
// GIVEN
String CarJson = "{}";
String expectedJson = "{\"status\":400,\"errors\":[\"Please provide a model\",\"Please provide a registration\"]}";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(CarJson, headers);
// WHEN
// send json with POST
ResponseEntity<String> response = restTemplate.postForEntity("/car", entity, String.class);
// THEN
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
JSONAssert.assertEquals(expectedJson, response.getBody(), false);
}
@Test
void save_fullModel_200() throws JSONException {
// GIVEN
String CarJson = "{\"model\":\"ABC\",\"registration\":\"ABC\"}";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(CarJson, headers);
// WHEN
// send json with POST
ResponseEntity<String> response = restTemplate.postForEntity("/car", entity, String.class);
// THEN
assertEquals(HttpStatus.OK, response.getStatusCode());
}
}
LauLem.com - Conditions Générales d'Utilisation - Informations Légales - Charte relative aux cookies - Charte sur la protection des données personnelles - A propos