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 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 extends ResponseVO {
    
    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> 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> getList() {
        return list;
    }
    public void setList(List> 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  void validateObj(T t, Class... groups) {
        if (groups == null || groups.length == 0) {
            groups = new Class[] { Default.class };
        }
        Set> cs = validator.validate(t, groups);
        if (!CollectionUtils.isEmpty(cs)) {
            throw new ConstraintViolationException(cs);
        }
    }
    
    /**
     * 分组校验,校验失败返回错误信息
     * @param t
     * @param groups
     * @return 
     */
    public static  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.
     * 

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}. *

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

你可能感兴趣的:(springboot全局异常处理,实体类@Validated注解校验,分组校验实现示例)