本文通过示例说明,在 Springboot 中如何自定义 Validator,以及如何实现国际化的错误信息返回。注意,本文代码千万别直接照抄,有可能会出大事情的。先留个悬念,读者朋友们能从中看出有什么问题吗?
springboot
官网中下载模板,直接通过示例中的
GreetingController
添加实现逻辑。
@RestController public class GreetingController {
private static final String template = "Hello, %s!"; private final AtomicLong counter = new AtomicLong();
@RequestMapping("/greeting") public Response<Greeting> greeting(@RequestParam(value = "name", defaultValue = "World") String name) { if (!"tangleithu".equals(name)) { throw new BadRequestException("user.notFound"); } return Response.ok(new Greeting(counter.incrementAndGet(), String.format(template, name))); } }
|
以上代码直接源自
官方 spring-guides 的 demo
,我稍微改吧改吧。正常情况下,能返回正确的结果:
# curl "localhost:8080/greeting?name=tangleithu&lang=en"
{ "code": 0, "data": { "content": "Hello, tangleithu!", "id": 9 }, "message": "success" }
|
JSR #380 Bean Validation 2.0
。
回到本文的 demo 中,假设在我们业务逻辑中需要传递一个
UserForm
,接收
age,name,param
三个参数。并对其中输入进行进行校验,其中,
param
没有具体的含义,只是为了说明问题。
public class UserForm { @Min(value = 0, message = "validate.userform.age") @Max(value = 120, message = "validate.userform.age") private int age;
@NotNull(message = "validate.userform.name.notEmpty") private String name;
@CustomParam(message = "validate.userform.param.custom") private String param; ... }
@RequestMapping("/user") public Response<Greeting> createUser(@Valid @RequestBody UserForm userForm) { return Response.ok(new Greeting(counter.incrementAndGet(), String.format(template, userForm.getName()))); }
|
代码如上,上面示例只用了很简单的
@Min, @Max, @NotNull
等约束条件,通过名字就能看出来含义。更多约束规则可以直接看对应源码
javax.validation.constraints.xxx
,比如有常见的
Email
等格式校验。
默认情况下,违反相应的约束条件后,默认的输出比较啰嗦,例如用这个请求
curl -H "Content-Type: application/json" -d "{}" "localhost:8080/user"
,对应的输出如下:
{ "error": "Bad Request", "errors": [ { "arguments": [ { "arguments": null, "code": "name", "codes": [ "userForm.name", "name" ], "defaultMessage": "name" } ], "bindingFailure": false, "code": "NotBlank", "codes": [ "NotBlank.userForm.name", "NotBlank.name", "NotBlank.java.lang.String", "NotBlank" ], "defaultMessage": "must not be blank", "field": "name", "objectName": "userForm", "rejectedValue": null } ], "message": "Validation failed for object='userForm'. Error count: 1", "path": "/user", "status": 400, "timestamp": "2020-05-10T08:44:12.952+0000" }
|
其实依葫芦画瓢,debug 的时候,把抛出的具体异常添加到前面的
GlobalExceptionHandler
,再修改下默认的行为即可。
@ExceptionHandler(BindException.class) @ResponseBody public ResponseEntity handle(HttpServletRequest request, BindException e){ String key = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); String i18message = getI18nMessage(key, request); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Response.error(400, i18message)); }
@ExceptionHandler(MethodArgumentNotValidException.class) @ResponseBody public ResponseEntity handle(HttpServletRequest request, MethodArgumentNotValidException e){ String key = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); String i18message = getI18nMessage(key, request); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Response.error(400, i18message)); }
@ExceptionHandler(ConstraintViolationException.class) @ResponseBody public ResponseEntity handle(HttpServletRequest request, ConstraintViolationException e){ String key = e.getConstraintViolations().iterator().next().getMessage(); String i18message = getI18nMessage(key, request); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Response.error(400, i18message)); }
|
改进后,增加自定义的 handler 后,返回信息结构一致方便前端统一处理,同时也简洁不少:
{ "code": 400, "data": null, "message": "validate.userform.name.notEmpty" }
|
再结合前面讲解的通过i18n的参数配置,又可以实现当没通过校验的时候,错误信息统一由对应的国际化资源文件进行配置了。
github
),但一定要注意不要完全照抄,上面说的这个安全漏洞还挺严重的。给予点提示,就是在
CustomValidator
的具体实现中,有朋友了解吗?欢迎留言讨论。我将在后面的文章中来讲述这个安全漏洞。