文档章节

springboot之全局处理异常封装

Purgeyao
 Purgeyao
发布于 09/22 10:38
字数 1819
阅读 81
收藏 0

springboot之全局处理异常封装

简介

在项目中经常出现系统异常的情况,比如NullPointerException等等。如果默认未处理的情况下,springboot会响应默认的错误提示,这样对用户体验不是友好,系统层面的错误,用户不能感知到,即使为500的错误,可以给用户提示一个类似服务器开小差的友好提示等。

在微服务里,每个服务中都会有异常情况,几乎所有服务的默认异常处理配置一致,导致很多重复编码,我们将这些重复默认异常处理可以抽出一个公共starter包,各个服务依赖即可,定制化异常处理在各个模块里开发。

配置

unified-dispose-springboot-starter

这个模块里包含异常处理以及全局返回封装等功能,下面。

完整目录结构如下:

├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── purgetiem
│   │   │           └── starter
│   │   │               └── dispose
│   │   │                   ├── GlobalDefaultConfiguration.java
│   │   │                   ├── GlobalDefaultProperties.java
│   │   │                   ├── Interceptors.java
│   │   │                   ├── Result.java
│   │   │                   ├── advice
│   │   │                   │   └── CommonResponseDataAdvice.java
│   │   │                   ├── annotation
│   │   │                   │   ├── EnableGlobalDispose.java
│   │   │                   │   └── IgnorReponseAdvice.java
│   │   │                   └── exception
│   │   │                       ├── GlobalDefaultExceptionHandler.java
│   │   │                       ├── category
│   │   │                       │   └── BusinessException.java
│   │   │                       └── error
│   │   │                           ├── CommonErrorCode.java
│   │   │                           └── details
│   │   │                               └── BusinessErrorCode.java
│   │   └── resources
│   │       ├── META-INF
│   │       │   └── spring.factories
│   │       └── dispose.properties
│   └── test
│       └── java

异常处理

@RestControllerAdvice 或者 @ControllerAdvicespring的异常处理注解。

我们先创建GlobalDefaultExceptionHandler 全局异常处理类:

@RestControllerAdvice
public class GlobalDefaultExceptionHandler {

  private static final Logger log = LoggerFactory.getLogger(GlobalDefaultExceptionHandler.class);

  /**
   * NoHandlerFoundException 404 异常处理
   */
  @ExceptionHandler(value = NoHandlerFoundException.class)
  @ResponseStatus(HttpStatus.NOT_FOUND)
  public Result handlerNoHandlerFoundException(NoHandlerFoundException exception) {
    outPutErrorWarn(NoHandlerFoundException.class, CommonErrorCode.NOT_FOUND, exception);
    return Result.ofFail(CommonErrorCode.NOT_FOUND);
  }

  /**
   * HttpRequestMethodNotSupportedException 405 异常处理
   */
  @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
  public Result handlerHttpRequestMethodNotSupportedException(
      HttpRequestMethodNotSupportedException exception) {
    outPutErrorWarn(HttpRequestMethodNotSupportedException.class,
        CommonErrorCode.METHOD_NOT_ALLOWED, exception);
    return Result.ofFail(CommonErrorCode.METHOD_NOT_ALLOWED);
  }

  /**
   * HttpMediaTypeNotSupportedException 415 异常处理
   */
  @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
  public Result handlerHttpMediaTypeNotSupportedException(
      HttpMediaTypeNotSupportedException exception) {
    outPutErrorWarn(HttpMediaTypeNotSupportedException.class,
        CommonErrorCode.UNSUPPORTED_MEDIA_TYPE, exception);
    return Result.ofFail(CommonErrorCode.UNSUPPORTED_MEDIA_TYPE);
  }

  /**
   * Exception 类捕获 500 异常处理
   */
  @ExceptionHandler(value = Exception.class)
  public Result handlerException(Exception e) {
    return ifDepthExceptionType(e);
  }

  /**
   * 二次深度检查错误类型
   */
  private Result ifDepthExceptionType(Throwable throwable) {
    Throwable cause = throwable.getCause();
    if (cause instanceof ClientException) {
      return handlerClientException((ClientException) cause);
    }
    if (cause instanceof FeignException) {
      return handlerFeignException((FeignException) cause);
    }
    outPutError(Exception.class, CommonErrorCode.EXCEPTION, throwable);
    return Result.ofFail(CommonErrorCode.EXCEPTION);
  }

  /**
   * FeignException 类捕获
   */
  @ExceptionHandler(value = FeignException.class)
  public Result handlerFeignException(FeignException e) {
    outPutError(FeignException.class, CommonErrorCode.RPC_ERROR, e);
    return Result.ofFail(CommonErrorCode.RPC_ERROR);
  }

