Validation for Functional Endpoints in Spring 5 – 在Spring 5中对功能端点进行验证

最后修改: 2018年 10月 18日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

It’s often useful to implement input validation for our APIs to avoid unexpected errors later when we’re processing the data.

为我们的API实现输入验证通常是有用的,以避免以后在处理数据时出现意外错误。

Unfortunately, in Spring 5 there’s no way to run validations automatically on functional endpoints as we do on annotated-based ones. We have to manage them manually.

不幸的是,在Spring 5中,没有办法像在基于注解的端点上那样在功能端点上自动运行验证。我们必须手动管理它们。

Still, we can make use of some useful tools provided by Spring to verify easily and in a clean manner that our resources are valid.

不过,我们还是可以利用Spring提供的一些有用的工具,轻松而干净地验证我们的资源是否有效。

2. Using Spring Validations

2.使用Spring验证

Let’s start by configuring our project with a working functional endpoint before diving into the actual validations.

在进入实际验证之前,让我们先用一个有效的功能端点来配置我们的项目。

Imagine we have the following RouterFunction:

想象一下,我们有以下RouterFunction

@Bean
public RouterFunction<ServerResponse> functionalRoute(
  FunctionalHandler handler) {
    return RouterFunctions.route(
      RequestPredicates.POST("/functional-endpoint"),
      handler::handleRequest);
}

This router uses the handler function provided by the following controller class:

这个路由器使用以下控制器类所提供的处理函数。

@Component
public class FunctionalHandler {

    public Mono<ServerResponse> handleRequest(ServerRequest request) {
        Mono<String> responseBody = request
          .bodyToMono(CustomRequestEntity.class)
          .map(cre -> String.format(
            "Hi, %s [%s]!", cre.getName(), cre.getCode()));
 
        return ServerResponse.ok()
          .contentType(MediaType.APPLICATION_JSON)
          .body(responseBody, String.class);
    }
}

As we can see, all we’re doing in this functional endpoint is formatting and retrieving the information we received in the request body, which is structured as a CustomRequestEntity object:

正如我们所看到的,我们在这个功能端点所做的是格式化和检索我们在请求体中收到的信息,它被结构化为一个CustomRequestEntity对象。

public class CustomRequestEntity {
    
    private String name;
    private String code;

    // ... Constructors, Getters and Setters ...
    
}

This works just fine, but let’s imagine that we now need to check that our input complies with some given constraints, for example, that none of the fields can be null and that the code should’ve more than 6 digits.

这样做很好,但让我们想象一下,我们现在需要检查我们的输入是否符合一些给定的约束条件,例如,没有一个字段可以是空的,代码应该超过6位。

We need to find a way of making these assertions efficiently and, if possible, separated from our business logic.

我们需要找到一种方法来有效地制作这些断言,如果可能的话,要与我们的业务逻辑分开。

2.1. Implementing a Validator

2.1.实现一个验证器

As it’s explained in this Spring Reference Documentation, we can use the Spring’s Validator interface to evaluate our resource’s values:

正如这个Spring参考文档中所解释的那样,我们可以使用Spring的Validator接口来评估我们资源的值

public class CustomRequestEntityValidator 
  implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return CustomRequestEntity.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(
          errors, "name", "field.required");
        ValidationUtils.rejectIfEmptyOrWhitespace(
          errors, "code", "field.required");
        CustomRequestEntity request = (CustomRequestEntity) target;
        if (request.getCode() != null && request.getCode().trim().length() < 6) {
            errors.rejectValue(
              "code",
              "field.min.length",
              new Object[] { Integer.valueOf(6) },
              "The code must be at least [6] characters in length.");
        }
    }
}

We won’t go into details about how the Validator works. It’s enough to know that all the errors are collected when validating an object – an empty error collection means that the object adheres to all our constraints.

我们将不对如何Validator工作的细节进行讨论。只要知道在验证一个对象时,所有的错误都被收集起来就足够了–空的错误收集意味着该对象遵守了我们所有的约束

So now that we have our Validator in place, we’ll have to explicitly call it’s validate before actually executing our business logic.

因此,现在我们已经有了我们的Validator,在实际执行我们的业务逻辑之前,我们必须明确调用它的validate

2.2. Executing the Validations

2.2.执行验证

At first, we can think that using a HandlerFilterFunction would be suitable in our situation.

起初,我们可以认为使用HandlerFilterFunction会适合我们的情况。

But we have to keep in mind that in those filters -same as in the handlers- we deal with asynchronous constructions -such as Mono and Flux.

但是我们必须记住,在这些过滤器中–与在处理程序中一样–我们处理异步结构–例如MonoFlux

