可以进一步优化的方向

Spring异常处理应该是使用了 HandlerExceptionResolver 接口的实现类,继承层次如下:

  1. HandlerExceptionResolver
  2. └─ AbstractHandlerExceptionResolver
  3. └─ DefaultHandlerExceptionResolver

这里可以去参考 DefaultHandlerExceptionResolver 去实现自己的统一异常处理

统一响应VO

  1. import java.util.HashMap;
  2. import java.util.Map;
  3. public class ResponseVo<T> {
  4. private int code;
  5. private String msg;
  6. private T data;
  7. private ResponseVo() {}
  8. private ResponseVo(int code, String msg, T data) {
  9. this.code = code;
  10. this.msg = msg;
  11. this.data = data;
  12. }
  13. public static <T> ResponseVo<T> ok(T data) {
  14. return new ResponseVo<>(200, "", data);
  15. }
  16. public static <T> ResponseVo<T> fail(int errorCode, String errorMessage) {
  17. return new ResponseVo<>(errorCode, errorMessage, null);
  18. }
  19. public Map<String, Object> toMap() {
  20. Map<String, Object> map = new HashMap<>();
  21. map.put("code", code);
  22. map.put("msg", msg);
  23. map.put("data", data);
  24. return map;
  25. }
  26. public int getCode() {
  27. return code;
  28. }
  29. public void setCode(int code) {
  30. this.code = code;
  31. }
  32. public String getMsg() {
  33. return msg;
  34. }
  35. public void setMsg(String msg) {
  36. this.msg = msg;
  37. }
  38. public T getData() {
  39. return data;
  40. }
  41. public void setData(T data) {
  42. this.data = data;
  43. }
  44. }

统一异常处理控制器

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.parenting.applet.controller.vo.ResponseVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.servlet.http.HttpServletRequest;

/**
 * 统一异常处理
 *
 * @author 吕相成
 * @date 2020-05-27 11:03
 */
@RestController
@ControllerAdvice
public class ExceptionController {
    private final static Logger LOG = LoggerFactory.getLogger(ExceptionController.class);

    /**
     * 数据绑定异常处理
     *
     * @param result 绑定结果
     * @return 统一响应对象
     */
    @ExceptionHandler(BindException.class)
    public ResponseVo bindException(HttpServletRequest request, BindingResult result) {
        StringBuilder builder = new StringBuilder();
        // 如果存在错误
        if (result.hasErrors()) {
            // 往错误消息中添加对象级错误
            for (ObjectError error : result.getGlobalErrors()) {
                String errorMessage = String.format("%s: %s; ", error.getObjectName(), error.getDefaultMessage());
                builder.append(errorMessage);
            }
            // 往错误消息中添加字段级错误
            for (FieldError error : result.getFieldErrors()) {
                String errorMessage;
                if (error.isBindingFailure()) {
                    // 数据绑定失败,默认信息会暴露太多细节,替换为自定义信息
                    errorMessage = String.format("%s: 非法数据; ", error.getField());
                } else {
                    // 数据校验不通过,返回默认信息
                    errorMessage = String.format("%s: %s; ", error.getField(), error.getDefaultMessage());
                }
                builder.append(errorMessage);
            }
        }

        LOG.info(String.format("拒绝访问。非法参数: %s -> %s", request.getServletPath(), builder.toString()));

        return ResponseVo.fail(400, builder.toString());
    }

    /**
     * 处理统一处理404异常
     * 此异常默认不会被抛出,需要进行一些配置
     * 首先配置spring.mvc.throw-exception-if-no-handler-found=true
     * 其次关闭ResourceHandler:spring.resources.add-mappings=false
     * 或者修改ResourceHandler路径:spring.mvc.static-path-pattern=/static/**
     *
     * @param ex 异常对象
     * @return 统一响应对象
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseVo notFoundException(NoHandlerFoundException ex) {
        String msg = String.format("拒绝访问。未找到资源: %s %s %s",
                ex.getHttpMethod(), ex.getRequestURL(), ex.getHeaders());
        LOG.info(msg);
        return ResponseVo.fail(404, "资源未找到");
    }

    /**
     * 处理缺少请求参数的异常
     *
     * @param ex 异常对象
     * @return 统一响应对象
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public ResponseVo missingReqParamException(HttpServletRequest request, MissingServletRequestParameterException ex) {
        String url = request.getServletPath();
        String paramName = ex.getParameterName();

        String msg = String.format("拒绝访问。缺少必需参数: %s -> %s", url, paramName);
        LOG.info(msg);

        return ResponseVo.fail(400, "缺少必需参数: " + paramName);
    }

    /**
     * 处理请求参数类型不匹配的异常
     *
     * @param ex 异常对象
     * @return 统一响应对象
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ResponseVo methodArgumentTypeMismatchException(HttpServletRequest request, MethodArgumentTypeMismatchException ex) {
        String url = request.getServletPath();
        String paramName = ex.getName();
        String paramValue = request.getParameter(paramName);

        String msg = String.format("拒绝访问。请求参数类型错误: %s -> %s=%s", url, paramName, paramValue);
        LOG.info(msg);

        return ResponseVo.fail(400, "参数类型错误" + paramName);
    }

    /**
     * 处理请求方法未支持异常
     *
     * @param request 请求对象
     * @param ex      异常对象
     * @return 统一响应对象
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ResponseVo methodNotSupportedException(HttpServletRequest request, HttpRequestMethodNotSupportedException ex) {
        String url = request.getServletPath();
        String method = ex.getMethod();

        String msg = String.format("拒绝访问。不支持的请求方法: [%s] %s", method, url);
        LOG.info(msg);

        return ResponseVo.fail(400, "不支持该请求方法: " + method);
    }

    /**
     * 处理Http请求转换失败的异常(非可读格式)
     *
     * @param ex 异常对象
     * @return 统一响应对象
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ResponseVo httpMessageNotReadableException(HttpMessageNotReadableException ex) {
        Throwable cause = ex.getCause();
        // Json绑定失败
        if (cause instanceof JsonMappingException) {
            LOG.info("Json绑定失败");
            return handleJsonMappingException((JsonMappingException) cause);
        }

        // Json解析失败
        if (cause instanceof JsonParseException) {
            LOG.info("Json解析失败");
            return ResponseVo.fail(400, "非法的json");
        }

        LOG.info("请求解析失败", ex);
        return ResponseVo.fail(400, "非法请求体");
    }

    /**
     * 更加友好的json数据转换异常处理
     * 从{@link JsonMappingException}中读取异常字段名,拼接到异常消息中
     *
     * @param ex 异常对象
     * @return 统一响应对象
     */
    private ResponseVo handleJsonMappingException(JsonMappingException ex) {
        StringBuilder builder = new StringBuilder();
        for (JsonMappingException.Reference reference : ex.getPath()) {
            builder.append(String.format("%s: 格式非法;", reference.getFieldName()));
        }

        String msg = builder.toString();
        return ResponseVo.fail(400, msg);
    }

    /**
     * 处理其他未定义的内部异常
     *
     * @param ex 异常对象
     * @return 统一响应对象
     */
    @ExceptionHandler(Exception.class)
    public ResponseVo globalException(Exception ex) {
        LOG.error("服务器内部异常!", ex);
        return ResponseVo.fail(500, "服务器内部异常");
    }

}

404异常统一处理需要在application.yml中做的配置

spring:
    mvc:
    static-path-pattern: /static/**
    throw-exception-if-no-handler-found: true

或者

spring:
  mvc:
    throw-exception-if-no-handler-found: true
  resources:
    add-mappings: false