04数据校验
1.业务层校验
首先我们来看一下最常见的做法,就是在业务层进行参数校验:
public String addUser(User user) {
if (user == null || user.getId() == null || user.getAccount() == null || user.getPassword() == null || user.getEmail() == null) {
return "对象或者对象字段不能为空";
}
}
代码繁琐不够优雅。
2.SpringMvc数据校验:
在SpringMvc中做数据校验有2中方式,一种是 Spring 自带的验证框架,另外一种是利用 JSR 实现。JSR 是一个规范,提供了完整得一套 API,通过标注给对象属性添加约束。Hibernate Validator 就是 对JSR 规范所有注解的具体实现,以及一些附加的约束注解。用户还可以自定义约束注解。
Validator提供得注解如下:
注解 | 作用目标 | 检查规则 |
---|---|---|
@Length(min=, max=) | 属性(String) | 检查字符串长度是否符合范围 |
@Max(value=) | 属性(以 numeric 或者 string 类型来表示一个数字) | 检查值是否小于或等于最大值 |
@Min(value=) | 属性(以 numeric 或者 string 类型来表示一个数字) | 检查值是否大于或等于最小值 |
@NotNull(value=) | 属性 | 检查值是否非空(not null) |
@NotEmpty(value=) | 属性(String,Object,Collection) | 检查值不能是null 不能是空字符 集合框架中的元素不能为空 |
@NotBlank(value=) | 属性(String) | 检查值不能是null或不能是空字符 |
@Future | 属性(date 或 calendar) | 检查日期是否是未来 |
@Pattern(regex=”regexp”, flag=) | 属性(string) | 检查属性是否与给定匹配标志的正则表达式相匹配 |
@Range(min=, max=) | 属性(以 numeric 或者 string 类型来表示一个数字) | 检查值是否在最小和最大值之间(包括临界值) |
@Size(min=, max=) | 属性(array,collection,map) | 检查元素大小是否在最小和最大值之间(包括临界值) |
@AssertFalse | 属性 | 检查方法的演算结果是否为 false(对以代码方式而不是注解表示的约束很有用) |
@AssertTrue | 属性 | 检查方法的演算结果是否为 true(对以代码方式而不是注解表示的约束很有用) |
@Valid | 属性(object) | 对关联对象递归进行验证。如果对象是集合或数组,就递归地验证其元素;如果对象是 Map,则递归验证其值元素 |
属性(String) | 检查字符串是否符合有效的 email 地址规范 | |
@Past | 属性(date 或 calendar) | 检查日期是否是过去 |
3.SpringBoot对数据校验的支持
默认提供的参数校验依赖于 hibernate-validator来实现。使用 Hibernate Validator校验数据,需要定义一个接收的数据模型,使用注解的形式描述字段校验的规则。
@Data
public class User {
@NotNull(message = "用户id不能为空")
private Long id;
@NotNull(message = "用户账号不能为空")
@Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")
private String account;
@NotNull(message = "用户密码不能为空")
@Size(min = 6, max = 11, message = "密码长度必须是6-16个字符")
private String password;
@NotNull(message = "用户邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}
- 在pom.xml文件中引入依赖:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.16.Final</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
-
实体类,对相关属性字段通过注解添加校验规则
-
Validator + 自动抛出异常
参数上加上@Valid注解即可完成校验
@PostMapping("/addUser") public String addUser(@RequestBody @Valid User user) { return userService.addUser(user); }
-
全局异常处理
参数校验失败会自动引发异常,SpringBoot全局异常处理:
类上加上@ControllerAdvice或@RestControllerAdvice注解
@RestControllerAdvice public class ExceptionControllerAdvice { @ExceptionHandler(MethodArgumentNotValidException.class) public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { // 从异常对象中拿到ObjectError对象 ObjectError objectError = e.getBindingResult().getAllErrors().get(0); // 然后提取错误提示信息进行返回 return objectError.getDefaultMessage(); } }
4.数据统一响应
现在我们规范好了参数校验方式和异常处理方式,然而还没有规范响应数据!比如我要获取一个分页信息数据,获取成功了呢自然就返回的数据列表,获取失败了后台就会响应异常信息,即一个字符串,就是说前端开发者压根就不知道后端响应过来的数据格式!所以,统一响应数据是前后端规范中必须要做的!
- 自定义统一响应体
统一数据响应第一步肯定要做的就是自定义一个响应体类,无论后台是运行正常还是发生异常,响应给前端的数据格式是不变的!
更优雅的设计异常响应体:
/**
* 通用返回对象
*/
@ApiModel(description = "结果对象")
@Data
@ToString
public class CommonResult<T> {
@ApiModelProperty(value = "状态码",required = true)
private int code;
@ApiModelProperty(value = "消息",required = true)
private String message;
@ApiModelProperty(value = "数据",required = true)
private T data;
protected CommonResult() {
}
protected CommonResult(int code, String message) {
this.code = code;
this.message = message;
}
protected CommonResult(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
/**
* 成功返回结果
*
*/
public static CommonResult success() {
return new CommonResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage());
}
/**
* 成功返回结果
*
* @param data 获取的数据
*/
public static <T> CommonResult<T> success(T data) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功返回结果
*
* @param data 获取的数据
* @param message 提示信息
*/
public static <T> CommonResult<T> success(T data, String message) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
}
/**
* 失败返回结果
* @param code 错误码
*/
public static <T> CommonResult<T> failed(IErrorCode errorCode) {
return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage());
}
/**
* 失败返回结果
* @param errorCode 错误码
* @param message 错误信息
*/
public static <T> CommonResult<T> failed(int code,String message) {
return new CommonResult<T>(code, message);
}
/**
* 失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> failed(String message) {
return new CommonResult<T>(ResultCode.FAILED.getCode(), message);
}
/**
* 失败返回结果
*/
public static <T> CommonResult<T> failed() {
return failed(ResultCode.FAILED);
}
/**
* 参数验证失败返回结果
*/
public static <T> CommonResult<T> validateFailed() {
return failed(ResultCode.VALIDATE_FAILED);
}
/**
* 参数验证失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> validateFailed(String message) {
return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message);
}
/**
* 参数校验失败
* @param data 详细信息
* @param <T>
* @return
*/
public static <T> CommonResult<T> methodArgumentNotValid(T data){
return new CommonResult<T>(ResultCode.METHOD_ARGUMENT_NOT_VALID.getCode(),ResultCode.METHOD_ARGUMENT_NOT_VALID.getMessage(),data);
}
/**
* 未登录返回结果
*/
public static <T> CommonResult<T> unauthorized() {
return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage());
}
/**
* 未授权返回结果
*/
public static <T> CommonResult<T> forbidden(T data) {
return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage());
}
/**
* 请求方法被禁止返回结果
*/
public static <T> CommonResult<T> validateMethodFailed() {
return new CommonResult<T>(ResultCode.VALIDATE_METHOD.getCode(), ResultCode.VALIDATE_METHOD.getMessage());
}
}
- 响应码枚举
/**
* 枚举了一些常用API操作码
*/
@ToString
public enum ResultCode implements IErrorCode {
SUCCESS(200, "操作成功"),
VALIDATE_FAILED(400, "Invalid input(无效的输入)"),
UNAUTHORIZED(401, "暂未登录或token已经过期"),
FORBIDDEN(403, "403 Forbidden(请求被拒绝)没有相关权限"),
NOTFOUND(404, "not found(没有找到相关资源)"),
VALIDATE_METHOD(405, "请求方法被禁止"),
FAILED(500, "服务器内部错误"),
METHOD_ARGUMENT_NOT_VALID(10000,"参数校验失败"),
REPETITIVE_OPERATION(10001, "请勿重复操作"),
ACCESS_LIMIT(10002, "请求太频繁, 请稍后再试");
private int code;
private String message;
private ResultCode(int code, String message) {
this.code = code;
this.message = message;
}
@Override
public int getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
- 封装API的错误码
/**
* 封装API的错误码
*/
public interface IErrorCode {
int getCode();
String getMessage();
}
- 设置全局异常处理的响应码:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionAdvice {
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public CommonResult exceptionHandler(HttpRequestMethodNotSupportedException e) {
log.error("Exception: ", e);
return CommonResult.validateMethodFailed();
}
@ExceptionHandler(Exception.class)
public CommonResult exceptionHandler(Exception e) {
log.error("Exception: ", e);
return CommonResult.failed(ResultCode.FAILED.getMessage());
}
@ExceptionHandler(BindException.class)
public CommonResult bindExceptionHandler(BindException e) {
log.error("BindException: ", e);
return CommonResult.validateFailed();
}
@ExceptionHandler(CustomException.class)
public CommonResult customExceptionHandler(CustomException e) {
log.error("CustomException: ", e);
return CommonResult.failed("响应失败:"+e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public CommonResult methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
log.error("MethodArgumentNotValidException: ", e);
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
return CommonResult.methodArgumentNotValid(objectError.getDefaultMessage());
}
}