This means that we’ll have access to the Publisher (the Mono or the Flux object) but not to the data that it will eventually provide.

这意味着我们可以访问PublisherMonoFlux对象),但不能访问它最终将提供的数据。

Therefore, the best thing we can do is validate the body when we are actually processing it in the handler function.

因此,我们能做的最好的事情就是在处理函数中实际处理它时验证主体。

Let’s go ahead and modify our handler method, including the validation logic:

让我们继续修改我们的处理方法,包括验证逻辑。

public Mono<ServerResponse> handleRequest(ServerRequest request) {
    Validator validator = new CustomRequestEntityValidator();
    Mono<String> responseBody = request
      .bodyToMono(CustomRequestEntity.class)
      .map(body -> {
        Errors errors = new BeanPropertyBindingResult(
          body,
          CustomRequestEntity.class.getName());
        validator.validate(body, errors);

        if (errors == null || errors.getAllErrors().isEmpty()) {
            return String.format("Hi, %s [%s]!", body.getName(), body.getCode());
        } else {
            throw new ResponseStatusException(
              HttpStatus.BAD_REQUEST,
              errors.getAllErrors().toString());
        }
    });
    return ServerResponse.ok()
      .contentType(MediaType.APPLICATION_JSON)
      .body(responseBody, String.class);
}

In a nutshell, our service will now retrieve a ‘Bad Request‘ response if the request’s body doesn’t comply with our restrictions.

简而言之,如果请求的主体不符合我们的限制,我们的服务现在将检索到一个’Bad Request‘响应。

Can we say we achieved our objective? Well, we’re almost there. We’re running the validations, but there are many drawbacks in this approach.

我们能说我们实现了我们的目标吗?好吧,我们几乎达到了。我们正在运行验证,但这种方法有很多缺点。

We are mixing the validations with business logic, and, to make things worse, we’ll have to repeat the code above in any handler where we want to carry our input validation.

我们把验证和业务逻辑混在一起,更糟糕的是,我们必须在任何我们想进行输入验证的处理程序中重复上面的代码。

Let’s try to improve this.

让我们试着改进这一点。

3. Working on a DRY Approach

3.采用DRY方法工作

To create a cleaner solution we’ll start by declaring an abstract class containing the basic procedure to process a request.

为了创建一个更简洁的解决方案,我们将首先声明一个包含处理请求的基本程序的抽象类

All the handlers that need input validation will extend this abstract class, so as to reuse its main scheme, and therefore following the DRY (don’t repeat yourself) principle.

所有需要输入验证的处理程序都将扩展这个抽象类,以便重复使用其主要方案,因此遵循DRY(不要重复自己)原则。

We’ll use generics so as to make it flexible enough to support any body type and its respective validator:

我们将使用泛型,以便使它足够灵活,支持任何主体类型及其各自的验证器。

public abstract class AbstractValidationHandler<T, U extends Validator> {

    private final Class<T> validationClass;

    private final U validator;

    protected AbstractValidationHandler(Class<T> clazz, U validator) {
        this.validationClass = clazz;
        this.validator = validator;
    }

    public final Mono<ServerResponse> handleRequest(final ServerRequest request) {
        // ...here we will validate and process the request...
    }
}

Now let’s code our handleRequest method with the standard procedure:

现在,让我们用标准程序编写我们的handleRequest方法。

public Mono<ServerResponse> handleRequest(final ServerRequest request) {
    return request.bodyToMono(this.validationClass)
      .flatMap(body -> {
        Errors errors = new BeanPropertyBindingResult(
          body,
          this.validationClass.getName());
        this.validator.validate(body, errors);

        if (errors == null || errors.getAllErrors().isEmpty()) {
            return processBody(body, request);
        } else {
            return onValidationErrors(errors, body, request);
        }
    });
}

As we can see, we’re using two methods that we have not created yet.

我们可以看到,我们正在使用两个我们还没有创建的方法。

Let’s define the one invoked when we have validation errors first:

让我们先定义当我们有验证错误时调用的那个。

protected Mono<ServerResponse> onValidationErrors(
  Errors errors,
  T invalidBody,
  ServerRequest request) {
    throw new ResponseStatusException(
      HttpStatus.BAD_REQUEST,
      errors.getAllErrors().toString());
}

This is just a default implementation though, it can be easily overridden by the child classes.

不过这只是一个默认的实现,它可以很容易地被子类所覆盖。

Finally, we’ll set the processBody method undefined -we’ll leave it up to the child classes to determine how to proceed in that case:

最后,我们将设置processBody方法未定义 – 我们将让子类决定在这种情况下如何进行

