在 Spring Boot 中校验 Boolean 类型

1、简介

在本文中,我们将学习如何在 Spring Boot 中的不同层(如 controller 或 service)上验证布尔(Boolean)类型以及执行验证的各种方式。

2、编程式验证

Boolean 类提供了两个创建该类实例的基本方法:Boolean.valueOf()Boolean.parseBoolean()

Boolean.valueOf() 可接受 Stringboolean 值。它会检查输入参数的值是 true 还是 false,并相应地提供一个 Boolean 对象。Boolean.parseBoolean() 方法只接受 String 值。

这些方法不区分大小写,例如,trueTrueTRUEfalseFALSE 都是可以的。

通过单元测试来验证 StringBoolean 的转换:

@Test
void givenInputAsString_whenStringToBoolean_thenValidBooleanConversion() {
    assertEquals(Boolean.TRUE, Boolean.valueOf("TRUE"));
    assertEquals(Boolean.FALSE, Boolean.valueOf("false"));
    assertEquals(Boolean.TRUE, Boolean.parseBoolean("True"));
}

验证从基本 boolean 值到 Boolean 封装类的转换:

@Test
void givenInputAsboolean_whenbooleanToBoolean_thenValidBooleanConversion() {
    assertEquals(Boolean.TRUE, Boolean.valueOf(true));
    assertEquals(Boolean.FALSE, Boolean.valueOf(false));
}

3、使用自定义 Jackson Deserializer 进行验证

Spring Boot API 经常需要处理 JSON 数据,我们还要了解如何通过数据反序列化验证 JSON 到 Boolean 值的转换。我们可以使用自定义 deserializer 反序列化 boolean 值的自定义表示。

考虑这样一种情况,即我们要解析表示 boolean 值的 JSON 数据,该布尔值包含 +(表示 true)和 -(表示 false)。

编写一个 JSON deserializer 来实现:

public class BooleanDeserializer extends JsonDeserializer<Boolean> {
    @Override
    public Boolean deserialize(JsonParser parser, DeserializationContext context) throws IOException {
        String value = parser.getText();
        if (value != null && value.equals("+")) {
            return Boolean.TRUE;
        } else if (value != null && value.equals("-")) {
            return Boolean.FALSE;
        } else {
            throw new IllegalArgumentException("Only values accepted as Boolean are + and -");
        }
    }
}

4、使用注解进行 Bean 验证(Validation)

Bean Validation 约束是验证字段的另一种常用方法。要使用这种方法,需要添加 Spring-boot-starter-validation 依赖。在所有可用的验证注解中,有三个可用于 Boolean 字段:

  • @NotNull: 如果 Boolean 字段为空,则验证失败。
  • @AssertTrue: 如果 Boolean 字段设置为 false,则验证失败。
  • @AssertFalse: 如果 Boolean 字段设置为 true,则验证失败。

注意,@AssertTrue@AssertFalse 都将 null 值视为合法值。也就是说,如果我们想确保只接受实际的 boolean 值,就需要将这两个注解与 @NotNull 结合使用。

5、Boolean Validation 示例

在 Controller 层和 Service 层使用 Bean 约束和自定义 JSON 反序列化器,来进行演示。

创建一个名为 BooleanObject 的自定义对象,其中包含四个 Boolean 类型的参数。每个参数都将使用不同的验证方法:

public class BooleanObject {

    @NotNull(message = "boolField cannot be null")
    Boolean boolField;

    @AssertTrue(message = "trueField must have true value")
    Boolean trueField;

    @NotNull(message = "falseField cannot be null")
    @AssertFalse(message = "falseField must have false value")
    Boolean falseField;

    @JsonDeserialize(using = BooleanDeserializer.class)
    Boolean boolStringVar;

    // get / set 方法省略
}

6、Controller 中的校验

当通过 RequestBody 向 REST 端点传递对象时,可以使用 @Valid 注解来校验对象。

@Valid 注解应用于方法参数时,Spring 就会校验相应的参数对象:

@RestController
public class ValidationController {

