1. 概述

本文将带你深入了解如何在开源框架 Jersey 中使用 Bean Validation。

正如我们在之前的文章中看到的,Jersey 是一个用于开发 RESTful Web 服务的开源框架。如果你对 Jersey 还不熟悉,可以先看看我们之前介绍如何 使用 Jersey 和 Spring 构建 API 的文章。

2. Jersey 中的 Bean Validation

验证(Validation)指的是校验数据是否满足一个或多个预定义约束的过程。在大多数应用中,这都是非常常见的场景。

Java Bean Validation 框架(JSR-380)已经成为 Java 中处理这类操作的事实标准。如果你对 Java Bean Validation 基础还不太熟悉,可以参考我们之前的 教程

Jersey 提供了一个扩展模块来支持 Bean Validation。要在项目中使用该功能,我们首先需要进行配置。接下来的部分将介绍如何配置我们的应用。

3. 应用配置

我们将基于之前文章中提到的简单 Fruit API 示例进行扩展,该示例来自 Jersey MVC 支持 一文。

3.1. Maven 依赖

首先,在 pom.xml 中添加 Bean Validation 的依赖:

<dependency>
    <groupId>org.glassfish.jersey.ext</groupId>
    <artifactId>jersey-bean-validation</artifactId>
    <version>3.1.1</version>
</dependency>

你可以从 Maven Central 获取最新版本。

3.2. 服务端配置

在 Jersey 中,通常我们需要在自定义的资源配置类中注册想要使用的扩展功能。

不过,对于 Bean Validation 扩展来说,幸运的是这是少数几个 Jersey 会自动注册的扩展之一,无需手动注册

最后,为了让验证错误能返回给客户端,我们需要在自定义资源配置类中添加一个服务端属性

public ViewApplicationConfig() {
    packages("com.baeldung.jersey.server");
    property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
}

4. 验证 JAX-RS 资源方法

本节将介绍两种使用约束注解验证输入参数的方式:

  • 使用内置的 Bean Validation API 约束
  • 自定义约束和验证器

4.1. 使用内置约束注解

我们先来看一下内置的约束注解:

@POST
@Path("/create")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public void createFruit(
    @NotNull(message = "Fruit name must not be null") @FormParam("name") String name, 
    @NotNull(message = "Fruit colour must not be null") @FormParam("colour") String colour) {

    Fruit fruit = new Fruit(name, colour);
    SimpleStorageService.storeFruit(fruit);
}

在这个例子中,我们通过两个表单参数 namecolour 创建一个新的 Fruit 对象。我们使用了 Bean Validation API 中的 @NotNull 注解。

这为我们的表单参数加上了一个简单的非空约束。**如果某个参数为 null,则会返回注解中定义的错误信息**。

我们可以通过单元测试来验证这一点:

@Test
public void givenCreateFruit_whenFormContainsNullParam_thenResponseCodeIsBadRequest() {
    Form form = new Form();
    form.param("name", "apple");
    form.param("colour", null);
    Response response = target("fruit/create").request(MediaType.APPLICATION_FORM_URLENCODED)
        .post(Entity.form(form));

    assertEquals("Http Response should be 400 ", 400, response.getStatus());
    assertThat(response.readEntity(String.class), containsString("Fruit colour must not be null"));
}

在上面的测试中,我们使用 JerseyTest 支持类来测试 fruit 资源。我们发送一个 colour 为 null 的 POST 请求,并验证返回的响应是否包含预期的错误信息。

更多内置验证约束可以参考 官方文档

4.2. 定义自定义约束注解

有时候我们需要更复杂的约束。这时可以通过自定义注解来实现

假设我们需要验证水果的序列号是否符合特定格式:

@PUT
@Path("/update")
@Consumes("application/x-www-form-urlencoded")
public void updateFruit(@SerialNumber @FormParam("serial") String serial) {
    //...
}

在这个例子中,serial 参数必须满足 @SerialNumber 注解定义的约束,接下来我们定义这个注解。

首先定义约束注解:

@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = { SerialNumber.Validator.class })
public @interface SerialNumber {

