Spring Boot 统一异常处理 | Eddie'Blog
Spring Boot 统一异常处理

Spring Boot 统一异常处理

eddie 534 2020-05-08

前言

现在流行的基本前后端交互都是通过Json格式,所以有一定规模的项目都会这样子做,往往后端定义统一规范给前端进行交互。 (基本的代码片在这,不提供额外的github地址)

响应处理

结果返回格式

{
    "success": false,
    "code": "G000000",
    "message": "环境:[dev], 微服务: [content-center], 接口 [/exception4] 业务异常",
    "resultTime": "2020-05-07T16:30:27.072",
    "data": null
}

结果包含内容

  • 是否响应成功标识;
  • 响应状态码;
  • 响应描述;

结果枚举

  • 返回字段:success、code、message
  • 枚举值可以根据自身增加
@Getter
@AllArgsConstructor
public enum ResultCodeEnum {

    /**
     * 成功
     */
    SUCCESS(
            true,
            "G200000",
            "成功"),
    /**
     * 失败 (错误请求)
     */
    FAIL(
            false,
            "G40000",
            "失败"),
    /**
     * 未认证(签名错误)
     */
    UNAUTHORIZED(
            false,
            "G401000",
            "未认证"),
    /**
     * 接口不存在
     */
    NOT_FOUND(
            false,
            "G404000",
            "接口不存在"),
    /**
     * 服务器内部错误
     */
    INTERNAL_SERVER_ERROR(
            false,
            "G500000",
            "服务器内部错误")
    ;

    /**
     * 是否成功
     */
    private Boolean success;

    /**
     * 状态码
     */
    private String code;

    /**
     * 返回信息
     */
    private String message;

}

结果类

  • 返回给前端的类
  • T 是任何对象形式传入返回
  • return this 为了链式编程
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> implements Serializable {

	private static final long serialVersionUID = 1573334432703774091L;

	private Boolean success;

	private String code;

	private String message;

	private LocalDateTime resultTime;

	private T data;

	@JsonInclude(JsonInclude.Include.NON_NULL)
	private Map<String, Object> param = new HashMap<>();

	public static Result<?> ok() {
		return Result.builder()
				.success(ResultCodeEnum.SUCCESS.getSuccess())
				.code(ResultCodeEnum.SUCCESS.getCode())
				.message(ResultCodeEnum.SUCCESS.getMessage())
				.resultTime(LocalDateTime.now())
				.build();
	}

	public static Result<?> error() {
		return Result.builder()
				.success(ResultCodeEnum.FAIL.getSuccess())
				.code(ResultCodeEnum.FAIL.getCode())
				.message(ResultCodeEnum.FAIL.getMessage())
				.resultTime(LocalDateTime.now())
				.build();
	}

	public static Result<?> setResult(ResultCodeEnum resultCodeEnum) {
		return Result.builder()
				.success(resultCodeEnum.getSuccess())
				.code(resultCodeEnum.getCode())
				.message(resultCodeEnum.getMessage())
				.resultTime(LocalDateTime.now())
				.build();
	}
	
	public Result<?> message(String message) {
		this.setMessage(message);
		return this;
	}

	public Result<?> code(String code) {
		this.setCode(code);
		return this;
	}

	public Result<?> success(Boolean success) {
		this.setSuccess(success);
		return this;
	}

	public Result<?> data(Object data) {
		this.setData((T) data);
		return this;
	}
	
}

异常处理

自定义全局异常类

  • 还有一种方式继承Exception,作为父类,提供给子类继承 (有经验一定Java基础必须懂)
@Data
public class BusinessException extends RuntimeException {

	private String code;

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

	public BusinessException(ResultCodeEnum resultCodeEnum) {
		super(resultCodeEnum.getMessage());
		this.code = resultCodeEnum.getCode();
	}

	@Override
	public String toString() {
		return "BusinessException{" + "code=" + code + ", message=" + this.getMessage() + '}';
	}
}

统一异常处理器

  • @ControllerAdvice 和 RestControllerAdvice 的区别就是返现Json格式,不然就需要加 @RequestBody
  • 除了全局异常HttpStatus=500,其余 HttpStatus=200
  • 加入了环境和服务名称
