Spring Boot 参数校验
作为服务端开发,验证前端传入的参数的合法性是一个必不可少的步骤,但是验证参数基本上是一个体力活,而且冗余代码繁多,也影响代码的可阅读性,所以有没有一个比较优雅的方式来解决这个问题?
JSR-303验证框架,JSR-303 是Java EE 6 中的一项子规范,叫做BeanValidation,官方参考实现是Hibernate Validator(与Hibernate ORM 没有关系),JSR 303 用于对Java Bean 中的字段的值进行验证,确保输入进来的数据在语义上是正确的,使验证逻辑从业务代码中脱离出来。JSR303是运行时数据验证框架,验证之后验证的错误信息会马上返回。
基于spring-boot的验证参数比较简单,在spring-boot-starter-web包里面有hibernate-validator包,它提供了一系列验证各种参数的方法,所以说spring-boot已经帮我们想好要怎么解决这个问题了。
image首先,在项目中引入 web 模块的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
具体以及常用的 constraint 包含如下:
@Data
public class Validate {
// 空和非空检查: @Null、@NotNull、@NotBlank、@NotEmpty
@Null(message = "验证是否为 null")
private Integer isNull;
@NotNull(message = "验证是否不为 null, 但无法查检长度为0的空字符串")
private Integer id;
@NotBlank(message = "检查字符串是不是为 null,以及去除空格后长度是否大于0")
private String name;
@NotEmpty(message = "检查是否为 NULL 或者是 EMPTY")
private List<String> stringList;
// Boolean值检查: @AssertTrue、@AssertFalse
@AssertTrue(message = " 验证 Boolean参数是否为 true")
private Boolean isTrue;
@AssertFalse(message = "验证 Boolean 参数是否为 false ")
private Boolean isFalse;
// 长度检查: @Size、@Length
@Size(min = 1, max = 2, message = "验证(Array,Collection,Map,String)长度是否在给定范围内")
private List<Integer> integerList;
@Length(min = 8, max = 30, message = "验证字符串长度是否在给定范围内")
private String address;
// 日期检查: @Future、@FutureOrPresent、@Past、@PastOrPresent
@Future(message = "验证日期是否在当前时间之后")
private Date futureDate;
@FutureOrPresent(message = "验证日期是否为当前时间或之后")
private Date futureOrPresentDate;
@Past(message = "验证日期是否在当前时间之前")
private Date pastDate;
@PastOrPresent(message = "验证日期是否为当前时间或之前")
private Date pastOrPresentDate;
// 其它检查: @Email、@CreditCardNumber、@URL、@Pattern、@ScriptAssert、@UniqueElements
@Email(message = "校验是否为正确的邮箱格式")
private String email;
@CreditCardNumber(message = "校验是否为正确的信用卡号")
private String creditCardNumber;
@URL(protocol = "http", host = "127.0.0.1", port = 8080, message = "校验是否为正确的URL地址")
private String url;
@Pattern(regexp = "^1[3|4|5|7|8][0-9]{9}$", message = "正则校验是否为正确的手机号")
private String phone;
// 对关联对象元素进行递归校验检查
@Valid
@UniqueElements(message = "校验集合中的元素是否唯一")
private List<CalendarEvent> calendarEvent;
@Data
@ScriptAssert(lang = "javascript", script = "_this.startDate.before(_this.endDate)",
message = "通过脚本表达式校验参数")
private class CalendarEvent {
private Date startDate;
private Date endDate;
}
// 数值检查: @Min、@Max、@Range、@DecimalMin、@DecimalMax、@Digits
@Min(value = 0, message = "验证数值是否大于等于指定值")
@Max(value = 100, message = "验证数值是否小于等于指定值")
@Range(min = 0, max = 100, message = "验证数值是否在指定值区间范围内")
private Integer score;
@DecimalMin(value = "10.01", inclusive = false, message = "验证数值是否大于等于指定值")
@DecimalMax(value = "199.99", message = "验证数值是否小于等于指定值")
@Digits(integer = 3, fraction = 2, message = "限制整数位最多为3,小数位最多为2")
private BigDecimal money;
}
里面使用到了lombok
简化代码,有疑惑的请百度,下面是lombok
依赖:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<scope>provided</scope>
</dependency>
Controller中开启验证:
常见的前后端分离开发模式,数据通信通常以 JSON 为主。针对 POST 和 PUT 请求,一般通过新建域(对象)模型来进行数据绑定和校验,constraint 通常附加在这些域模型的字段上(如上):
/**
* Valid注解标明要对参数对象进行数据校验
*/
@PutMapping
@PostMapping
public Map<String, Object> test01(@RequestBody @Valid Validate validate, BindingResult bindingResult) {
Map<String, Object> map = new HashMap<>(4);
if (bindingResult.hasErrors()) {
String errorMsg = bindingResult.getFieldErrors().stream().map(FieldError::getDefaultMessage)
.collect(Collectors.joining(","));
map.put("errorMsg", errorMsg);
}
map.put("params", validate.toString());
return map;
}
此外,对于 GET 和 DELETE 请求,参数通常为 key1=value1&key2=value2 这种形式。默认情况下,Hibernate Validator 只能对 Object 属性进行校验,并不能对单个参数进行校验,Spring 在此基础上进行了扩展,通过配置 MethodValidationPostProcessor 处理器,可以实现对方法参数的拦截校验。
@Configuration
public class ValidateConfig {
/**
* 配置MethodValidationPostProcessor拦截器,以实现对方法参数的校验
*/
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
注意,要在 Controller 类上明确标明@Validated
:
@Validated
@RestController
@RequestMapping("validate")
public class ValidateController {
@GetMapping
@DeleteMapping
public Map<String, Object> test02(@NotNull(message = "id不能为空") @Range(min = 1, max = 100, message = "id最小为1最大为100") Integer id,
@NotBlank(message = "email不能为空") @Email(message = "邮箱格式错误") String email,
@ModelAttribute @Valid Validate validate) {
Map<String, Object> map = new HashMap<>(4);
map.put("id", id);
map.put("email", email);
map.put("params", validate.toString());
return map;
}
}
上述这种形式的参数要是校验失败,错误提示明显并不友好,通过捕获此类异常就可以解决:
异常统一处理类部分代码:
/**
* 数据校验异常
*
* @param e 数据校验异常
* @return
*/
@ExceptionHandler(value = BindException.class)
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result handleResourceBindException(BindException e) {
log.error("业务异常,{},{},{}", DateUtils.formatLog(), 400, e.getMessage());
return getResultView("参数错误", e);
}
/**
* 数据校验异常
*
* @param e 数据校验异常
* @return
*/
@ExceptionHandler(value = ConstraintViolationException.class)
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result handleResourceConstraintViolationException(ConstraintViolationException e) {
log.error("业务异常,{},{},{}", DateUtils.formatLog(), 400, e.getMessage());
return getResultView("参数错误", e);
}
/**
* 参数异常
*
* @param msg
* @param e
* @return
*/
private Result getResultView(String msg, BindException e) {
BindingResult bindingResult = e.getBindingResult();
List<ObjectError> allErrors = bindingResult.getAllErrors();
Set<BindingResultObject> errorMessage = new HashSet<>();
allErrors.forEach(item -> errorMessage.add(BindingResultObject.builder().build().setMessage(item.getDefaultMessage()).setField(((DefaultMessageSourceResolvable) Objects.requireNonNull(item.getArguments())[0]).getDefaultMessage())));
return Result.builder().build().setMsg(msg).setCode(400).setData(errorMessage);
}
/**
* 参数异常
*
* @param msg
* @param e
* @return
*/
private Result getResultView(String msg, ConstraintViolationException e) {
Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
Set<BindingResultObject> errorMessage = new HashSet<>();
try {
constraintViolations.forEach(item -> {
String message = item.getMessage();
String s = item.getPropertyPath().toString();
String[] split = s.split("\\.");
errorMessage.add(BindingResultObject.builder().build().setMessage(message).setField(split[split.length - 1]));
});
} catch (Exception e1) {
e1.printStackTrace();
}
return Result.builder().build().setCode(400).setMsg(msg).setData(errorMessage);
}
@Builder
@Data
class BindingResultObject implements Serializable {
private String field;
private String message;
public BindingResultObject setField(String field) {
this.field = field;
return this;
}
public BindingResultObject setMessage(String message) {
this.message = message;
return this;
}
}
返回统一消息类:
import lombok.Builder;
import lombok.Data;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
@Data
@Builder
public class Result<T> implements Serializable {
private int code;
private boolean result;
private String msg;
private Object data;
public Result setCode(int code) {
this.code = code;
return this;
}
public Result setResult(boolean result) {
this.result = result;
return this;
}
public Result setMsg(String msg) {
this.msg = msg;
return this;
}
public Result setData(Object data) {
this.data = data;
return this;
}
/**
* 返回消息
*
* @param msg 消息
* @param code 状态码
* @param re 成功标识
* @param data 数据
* @return 消息
*/
public static Result msg(String msg, int code, boolean re, Object data) {
return Result.builder().build().setMsg(msg).setCode(code).setResult(re).setData(data);
}
/**
* 返回成功消息
*
* @return 成功消息
*/
public static Result success() {
String message = "操作成功";
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
String method = request.getMethod();
switch (method.toUpperCase()) {
case "GET":
message = "获取数据成功";
break;
case "POST":
message = "提交数据成功";
break;
case "PUT":
message = "更新数据成功";
break;
case "DELETE":
message = "删除数据成功";
break;
}
return Result.success(message);
}
/**
* 返回成功消息
*
* @param msg 消息
* @return 成功消息
*/
public static Result success(String msg) {
return Result.success(msg, 200);
}
/**
* 返回成功消息
*
* @param msg 消息
* @param code 状态码
* @return 成功消息
*/
public static Result success(String msg, int code) {
return Result.success(msg, code, true);
}
/**
* 返回成功消息
*
* @param msg 消息
* @param code 状态码
* @param re 标识
* @return 成功消息
*/
public static Result success(String msg, int code, boolean re) {
return Result.msg(msg, code, re, null);
}
/**
* 返回成功消息
*
* @param data 数据
* @return 成功消息
*/
public static Result success(Object data) {
return Result.msg("操作成功", 200, true, data);
}
/**
* 返回失败消息
*
* @return 失败消息
*/
public static Result error() {
String message = "操作失败";
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
String method = request.getMethod();
switch (method.toUpperCase()) {
case "GET":
message = "获取数据失败";
break;
case "POST":
message = "提交数据失败";
break;
case "PUT":
message = "更新数据失败";
break;
case "DELETE":
message = "删除数据失败";
break;
}
return Result.success(message);
}
/**
* 返回失败消息
*
* @param msg 消息
* @return 失败消息
*/
public static Result error(String msg) {
return Result.success(msg, 500);
}
/**
* 返回失败消息
*
* @param msg 消息
* @param code 状态码
* @return 失败消息
*/
public static Result error(String msg, int code) {
return Result.success(msg, code, false);
}
/**
* 返回失败消息
*
* @param msg 消息
* @param code 状态码
* @param re 标识
* @return 失败消息
*/
public static Result error(String msg, int code, boolean re) {
return Result.msg(msg, code, re, null);
}
/**
* 返回失败消息
*
* @param data 数据
* @return 失败消息
*/
public static Result error(Object data) {
return Result.msg("操作失败", 500, false, data);
}
}