在开放平台的时候,尤其是坐Rest服务的时候,因为用户可以给你传输任何数据,因此,我们需要在服务的入口处添加一层Validator以保证所有无效的数据不泛滥到后续的处理流程中,该Validator会以Fail Fast的形式,尽早告诉客户请求不合法,以及为什么不合法。在Spring MVC框架中可以非常容易实现这样的一层Validator机制。
在Rest服务中,需要验证的输入分为2个部分:
[list]
[*]URL路径参数
[*]Request Bean Data,通常情况下为一个Bean的Json格式表述。
[/list]
[size=large][b]验证URL参数的验证[/b][/size]
对于URL参数的验证,Spring只能帮我们做到限制类型,剩下的还是需要我们自己写代码处理。栗子:
这个服务中,首先要求URL参数requestId为Int类型,然后在代码中进一步限制必须小于10,如果用户输入的RequestId不为数字,或者超出整数的范围,Spring会抛出TypeMismatchException, 然后在我上一篇文章([url]http://ningandjiao.iteye.com/admin/blogs/1995270[/url])中提到的ExceptionHandler中捕获这个异常,给用户反馈就行了.
[b]测试代码:[/b]
[b]实现代码:[/b]
如果想处理的更细致一点,可以读取TypeMismatchException中的root cause做更为细致的Response处理。
[size=large][b]Request Bean Validator[/b][/size]
首先,需明确的Bean Validation是一个JAVA规范(JSR-303/JSR-349 Validator),并不是专属于Spring的一个概念,Spring MVC只是让该规范可以更加友好的运用到Spring MVC应用中。只要你把Bean Validation Provider的jar包添加到classpath下,Spring就可以自动的侦测到,并把Bean Validator机制应用到所有的Controller中。 目前常用的Bean Validator是Hibernate Validator。栗子:
假设我们又一个服务叫做Propagate, 是一个操作, Request是一个AuditDeatail对象,其中包含userId,dateTime两个Property,即谁在什么时候做了这个操作。但是,应用的限制条件是userId为一个小于10位的字符串,dateTime的格式必须为“yyyy-MM-dd HH:mm:ss”,并且不能是未来的时间。如果验证失败则返回对应的错误信息。
[b]测试代码:[/b]
[b]实现代码:[/b]
首先,把Hibernate Validator相关的依赖包添加到classpath下:
然后,添加服务,在需要Bean Validator的参数上添加@Valid注解
接着,创建Bean,并为property添加想要的限制条件,其中的CustomDateSerializer,CustomDateSerializer类是辅助json序列化的。
经过上面的步骤,应用已经具备了验证Property的功能,但是,验证失败了我们如何捕获失败,并生成响应的异常呢? 同样的,需要通过异常处理机制来做这个工作,在SpringMVC中,当Json序列化,反序列化失败的时候,会抛出HttpMessageNotReadableException异常, 当Bean validation失败的时候,会抛出MethodArgumentNotValidException异常,因此,只需要在ExceptionHandler类中添加处理对应异常的方法即可。
除了栗子中用到的这些验证机制,Hibernate Validator还提供了很多内置的验证规则, 具体的请查看:[url]http://docs.jboss.org/hibernate/validator/5.0/reference/en-US/html_single/#section-builtin-constraints[/url]
本文中的所有代码可以从github中拿到:[url]https://github.com/xianlinbox/TDD_Demo/tree/master/spring-ws-rest[/url]
在Rest服务中,需要验证的输入分为2个部分:
[list]
[*]URL路径参数
[*]Request Bean Data,通常情况下为一个Bean的Json格式表述。
[/list]
[size=large][b]验证URL参数的验证[/b][/size]
对于URL参数的验证,Spring只能帮我们做到限制类型,剩下的还是需要我们自己写代码处理。栗子:
@RequestMapping(value = "/requests/{requestId}", method = RequestMethod.GET)
public Request get(@PathVariable int requestId, @RequestParam(value = "userId") String userId) {
if (requestId > 10) {
throw new InvalidRequestIdException("Request id must less than 10");
}
if (requestId == 10) {
throw new RuntimeException("Unexpected Server Error");
}
return new Request(userId, requestId, "GET");
}
这个服务中,首先要求URL参数requestId为Int类型,然后在代码中进一步限制必须小于10,如果用户输入的RequestId不为数字,或者超出整数的范围,Spring会抛出TypeMismatchException, 然后在我上一篇文章([url]http://ningandjiao.iteye.com/admin/blogs/1995270[/url])中提到的ExceptionHandler中捕获这个异常,给用户反馈就行了.
[b]测试代码:[/b]
@Test
public void shouldGetStatus404WhenRquestIdOutOfIntegerRanger() throws Exception {
mockMvc.perform(get("/requests/11123456789")
.contentType(MediaType.APPLICATION_JSON)
.param("userId", "xianlinbox")
)
.andExpect(status().isBadRequest())
.andExpect(content().string("Request id must be an integer"));
}
@Test
public void shouldGetStatus404WhenRquestIdIsNotInteger() throws Exception {
mockMvc.perform(get("/requests/123ss")
.contentType(MediaType.APPLICATION_JSON)
.param("userId", "xianlinbox")
)
.andExpect(status().isBadRequest())
.andExpect(content().string("Request id must be an integer"));
}
[b]实现代码:[/b]
@ExceptionHandler(TypeMismatchException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ResponseBody
public String handleTypeMismatchException(TypeMismatchException ex) {
return "Request id must be an integer";
}
如果想处理的更细致一点,可以读取TypeMismatchException中的root cause做更为细致的Response处理。
[size=large][b]Request Bean Validator[/b][/size]
首先,需明确的Bean Validation是一个JAVA规范(JSR-303/JSR-349 Validator),并不是专属于Spring的一个概念,Spring MVC只是让该规范可以更加友好的运用到Spring MVC应用中。只要你把Bean Validation Provider的jar包添加到classpath下,Spring就可以自动的侦测到,并把Bean Validator机制应用到所有的Controller中。 目前常用的Bean Validator是Hibernate Validator。栗子:
假设我们又一个服务叫做Propagate, 是一个操作, Request是一个AuditDeatail对象,其中包含userId,dateTime两个Property,即谁在什么时候做了这个操作。但是,应用的限制条件是userId为一个小于10位的字符串,dateTime的格式必须为“yyyy-MM-dd HH:mm:ss”,并且不能是未来的时间。如果验证失败则返回对应的错误信息。
[b]测试代码:[/b]
@Test
public void testPropagateWithIllegalUserId() throws Exception {
mockMvc.perform(post("/propagate")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content("{\"userId\":\"xianlinbox2\",\"dateTime\":\"2013-12-25 10:10:00\"}")
)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("001"))
.andExpect(jsonPath("$.message").value("Invalid Request:\nuserId must less than 10 digits\n"));
}
@Test
public void testPropagateWithFutureDateTime() throws Exception {
mockMvc.perform(post("/propagate")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content("{\"userId\":\"xianlinbox\",\"dateTime\":\"2014-12-25 10:10:00\"}")
)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("001"))
.andExpect(jsonPath("$.message").value("Invalid Request:\nDateTime should not in future\n"));
}
@Test
public void testPropagateWithWrongFormatDateTime() throws Exception {
mockMvc.perform(post("/propagate")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content("{\"userId\":\"xianlinbox\",\"dateTime\":\"2014-12-25\"}")
)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("001"))
.andExpect(jsonPath("$.message").value("Invalid format: \"2014-12-25\" is too short"));
}
@Test
public void testPropagateWithBothIllegalUserIdAndIllegalDateTime() throws Exception {
mockMvc.perform(post("/propagate")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content("{\"userId\":\"xianlinbox2\",\"dateTime\":\"2014-12-25 10:10:00\"}")
)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("001"))
.andExpect(jsonPath("$.message").value(containsString("DateTime should not in future")))
.andExpect(jsonPath("$.message").value(containsString("userId must less than 10 digits")));
}
[b]实现代码:[/b]
首先,把Hibernate Validator相关的依赖包添加到classpath下:
org.hibernate:hibernate-validator:5.0.2.Final",
"javax.el:javax.el-api:3.0.0",
'org.glassfish.web:el-impl:2.2',
然后,添加服务,在需要Bean Validator的参数上添加@Valid注解
@RequestMapping(value = "/propagate", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.ACCEPTED)
public AuditDetail propagate(@Valid @RequestBody AuditDetail operationBy) {
return operationBy;
}
接着,创建Bean,并为property添加想要的限制条件,其中的CustomDateSerializer,CustomDateSerializer类是辅助json序列化的。
public class AuditDetail {
@NotNull
@Length(max = 10, message = "userId must less than 10 digits")
private String userId;
@Past(message = "DateTime should not in future")
private DateTime dateTime;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
@JsonSerialize(using = CustomDateSerializer.class)
public DateTime getDateTime() {
return dateTime;
}
@JsonDeserialize(using = CustomDateSerializer.class)
public void setDateTime(DateTime dateTime) {
this.dateTime = dateTime;
}
}
public class CustomDateSerializer extends JsonSerializer<DateTime> {
@Override
public void serialize(DateTime value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
jgen.writeString(value.toString("yyyy-MM-dd HH:mm:ss"));
}
}
public class CustomDateDeserializer extends JsonDeserializer<DateTime> {
@Override
public DateTime deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
return DateTime.parse(jp.getText(), DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"));
}
}
经过上面的步骤,应用已经具备了验证Property的功能,但是,验证失败了我们如何捕获失败,并生成响应的异常呢? 同样的,需要通过异常处理机制来做这个工作,在SpringMVC中,当Json序列化,反序列化失败的时候,会抛出HttpMessageNotReadableException异常, 当Bean validation失败的时候,会抛出MethodArgumentNotValidException异常,因此,只需要在ExceptionHandler类中添加处理对应异常的方法即可。
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ResponseBody
public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
String errorMesssage = "Invalid Request:\n";
for (FieldError fieldError : bindingResult.getFieldErrors()) {
errorMesssage += fieldError.getDefaultMessage() + "\n";
}
return new ErrorResponse("001", errorMesssage);
}
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ResponseBody
public ErrorResponse handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) {
return new ErrorResponse("001", ex.getRootCause().getMessage());
}
除了栗子中用到的这些验证机制,Hibernate Validator还提供了很多内置的验证规则, 具体的请查看:[url]http://docs.jboss.org/hibernate/validator/5.0/reference/en-US/html_single/#section-builtin-constraints[/url]
本文中的所有代码可以从github中拿到:[url]https://github.com/xianlinbox/TDD_Demo/tree/master/spring-ws-rest[/url]