springboot中优雅实现异常拦截和返回统一结构数据

做前后端分离的项目,为了方便前端处理数据,都会将返回的数据封装到统一的结构下,这样前端拿到数据可以根据指定的字段做不同的业务逻辑处理。

1、异常信息统一拦截

项目开发中,难免会发生异常,如果不做拦截,当项目发生异常时会把异常的堆栈信息返回给前端,这样不仅对前端没有意义,而且会把服务器的相关信息暴露给外部用户造成信息的泄露,在springboot中,可以通过 @RestControllerAdvice 注解实现对异常信息的拦截处理,拦截到异常后给前端返回友好的提示,实现逻辑如下:

  1. 定义一个异常信息拦截类,接口调用中抛出的异常都会被拦截到,拦截到的异常会进入不同的处理方法,将处理后的数据返回给请求端:
import org.example.pojo.ApiResult;
import org.example.pojo.StatusCode;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
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.UnsatisfiedServletRequestParameterException;
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.client.RestClientException;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;

import java.util.List;
import java.util.StringJoiner;

/**
 * 统一异常处理
 *
 * @Author xingo
 * @Date 2023/12/7
 */
@RestControllerAdvice
@ConditionalOnWebApplication
public class ApiExceptionHandler {

    /**
     * 自定义异常处理
     */
    @ExceptionHandler(BusinessException.class)
    public ApiResult businessExceptionHandler(BusinessException e) {
        return ApiResult.fail(e.getCode(), e.getMessage());
    }

    /**
     * 参数错误异常处理
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ApiResult illegalArgumentExceptionHandler(IllegalArgumentException e) {
        e.printStackTrace();
        return ApiResult.fail(StatusCode.C_10004);
    }

    /**
     * 参数校验异常处理
     */
    @ExceptionHandler(BindException.class)
    public ApiResult bindExceptionHandler(BindException e) {
        List<FieldError> errors = e.getBindingResult().getFieldErrors();
        if(errors != null && !errors.isEmpty()) {
            return ApiResult.fail(StatusCode.C_10004).setMessage(errors.get(0).getDefaultMessage());
        }
        return ApiResult.fail(StatusCode.C_10004);
    }

