springboot全局异常处理,实体类@Validated注解校验,分组校验实现示例

统一返回格式

我们在开发微服务的时候,为了规范,往往约定一个固定的数据格式返回给前端,比如如下格式:

{
    "code":"0",
    "msg":"成功",
    "result":{}
}

或者分页格式:

{
   "code":"0",
   "msg":"成功",
   "page":1,
   "records":0,
   "result":[],
   "totalPages":0,
   "totalRecords":0
}

其中包含了状态码(code),状态码注释(msg),返回结果(result),当前页(page),当前页返回的条数(records),总页数(totalPages),总条数(totalRecords)等信息。
如此,在后台往往建一个公共的返回结果类,比如ResponseVO(用于普通结果),ResponsePageVO(用户分页格式):
新建ResponseVO

package com.zhaohy.app.entity;

import java.io.Serializable;

/**
 * 响应pojo
 *
 */
public class ResponseVO<T> implements Serializable {
    
    private static final long serialVersionUID = -261786375530220465L;

    private String code;

    private String msg;

    private T result;

    public ResponseVO() {
        this.code = ErrorCode.SUCCESS.code();
        this.msg = ErrorCode.SUCCESS.msg("");
    }

    public ResponseVO(String code) {
        this.code = code;
        this.msg = ErrorCode.getMsgByCode(code,"");
    }