  /**
   * ClientException 类捕获
   */
  @ExceptionHandler(value = ClientException.class)
  public Result handlerClientException(ClientException e) {
    outPutError(ClientException.class, CommonErrorCode.RPC_ERROR, e);
    return Result.ofFail(CommonErrorCode.RPC_ERROR);
  }

  /**
   * BusinessException 类捕获
   */
  @ExceptionHandler(value = BusinessException.class)
  public Result handlerBusinessException(BusinessException e) {
    outPutError(BusinessException.class, CommonErrorCode.BUSINESS_ERROR, e);
    return Result.ofFail(e.getCode(), e.getMessage());
  }

  /**
   * HttpMessageNotReadableException 参数错误异常
   */
  @ExceptionHandler(HttpMessageNotReadableException.class)
  public Result handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
    outPutError(HttpMessageNotReadableException.class, CommonErrorCode.PARAM_ERROR, e);
    String msg = String.format("%s : 错误详情( %s )", CommonErrorCode.PARAM_ERROR.getMessage(),
        e.getRootCause().getMessage());
    return Result.ofFail(CommonErrorCode.PARAM_ERROR.getCode(), msg);
  }

  /**
   * BindException 参数错误异常
   */
  @ExceptionHandler(BindException.class)
  public Result handleMethodArgumentNotValidException(BindException e) {
    outPutError(BindException.class, CommonErrorCode.PARAM_ERROR, e);
    BindingResult bindingResult = e.getBindingResult();
    return getBindResultDTO(bindingResult);
  }

  private Result getBindResultDTO(BindingResult bindingResult) {
    List<FieldError> fieldErrors = bindingResult.getFieldErrors();
    if (log.isDebugEnabled()) {
      for (FieldError error : fieldErrors) {
        log.error("{} -> {}", error.getDefaultMessage(), error.getDefaultMessage());
      }
    }

    if (fieldErrors.isEmpty()) {
      log.error("validExceptionHandler error fieldErrors is empty");
      Result.ofFail(CommonErrorCode.BUSINESS_ERROR.getCode(), "");
    }

    return Result
        .ofFail(CommonErrorCode.PARAM_ERROR.getCode(), fieldErrors.get(0).getDefaultMessage());
  }

  public void outPutError(Class errorType, Enum secondaryErrorType, Throwable throwable) {
    log.error("[{}] {}: {}", errorType.getSimpleName(), secondaryErrorType, throwable.getMessage(),
        throwable);
  }

  public void outPutErrorWarn(Class errorType, Enum secondaryErrorType, Throwable throwable) {
    log.warn("[{}] {}: {}", errorType.getSimpleName(), secondaryErrorType, throwable.getMessage());
  }

}

大致内容处理了一些项目常见的异常Exception,BindException参数异常等。

这里将默认的404405415等默认http状态码也重写了。

重写这个默认的状态码需要配置throw-exception-if-no-handler-found以及add-mappings

# 出现错误时, 直接抛出异常(便于异常统一处理,否则捕获不到404)
spring.mvc.throw-exception-if-no-handler-found=true
# 是否开启默认的资源处理,默认为true
spring.resources.add-mappings=false

ps: 请注意这两个配置会将静态资源忽略。

请产考WebMvcAutoConfiguration#addResourceHandlers

Exception为了防止未知的异常没有防护到,默认给用户返回服务器开小差,请稍后再试等提示。

具体异常默认会以小到大去匹配。

如果抛出BindException,自定义有BindException就会去这个处理器里处理。没有就会走到它的父类去匹配,请参考java-异常体系

img

其他已知异常可以自己用@ExceptionHandler注解进行捕获处理。

通用异常枚举

为了避免异常值不好维护,我们使用CommonErrorCode枚举把常见的异常提示维护起来。

@Getter
public enum CommonErrorCode {

  /**
   * 404 Web 服务器找不到您所请求的文件或脚本。请检查URL 以确保路径正确。
   */
  NOT_FOUND("CLOUD-404",
      String.format("哎呀,无法找到这个资源啦(%s)", HttpStatus.NOT_FOUND.getReasonPhrase())),

  /**
   * 405 对于请求所标识的资源,不允许使用请求行中所指定的方法。请确保为所请求的资源设置了正确的 MIME 类型。
   */
  METHOD_NOT_ALLOWED("CLOUD-405",
      String.format("请换个姿势操作试试(%s)", HttpStatus.METHOD_NOT_ALLOWED.getReasonPhrase())),