    /**
     * 参数校验异常处理
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResult methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        List<FieldError> errors = e.getBindingResult().getFieldErrors();
        StringBuilder builder = new StringBuilder();
        if(null != errors) {
            for(FieldError error : errors) {
                builder.append(",").append(error.getField()).append(":").append(error.getDefaultMessage());
            }
            return ApiResult.fail(HttpStatus.BAD_REQUEST.value(), "参数校验失败-[" + builder.substring(1) + "]");
        }
        return ApiResult.fail(StatusCode.C_10004);
    }

    /**
     * 不支持的请求方法
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ApiResult<Object> httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException e) {
        e.printStackTrace();
        return ApiResult.fail(HttpStatus.METHOD_NOT_ALLOWED.value(), "不支持的请求方法");
    }

    /**
     * 请求类型错误
     */
    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    public ApiResult<Object> httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedException e) {
        e.printStackTrace();
        return ApiResult.fail(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(), "请求类型错误");
    }

    /**
     * 请求超时
     */
    @ExceptionHandler(AsyncRequestTimeoutException.class)
    public ApiResult<Object> asyncRequestTimeoutExceptionHandler(AsyncRequestTimeoutException e) {
        e.printStackTrace();
        return ApiResult.fail(HttpStatus.REQUEST_TIMEOUT.value(), "请求超时");
    }

    /**
     * 操作数据库出现异常
     */
    @ExceptionHandler(DataAccessException.class)
    public ApiResult<Object> handleSqlException(DataAccessException e) {
        e.printStackTrace();
        return ApiResult.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), "操作数据库出现异常");
    }

    /**
     * 关联接口请求失败
     */
    @ExceptionHandler(RestClientException.class)
    public ApiResult<Object> restClientExceptionHandler(RestClientException e) {
        e.printStackTrace();
        return ApiResult.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), "关联接口请求失败");
    }

    /**
     * 请求参数缺失
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public ApiResult<Object> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException e) {
        e.printStackTrace();
        return ApiResult.fail(HttpStatus.BAD_REQUEST.value(), "请求参数" + e.getParameterName() + "缺失,数据类型:" + e.getParameterType());
    }

    /**
     * 请求参数类型错误
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ApiResult<Object> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException e) {
        e.printStackTrace();
        return ApiResult.fail(HttpStatus.BAD_REQUEST.value(), "请求参数类型错误");
    }

    /**
     * 请求地址不存在
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    public ApiResult<Object> handleNoFoundException(NoHandlerFoundException e) {
        e.printStackTrace();
        return ApiResult.fail(HttpStatus.NOT_FOUND.value(), "请求地址不存在");
    }

    /**
     * 必须的请求参数不能为空
     */
    @ExceptionHandler(MissingServletRequestPartException.class)
    public ApiResult<Object> missingServletRequestPartExceptionHandler(MissingServletRequestPartException e) {
        e.printStackTrace();
        return ApiResult.fail(HttpStatus.BAD_REQUEST.value(), "必须的请求参数" + e.getRequestPartName() + "不能为空");
    }

    /**
     * 请求参数解析异常
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ApiResult<Object> httpMessageNotReadableExceptionHandler(HttpMessageNotReadableException e) {
        e.printStackTrace();
        return ApiResult.fail(HttpStatus.BAD_REQUEST.value(), "请求参数解析异常");
    }

    /**
     * 请求参数不满足
     */
    @ExceptionHandler(UnsatisfiedServletRequestParameterException.class)
    public ApiResult<Object> unsatisfiedServletRequestParameterExceptionHandler(UnsatisfiedServletRequestParameterException e) {
        String conditions = StringUtils.arrayToDelimitedString(e.getParamConditions(), ",");
        StringJoiner params = new StringJoiner(",");
        e.getActualParams().forEach((k, v) -> params.add(k.concat("=").concat(ObjectUtils.nullSafeToString(v))));

        e.printStackTrace();
        return ApiResult.fail(HttpStatus.BAD_REQUEST.value(), "请求参数异常:" + conditions + "|" + params);
    }

    /**
     * 未捕获的异常
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Exception.class)
    public ApiResult exceptionHandler(Exception e) {
        e.printStackTrace();
        return ApiResult.fail(StatusCode.C_ERROR);
    }
}
  1. 自定义了一个异常类,当业务逻辑处理时不符合要求可以抛出下面的异常:
import org.example.pojo.StatusCode;

/**
 * 自定义业务异常
 *
 * @Author xingo
 * @Date 2023/12/7
 */
public class BusinessException extends RuntimeException {

    private int code;

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

    public static BusinessException fail(int code, String message) {
        return new BusinessException(code, message);
    }

    public static BusinessException fail(StatusCode statusCode) {
        return new BusinessException(statusCode.getCode(), statusCode.getMessage());
    }

    public int getCode() {
        return code;
    }
}
  1. 接口统一返回的数据实体类,它的定义如下:
import java.io.Serializable;

/**
 * @Author xingo
 * @Date 2023/10/27
 */
public class ApiResult<T> implements Serializable {

    /**
     * 响应状态码:200-成功;其他-失败
     */
    private int code;
    /**
     * 响应数据
     */
    private T data;
    /**
     * 响应结果描述
     */
    private String message = "";
    /**
     * 响应耗时:毫秒
     */
    private long time;

    public ApiResult() {

    }

    public ApiResult(T data) {
        this.data = data;
    }

