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);
}
在这个例子中,我们通过两个表单参数 name 和 colour 创建一个新的 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 上找到。