SpringBoot全局异常处理请求参数校验及响应体包装

一、响应体包装

  • 全局接口响应体包装,返回json数据
  • 支持对部分接口或者类放行
 # mvc配置
 mvc:
   body-exclude-paths:
     - /test/**
   body-exclude-classes:
     - com.qiangesoft.rdp.starter.XXX
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

/**
 * mvc接口包装配置
 *
 * @author qiangesoft
 * @date 2023-09-18
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "mvc")
public class MvcProperties {

    /**
     * 忽略包装的接口
     */
    private List<String> bodyExcludePaths = new ArrayList<>();

    /**
     * 忽略包装的类
     */
    private List<Class> bodyExcludeClasses = new ArrayList<>();
}
import java.lang.annotation.*;

/**
 * 返回结果包装忽略注解
 *
 * @author qiangesoft
 * @date 2023-09-18
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@ResponseBody
public @interface ResponseBodyIgnore {
}
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qiangesoft.rdp.starter.mvc.config.MvcProperties;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.lang.annotation.Annotation;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * 返回结果包装
 *
 * @author qiangesoft
 * @date 2023-09-18
 */
@Slf4j
@RestControllerAdvice
public class RdpResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private MvcProperties mvcProperties;

    /**
     * 接口忽略包装的注解
     */
    private static final Class<? extends Annotation> ANNOTATION_TYPE = ResponseBodyIgnore.class;

    /**
     * 判断类或者方法是否使用了@ResponseBodyIgnore
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return !AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ANNOTATION_TYPE) && !returnType.hasMethodAnnotation(ANNOTATION_TYPE);
    }

    /**
     * 当类或者方法使用了 @ResponseResultBody 就会调用这个方法
     */
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 配置接口忽略包装
        boolean ignorePath = this.isIgnore(request);
        if (ignorePath) {
            return body;
        }
        // 配置类忽略包装
        boolean ignoreClazz = this.isIgnore(returnType);
        if (ignoreClazz) {
            return body;
        }

        // 未拦截到的错误信息返回json
        Class<?> returnClass = returnType.getMethod().getReturnType();
        if (Objects.equals(returnClass, ResponseEntity.class) && body instanceof LinkedHashMap) {
            Map map = (LinkedHashMap) body;
            Integer status = (Integer) map.get("status");
            String error = (String) map.get("error");
            return ResponseInfo.fail(status, error);
        }

        // 如果返回类型是string,那么springmvc是直接返回的,此时需要手动转化为json
        if (body instanceof String) {
            return objectMapper.writeValueAsString(ResponseInfo.success(body));
        }

        // 防止重复包裹的问题出现
        if (body instanceof ResponseInfo) {
            return body;
        }

        return ResponseInfo.success(body);
    }

    /**
     * 是否忽略包装
     *
     * @param request
     * @return
     */
    private boolean isIgnore(ServerHttpRequest request) {
        String path = request.getURI().getPath();
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        List<String> bodyExcludePaths = mvcProperties.getBodyExcludePaths();
        for (String excludePath : bodyExcludePaths) {
            boolean match = antPathMatcher.match(excludePath, path);
            if (match) {
                return true;
            }
        }
        return false;
    }

    /**
     * 是否忽略包装
     *
     * @param returnType
     * @return
     */
    private boolean isIgnore(MethodParameter returnType) {
        Class clazz = returnType.getContainingClass();
        List<Class> bodyExcludeClasses = mvcProperties.getBodyExcludeClasses();
        for (Class excludeClazz : bodyExcludeClasses) {
            if (excludeClazz.equals(clazz)) {
                return true;
            }
        }
        return false;
    }
}

二、全局异常处理

  • 全局异常拦截
  • 支持异常扩展
import com.qiangesoft.rdp.starter.mvc.exception.result.ExceptionResultHandler;
import com.qiangesoft.rdp.starter.mvc.exception.result.ResultMessageEnum;

/**
 * 异常基类
 *
 * @author qiangesoft
 * @date 2023-09-18
 */
public class BaseException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    /**
     * 错误码
     */
    public int code;

    /**
     * 错误提示
     */
    public String message;

    /**
     * 空构造方法,避免反序列化问题
     */
    public BaseException() {
    }

    public BaseException(String message) {
        this.code = ResultMessageEnum.INTERNAL_SERVER_ERROR.getCode();
        this.message = message;
    }

    public BaseException(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public BaseException(ExceptionResultHandler exceptionAdvice) {
        this.code = exceptionAdvice.getCode();
        this.message = exceptionAdvice.getMessage();
    }

    public int getCode() {
        return this.code;
    }

    public String getMessage() {
        return this.message;
    }

}
/**
 * 异常信息接口
 *
 * @author qiangesoft
 * @date 2023-09-18
 */
