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.

Zobacz kod na GitHubie i zapisz się na bezpłatny newsletter!

.

Leave a comment

Your email address will not be published.


*