@Slf4j
@RestControllerAdvice
public class GlobalExceptionAdapter {

	@Value("${spring.profiles.active}")
	String profile;

	@Value("${spring.application.name}")
	String appName;

	/**
	 * 全局异常
	 */
	@ExceptionHandler(Exception.class)
	@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
	public Result<?> exception(Exception e) {
		log.info("全局异常信息 ex={}", e.getMessage(), e);
		HttpServletRequest request = getHttpServletRequest();
		return Result.error().message("环境:[" + profile + "], 微服务: [" + appName + "], 接口 [" + request.getRequestURI() + "] 内部错误,请联系管理员");
	}

	/**
	 * 方法接收到非法参数异常
	 */
	@ExceptionHandler(IllegalArgumentException.class)
	@ResponseStatus(HttpStatus.OK)
	public Result<?> illegalArgumentException(IllegalArgumentException e) {
		log.error("方法接收到非法参数异常 ex={}", e.getMessage(), e);
		HttpServletRequest request = getHttpServletRequest();
		return Result.error().message("环境:[" + profile + "], 微服务: [" + appName + "], 接口 [" + request.getRequestURI() + "] 非法参数异常");
	}

	/**
	 * 处理文件上传大小过大异常
	 */
	@ExceptionHandler(MultipartException.class)
	@ResponseStatus(HttpStatus.OK)
	public Result<?> multipartException(MultipartException e) {
		log.error("上传文件大小超过最大限制 最大文件大小限制为:10M ex={}", e.getMessage(), e);
		HttpServletRequest request = getHttpServletRequest();
		return Result.error().message("环境:[" + profile + "], 微服务: [" + appName + "], 接口 [" + request.getRequestURI() + "] 上传大小过大异常");
	}

	/**
	 * 方法无效异常
	 */
	@ExceptionHandler(MethodArgumentNotValidException.class)
	@ResponseStatus(HttpStatus.OK)
	public Result<?> businessException(MethodArgumentNotValidException e) {
		log.error(CommonConstants.GLOBAL_EXCEPTION, e.getMessage(), e);
		HttpServletRequest request = getHttpServletRequest();
		List<String> messages = e.getBindingResult().getFieldErrors()
				.stream()
				.map(DefaultMessageSourceResolvable::getDefaultMessage)
				.collect(Collectors.toList()
				);
		return Result.error().message("环境:[" + profile + "], 微服务: [" + appName + "], 接口 [" + request.getRequestURI() + "] 方法无效异常:");
	}

	/**
	 * 业务异常
	 */
	@ExceptionHandler(BusinessException.class)
	@ResponseStatus(HttpStatus.OK)
	public Result<?> businessException(BusinessException e) {
		log.error(CommonConstants.GLOBAL_EXCEPTION, e.getMessage(), e);
		HttpServletRequest request = getHttpServletRequest();
		return Result.error().code(e.getCode()).message("环境:[" + profile + "], 微服务: [" + appName + "], 接口 [" + request.getRequestURI() + "] 业务异常");
	}

	/**
	 * 方法参数异常
	 */
	@ExceptionHandler(BindException.class)
	@ResponseStatus(HttpStatus.OK)
	public Result<?> businessException(BindException e) {
		log.error(CommonConstants.GLOBAL_EXCEPTION, e.getMessage(), e);
		HttpServletRequest request = getHttpServletRequest();
		List<String> messages = e.getBindingResult().getFieldErrors()
				.stream()
				.map(DefaultMessageSourceResolvable::getDefaultMessage)
				.collect(Collectors.toList()
				);
		return Result.error().message("环境:[" + profile + "], 微服务: [" + appName + "], 接口 [" + request.getRequestURI() + "] 方法参数异常");
	}

	public static HttpServletRequest getHttpServletRequest() {
		RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
		ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
		return Objects.requireNonNull(attributes).getRequest();
	}

}

TIPS

  • MethodArgumentNotValidException拦截问题

那么还需要在控制层添加BingdingResult吗? 答案是需要的。不然你所有的错误都会给 MethodArgumentNotValidException.class拦截,原因是@Validated需要和 BingdingResult绑定使用。详情可以看底层源码了解!


# Java