public interface ExceptionResultHandler {

    /**
     * 编码
     *
     * @return
     */
    int getCode();

    /**
     * 详细信息
     *
     * @return
     */
    String getMessage();
}

/**
 * 业务异常枚举
 *
 * @author qiangesoft
 * @date 2023-09-18
 */
public enum ResultMessageEnum implements ExceptionResultHandler {

    /***********基础响应码***********/

    SUCCESS(200, "请求成功"),

    FAIL(500, "请求失败"),

    /***********常规响应码***********/

    PARAM_ERROR(4001, "请求参数错误"),

    PARAM_TYPE_ERROR(4002, "参数类型错误"),

    MESSAGE_NOT_READABLE(4003, "参数不可读"),

    BODY_MEDIA_TYPE_NOT_SUPPORT(4004, "请求体MediaType不支持"),

    UNAUTHORIZED(4011, "用户未登录"),

    FORBIDDEN(4031, "权限不足"),

    NOT_FOUND(4041, "请求资源不存在"),

    METHOD_NOT_ALLOWED(4051, "请求方式不正确"),

    INTERNAL_SERVER_ERROR(5001, "服务器内部错误,请稍后再试");

    private int code;

    private String message;

    ResultMessageEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }

    @Override
    public int getCode() {
        return this.code;
    }

    @Override
    public String getMessage() {
        return this.message;
    }
}

import com.qiangesoft.rdp.starter.mvc.exception.BaseException;
import com.qiangesoft.rdp.starter.mvc.exception.NotLoginException;
import com.qiangesoft.rdp.starter.mvc.exception.result.ResultMessageEnum;
import com.qiangesoft.rdp.starter.mvc.response.ResponseInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.nio.file.AccessDeniedException;
import java.util.List;
import java.util.Set;

/**
 * 全局异常处理
 *  * @author qiangesoft
 * @date 2023-09-18
 */
@Slf4j
@RestControllerAdvice
public class ExceptionHandlerAdvice {

