1. 概述

本文介绍 Spring Data REST 验证器的基础知识。如果需要先了解 Spring Data REST 的基本概念,建议先阅读这篇入门文章

简单来说,Spring Data REST 允许我们通过 REST API 直接向数据库添加新数据,但在实际持久化前必须确保数据有效性。

本文基于现有项目继续开发,将复用已搭建的项目结构。

如果准备快速上手 Spring Data REST,以下是高效入门方案:

2. 使用验证器

自 Spring 3 起,框架提供了 Validator 接口用于对象验证。

2.1. 验证动机

在之前的文章中,我们定义了包含 nameemail 两个属性的实体类。

创建新资源只需执行:

curl -i -X POST -H "Content-Type:application/json" -d 
  '{ "name" : "Test", "email" : "test@example.com" }' 
  http://localhost:8080/users

此 POST 请求会将 JSON 对象存入数据库,返回结果:

{
  "name" : "Test",
  "email" : "test@example.com",
  "_links" : {
    "self" : {
        "href" : "http://localhost:8080/users/1"
    },
    "websiteUser" : {
        "href" : "http://localhost:8080/users/1"
    }
  }
}

由于提供了有效数据,结果符合预期。但如果移除 name 属性或将其设为空字符串会怎样?

测试第一种场景,将 name 设为空字符串:

curl -i -X POST -H "Content-Type:application/json" -d 
  '{ "name" : "", "email" : "Baggins" }' http://localhost:8080/users

响应结果:

{
  "name" : "",
  "email" : "Baggins",
  "_links" : {
    "self" : {
        "href" : "http://localhost:8080/users/1"
    },
    "websiteUser" : {
        "href" : "http://localhost:8080/users/1"
    }
  }
}

测试第二种场景,移除 name 属性:

curl -i -X POST -H "Content-Type:application/json" -d 
  '{ "email" : "Baggins" }' http://localhost:8080/users

响应结果:

{
  "name" : null,
  "email" : "Baggins",
  "_links" : {
    "self" : {
        "href" : "http://localhost:8080/users/2"
    },
    "websiteUser" : {
        "href" : "http://localhost:8080/users/2"
    }
  }
}

❌ 两个请求都成功返回了 201 状态码和对象链接。这种无法阻止不完整数据入库的行为显然不可接受。

2.2. Spring Data REST 事件

每次调用 Spring Data REST 接口时,导出器会生成以下事件:

  • BeforeCreateEvent
  • AfterCreateEvent
  • BeforeSaveEvent
  • AfterSaveEvent
  • BeforeLinkSaveEvent
  • AfterLinkSaveEvent
  • BeforeDeleteEvent
  • AfterDeleteEvent

所有事件处理方式类似,这里仅以 beforeCreateEvent 为例(对象入库前触发)。

2.3. 定义验证器

自定义验证器需实现 org.springframework.validation.Validator 接口:

public class WebsiteUserValidator implements Validator {

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

    @Override
    public void validate(Object obj, Errors errors) {
        WebsiteUser user = (WebsiteUser) obj;
        if (checkInputString(user.getName())) {
            errors.rejectValue("name", "name.empty");
        }
   
        if (checkInputString(user.getEmail())) {
            errors.rejectValue("email", "email.empty");
        }
    }

    private boolean checkInputString(String input) {
        return (input == null || input.trim().length() == 0);
    }
}

Errors 对象用于收集所有验证错误。通过 errors.rejectValue(字段名, 错误消息) 添加错误。

定义验证器后,需将其映射到特定事件。有三种注册方式:

方式一:使用 @Component 注解

@Component("beforeCreateWebsiteUserValidator")
public class WebsiteUserValidator implements Validator {
    ...
}

Spring Boot 会自动识别 beforeCreate 前缀和 WebsiteUser 类名。

方式二:使用 @Bean 注解

@Bean
public WebsiteUserValidator beforeCreateWebsiteUserValidator() {
    return new WebsiteUserValidator();
}

方式三:手动注册

@SpringBootApplication
public class SpringDataRestApplication implements RepositoryRestConfigurer {
    public static void main(String[] args) {
        SpringApplication.run(SpringDataRestApplication.class, args);
    }
    
    @Override
    public void configureValidatingRepositoryEventListener(
      ValidatingRepositoryEventListener v) {
        v.addValidator("beforeCreate", new WebsiteUserValidator());
    }
}

此方式无需在验证器类添加注解。

2.4. 事件发现 Bug

⚠️ 当前 Spring Data REST 存在事件发现 Bug,可能导致验证器未被触发。

临时解决方案:手动注册所有事件到 ValidatingRepositoryEventListener

@Configuration
public class ValidatorEventRegister implements InitializingBean {

    @Autowired
    ValidatingRepositoryEventListener validatingRepositoryEventListener;

    @Autowired
    private Map<String, Validator> validators;

    @Override
    public void afterPropertiesSet() throws Exception {
        List<String> events = Arrays.asList("beforeCreate");
        for (Map.Entry<String, Validator> entry : validators.entrySet()) {
            events.stream()
              .filter(p -> entry.getKey().startsWith(p))
              .findFirst()
              .ifPresent(
                p -> validatingRepositoryEventListener
               .addValidator(p, entry.getValue()));
        }
    }
}

3. 测试验证

2.1 节中我们看到,没有验证器时缺失 name 的对象也能入库。现在添加验证器后测试:

curl -i -X POST -H "Content-Type:application/json" -d 
  '{ "email" : "another@example.com" }' http://localhost:8080/users

响应变为:

{  
   "timestamp":1472510818701,
   "status":406,
   "error":"Not Acceptable",
   "exception":"org.springframework.data.rest.core.
    RepositoryConstraintViolationException",
   "message":"Validation failed",
   "path":"/users"
}

✅ 缺失数据被成功拦截,对象未入库。但错误信息不够友好,需自定义异常处理:

@ControllerAdvice
public class RestResponseEntityExceptionHandler extends
  ResponseEntityExceptionHandler {

    @ExceptionHandler({ RepositoryConstraintViolationException.class })
    public ResponseEntity<Object> handleAccessDeniedException(
      Exception ex, WebRequest request) {
          RepositoryConstraintViolationException nevEx = 
            (RepositoryConstraintViolationException) ex;

          String errors = nevEx.getErrors().getAllErrors().stream()
            .map(p -> p.toString()).collect(Collectors.joining("\n"));
          
          return new ResponseEntity<Object>(errors, new HttpHeaders(),
            HttpStatus.PARTIAL_CONTENT);
    }
}

现在响应会包含具体错误信息,便于调试。

4. 总结

本文验证了验证器在 Spring Data REST 中的必要性,它为数据插入提供了关键的安全保障。通过注解方式创建验证器简单高效,能有效防止脏数据入库。

完整代码示例可在 GitHub 项目 中获取。


原始标题:Guide to Spring Data REST Validators