abstract protected Mono<ServerResponse> processBody(
  T validBody,
  ServerRequest originalRequest);

There are a few aspects to analyze in this class.

这门课有几个方面需要分析。

First of all, by using generics the child implementations will have to explicitly declare the type of content they’re expecting and the validator that will be used to evaluate it.

首先,通过使用泛型,子实现必须明确声明他们所期望的内容类型以及将用于评估的验证器。

This also makes our structure robust, since it limits our methods’ signatures.

这也使我们的结构变得稳健,因为它限制了我们方法的签名。

On runtime, the constructor will assign the actual validator object and the class used to cast the request body.

在运行时,构造函数将分配实际的验证器对象和用于铸造请求体的类。

We can have a look at the complete class here.

我们可以看一下完整的类这里

Let’s now see how we can benefit from this structure.

现在让我们看看我们如何能从这个结构中受益。

3.1. Adapting Our Handler

3.1.调整我们的处理程序

The first thing we’ll have to do, obviously, is extending our handler from this abstract class.

显然,我们要做的第一件事是从这个抽象类中扩展我们的处理程序。

By doing that, we’ll be forced to use the parent’s constructor and to define how we’ll handle our request in the processBody method:

通过这样做,我们将被迫使用父类的构造函数,并在processBodymethod中定义我们将如何处理我们的请求。

@Component
public class FunctionalHandler
  extends AbstractValidationHandler<CustomRequestEntity, CustomRequestEntityValidator> {

    private CustomRequestEntityValidationHandler() {
        super(CustomRequestEntity.class, new CustomRequestEntityValidator());
    }

    @Override
    protected Mono<ServerResponse> processBody(
      CustomRequestEntity validBody,
      ServerRequest originalRequest) {
        String responseBody = String.format(
          "Hi, %s [%s]!",
          validBody.getName(),
          validBody.getCode());
        return ServerResponse.ok()
          .contentType(MediaType.APPLICATION_JSON)
          .body(Mono.just(responseBody), String.class);
    }
}

As we can appreciate, our child handler is now much simpler than the one we obtained in the previous section, since it avoids messing with the actual validation of the resources.

正如我们所欣赏的,我们的子处理程序现在比我们在上一节中获得的要简单得多,因为它避免了对资源的实际验证进行干扰。

4. Support to Bean Validation API Annotations

4.支持Bean Validation API注解

With this approach, we can also take advantage of the powerful Bean Validation’s annotations provided by the javax.validation package.

通过这种方法,我们还可以利用javax.validation包所提供的强大的Bean Validation的注释

For example, let’s define a new entity with annotated fields:

例如,让我们定义一个带有注解字段的新实体。

public class AnnotatedRequestEntity {
 
    @NotNull
    private String user;

    @NotNull
    @Size(min = 4, max = 7)
    private String password;

    // ... Constructors, Getters and Setters ...
}

We can now simply create a new handler injected with the default Spring Validator provided by the LocalValidatorFactoryBean bean:

我们现在可以简单地创建一个新的处理程序,并注入由LocalValidatorFactoryBeanBean提供的默认SpringValidator

public class AnnotatedRequestEntityValidationHandler
  extends AbstractValidationHandler<AnnotatedRequestEntity, Validator> {

    private AnnotatedRequestEntityValidationHandler(@Autowired Validator validator) {
        super(AnnotatedRequestEntity.class, validator);
    }

    @Override
    protected Mono<ServerResponse> processBody(
      AnnotatedRequestEntity validBody,
      ServerRequest originalRequest) {

        // ...

    }
}

We have to keep in mind that if there are other Validator beans present in the context, we might have to explicitly declare this one with the @Primary annotation:

我们必须记住,如果上下文中还有其他Validatorbeans,我们可能必须用@Primary注解明确地声明这个beans。

@Bean
@Primary
public Validator springValidator() {
    return new LocalValidatorFactoryBean();
}

5. Conclusion

5.总结

To summarize, in this post we’ve learned how to validate input data in Spring 5 functional endpoints.

总结一下,在这篇文章中,我们已经学会了如何在Spring 5功能端点中验证输入数据。

We created a nice approach to handle validations gracefully by avoiding mingling its logic with the business one.

我们创建了一个很好的方法来优雅地处理验证,避免将其逻辑与业务逻辑混为一谈。

Of course, the suggested solution might not be suitable for just any scenario. We’ll have to analyze our situation and probably adapt the structure to our needs.

当然,建议的解决方案可能并不适合于任何情况。我们必须分析我们的情况,并可能根据我们的需要调整结构。

If we want to see the whole working example we can find it in our GitHub repo.

如果我们想看到整个工作实例,我们可以在我们的GitHub repo中找到它。