在 Spring Boot 应用程序中验证数据

x33g5p2x  于2022-10-06 转载在 Spring  
字(7.2k)|赞(0)|评价(0)|浏览(646)

一个好的服务总是在处理它之前验证它的数据。在本教程中,我们将研究 Bean Validation API 并在 Spring Boot 应用程序中使用它的参考实现。

**Important**: Until Spring Boot version 2.2 the starter spring-boot-starter-web had as dependency the starter **spring-boot-starter-validation**. In Spring Boot 2.3 the starter spring-boot-starter-validation is NOT a dependency of the starter spring-boot-starter-web anymore so you need to add **explicitly**. the **spring-boot-starter-validation**:

<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-validation</artifactId> 
</dependency>

Bean Validation API 提供了许多可用于验证 bean 的注解。

创建验证涉及两个步骤:

  1. 在控制器方法上启用验证。

  2. 在 bean 上添加验证。

验证 Spring Boot 控制器

验证数据的最简单方法是在 Controller 方法中包含约束。看这个例子:

@Component
public class CustomerRepository {
  List<Customer> customerList = new ArrayList<Customer>();

  @PostConstruct
  public void init() {
    customerList.add(new Customer(1, "frank", "USA"));
    customerList.add(new Customer(2, "john", "USA"));
  }

  public List<Customer> getData() {
    return customerList;
  }

  public Customer save(Customer c) {
    customerList.add(c);
    return c;
  }
}

这是控制器类:

@Validated
@RestController
public class CustomerController {
  @Autowired CustomerRepository repository;

  @RequestMapping("/list")
  public List<Customer> findAll() {
    return repository.getData();
  }

  @RequestMapping("/find/{id}")
  public Customer findOne(@PathVariable @Min(1) @Max(3) int id) {
    return repository.getData().get(id);
  }
}

如您所见,控制器具有 @Min@Max 约束,它将检查我们是否正在为客户搜索有效的 id。

此外,在这种情况下,@Validated annotation 是在类级别上评估的,即使它允许在方法上使用。

如果我们运行上面的例子,会报如下错误:

curl -s http://localhost:8080/find/5 | jq {   "timestamp": "2020-04-02T07:39:58.172+0000",   "status": 500,   "error": "Internal Server Error",   "message": "findOne.id: must be less than or equal to 3",   "path": "/find/5" }

如果我们想返回一个 HTTP 状态 400(这是有道理的,因为客户端提供了一个无效的参数,这使它成为一个错误的请求),我们可以添加一个 GlobalExceptionHandler 到我们的控制器:

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
  @ExceptionHandler(ConstraintViolationException.class)
  public void constraintViolationException(HttpServletResponse response) throws IOException {
    response.sendError(HttpStatus.BAD_REQUEST.value());
  }
}

现在再次执行请求,您将收到“错误请求”错误,表明请求未正确组合:

curl -s http://localhost:8080/find/5 | jq {   "timestamp": "2020-04-02T07:40:44.739+0000",   "status": 400,   "error": "Bad Request",   "message": "findOne.id: must be less than or equal to 3",   "path": "/find/5" }

验证 Bean 数据

验证也可以放在 Bean 数据上。

验证 Bean 数据可能会变成反模式! 但是,您应该考虑,如果我们只在 Bean 层进行验证,我们会接受 Web 和业务层使用无效数据的风险。无效数据可能会导致业务层出现严重错误,因此 Bean 或持久层中的验证可以作为额外的安全网,但不是唯一的验证场所。

在我们的客户 Bean 中放置了以下约束:

public class Customer {
  private int id;
  @Size(min = 5, message = "Enter at least 5 Characters.") private String name;
  @Country private String country;
  public Customer(int id, String name, String country) {
    super();
    this.id = id;
    this.name = name;
    this.country = country;
  }

当我们尝试持久化一个实体时,将验证上述约束:

@PostMapping("/save") public Customer newCustomer(@Valid @RequestBody Customer customer) {
  return repository.save(customer);
}

此外,我们将向我们的 GlobalExceptionHandler 添加以下方法,以在输入数据无效的情况下提供有意义的响应:

@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(x -> x.getDefaultMessage()).collect(Collectors.toList());
  body.put("errors", errors);
  return new ResponseEntity < > (body, headers, status);
}