    /**
     * 认证异常
     *
     * @param exception
     * @return
     */
    @ExceptionHandler(NotLoginException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ResponseInfo handleNotLoginException(NotLoginException exception) {
        return ResponseInfo.fail(ResultMessageEnum.UNAUTHORIZED);
    }

    /**
     * 权限异常
     *
     * @param exception
     * @return
     */
    @ExceptionHandler(AccessDeniedException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public ResponseInfo handleAccessDeniedException(AccessDeniedException exception) {
        return ResponseInfo.fail(ResultMessageEnum.FORBIDDEN);
    }

    /**
     * 请求方式异常
     *
     * @param exception
     * @return
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    public ResponseInfo handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException exception, HttpServletRequest request) {
        String method = request.getMethod();
        return ResponseInfo.fail(ResultMessageEnum.METHOD_NOT_ALLOWED.getCode(), String.format("请求方式%s不支持", method));
    }

    /**
     * 参数合法性校验异常
     * {@link org.springframework.validation.BindException}(以form-data形式传参)
     * {@link org.springframework.web.bind.MethodArgumentNotValidException}(常以body传参)
     *
     * @param exception
     * @return
     */
    @ExceptionHandler({BindException.class, MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseInfo handleBindException(BindException exception) {
        List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
        StringBuilder message = new StringBuilder();
        for (FieldError fieldError : fieldErrors) {
            message.append(fieldError.getField()).append(fieldError.getDefaultMessage()).append(";");
        }
        return ResponseInfo.fail(ResultMessageEnum.PARAM_ERROR.getCode(), message.toString());
    }

    /**
     * 参数合法性校验异常(通常是query或者form-data传参时的异常)
     *
     * @param exception
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseInfo handleConstraintViolationException(ConstraintViolationException exception) {
        Set<ConstraintViolation<?>> constraintViolations = exception.getConstraintViolations();
        StringBuilder message = new StringBuilder();
        for (ConstraintViolation<?> constraintViolation : constraintViolations) {
            message.append(constraintViolation.getPropertyPath()).append(constraintViolation.getMessage()).append(";");
        }
        return ResponseInfo.fail(ResultMessageEnum.PARAM_ERROR.getCode(), message.toString());
    }

    /**
     * ValidationException异常
     *
     * @param exception
     * @return
     */
    @ExceptionHandler(value = ValidationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseInfo handleValidationException(ValidationException exception) {
        return ResponseInfo.fail(ResultMessageEnum.PARAM_ERROR);
    }

    /**
     * 参数校验异常(以@RequestParam的传参的校验)
     * 例如:接口上设置了@RequestParam("xx")参数,结果并未传递xx参数
     *
     * @param exception
     * @return
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseInfo handleMissingServletRequestParameterException(MissingServletRequestParameterException exception) {
        return ResponseInfo.fail(exception.getMessage());
    }

    /**
     * 参数类型异常(通常为表单传参错误导致参数无法类型转换)
     * 例如:接口上设置了@RequestParam("xx")参数为Integer,结果传递xx参数类型为String
     *
     * @param exception
     * @return
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseInfo handleMissingServletRequestParameterException(MethodArgumentTypeMismatchException exception) {
        return ResponseInfo.fail(ResultMessageEnum.PARAM_TYPE_ERROR.getCode(), exception.getName() + ResultMessageEnum.PARAM_TYPE_ERROR.getMessage());
    }

    /**
     * 参数不可读异常(通常为传参错误导致参数无法解析映射实体属性,body传参时参数类型错误也会进来)
     *
     * @param exception
     * @return
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseInfo handleHttpMessageNotReadableException(HttpMessageNotReadableException exception) {
        return ResponseInfo.fail(ResultMessageEnum.MESSAGE_NOT_READABLE.getCode(), exception.getMessage());
    }

    /**
     * MediaType不支持异常(通常为body传参时contentType错误)
     *
     * @param exception
     * @return
     */
    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
    public ResponseInfo handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException exception) {
        return ResponseInfo.fail(ResultMessageEnum.BODY_MEDIA_TYPE_NOT_SUPPORT.getCode(), exception.getMessage());
    }

    /**
     * 【业务校验】过程中的非法参数异常
     * 该异常基本由{@link org.springframework.util.Assert}抛出
     *
     * @param exception
     * @return
     */
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.OK)
    public ResponseInfo handleIllegalArgumentException(IllegalArgumentException exception) {
        return ResponseInfo.fail(ResultMessageEnum.INTERNAL_SERVER_ERROR.getCode(), exception.getMessage());
    }

    /**
     * 业务异常
     * 该异常基本由{@link com.qiangesoft.rdp.starter.mvc.exception.BaseException}及其子类抛出
     *
     * @param exception
     * @return
     */
    @ExceptionHandler(BaseException.class)
    @ResponseStatus(HttpStatus.OK)
    public ResponseInfo handleBaseException(BaseException exception) {
        return ResponseInfo.fail(exception.getCode(), exception.getMessage());
    }

    /**
     * 未知异常和错误(兜底处理)
     *
     * @param throwable
     * @return
     */
    @ExceptionHandler(Throwable.class)
    @ResponseStatus(HttpStatus.OK)
    public ResponseInfo handleThrowable(Throwable throwable) {
        return ResponseInfo.fail(ResultMessageEnum.INTERNAL_SERVER_ERROR);
    }
}

三、参数校验

1.使用hibernate-validator

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-validationartifactId>
dependency>

2.枚举值处理

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

/**
 * 枚举值校验注解
 *
 * @author qiangesoft
 * @date 2023-09-18
 */
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = EnumValueValidator.class)
public @interface EnumValue {

    String message() default "参数必须为指定的值!";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    Class<? extends IEnum> clazz();
}

import lombok.SneakyThrows;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Objects;

/**
 * 枚举值校验器
 *
 * @author qiangesoft
 * @date 2023-09-18
 */
public class EnumValueValidator implements ConstraintValidator<EnumValue, String> {

    private IEnum[] enums;

    @SneakyThrows
    @Override
    public void initialize(EnumValue constraintAnnotation) {
        enums = constraintAnnotation.clazz().getEnumConstants();
    }

    @Override
    public boolean isValid(String code, ConstraintValidatorContext context) {
        if (Objects.isNull(code)) {
            return false;
        }
        for (IEnum iEnum : enums) {
            if (iEnum.getCode().equals(code)) {
                return true;
            }
        }
        return false;
    }
}

3.业务枚举类需要实现此接口并重写getCode(),getDesc()

/**
 * 枚举定义
 *
 * @author qiangesoft
 * @date 2023-09-18
 */
public interface IEnum {

    /**
     * 获取枚举code
     */
    String getCode();

    /**
     * 获取枚举描述
     */
    String getDesc();
}

源码地址

https://gitee.com/qiangesoft/rdp-starter/tree/master/rdp-starter-mvc

你可能感兴趣的:(Spring系列,spring,boot,异常处理,参数校验,响应体包装)