Rest API i walidacja beana
Rest API i walidacja beana
W tym artykule zaprezentuję w jaki sposób wykonać walidację beana zwracając w Response Body wynik poszczególnych walidacji. Napiszemy również własną walidację w postaci wygodnej do użycia adnotacji. Do dzieła! Zacznijmy od dodania niezbędnych zależności – plik pom.xml:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.197</version> <scope>runtime</scope> </dependency> <!--Starting with Boot 2.3, we also need to explicitly add the spring-boot-starter-validation dependency:--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</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-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
Przykładowy model – dla uproszczenia pominięto metody dostępowe:
@Entity public class Employee { @Id private int id; @NotBlank private String name; @NotBlank private String surname; @NotBlank private String salary; @EmployeeRoleValid private String role; }
Adnotacja @EmployeeRoleValid – dokonamy walidacji roli pracownika. Tylko wartość „DEV” jest prawidłowa:
@Documented @Constraint(validatedBy = RoleEmployeeValidator.class) @Target( { ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface EmployeeRoleValid { String message() default "Invalid employee role"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
Klasa gdzie definiujemy właściwą walidację:
public class RoleEmployeeValidator implements ConstraintValidator<EmployeeRoleValid, String> { @Override public void initialize(EmployeeRoleValid employeeRole) { } @Override public boolean isValid(String employeeRole, ConstraintValidatorContext cxt) { return (employeeRole.contains("DEV")) ? true: false; } }
Klasa kontrolera:
@RestController public class EmployeeController { @PostMapping("/emp") public ResponseEntity<String> addEmployee(@Valid @RequestBody Employee employee) { return ResponseEntity.ok("employee is valid"); } }
Po wysłaniu przykładowego zapytania POST:
{"name":"","surname":"Leader","salary":1000,"role":"Lead"}
otrzymamy:
{ "timestamp": "2021-10-13T16:36:29.307+00:00", "status": 400, "error": "Bad Request", "path": "/emp" }
co nie jest zadowalającym wynikiem – poprawny to dodając @ExceptionHandler:
@ControllerAdvice public class GlobalExceptionHandler { @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleValidationExceptions( MethodArgumentNotValidException ex) { List<String> errors = new ArrayList<>(); ex.getBindingResult().getFieldErrors().forEach(err -> errors.add(err.getDefaultMessage())); Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("errors", errors); errorResponse.put("status", HttpStatus.BAD_REQUEST.toString()); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); } }
wynik:
{ "errors": [ "Invalid employee role", "nie może być odstępem" ], "status": "400 BAD_REQUEST" }
Zobaczmy teraz jak działa Spring Validator – dodajmy klasę która zweryfikuje nam czy na polach name & surname nie znajdują się zakazane znaki:
@Component public class EmployeeValidator implements Validator { String[] blackList = {"?", "|", "-", "="}; public boolean supports(Class clazz) { return Employee.class.equals(clazz); } public void validate(Object obj, Errors e) { Employee employee = (Employee) obj; boolean blackListCheckName = Arrays.stream(blackList).anyMatch(employee.getName()::contains); boolean blackListCheckSurname = Arrays.stream(blackList).anyMatch(employee.getSurname()::contains); if(blackListCheckName) { e.reject("name", "name contains forbidden characters"); } if(blackListCheckSurname) { e.reject("surname", "surname contains forbidden characters"); } } }
W klasie kontrolera dodajmy powiązanie z wyżej przedstawioną klasa walidatora:
@InitBinder("employee") private void initBinder(WebDataBinder webDataBinder) { webDataBinder.addValidators(employeeValidator); }
Zmodyfikujmy ExceptionHandler w taki sposób aby były pobierane wszystkie błędy a nie tylko z pól poprzez metodę getFieldErrors():
@ControllerAdvice public class GlobalExceptionHandler { @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { List<String> errors = new ArrayList<>(); ex.getAllErrors().forEach(error -> errors.add((error.getDefaultMessage()))); Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("errors", errors); errorResponse.put("status", HttpStatus.BAD_REQUEST.toString()); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); } }
Wykonajmy test wysyłając żądanie:
{"name":" m?","surname":"Leader","salary":1000,"role":"Leader"}
wynik:
{ "errors": [ "Invalid employee role", "name contains forbidden characters" ], "status": "400 BAD_REQUEST" }
widać, że zostały przechwycone błędy z pól oraz błędy z walidatora Springa.
Leave a comment