    public ApiResult(int code, T data, String message) {
        this.code = code;
        this.data = data;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public ApiResult setCode(int code) {
        this.code = code;
        return this;
    }

    public String getMessage() {
        return message;
    }

    public ApiResult setMessage(String message) {
        this.message = message;
        return this;
    }

    public T getData() {
        return data;
    }

    public ApiResult setData(T data) {
        this.data = data;
        return this;
    }

    public long getTime() {
        return this.time;
    }

    public ApiResult setTime(long time) {
        this.time = time;
        return this;
    }

    /**
     * 成功
     *
     * @return
     */
    public static ApiResult success() {
        ApiResult result = new ApiResult(StatusCode.C_200.getCode(), null, StatusCode.C_200.getMessage());
        return result;
    }

    /**
     * 成功
     *
     * @param data
     * @param <T>
     * @return
     */
    public static <T> ApiResult success(T data) {
        ApiResult result = new ApiResult(StatusCode.C_200.getCode(), data, StatusCode.C_200.getMessage());
        return result;
    }

    /**
     * 失败
     *
     * @param statusCode
     * @return
     */
    public static ApiResult fail(StatusCode statusCode) {
        return new ApiResult().setCode(statusCode.getCode()).setMessage(statusCode.getMessage());
    }

    /**
     * 失败
     *
     * @param code
     * @param message
     * @return
     */
    public static ApiResult fail(int code, String message) {
        return new ApiResult().setCode(code).setMessage(message);
    }

    /**
     * 异常
     *
     * @return
     */
    public static ApiResult error() {
        return new ApiResult().setCode(StatusCode.C_ERROR.getCode()).setMessage(StatusCode.C_ERROR.getMessage());
    }

    /**
     * 判断响应是否为成功响应
     *
     * @return
     */
    public boolean isSuccess() {
        if (this.code != 200) {
            return false;
        }
        return true;
    }

    public static ApiResult copyCodeAndMessage(ApiResult result) {
        return new ApiResult().setCode(result.getCode()).setMessage(result.getMessage());
    }
}
/**
 * 状态枚举
 */
public enum StatusCode {
    /**
     * 正常
     */
    C_200(200, "success"),
    /**
     * 系统繁忙
     */
    C_ERROR(-1, "系统繁忙"),
    /**
     * 特殊错误信息
     */
    C_10000(10000, "特殊错误信息"),
    /**
     * 用户未登录
     */
    C_10001(10001, "用户未登录"),
    /**
     * 用户无访问权限
     */
    C_10002(10002, "用户无访问权限"),
    /**
     * 用户身份验证失败
     */
    C_10003(10003, "用户身份验证失败"),
    /**
     * 请求参数错误
     */
    C_10004(10004, "请求参数错误"),
    /**
     * 请求信息不存在
     */
    C_10005(10005, "请求信息不存在"),
    /**
     * 更新数据失败
     */
    C_10006(10006, "更新数据失败"),
    ;

    private Integer code;
    private String message;

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

    public Integer getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    public static StatusCode getByCode(int code) {
        StatusCode[] values = StatusCode.values();
        for (StatusCode value : values) {
            if (code == value.code) {
                return value;
            }
        }
        return StatusCode.C_ERROR;
    }
}
  1. 写一个测试接口,在方法中抛出异常,会发现返回的信息不是异常的堆栈内容,而是一个友好的提示内容:
import org.example.handler.BusinessException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.*;

/**
 * @Author xingo
 * @Date 2023/12/7
 */
@RestController
public class DemoController {

    @GetMapping("/demo1")
    public Object demo1() {
        int i = 1, j = 0;

        return i / j;
    }

    @GetMapping("/demo2")
    public Object demo2() {
        if(System.currentTimeMillis() > 1) {
            throw BusinessException.fail(88888, "业务数据不合法");
        }

        return System.currentTimeMillis();
    }

    @GetMapping("/demo3")
    public Map<String, Object> demo3() {
        Map<String, Object> map = new HashMap<>();
        map.put("key1", "Hello,world!");
        map.put("key2", new Date());

        return map;
    }