  /**
   * 415 Unsupported Media Type
   */
  UNSUPPORTED_MEDIA_TYPE("CLOUD-415",
      String.format("呀,不支持该媒体类型(%s)", HttpStatus.UNSUPPORTED_MEDIA_TYPE.getReasonPhrase())),

  /**
   * 系统异常 500 服务器的内部错误
   */
  EXCEPTION("CLOUD-500", "服务器开小差,请稍后再试"),

  /**
   * 系统限流
   */
  TRAFFIC_LIMITING("CLOUD-429", "哎呀,网络拥挤请稍后再试试"),

  /**
   * 服务调用异常
   */
  API_GATEWAY_ERROR("API-9999", "网络繁忙,请稍后再试"),

  /**
   * 参数错误
   */
  PARAM_ERROR("CLOUD-100", "参数错误"),

  /**
   * 业务异常
   */
  BUSINESS_ERROR("CLOUD-400", "业务异常"),

  /**
   * rpc调用异常
   */
  RPC_ERROR("RPC-510", "呀,网络出问题啦!");

  private String code;

  private String message;

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

其实starter包中不建议使用@Getterlombok注解,防止他人未使用lombok依赖该项目出现问题。

通用业务异常

这两个类完成基本可以正常使用异常拦截了,不过为了业务方便,我们创建一个一般通用的业务异常。

BusinessException继承RuntimeException即可。

@Getter
public class BusinessException extends RuntimeException {

  private String code;
  private boolean isShowMsg = true;

  /**
   * 使用枚举传参
   *
   * @param errorCode 异常枚举
   */
  public BusinessException(BusinessErrorCode errorCode) {
    super(errorCode.getMessage());
    this.code = errorCode.getCode();
  }

  /**
   * 使用自定义消息
   *
   * @param code 值
   * @param msg 详情
   */
  public BusinessException(String code, String msg) {
    super(msg);
    this.code = code;
  }

}

BusinessException加入GlobalDefaultExceptionHandler全局异常拦截。

/**
 * BusinessException 类捕获
 */
@ExceptionHandler(value = BusinessException.class)
public Result handlerBusinessException(BusinessException e) {
  outPutError(BusinessException.class, CommonErrorCode.BUSINESS_ERROR, e);
  return Result.ofFail(e.getCode(), e.getMessage());
}

程序主动抛出异常可以通过下面方式:

throw new BusinessException(BusinessErrorCode.BUSINESS_ERROR);
// 或者
throw new BusinessException("CLOUD800","没有多余的库存");

通常不建议直接抛出通用的BusinessException异常,应当在对应的模块里添加对应的领域的异常处理类以及对应的枚举错误类型。

如会员模块: 创建UserException异常类、UserErrorCode枚举、以及UserExceptionHandler统一拦截类。

UserException:

@Data
public class UserException extends RuntimeException {

  private String code;
  private boolean isShowMsg = true;

  /**
   * 使用枚举传参
   *
   * @param errorCode 异常枚举
   */
  public UserException(UserErrorCode errorCode) {
    super(errorCode.getMessage());
    this.setCode(errorCode.getCode());
  }

}

UserErrorCode:

@Getter
public enum UserErrorCode {
    /**
     * 权限异常
     */
    NOT_PERMISSIONS("CLOUD401","您没有操作权限"),
    ;

    private String code;

    private String message;

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

UserExceptionHandler:

@Slf4j
@RestControllerAdvice
public class UserExceptionHandler {

  /**
   * UserException 类捕获
   */
  @ExceptionHandler(value = UserException.class)
  public Result handler(UserException e) {
    log.error(e.getMessage(), e);
    return Result.ofFail(e.getCode(), e.getMessage());
  }

}

最后业务使用如下:

// 判断是否有权限抛出异常
throw new UserException(UserErrorCode.NOT_PERMISSIONS);

加入spring容器

最后将GlobalDefaultExceptionHandlerbean的方式注入spring容器。

@Configuration
@EnableConfigurationProperties(GlobalDefaultProperties.class)
@PropertySource(value = "classpath:dispose.properties", encoding = "UTF-8")
public class GlobalDefaultConfiguration {

  @Bean
  public GlobalDefaultExceptionHandler globalDefaultExceptionHandler() {
    return new GlobalDefaultExceptionHandler();
  }