如果我们尝试保存未通过验证的新客户,将返回以下错误:

curl -v -X POST localhost:8080/save -H "Content-type:application/json" -d "{\"name\":\"Carl\", \"country\":\"USA\"}"   {"timestamp":"2020-04-02T09:26:04.546+0000","status":400,"errors":["Enter at least 5 Characters."]}

值得注意的是,可以使用以下验证约束:

  • @AssertFalse :检查是否为假。 @Assert 检查是否为真。
  • @Future : 带注释的元素必须是未来的日期。
  • @Past : 带注释的元素必须是过去的日期。
  • @Max : 被注释的元素必须是一个数字,其值必须小于
  • @Min : 被注释的元素必须是一个数字,其值必须更高
  • @NotNull : 带注释的元素不能为空。
  • @Pattern : 带注释的 {@code CharSequence} 元素必须匹配指定的正则表达式。正则表达式遵循 Java 正则表达式约定。
  • @Size : 标注的元素大小必须在指定的边界内。

使用自定义验证器

如果您对可用的默认约束不满意,您可以创建自定义约束并将它们放置在您的代码中。例如,让我们编写一个自定义约束,它将检查客户 Bean 的国家/地区,只允许来自“美国”或“加拿大”的客户。为此,让我们添加实现 ConstraintValidator 的 CountryValidator

public class CountryValidator implements ConstraintValidator<Country, String> {
  List<String> authors = Arrays.asList("USA", "Canada");

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    return authors.contains(value);
  }
}

要编译它,我们需要 Country 接口:

@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = CountryValidator.class)
@Documented
public @interface Country {
  String message() default "We only accept customers from USA and Canada";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};
}

有了这个,如果我们尝试使用无效国家/地区保存客户,它将显示以下错误:

curl -v -X POST localhost:8080/save -H "Content-type:application/json" -d "{\"name\":\"Federick\", \"country\":\"Germany\"}" {"timestamp":"2020-04-02T08:01:16.205+0000","status":400,"errors":["We only accept customers from USA and Canada"]}

验证 Web 应用程序中的输入数据

要了解验证如何在简单的 MVC 应用程序中处理输入数据,让我们以本教程中的示例为例:如何使用 H2 数据库创建 Spring Boot CRUD 应用程序

在这里,可以在前端层添加客户数据,如下所示:

<form action="#" th:action="@{/addcustomer}" th:object="${customer}" method="post">
	<div class="row">
		<div >
			<label for="name">Name</label>
			<input type="text" th:field="*{name}" id="name" placeholder="Name">
				<span th:if="${#fields.hasErrors('name')}" th:errors="*{name}" ></span>
			</div>
			<div >
				<label for="surname" class="col-form-label">Surname</label>
				<input type="text" th:field="*{surname}"  id="surname" placeholder="Surname">
					<span th:if="${#fields.hasErrors('surname')}" th:errors="*{surname}" ></span>
				</div>
				<div >
					<label for="email" class="col-form-label">Email</label>
					<input type="text" th:field="*{email}"  id="email" placeholder="Email">
						<span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" ></span>
					</div>
				</div>
				<div class="row">
					<div >
						<input type="submit"  value="Add Customer">
						</div>
					</div>
				</form>

Customer Bean 包含以下约束:

@Entity public class Customer {
  @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id;
  @NotBlank(message = "Name is required") private String name;
  @Size(min = 3, message = "Surname requires at least 3 characters.") private String surname;
  @Pattern(regexp = ".+@.+..+", message = "Please provide a valid email address") private String email;
  public Customer() {}
  public Customer(String name, String email) {
    this.name = name;
    this.email = email;
  }
  // Getter / Setters here  
}

因此,如果您尝试插入无效的客户,则会在输入表单中直接打印以下错误:

本教程的源代码:https://github.com/fmarchioni/masterspringboot/tree/master/rest/demo-validation

相关文章