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. Galaxy Project | 生信人最值得学习的开源项目之一

    我与 Galaxy Project 的渊源可以追溯到我刚毕业,还在华大实习的那一段时间,这个项目应该是我职业生涯中最重要的一段经历.虽然这么对年以来一直都关注着这个项目,但大多数都是浅尝辄止,对源码层 ...

  2. 基于 Dash Bio 的生物信息学数据可视化

    Dash 是用于搭建响应式 Web 应用的 Python 开源库.Dash Bio 是面向生物信息学,且与 Dash 兼容的组件,它可以将生物信息学领域中常见的数据整合到 Dash 应用程序,以实现响 ...

  3. SRE 的工作介绍

    哈喽大家好,我是咸鱼 今天看到了一篇很不错的文章,作者是一名 SRE 工程师,在 Shopee 工作,base 新加坡 分享出来给大家看看 作者:卡瓦邦噶 原文链接:https://www.kawab ...

  4. midjourney国内版上线! 快来体验一下midjourney的强大功能

    最近大火的midjourney国内版上线了!该网站对接了midjourneyAPI,以文生图.以图生图功能都支持,下面我们来体验一下它的功能. 网址:https://www.weijiwangluo. ...

  5. 跑得更快!华为云GaussDB以出色的性能守护“ERP的心脏”

    摘要:GaussDB已经全面支撑起MetaERP,在包括库存服务在内的9大核心模块中稳定运行,端到端业务效率得到10倍提升. 本文分享自华为云社区<跑得更快!华为云GaussDB以出色的性能守护 ...

  6. CentOS 7相关操作

    防火墙操作 开启防火墙 sudo systemctl start firewalld.service 查看防火墙状态 sudo systemctl status firewalld.service 关 ...

  7. Lock同步_小记

    使用同步机制的这种方式解决线程安全问题,但是不知道具体的锁对象在哪里添加,并且锁对象在哪里释放锁对象,对于这种情况Jdk5以后Java提供了一个更具体的锁对象:Lock Lock 实现提供了比使用 s ...

  8. Redis的设计与实现(3)-字典

    Redis 的数据库使用字典实现, 对数据库的增, 删, 查, 改也是构建在对字典的操作之上的. 字典是哈希键的底层实现之一: 当一个哈希键包含的键值对比较多, 又或者键值对中的元素都是比较长的字符串 ...

  9. Mysql基础7-约束

    一.约束的基本概念 1.概念:约束是作用于表中字段上的规则,用于限制储存在表中的数据 2.目的:保证数据库中的数据的正确性,有效性和完整性 3.分类 非空约束(not null):限制该字段的数据不能 ...

  10. Python将大的csv文件拆分多个小的csv文件

    #ecoding=utf-8 import os import time # 2019/9/8 将大的csv文件拆分多个小的csv文件 def mkSubFile(lines, head, srcNa ...