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. docker容器中下载vim指令的速度特别慢,解决方案

    1 首先要进入容器内执行,保存目前源 mv /etc/apt/sources.list /etc/apt/sources.list.bak 2修改源,由于docker默认没有vim的包 所以无法使用v ...

  2. 可以,很强,68行代码实现Bean的异步初始化,粘过去就能用。

    你好呀,我是歪歪. 前两天在看 SOFABoot 的时候,看到一个让我眼前一亮的东西,来给大家盘一下. SOFABoot,你可能不眼熟,但是没关系,本文也不是给你讲这个东西的,你就认为它是 Sprin ...

  3. 驱动开发:内核扫描SSDT挂钩状态

    在笔者上一篇文章<驱动开发:内核实现SSDT挂钩与摘钩>中介绍了如何对SSDT函数进行Hook挂钩与摘钩的,本章将继续实现一个新功能,如何检测SSDT函数是否挂钩,要实现检测挂钩状态有两种 ...

  4. C#/VB.NET:快速而简单的免费SVG到PDF转换技巧

    在日常工作中,我们常常需要将SVG转换为PDF格式.这是因为SVG格式的图像在打印时可能会出现问题,例如失去分辨率或无法正确适应纸张大小.与此相比,PDF格式则专门用于打印和共享文档,可以确保高质量输 ...

  5. 【保姆级教程】Vue项目调试技巧

    前言 在Vue项目开发过程中,当遇到应用逻辑出现错误,但又无法准确定位的时候,知晓Vue项目调试技巧至关重要,debug是必备技能. 同后台项目开发一样,可以在JS实现的应用逻辑中设置断点,并进行单步 ...

  6. 4. Mybatis的增删改查(CRUD)

    1.新增 ‍ <!--int insertUser();--> <insert id="insertUser"> insert into t_user va ...

  7. 如何优化数据warehouse的搜索和查询

    目录 1. 引言 2. 技术原理及概念 2.1 基本概念解释 2.2 技术原理介绍 2.2.1 查询优化 2.2.2 索引优化 2.2.3 数据访问优化 2.3 相关技术比较 2.3.1 SQL 2. ...

  8. spring-boot-maven-plugin插件详解

    一. 为什么Spring Boot项目自带这个插件 当我们在SpringBoot官方下载一个脚手架时,会发现pom.xml会自带spring-boot-maven-plugin插件 <?xml ...

  9. 高并发场景下,6种解决SimpleDateFormat类的线程安全问题方法

    摘要:解决SimpleDateFormat类在高并发场景下的线程安全问题可以有多种方式,这里,就列举几个常用的方式供参考. 本文分享自华为云社区<[高并发]更正SimpleDateFormat类 ...

  10. Mac pt-online-schema-change 图文并茂、不锁表在线修改 MySQL 表结构、添加表索引、添加表字段、修改表字段、删除表字段

    导读 percona-toolkit 源自 Maatkit 和 Aspersa 工具,这两个工具是管理 MySQL 的最有名的工具,但 Maatkit 已经不维护了,全部归并到 percona-too ...