    @GetMapping("/demo4")
    public List<Object> demo4() {
        List<Object> list = new ArrayList<>();
        list.add(new Date());
        list.add("Hello,world!");
        list.add(Long.MAX_VALUE);
        list.add(Integer.MAX_VALUE);

        return list;
    }
}
2、返回统一数据结构

如果接口返回的数据格式不统一,接口请求端处理数据就会非常麻烦,在接口交互中,可以定义好一个数据结构,接口服务端和请求端根据这个结构封装数据,这样处理数据就会变得容易,假设现在统一的数据结构是ApiResult这个结构,规定code=200时表示成功,其他的code都是失败并且定义好code的值表示的失败含义。要达到统一数据结构返回,需要借助springboot中的ResponseBodyAdvice接口,它的主要作用是:拦截Controller方法的返回值,统一处理返回值/响应体,一般用来统一返回格式,加解密,签名等等。只要借助这个接口里面的两个方法:supports()和beforeBodyWrite()就可以实现我们想要的功能:

import org.example.pojo.ApiResult;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.lang.annotation.Annotation;
import java.util.Objects;

/**
 * @Author xingo
 * @Date 2023/12/7
 */
@RestControllerAdvice
public class ApiResultResponseBodyAdvice implements ResponseBodyAdvice<Object>, Ordered {

    private static final Class<? extends Annotation> ANNOTATION_TYPE = ResponseBody.class;

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

    /**
     * 当写入body之前调用这个方法
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        // String类型的返回值要单独处理否则会报错:将数据写入data字段然后再序列化为json字符串
        Class<?> returnClass = returnType.getMethod().getReturnType();
        if(body instanceof String || Objects.equals(returnClass, String.class)) {
            return JacksonUtils.toJSONString(ApiResult.success(body));
        }
        // 已经是目标数据类型不处理
        if(body instanceof ApiResult) {
            return body;
        }
        // 封装统一对象
        return ApiResult.success(body);
    }

    @Override
    public int getOrder() {
        return Integer.MIN_VALUE + 1;
    }
}

这里面用到了json序列化,使用jackson封装了一个json处理类对实体类进行序列化操作:

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;

import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

/**
 * @Author xingo
 * @Date 2023/12/7
 */
public class JacksonUtils {

    private static final ObjectMapper mapper = new ObjectMapper();

    static {
        JavaTimeModule module = new JavaTimeModule();

        // 序列化时对Long类型进行处理,避免前端js处理数据时精度缺失
        module.addSerializer(Long.class, ToStringSerializer.instance);
        module.addSerializer(Long.TYPE, ToStringSerializer.instance);
        // java8日期处理
        module.addSerializer(LocalDateTime.class,
                new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        module.addSerializer(LocalDate.class,
                new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        module.addSerializer(LocalTime.class,
                new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
        module.addDeserializer(LocalDateTime.class,
                new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        module.addDeserializer(LocalDate.class,
                new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        module.addDeserializer(LocalTime.class,
                new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));

        mapper.registerModules(module);
        // 反序列化的时候如果多了其他属性,不抛出异常
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 如果是空对象的时候,不抛异常
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        // 空对象不序列化
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        // 日期格式化
        mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        // 设置时区
        mapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
        // 驼峰转下划线
//        mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
        // 语言
        mapper.setLocale(Locale.SIMPLIFIED_CHINESE);
    }

    /**
     * 反序列化
     * @param json      json字符串
     * @param clazz     发序列化类型
     * @return
     * @param <T>
     */
    public static <T> T parseObject(String json, Class<T> clazz) {
        try {
            return mapper.readValue(json, clazz);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 反序列化列表
     * @param json
     * @return
     * @param <T>
     */
    public static <T> List<T> parseArray(String json) {
        try {
            TypeReference<List<T>> type = new TypeReference<List<T>>(){};
            return mapper.readValue(json, type);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 写为json串
     * @param obj   对象
     * @return
     */
    public static String toJSONString(Object obj) {
        try {
            return mapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }
}

通过上面的封装处理,只要接口返回的数据不是ApiResult类型,都会封装成该类型返回,达到统一数据类型的目的。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值