    String message() default "Fruit serial number is not valid";

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

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

然后定义验证器类 SerialNumber.Validator

public class Validator implements ConstraintValidator<SerialNumber, String> {
    @Override
    public void initialize(SerialNumber serial) {
    }

    @Override
    public boolean isValid(String serial, 
        ConstraintValidatorContext constraintValidatorContext) {
        
        String serialNumRegex = "^\\d{3}-\\d{3}-\\d{4}$";
        return Pattern.matches(serialNumRegex, serial);
    }
}

这里的关键是 Validator 类必须实现 ConstraintValidator,其中泛型参数 T 是我们要验证的数据类型,在这里是 String

最后,在 isValid 方法中实现我们自定义的验证逻辑

5. 资源对象验证

Bean Validation API 还允许我们使用 @Valid 注解来验证整个对象

接下来我们将介绍两种使用该注解验证资源对象的方式:

  • 请求资源验证
  • 响应资源验证

我们先给 Fruit 类加上 @Min 注解:

@XmlRootElement
public class Fruit {

    @Min(value = 10, message = "Fruit weight must be 10 or greater")
    private Integer weight;
    //...
}

5.1. 请求资源验证

FruitResource 类中使用 @Valid 来启用验证:

@POST
@Path("/create")
@Consumes("application/json")
public void createFruit(@Valid Fruit fruit) {
    SimpleStorageService.storeFruit(fruit);
}

在这个例子中,如果我们尝试创建一个重量小于 10 的水果,就会触发验证错误

5.2. 响应资源验证

同样地,我们也可以验证响应资源:

@GET
@Valid
@Produces("application/json")
@Path("/search/{name}")
public Fruit findFruitByName(@PathParam("name") String name) {
    return SimpleStorageService.findByName(name);
}

注意,这里我们同样使用了 @Valid 注解。但这次我们将其用在资源方法上,确保返回的响应是有效的

6. 自定义异常处理器

在最后一部分,我们将简单介绍如何创建自定义异常处理器。这在我们需要返回自定义错误响应时非常有用

我们先定义 FruitExceptionMapper

public class FruitExceptionMapper implements ExceptionMapper<ConstraintViolationException> {

    @Override
    public Response toResponse(ConstraintViolationException exception) {
        return Response.status(Response.Status.BAD_REQUEST)
            .entity(prepareMessage(exception))
            .type("text/plain")
            .build();
    }

    private String prepareMessage(ConstraintViolationException exception) {
        StringBuilder message = new StringBuilder();
        for (ConstraintViolation<?> cv : exception.getConstraintViolations()) {
            message.append(cv.getPropertyPath() + " " + cv.getMessage() + "\n");
        }
        return message.toString();
    }
}

我们通过实现 ExceptionMapper 接口来定义自定义异常映射器,使用 ConstraintViolationException 作为泛型参数。

这样当该异常被抛出时,自定义异常映射器的 toResponse 方法就会被调用

在这个简单示例中,我们遍历所有违反约束的情况,并将每个属性和错误信息拼接到响应中。

要使用自定义异常映射器,我们需要在配置中注册它

@Override
protected Application configure() {
    ViewApplicationConfig config = new ViewApplicationConfig();
    config.register(FruitExceptionMapper.class);
    return config;
}

最后,我们添加一个会返回无效 Fruit 的接口来演示异常处理器的效果:

@GET
@Produces(MediaType.TEXT_HTML)
@Path("/exception")
@Valid
public Fruit exception() {
    Fruit fruit = new Fruit();
    fruit.setName("a");
    fruit.setColour("b");
    return fruit;
}

7. 总结

总结一下,本文我们深入探讨了 Jersey 中的 Bean Validation API 扩展。

✅ 首先介绍了如何在 Jersey 中使用 Bean Validation API
✅ 然后展示了如何配置一个示例 Web 应用
✅ 接着讲解了多种在 Jersey 中进行验证的方式
✅ 最后介绍了如何编写自定义异常处理器

如往常一样,本文的完整源代码可以在 GitHub 上找到。


原始标题:Bean Validation in Jersey