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"

}

Springboot优雅参数校验,统一响应,异常处理的更多相关文章

  1. 【全网最全】springboot整合JSR303参数校验与全局异常处理

    一.前言 我们在日常开发中,避不开的就是参数校验,有人说前端不是会在表单中进行校验的吗?在后端中,我们可以直接不管前端怎么样判断过滤,我们后端都需要进行再次判断,为了安全.因为前端很容易拜托,当测试使 ...

  2. SpringBoot Validation参数校验 详解自定义注解规则和分组校验

    前言 Hibernate Validator 是 Bean Validation 的参考实现 .Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的 ...

  3. springboot 接口参数校验

    前言 在开发接口的时候,参数校验是必不可少的.参数的类型,长度等规则,在开发初期都应该由产品经理或者技术负责人等来约定.如果不对入参做校验,很有可能会因为一些不合法的参数而导致系统出现异常. 上一篇文 ...

  4. 更加灵活的参数校验,Spring-boot自定义参数校验注解

    上文我们讨论了如何使用@Min.@Max等注解进行参数校验,主要是针对基本数据类型和级联对象进行参数校验的演示,但是在实际中我们往往需要更为复杂的校验规则,比如注册用户的密码和确认密码进行校验,这个时 ...

  5. SpringBoot 为API添加统一的异常处理(一)

    首先我把异常分为两种,一种是可控制的,或者是由我们发现条件不正确主动抛出的异常,就像前城市编号不存在那个粟子:另一种是不可控制的,或者说是程序存在bug引起的异常,但这种异常也不想变态的就直接给前端抛 ...

  6. springboot中参数校验

    <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring- ...

  7. SpringBoot Validation优雅的全局参数校验

    前言 我们都知道在平时写controller时候,都需要对请求参数进行后端校验,一般我们可能会这样写 public String add(UserVO userVO) { if(userVO.getA ...

  8. 使用Spring Validation优雅地校验参数

    写得好的没我写得全,写得全的没我写得好 引言 不知道大家平时的业务开发过程中 controller 层的参数校验都是怎么写的?是否也存在下面这样的直接判断? public String add(Use ...

  9. 利用 Bean Validation 来简化接口请求参数校验

    团队新来了个校招实习生静静,相互交流后发现竟然是我母校同实验室的小学妹,小学妹很热情地认下了我这个失散多年的大湿哥,后来... 小学妹:大湿哥,咱们项目里的 Controller 怎么都看不到参数校验 ...

  10. SpringBoot_@valid_参数校验

    SpringBoot @valid 参数校验 空检查 @Null 验证对象是否为null @NotNull 验证对象是否不为null, 无法查检长度为0的字符串 @NotBlank 检查约束字符串是不 ...

随机推荐

  1. 【python基础】基本数据类型-数字类型

    Python3 支持int(整型数据).float(浮点型数据).bool(布尔类型) 1.int(整型数据) 在Python 3里,只有一种整数类型 int,表示为长整型.像大多数语言一样,数值类型 ...

  2. 一文吃透Java并发高频面试题

    内容摘自我的学习网站:topjavaer.cn 分享50道Java并发高频面试题. 线程池 线程池:一个管理线程的池子. 为什么平时都是使用线程池创建线程,直接new一个线程不好吗? 嗯,手动创建线程 ...

  3. JVM 诊断神器-Arthas实战

    什么是Arthas(阿尔萨斯) 阿里开源的Java诊断工具,它可以在运行时对Java应用程序进行动态诊断和调试 当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决 这个类从哪个 jar 包加 ...

  4. SpringBoot打包成WAR包的时候把第三方jar包打到LIB文件夹下和把第三方jar包打入到SpringBoot jar包中

    SpringBoot打包成WAR包的时候把第三方jar包打到LIB文件夹下和把第三方jar包打入到SpringBoot jar包中 转载 首先我们应该知道我们把SPRINGBOOT项目打包成WAR包和 ...

  5. 【Python&RS】GDAL计算遥感影像光谱指数(如NDVI、NDWI、EVI等)

            GDAL(Geospatial Data Abstraction Library)是一个在X/MIT许可协议下的开源栅格空间数据转换库.它利用抽象数据模型来表达所支持的各种文件格式.它 ...

  6. shell编程-发送消息

    需求:利用 Linux 自带的 mesg 和 write 工具,编写一个向用户快速发送消息的脚本,输入用户名作为第一个参数,消息内容为第二个参数.脚本需要检测用户是否登录,是否打开消息功能,以及当前发 ...

  7. NOIP模拟测试A3 赛后总结

    T1 谜之阶乘 可以发现题目要求我们求的实际上是若干个连续整数 \(c_i\) ,使得 \(\displaystyle \prod c_i = n\),通过打表可以发现这些连续整数的长度 \(d\) ...

  8. 计算机视觉重磅会议VAlSE2023召开,合合信息分享智能文档处理技术前沿进展

    近期,2023年度视觉与学习青年学者研讨会 (Vision And Learning SEminar, VALSE) 圆满落幕.会议由中国人工智能学会.中国图象图形学学会主办,江南大学和无锡国家高新技 ...

  9. 【tvm解析】PACKFUNC机制

    为实现多种语言支持,需要满足以下几点: 部署:编译结果可以从python/javascript/c++调用. Debug: 在python中定义一个函数,在编译函数中调用. 链接:编写驱动程序以调用设 ...

  10. Educational Codeforces Round 150 (Rated for Div. 2) A-E

    比赛链接 A 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; bool solve() { i ...