Springboot优雅参数校验,统一响应,异常处理
1.统一响应
(1)统一状态码
首先定义一个状态码接口,所有状态码都需要实现它
public interface StatusCode {
public int getCode();
public String getMsg();
}
枚举类实现接口
@Getter
public enum ResultCode implements StatusCode{
SUCCESS(1000,"请求成功"),
FAILED(1001,"请求失败"),
VALIDATE_ERROR(1002,"参数校验失败"),
RESPONSE_PACK_ERROR(1003,"response返回包装失败");
private int code;
private String msg;
ResultCode(int code,String msg){
this.code = code;
this.msg = msg;
}
}
开始写 ResultVo 包装类了,我们预设了几种默认的方法,比如成功的话就默认传入 object 就可以了,我们自动包装成 success。
@AllArgsConstructor
@NoArgsConstructor
@Data
public class ResultVo {
private int code;
private String msg;
private Object data;
public static ResultVo success(Object data){
return new ResultVo(ResultCode.SUCCESS.getCode(),ResultCode.SUCCESS.getMsg(),data);
}
public static ResultVo success(String msg,Object data){
return new ResultVo(ResultCode.SUCCESS.getCode(),msg,data);
}
public static ResultVo fail(Integer code,String msg){
return new ResultVo(code,msg,null);
}
}
2.统一校验
@Validated 参数校验
有了 @Validated 我们只需要再 vo 上面加一点小小的注解,便可以完成校验功能
@Data
public class ProductInfoVo {
@NotNull(message = "商品名称不允许为空")
private String productName;
@Min(value = 0, message = "商品价格不允许为负数")
private BigDecimal productPrice;
private Integer productStatus;
}
@PostMapping("/findByVo")
public ProductInfo findByVo(@Validated ProductInfoVo vo) {
ProductInfo productInfo = new ProductInfo();
BeanUtils.copyProperties(vo, productInfo);
return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));
}
运行看看,如果参数不对会发生什么?
{
"timestamp": "2020-04-19T03:06:37.268+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"Min.productInfoVo.productPrice",
"Min.productPrice",
"Min.java.math.BigDecimal",
"Min"
],
"arguments": [
{
"codes": [
"productInfoVo.productPrice",
"productPrice"
],
"defaultMessage": "productPrice",
"code": "productPrice"
},
0
],
"defaultMessage": "商品价格不允许为负数",
"objectName": "productInfoVo",
"field": "productPrice",
"rejectedValue": -1,
"bindingFailure": false,
"code": "Min"
}
],
}
并不是想要的返回结果
2.1分组校验
分组校验解决的问题:例如当对传来的DTO校验时,添加时id可以未空,但是修改时id不能为空。想要重复使用一个dto就遇到不能使用@NotNull进行校验,所以可以使用分组校验。
流程:
在common中新建valid包,里面新建两个空接口AddGroup,UpdateGroup用来分组
给校验注解,标注上groups,指定什么情况下才需要进行校验
如:指定在更新和添加的时候,都需要进行校验
@NotEmpty
@NotBlank(message = "品牌名必须非空",groups = {UpdateGroup.class,AddGroup.class})
private String name;
如果没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加groups。
业务方法参数上使用@Validated注解,并在value中给出group接口,标记当前校验是哪个组
@RequestMapping("/save")
public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){
...
}
2.2 自定义参数校验注解
项目中引入spring-boot-starter-validation的starter依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
hibernate-validator提供了很多校验器注解,例如@Email,参考可以自定义自己的注解
下面以日期格式校验规则为例:
(1)定义注解类
package com.validator.demo.api.valid;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import org.apache.commons.lang3.StringUtils;
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = { ValidDateValidator.class })
public @interface ValidDate {
String pattern() default "yyyy-MM-dd";
String message() default StringUtils.EMPTY;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Constraint —— 表示我们判断逻辑的具体实现类是什么。
(2)定义逻辑实现类
package com.validator.demo.api.valid;
import java.text.ParseException;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
public class ValidDateValidator implements ConstraintValidator<ValidDate, String> {
private String pattern = StringUtils.EMPTY;
@Override
public void initialize(ValidDate constraintAnnotation) {
pattern = constraintAnnotation.pattern();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StringUtils.isBlank(value)) {
return true;
}
try {
DateUtils.parseDateStrictly(value, pattern);
} catch (ParseException e) {
return false;
}
return true;
}
}
注意:ConstraintValidator<ValidDate, String> 第一个参数时注解,第二个参数时要校验的类型
(3)在实体类中添加相应的注解
(4)测试
3.优化异常处理
spring提供了一个 @RestControllerAdvice 来增强所有 @RestController,然后使用 @ExceptionHandler 注解,就可以拦截到对应的异常。
@RestControllerAdvice
public class ControllerExceptionAdvice {
@ExceptionHandler(value = Exception.class)
public ResultVo MethodArgumentNotValidExceptionHandler(Exception e){
return ResultVo.fail(ResultCode.VALIDATE_ERROR.getCode(),e.getMessage());
}
}
4.统一异常
每个系统都会有自己的业务异常,比如库存不能小于 0 子类的,这种异常并非程序异常,而是业务操作引发的异常,我们也需要进行规范的编排业务异常状态码,并且写一个专门处理的异常类,最后通过刚刚学习过的异常拦截统一进行处理,以及打日志。
(1)异常状态码枚举,既然是状态码,那就肯定要实现我们的标准接口 StatusCode。
@Getter
public enum AppCode implements StatusCode {
APP_ERROR(2000, "业务异常"),
PRICE_ERROR(2001, "价格异常");
private int code;
private String msg;
AppCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
(2)异常类
异常类,这里需要强调一下,code 代表 AppCode 的异常状态码,也就是 2000;msg 代表业务异常,这只是一个大类,一般前端会放到弹窗 title 上;最后 super(message); 这才是抛出的详细信息,在前端显示在弹窗体中,在 ResultVo 则保存在 data 中。
@Getter
public class APIException extends RuntimeException {
private int code;
private String msg;
// 手动设置异常
public APIException(StatusCode statusCode, String message) {
// message用于用户设置抛出错误详情,例如:当前价格-5,小于0
super(message);
// 状态码
this.code = statusCode.getCode();
// 状态码配套的msg
this.msg = statusCode.getMsg();
}
// 默认异常使用APP_ERROR状态码
public APIException(String message) {
super(message);
this.code = AppCode.APP_ERROR.getCode();
this.msg = AppCode.APP_ERROR.getMsg();
}
}
(3)最后进行统一异常的拦截,这样无论在 service 层还是 controller 层,开发人员只管抛出 API 异常,不需要关系怎么返回给前端,更不需要关心日志的打印。
@RestControllerAdvice
public class ControllerExceptionAdvice {
@ExceptionHandler({BindException.class})
public ResultVo MethodArgumentNotValidExceptionHandler(BindException e) {
// 从异常对象中拿到ObjectError对象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
return new ResultVo(ResultCode.VALIDATE_ERROR, objectError.getDefaultMessage());
}
@ExceptionHandler(APIException.class)
public ResultVo APIExceptionHandler(APIException e) {
return new ResultVo(e.getCode(), e.getMsg(), e.getMessage());
}
}
最后使用,我们的代码只需要这么写。
if (null == orderMaster) {
throw new APIException(AppCode.ORDER_NOT_EXIST, "订单号不存在:" + orderId);
}
{
"code": 2003,
"msg": "订单不存在",
"data": "订单号不存在:1998"
}
本文作者:spiderMan1-1
本文链接:https://www.cnblogs.com/cgy1995/p/16421988.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步