    @Autowired
    ValidationService service;

    @PostMapping("/validateBoolean")
    public ResponseEntity<String> processBooleanObject(@RequestBody @Valid BooleanObject booleanObj) {
        return ResponseEntity.ok("BooleanObject is valid");
    }

    @PostMapping("/validateBooleanAtService")
    public ResponseEntity<String> processBooleanObjectAtService() {
        BooleanObject boolObj = new BooleanObject();
        boolObj.setBoolField(Boolean.TRUE);
        boolObj.setTrueField(Boolean.FALSE);
        service.processBoolean(boolObj);
        return ResponseEntity.ok("BooleanObject is valid");
    }
}

校验完成后,如果发现任何校验失败的情况,Spring 会抛出 MethodArgumentNotValidException。可以使用带有相关 ExceptionHandler 方法的 ControllerAdvice 来处理校验失败异常。

创建三个方法,分别处理 controller 层和 service 层抛出的异常:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    public String handleValidationException(MethodArgumentNotValidException ex) {
        return ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(e -> e.getDefaultMessage())
            .collect(Collectors.joining(","));
    }

    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    public String handleIllegalArugmentException(IllegalArgumentException ex) {
        return ex.getMessage();
    }

    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public String handleConstraintViolationException(ConstraintViolationException ex) {
       return ex.getMessage();
    }
}

在测试 REST 功能之前,建议先在 Spring Boot 中测试 API。

创建 controller 测试类:

@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = ValidationController.class)
class ValidationControllerUnitTest {

    @Autowired
    private MockMvc mockMvc;

    @TestConfiguration
    static class EmployeeServiceImplTestContextConfiguration {
        @Bean
        public ValidationService validationService() {
            return new ValidationService() {};
        }
    }

    @Autowired
    ValidationService service;
}

有了这些,我们现在就可以测试我们在类中使用的 Validation 注解了。

6.1、@NotNull 注解

让我们看看 @NotNull 是如何工作的。当我们传递带有 null Boolean 参数的 BooleanObject 时,@Valid 注解将校验 bean 并抛出一个 “400 Bad Request” 的 HTTP 响应:

@Test
void whenNullInputForBooleanField_thenHttpBadRequestAsHttpResponse() throws Exception {
    String postBody = "{\"boolField\":null,\"trueField\":true,\"falseField\":false,\"boolStringVar\":\"+\"}";

    mockMvc.perform(post("/validateBoolean").contentType("application/json")
            .content(postBody))
        .andExpect(status().isBadRequest());
}

6.2、@AssertTrue 注解

接下来,测试 @AssertTrue。当我们传递带有 false Boolean 值参数的 BooleanObject 时,@Valid 注解将校验 bean 并抛出一个 “400 Bad Request” 的 HTTP 响应。如果我们捕获响应体,就能获得 @AssertTrue 注解中设置的错误信息:

 @Test
 void whenInvalidInputForTrueBooleanField_thenErrorResponse() throws Exception {
     String postBody = "{\"boolField\":true,\"trueField\":false,\"falseField\":false,\"boolStringVar\":\"+\"}";

     String output = mockMvc.perform(post("/validateBoolean").contentType("application/json")
             .content(postBody))
         .andReturn()
         .getResponse()
         .getContentAsString();

     assertEquals("trueField must have true value", output);
 }

如果提供 null 值会发生什么?由于我们只用 @AssertTrue 注解了字段,而没有用 @NotNull 注解,因此不会出现校验失败的异常。

 @Test
 void whenNullInputForTrueBooleanField_thenCorrectResponse() throws Exception {
    String postBody = "{\"boolField\":true,\"trueField\":null,\"falseField\":false,\"boolStringVar\":\"+\"}";

    mockMvc.perform(post("/validateBoolean").contentType("application/json")
                    .content(postBody))
            .andExpect(status().isOk());
}

6.3、@AssertFalse 注解

