Spring Data REST验证器指南

2023/05/18

1. 概述

本文是对Spring Data REST验证器的基本介绍。如果你需要先了解Spring Data REST的基础知识,请务必阅读本文

简单地说,使用Spring Data REST,我们可以简单地通过REST API向数据库中添加一条新记录,但是我们当然也需要确保数据在实际持久化之前是有效的。

本文基于之前的文章,重用之前构建过的项目。

2. 使用验证器

从Spring 3开始,该框架具有Validator接口,它可用于验证对象。

2.1 动机

在之前的文章中,我们定义了具有两个属性name和email的实体WebsiteUser。

因此,要创建一个新资源,我们只需要运行:

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

这个POST请求会将提供的JSON对象保存到我们的数据库中,返回的内容为:

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

因为我们提供了有效的数据,所有预计会得到正确的结果。但是,如果我们删除属性name,或者只是将name的值设置为空字符串会发生什么?

为了测试第一个场景,我们修改之前运行过的命令,将空字符串设置为属性name的值:

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

执行以上cURL命令,我们将得到以下响应:

{
	"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状态代码和到我们对象的API链接来确认这一点。

但是这种行为是不可接受的,因为我们希望避免将部分数据插入数据库。

2.2 Spring Data REST事件

在每次调用Spring Data REST API时,Spring Data REST导出器都会生成各种事件,如下所示:

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

由于所有事件都是以类似的方式处理,因此我们只演示如何处理在将新对象保存到数据库之前生成的beforeCreateEvent。

2.3 定义验证器

为了创建我们自己的验证器,我们需要实现org.springframework.validation.Validator接口,并实现supports和validate方法。support方法检查验证器是否支持提供的请求,而validate方法验证请求中提供的数据。

下面我们定义一个WebsiteUserValidator类:

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对象是一个特殊的类,旨在包含validate方法中提供的所有错误。在本文的后面,我们会演示如何使用包含在Errors对象中的提供的消息。要添加新的错误消息,我们必须调用errors.rejectValue(nameOfField, errorMessage)。

在定义了验证器之后,我们需要将其映射到请求被接受后生成的特定事件。

例如,在我们的例子中,生成beforeCreateEvent是因为我们想将一个新对象插入到我们的数据库中。但由于我们想要验证请求中的对象,我们需要首先定义验证器。

这可以通过三种方式实现:

  • 添加name属性为“beforeCreateWebsiteUserValidator”的组件注解。Spring Boot会识别前缀beforeCreate来确定我们要捕获的事件,并且它还会从组件名称中识别WebsiteUser类。

    @Component("beforeCreateWebsiteUserValidator")
    public class WebsiteUserValidator implements Validator {
        // ...
    }
    
  • 使用@Bean注解在应用程序上下文中创建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());
        }
    }
    
    • 对于这种情况,你不需要在WebsiteUserValidator类上添加任何注解。

2.4 事件发现错误

目前,Spring Data REST中存在一个错误,这会影响事件发现。

如果我们调用生成beforeCreate事件的POST请求,我们的应用程序将不会调用验证器,因为由于这个错误,该事件将不会被发现。

解决此问题的一个简单方法是将所有事件插入Spring Data REST 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属性的对象添加到我们的数据库中,而这不是我们想要的行为,因为这不会检查数据的完整性。

如果我们想添加没有name属性相同对象,并且使用我们提供的验证器,则会得到以下错误:

curl -i -X POST -H "Content-Type:application/json" -d '{"email": "test@test.com"}' http://localhost:8080/users
{
	"timestamp": 1472510818701,
	"status": 406,
	"error": "Not Acceptable",
	"exception": "org.springframework.data.rest.core.RepositoryConstraintViolationException",
	"message": "Validation failed",
	"path": "/users"
}

如我们所见,检测到请求中缺少数据,并且没有将对象保存到数据库中。我们的请求返回了500 HTTP码和内部错误消息。

错误消息没有说明我们请求中的问题。如果我们想让它更具信息性,我们将不得不修改响应对象。

Spring中的异常处理一文中,我们演示了如何处理框架生成的异常,因此在这一点上,这绝对是一篇不错的文章。

由于我们的应用程序生成了一个RepositoryConstraintViolationException异常,我们将为该特定异常创建一个处理程序,它将修改响应消息。

下面是我们的RestResponseEntityExceptionHandle类:

@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 API都是必不可少的,它为数据插入提供了额外的安全层。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章