    public ResponseVO(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public ResponseVO(String code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.result = data;
    }

    public ResponseVO(T data) {
        this.code = ErrorCode.SUCCESS.code();
        this.msg = ErrorCode.SUCCESS.msg("");
        this.result = data;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public void setCode(String code,String lang, Object... args) {
        this.code = code;
        this.msg = String.format(ErrorCode.getMsgByCode(code, lang), args);
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getResult() {
        return result;
    }

    public void setResult(T result) {
        this.result = result;
    }
}

新建ResponsePageVO:

package com.zhaohy.app.entity;

public class ResponsePageVO<T> extends ResponseVO<T> {
    
    private static final long serialVersionUID = -6754720164702078582L;

    private int totalPages;
    
    private int totalRecords;
    
    private int  page;
    
    private int records;
    
    public ResponsePageVO() {
        super();
    }
    
    public int getTotalPages() {
        return totalPages;
    }

    public void setTotalPages(int totalPages) {
        this.totalPages = totalPages;
    }


    public int getTotalRecords() {
        return totalRecords;
    }


    public void setTotalRecords(int totalRecords) {
        this.totalRecords = totalRecords;
    }

    public int getPage() {
        return page;
    }

    public void setPage(int page) {
        this.page = page;
    }

    public int getRecords() {
        return records;
    }

    public void setRecords(int records) {
        this.records = records;
    }

}

新建ErrorCode枚举类:

package com.zhaohy.app.entity;

import org.springframework.util.StringUtils;


public enum ErrorCode {
    SUCCESS("0", "成功", "Success"),
    E_2001("2001", "缺少请求参数 %s", "Missing request parameter %s"),
    E_3104("3104", "参数不合法 %s", "Illegal parameter %s"),
    E_3001("3001", "运行时异常,失败", "Runtime exception, failed");
    
    private String code;
    private String zhName;
    private String enName;
    
    private ErrorCode(String code, String zhName, String enName) {
        this.code = code;
        this.zhName = zhName;
        this.enName = enName;
    }
    
    public String code() {
        return this.code;
    }
    
    public String msg(String lang) {
        if(StringUtils.isEmpty(lang)) {
            lang = "cn";
        }
        if("cn".equals(lang)) {
            return this.zhName;
        } else {
            return this.enName;
        }
//      String errorMsg = PropertieUtils.config.getProperty(erroKey(code, lang));
//      if (StringUtils.isBlank(errorMsg)) {
//          if (lang.equals("cn")) {
//              logger.info("errorCode={},未获取到错误描述!",code);
//              return UNKNOW_ERROR;
//          } else {
//              logger.info("errorCode={},未获取到错误描述!",code);
//              return UNKNOW_ERROR_EN;
//          }
//      }
        //return errorMsg;
    }
    public static String getMsgByCode(String code, String lang) {
        String message = null;
        for(ErrorCode ec : ErrorCode.values()) {
            if(ec.code().equals(code)) {
                message = ec.msg(lang);
            }
        }
        return message;
    }
//  private static String erroKey(String errorCode, String language) {
//      return "error.msg." + errorCode + "." + language;
//  }
}

为了方便演示上面的错误提示信息直接定义在了枚举类里面,当然最好定义在nacos里,方便随时改提示语句。

关于全局异常处理的好处

如上,当我们在业务代码中出现异常情况可以try catch这个异常然后在catch代码块里定义好code,msg,然后return给前端,这样在前端就能看到后台发生了什么错误。

但是,这种处理方式还不是最好的,因为try catch之后业务代码里的事务就不会回滚了,所以既想要可以回滚又可以出错时给前端返回相应自定义错误信息的情况,全局异常处理是可以满足的。

spring-web包中提供了@ControllerAdvice、@RestControllerAdvice注解(后者比前者多了一个@ResponseBody,省了自己转化json格式)和@ExceptionHandler(作用于方法上用来处理特定的异常)。

新建GlobalExceptionAdvice:

package com.zhaohy.app.advice;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.zhaohy.app.entity.ErrorCode;
import com.zhaohy.app.entity.ResponseVO;
import com.zhaohy.app.exception.DatabaseException;

/**
 * 全局错误异常捕获类
 */
@RestControllerAdvice
public class GlobalExceptionAdvice {

    /**
     * 数据库操作异常
     * @param response
     * @param de
     * @return
     */
    @ExceptionHandler(DatabaseException.class)
    public ResponseVO<?> databaseErrorHandler(HttpServletResponse response, DatabaseException de){
        //LogUtils.error("数据库操作异常", de);
        response.setStatus(HttpStatus.OK.value());
        return new ResponseVO<>(ErrorCode.ERROR_DATEBASE_CODE.code());
    }

}

比如我们在业务代码里自定义一个数据库操作异常类DatabaseException:

package com.zhaohy.app.exception;

public class DatabaseException extends RuntimeException {
    
    private static final long serialVersionUID = -8068938545385669507L;

}

如此在业务代码里就可以随便抛异常了,这里spring内部是基于AOP做了一个后置处理,业务代码在抛出异常后,spring利用切面全局捕获之后运行上面自定义的databaseErrorHandler方法,此时会统一组装格式给前端,也不会影响事务回滚。

springboot自带的实体类校验注解

对于实体类属性值的校验,spring主要用到hibernate-validator这个jar包,看过依赖后知道,在spring-boot-starter-web下面的spring-boot-starter-validation下面,所以引入springboot之后就直接可以调用了。

前端请求过来的参数往往可以用实体类来接收,在controller接口方法的参数里用@RequestBody接收request流转化为参数实体类,用@Validated指定校验:

@RequestMapping("/test/getTest.do")
    @ResponseBody
    public ResponseVO<?> getTest(HttpServletRequest request,
            @RequestBody @Validated UserParams params
            ) {
        return testService.getTest();
    }

新建UserParams :

package com.zhaohy.app.entity;

import java.util.List;
import java.util.Map;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

import com.zhaohy.app.groups.OtherGroup;

public class UserParams {
    @NotBlank
    private String UserType;
    @NotNull(message = "用户id不能为空")
    private Integer userId;
    @NotBlank
    private String userName;
    @Email
    private String email;
    @NotEmpty
    private List<Map<String, Object>> list;
    @NotBlank(groups = {OtherGroup.class})
    private String others;
    public Integer getUserId() {
        return userId;
    }
    public void setUserId(Integer userId) {
        this.userId = userId;
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public List<Map<String, Object>> getList() {
        return list;
    }
    public void setList(List<Map<String, Object>> list) {
        this.list = list;
    }
    public String getOthers() {
        return others;
    }
    public void setOthers(String others) {
        this.others = others;
    }
    public String getUserType() {
        return UserType;
    }
    public void setUserType(String userType) {
        UserType = userType;
    }
}

如上参数实体类中加入一些校验注解,下图是常用的注解解释


image.png

默认情况下会校验加了注解的属性值,如果想根据不同条件校验不同的值,则需要加分组如上面的other属性

    @NotBlank(groups = {OtherGroup.class})
    private String others;

这是加分组的格式,其中OtherGroup是一个interface接口类,空的,只起一个标识作用:

package com.zhaohy.app.groups;

public interface OtherGroup {

}

加了分组之后可以按下面的方式做灵活校验:
新建分组校验工具类HibernateValidateUtils

package com.zhaohy.app.utils;

import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.groups.Default;

import org.hibernate.validator.HibernateValidator;
import org.springframework.util.CollectionUtils;

/**
 * 分组校验工具类
 * @author ly-zhaohy
 *
 */
public class HibernateValidateUtils {

    private static Validator validator;

    static {
        ValidatorFactory factory = Validation.byProvider(HibernateValidator.class).configure().failFast(true)
                .buildValidatorFactory();
        validator = factory.getValidator();
    }
    
    /**
     * 分组校验
     * @param t
     * @param groups
     * @return
     */
    public static <T> void validateObj(T t, Class<?>... groups) {
        if (groups == null || groups.length == 0) {
            groups = new Class[] { Default.class };
        }
        Set<ConstraintViolation<T>> cs = validator.validate(t, groups);
        if (!CollectionUtils.isEmpty(cs)) {
            throw new ConstraintViolationException(cs);
        }
    }
    
    /**
     * 分组校验,校验失败返回错误信息
     * @param t
     * @param groups
     * @return 
     */
    public static <T> String validateObjMsg(T t, Class<?>... groups) {
        try {               
            validateObj(t, groups);
        } catch (ConstraintViolationException e) {
            for (ConstraintViolation<?> violation : e.getConstraintViolations()) {
                String field = violation.getPropertyPath().toString();
                Class<?> annotationType = violation.getConstraintDescriptor().getAnnotation().annotationType();
                if (NotNull.class.equals(annotationType)
                        || NotBlank.class.equals(annotationType)
                        || NotEmpty.class.equals(annotationType)) {
                    return field+"参数不存在";
                } else {
                    return field+"参数无效";
                }
            }
        }
        return "";
    }

}

controller改动:

@RequestMapping("/test/getTest.do")
    @ResponseBody
    public ResponseVO<?> getTest(HttpServletRequest request,
            @RequestBody @Validated UserParams params
            ) {
        if("1".equals(params.getUserType())) {
            HibernateValidateUtils.validateObj(params, OtherGroup.class);
        }
        
        return testService.getTest();
    }

如此,当用户类型为1时则除了其他属性的常规校验外,有OtherGroup分组标识的属性也会被校验(默认不会被校验)。

校验之后是通过抛出异常来实现的,所以还要在全局异常类里面加入统一处理代码:

package com.zhaohy.app.advice;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.zhaohy.app.entity.ErrorCode;
import com.zhaohy.app.entity.ResponseVO;
import com.zhaohy.app.exception.DatabaseException;

/**
 * 全局错误异常捕获类
 */
@RestControllerAdvice
public class GlobalExceptionAdvice {

    /**
     * 数据库操作异常
     * @param response
     * @param de
     * @return
     */
    @ExceptionHandler(DatabaseException.class)
    public ResponseVO<?> databaseErrorHandler(HttpServletResponse response, DatabaseException de){
        //LogUtils.error("数据库操作异常", de);
        response.setStatus(HttpStatus.OK.value());
        return new ResponseVO<>(ErrorCode.ERROR_DATEBASE_CODE.code());
    }

    /**
     *  使用validation的参数校验注解,参数校验不通过抛的异常处理
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseVO<?> argumentNotValidErrorHandler(HttpServletRequest request,HttpServletResponse response, MethodArgumentNotValidException se){
        response.setStatus(HttpStatus.OK.value());
        ResponseVO<?> res = new ResponseVO<>();
        String language = request.getHeader("lang");
        String code = se.getBindingResult().getFieldError().getCode();
        String field = se.getBindingResult().getFieldError().getField();
        if (NotNull.class.getSimpleName().equals(code)
                || NotBlank.class.getSimpleName().equals(code)
                || NotEmpty.class.getSimpleName().equals(code)) {
            res.setCode(ErrorCode.E_2001.code(), language, field);
        } else {
            res.setCode(ErrorCode.E_3104.code(), language, field);
        }
        return res;
    }
    
    /**
     *  使用validation的参数校验注解,参数校验不通过抛的异常处理
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseVO<?> argumentNotValidErrorHandler(HttpServletRequest request,HttpServletResponse response, ConstraintViolationException se){
        response.setStatus(HttpStatus.OK.value());
        ResponseVO<?> res = new ResponseVO<>();
        String language = request.getHeader("lang");
        for (ConstraintViolation<?> violation : se.getConstraintViolations()) {
            String field = violation.getPropertyPath().toString();
            Class<?> annotationType = violation.getConstraintDescriptor().getAnnotation().annotationType();
            if (NotNull.class.equals(annotationType)
                    || NotBlank.class.equals(annotationType)
                    || NotEmpty.class.equals(annotationType)) {
                res.setCode(ErrorCode.E_2001.code(), language, field);
            } else {
                res.setCode(ErrorCode.E_3104.code(), language, field);
            }
            return res;
        }
        throw se;
    }
       
    /**
     * 接收参数格式转换异常
     * @param request
     * @param response
     * @param e
     * @return
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ResponseVO<?> argumentNotValidErrorHandler(HttpServletRequest request, HttpServletResponse response,
            HttpMessageNotReadableException e){
        response.setStatus(HttpStatus.OK.value());
        if (e.getCause() instanceof InvalidFormatException) {
            InvalidFormatException ex = (InvalidFormatException) e.getCause();
            String language = request.getHeader("lang");
            ResponseVO<?> res = new ResponseVO<>();
            res.setCode(ErrorCode.E_3104.code(), language, ex.getPath().get(0).getFieldName());
            return res;
        } else if (e.getCause() instanceof JsonMappingException) {
            JsonMappingException ex = (JsonMappingException) e.getCause();
            String language = request.getHeader("lang");
            ResponseVO<?> res = new ResponseVO<>();
            res.setCode(ErrorCode.E_3104.code(), language, ex.getPath().get(0).getFieldName());
            return res;
        }
        return new ResponseVO<>(ErrorCode.E_3104.code());
    }
    
    @ExceptionHandler(RuntimeException.class)
    public ResponseVO<?> handle(HttpServletRequest request, HttpServletResponse response,Exception e){
        //LogUtils.error("exception occurred: ", e);
        response.setStatus(HttpStatus.OK.value());
        String language = request.getHeader("lang");
        ResponseVO<?> res = new ResponseVO<>();
        if(e instanceof HttpMessageNotReadableException){
            res.setCode(ErrorCode.E_3104.code());
            String msg = ErrorCode.E_3001.msg(language);
            res.setMsg(String.format(msg, ""));
        }else{
            res.setCode(ErrorCode.E_3001.code());
            res.setMsg(ErrorCode.E_3001.msg(language));
        }
        
        
        return res; //自己需要实现的异常处理
    }
}

如上,spring在校验后会抛出MethodArgumentNotValidException,ConstraintViolationException(分组校验异常),HttpMessageNotReadableException,处理之后即可自动返回给前端提示信息。

其中的

@ExceptionHandler(RuntimeException.class)
    public ResponseVO<?> handle(HttpServletRequest request, HttpServletResponse response,Exception e){
        //LogUtils.error("exception occurred: ", e);
        response.setStatus(HttpStatus.OK.value());
        String language = request.getHeader("lang");
        ResponseVO<?> res = new ResponseVO<>();
        if(e instanceof HttpMessageNotReadableException){
            res.setCode(ErrorCode.E_3104.code());
            String msg = ErrorCode.E_3001.msg(language);
            res.setMsg(String.format(msg, ""));
        }else{
            res.setCode(ErrorCode.E_3001.code());
            res.setMsg(ErrorCode.E_3001.msg(language));
        }
        
        
        return res; //自己需要实现的异常处理
    }

是除去自定义的异常外,其他无法识别的异常也会统一被处理返回。

至此,全局异常处理加实体类@Validated注解校验,分组校验就基本可以实现了。

@Valid和@Validated的区别

这两个注解的源码:

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {

    /**
     * Specify one or more validation groups to apply to the validation step
     * kicked off by this annotation.
     * <p>JSR-303 defines validation groups as custom annotations which an application declares
     * for the sole purpose of using them as type-safe group arguments, as implemented in
     * {@link org.springframework.validation.beanvalidation.SpringValidatorAdapter}.
     * <p>Other {@link org.springframework.validation.SmartValidator} implementations may
     * support class arguments in other ways as well.
     */
    Class<?>[] value() default {};

}
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface Valid {
}

看下这两个注解的源码发现:
@Valid:没有分组的功能。
@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制
@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上

@Validated比较强大一点,一般用@Validated足够了。

参考:https://blog.csdn.net/qq_32352777/article/details/108424932
https://www.jianshu.com/p/accec85b4039

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容