现在我们来了解 @AssertFalse 的工作原理。当为 @AssertFalse 参数传递一个 true 值时,@Valid 注解会抛出一个错误的请求(Bad Request)。我们可以在响应体中获取针对 @AssertFalse 注解设置的错误信息。

 @Test
 void whenInvalidInputForFalseBooleanField_thenErrorResponse() throws Exception {
     String postBody = "{\"boolField\":true,\"trueField\":true,\"falseField\":true,\"boolStringVar\":\"+\"}";

     String output = mockMvc.perform(post("/validateBoolean").contentType("application/json")
             .content(postBody))
         .andReturn()
         .getResponse()
         .getContentAsString();

     assertEquals("falseField must have false value", output);
 }

我们再来看看如果提供 null 值会发生什么。我们同时用 @AssertFalse@NotNull 对字段进行了注解,因此会出现校验失败异常。

 @Test
 void whenNullInputForFalseBooleanField_thenHttpBadRequestAsHttpResponse() throws Exception {
    String postBody = "{\"boolField\":true,\"trueField\":true,\"falseField\":null,\"boolStringVar\":\"+\"}";

    mockMvc.perform(post("/validateBoolean").contentType("application/json")
                    .content(postBody))
            .andExpect(status().isBadRequest());
 }

6.4、自定义 JSON Deserializer 来验证 Boolean 类型

使用自定义 JSON deserializer 来验证标记的参数。自定义 deserializer 只接受 +- 值。如果传递任何其他值,验证将失败并抛出异常。

在输入的 JSON 中传递 + 文本值,看看校验是如何工作的:

@Test
void whenInvalidBooleanFromJson_thenErrorResponse() throws Exception {
    String postBody = "{\"boolField\":true,\"trueField\":true,\"falseField\":false,\"boolStringVar\":\"plus\"}";

    String output = mockMvc.perform(post("/validateBoolean").contentType("application/json")
            .content(postBody))
        .andReturn()
        .getResponse()
        .getContentAsString();

    assertEquals("Only values accepted as Boolean are + and -", output);
}

最后,测试一下最理想的情况。将 + 号作为自定义反序列化字段的输入。由于这是一个有效的输入,验证将通过并给出成功的响应。

 @Test
 void whenAllBooleanFieldsValid_thenCorrectResponse() throws Exception {
     String postBody = "{\"boolField\":true,\"trueField\":true,\"falseField\":false,\"boolStringVar\":\"+\"}";

     String output = mockMvc.perform(post("/validateBoolean").contentType("application/json")
             .content(postBody))
         .andReturn()
         .getResponse()
         .getContentAsString();

     assertEquals("BooleanObject is valid", output);
 }

7、Service 中的校验

现在我们来看看 service 层的校验。使用 @Validated 注解 service 类,并将 @Valid 注解放在方法参数上。这两个注解的组合会对方法参数进行校验。

与 controller 层的 @RequestBody 不同,service 层的校验是针对简单的 Java 对象进行的,因此框架会在校验失败时抛出 ConstraintViolationException 异常。在这种情况下,Spring 框架会返回 HttpStatus.INTERNAL_SERVER_ERROR 状态的响应。

在 controller 层创建或修改对象,然后将其传递到 service 层进行处理时,最好使用 service 层验证。

service 类大体如下:

@Service
@Validated
public class ValidationService {

    public void processBoolean(@Valid BooleanObject booleanObj) {
        // 业务逻辑
    }
}

在前面的部分中,我们创建了一个端点来测试 service 层,并编写了异常处理方法来处理 ConstraintViolationException

现在编写一个新的测试用例来检查这个功能:

@Test
void givenAllBooleanFieldsValid_whenServiceValidationFails_thenErrorResponse() throws Exception {
    mockMvc.perform(post("/validateBooleanAtService").contentType("application/json"))
        .andExpect(status().isInternalServerError());
}

8、总结

本文详细介绍了如何通过编程式验证、Bean Validation 注解和使用自定义 JSON deserializer 在 controller 层和 service 层验证 Boolean 类型。


参考:https://www.baeldung.com/spring-boot-validate-boolean-type