  @Bean
  public CommonResponseDataAdvice commonResponseDataAdvice(GlobalDefaultProperties globalDefaultProperties){
    return new CommonResponseDataAdvice(globalDefaultProperties);
  }

}

GlobalDefaultConfigurationresources/META-INF/spring.factories文件下加载。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.purgetime.starter.dispose.GlobalDefaultConfiguration

不过我们这次使用注解方式开启。其他项目依赖包后,需要添加@EnableGlobalDispose才可以将全局拦截的特性开启。

将刚刚创建的spring.factories注释掉,创建EnableGlobalDispose注解。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(GlobalDefaultConfiguration.class)
public @interface EnableGlobalDispose {

}

使用@ImportGlobalDefaultConfiguration导入即可。

使用

添加依赖

<dependency>
  <groupId>com.purgeteam</groupId>
  <artifactId>unified-dispose-deepblueai-starter</artifactId>
  <version>0.1.1.RELEASE</version>
</dependency>

启动类开启@EnableGlobalDispose注解即可。

总结

项目里很多重复的code,我们可以通过一定的方式去简化,以达到一定目的减少开发量。

示例代码地址:unified-dispose-springboot

作者GitHub: Purgeyao 欢迎关注

© 著作权归作者所有

Purgeyao
粉丝 0
博文 11
码字总数 12094
作品 0
崇明
私信 提问
SpringBoot基于@ControllerAdvice配置全局异常处理

版权声明:欢迎访问,本文为小编原创文章 https://blog.csdn.net/changyinling520/article/details/81431490 SpringBoot默认全局异常处理 SpringBoot提供了一个默认的映射:/error,当处理中抛...

火腿编程
2018/08/05
0
0
SpringBoot RESTful 应用中的异常处理小结

@ControllerAdvice 和 @ExceptionHandler 的区别 ExceptionHandler, 方法注解, 作用于 Controller 级别. ExceptionHandler 注解为一个 Controler 定义一个异常处理器. ControllerAdvice, 类注......

xiaogong1688
2018/06/29
0
0
SpringBoot系列教程web篇之自定义异常处理HandlerExceptionResolver

关于Web应用的全局异常处理,上一篇介绍了结合的方式来实现web应用的全局异常管理; 本篇博文则带来另外一种并不常见的使用方式,通过实现自定义的,来处理异常状态 上篇博文链接: SpringBoo...

小灰灰Blog
10/14
8
0
不学无数——SpringBoot入门V

SpringBoot 1.开发一个Web程序 SpringBoot是非常适合开发Web应用的,因为他内嵌有Tomcat、Jetty、Undertow或者Netty。大部分的应用可以通过加载spring-boot-starter-web模块能够快速的创建并...

不学无数d
2018/07/28
0
0
springboot(十七):使用Spring Boot上传文件

上传文件是互联网中常常应用的场景之一,最典型的情况就是上传头像等,今天就带着带着大家做一个Spring Boot上传文件的小案例。 1、pom包配置 我们使用Spring Boot最新版本1.5.9、jdk使用1.8...

ityouknow
2018/10/28
0
0

没有更多内容

加载失败,请刷新页面

加载更多

32位与64位Linux系统下各类型长度对比

64 位的优点:64 位的应用程序可以直接访问 4EB 的内存和文件大小最大达到4 EB(2 的 63 次幂);可以访问大型数据库。本文介绍的是64位下C语言开发程序注意事项。 1. 32 位和 64 位C数据类型...

mskk
12分钟前
2
0
Vue 实现点击空白处隐藏某节点(三种方式:指令、普通、遮罩)

在项目中往往会有这样的需求: 弹出框(或Popover)在 show 后,点击空白处可以将其 hide。 针对此需求,整理了三种实现方式,大家按实际情况选择。 当然,我们做项目肯定会用到 UI 框架,常...

张兴华ZHero
18分钟前
3
0
SpringBoot激活profiles你知道几种方式?

多环境是最常见的配置隔离方式之一,可以根据不同的运行环境提供不同的配置信息来应对不同的业务场景,在SpringBoot内支持了多种配置隔离的方式,可以激活单个或者多个配置文件。 激活Profi...

恒宇少年
20分钟前
5
0
PDF修改文字的方法有哪些?怎么修改PDF文件中的文字

PDF修改文字一直以来都是一个难以解决的问题,很多的办公族在办公的时候会有修改PDF文件中的文字的需要,可是PDF文件一般是不能进行编辑和修改的,难道就没有什么办法解决这个问题了嘛?不要...

趣味办公社
23分钟前
2
0
企业组织中采用服务网格的挑战

作者:Christian Posta 译者:罗广明 原文:https://blog.christianposta.com/challenges-of-adopting-service-mesh-in-enterprise-organizations/ 编者按 本文作者介绍了企业组织采用服务网...

jimmysong
32分钟前
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部