微服务架构统一异常处理规范

1. 统一使用ResponseEntity类 : 用于统一响应格式

  • 使用org.springframework.http.ResponseEntity作为统一响应格式

  • 示例:

    @GetMapping("/as/{id}")
    @Timed
    public ResponseEntity<A> getA(@PathVariable Long id) {
        log.debug("REST request to get A : {}", id);
        A a = aService.findOne(id);
        return ResponseUtil.wrapOrNotFound(Optional.ofNullable(a));
    }
    

2.统一的Error类 : 用于统一异常格式

  • 统一使用org.zalando.problem.ThrowableProblem的子类作为异常格式
image.png
  • 统一使用httpStatus作为错误编码

    public enum Status implements StatusType {
       CONTINUE(100, "Continue"),
       SWITCHING_PROTOCOLS(101, "Switching Protocols"),
       PROCESSING(102, "Processing"),
       CHECKPOINT(103, "Checkpoint"),
       OK(200, "OK"),
       CREATED(201, "Created"),
       ACCEPTED(202, "Accepted"),
       NON_AUTHORITATIVE_INFORMATION(203, "Non-Authoritative Information"),
       NO_CONTENT(204, "No Content"),
       RESET_CONTENT(205, "Reset Content"),
       PARTIAL_CONTENT(206, "Partial Content"),
       MULTI_STATUS(207, "Multi-Status"),
       ALREADY_REPORTED(208, "Already Reported"),
       IM_USED(226, "IM Used"),
       MULTIPLE_CHOICES(300, "Multiple Choices"),
       MOVED_PERMANENTLY(301, "Moved Permanently"),
       FOUND(302, "Found"),
       SEE_OTHER(303, "See Other"),
       NOT_MODIFIED(304, "Not Modified"),
       USE_PROXY(305, "Use Proxy"),
       TEMPORARY_REDIRECT(307, "Temporary Redirect"),
       PERMANENT_REDIRECT(308, "Permanent Redirect"),
       BAD_REQUEST(400, "Bad Request"),
       UNAUTHORIZED(401, "Unauthorized"),
       PAYMENT_REQUIRED(402, "Payment Required"),
       FORBIDDEN(403, "Forbidden"),
       NOT_FOUND(404, "Not Found"),
       METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
       NOT_ACCEPTABLE(406, "Not Acceptable"),
       PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required"),
       REQUEST_TIMEOUT(408, "Request Timeout"),
       CONFLICT(409, "Conflict"),
       GONE(410, "Gone"),
       LENGTH_REQUIRED(411, "Length Required"),
       PRECONDITION_FAILED(412, "Precondition Failed"),
       REQUEST_ENTITY_TOO_LARGE(413, "Request Entity Too Large"),
       REQUEST_URI_TOO_LONG(414, "Request-URI Too Long"),
       UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"),
       REQUESTED_RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"),
       EXPECTATION_FAILED(417, "Expectation Failed"),
       I_AM_A_TEAPOT(418, "I'm a teapot"),
       UNPROCESSABLE_ENTITY(422, "Unprocessable Entity"),
       LOCKED(423, "Locked"),
       FAILED_DEPENDENCY(424, "Failed Dependency"),
       UPGRADE_REQUIRED(426, "Upgrade Required"),
       PRECONDITION_REQUIRED(428, "Precondition Required"),
       TOO_MANY_REQUESTS(429, "Too Many Requests"),
       REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large"),
       INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
       NOT_IMPLEMENTED(501, "Not Implemented"),
       BAD_GATEWAY(502, "Bad Gateway"),
       SERVICE_UNAVAILABLE(503, "Service Unavailable"),
       GATEWAY_TIMEOUT(504, "Gateway Timeout"),
       HTTP_VERSION_NOT_SUPPORTED(505, "HTTP Version Not Supported"),
       VARIANT_ALSO_NEGOTIATES(506, "Variant Also Negotiates"),
       INSUFFICIENT_STORAGE(507, "Insufficient Storage"),
       LOOP_DETECTED(508, "Loop Detected"),
       BANDWIDTH_LIMIT_EXCEEDED(509, "Bandwidth Limit Exceeded"),
       NOT_EXTENDED(510, "Not Extended"),
       NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required");
    
       private final int code;
       private final String reason;
    
       private Status(int statusCode, String reasonPhrase) {
           this.code = statusCode;
           this.reason = reasonPhrase;
       }
    
       public int getStatusCode() {
           return this.code;
      }
    
       public String getReasonPhrase() {
           return this.reason;
       }
     }
    

3.自定义异常 : 区分不同场景的异常

  • 请求异常

    public class BadRequestAlertException extends AbstractThrowableProblem{
    
       private final String entityName;
    
       private final String errorKey;
    
       public BadRequestAlertException(String defaultMessage, String entityName, String errorKey) {
           this(ErrorConstants.DEFAULT_TYPE, defaultMessage, entityName, errorKey);
       }
    
       public BadRequestAlertException(URI type, String defaultMessage, String entityName, String errorKey) {
           super(type, defaultMessage, Status.BAD_REQUEST, null, null, null, getAlertParameters(entityName, errorKey));
           this.entityName = entityName;
           this.errorKey = errorKey;
       }
    
       public String getEntityName() {
           return entityName;
       }
    
       public String getErrorKey() {
           return errorKey;
       }
    
       private static Map<String, Object> getAlertParameters(String entityName, String errorKey) {
           Map<String, Object> parameters = new HashMap<>();
           parameters.put("message", "error." + errorKey);
           parameters.put("params", entityName);
           return parameters;
       }
    

    }

  • 参数异常

    public class CustomParameterizedException extends AbstractThrowableProblem {
    
       private static final long serialVersionUID = 1L;
    
       private static final String PARAM = "param";
    
       public CustomParameterizedException(String message, String... params) {
           this(message, toParamMap(params));
       }
    
       public CustomParameterizedException(String message, Map<String, Object> paramMap) {
           super(ErrorConstants.PARAMETERIZED_TYPE, "Parameterized Exception", BAD_REQUEST, null, null, null, toProblemParameters(message, paramMap));
       }
    
       public static Map<String, Object> toParamMap(String... params) {
           Map<String, Object> paramMap = new HashMap<>();
           if (params != null && params.length > 0) {
               for (int i = 0; i < params.length; i++) {
                   paramMap.put(PARAM + i, params[i]);
               }
           }
           return paramMap;
       }
    
       public static Map<String, Object> toProblemParameters(String message, Map<String, Object> paramMap) {
           Map<String, Object> parameters = new HashMap<>();
           parameters.put("message", message);
           parameters.put("params", paramMap);
           return parameters;
       }
    

    }

  • 并发异常
    org.springframework.dao.ConcurrencyFailureException

  • 其他异常
    按需定义,继承org.zalando.problem.ThrowableProblem即可

4.实现ExceptionHandler : 用于拦截处理异常

@ControllerAdvice
public class ExceptionTranslator implements ProblemHandling {

  /**
   * Post-process Problem payload to add the message key for front-end if needed
   */
  @Override
  public ResponseEntity<Problem> process(@Nullable ResponseEntity<Problem> entity, NativeWebRequest request) {
      if (entity == null || entity.getBody() == null) {
          return entity;
      }
      Problem problem = entity.getBody();
      if (!(problem instanceof ConstraintViolationProblem || problem instanceof DefaultProblem)) {
          return entity;
      }
      ProblemBuilder builder = Problem.builder()
        .withType(Problem.DEFAULT_TYPE.equals(problem.getType()) ? ErrorConstants.DEFAULT_TYPE : problem.getType())
        .withStatus(problem.getStatus())
        .withTitle(problem.getTitle())
        .with("path",   request.getNativeRequest(HttpServletRequest.class).getRequestURI());

      if (problem instanceof ConstraintViolationProblem) {
          builder
            .with("violations", ((ConstraintViolationProblem) problem).getViolations())
            .with("message", ErrorConstants.ERR_VALIDATION);
        return new ResponseEntity<>(builder.build(), entity.getHeaders(), entity.getStatusCode());
      } else {
          builder
            .withCause(((DefaultProblem) problem).getCause())
            .withDetail(problem.getDetail())
            .withInstance(problem.getInstance());
        problem.getParameters().forEach(builder::with);
        if (!problem.getParameters().containsKey("message") && problem.getStatus() != null) {
            builder.with("message", "error.http." + problem.getStatus().getStatusCode());
        }
        return new ResponseEntity<>(builder.build(), entity.getHeaders(), entity.getStatusCode());
    }
}

  @Override
  public ResponseEntity<Problem> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, @Nonnull NativeWebRequest request) {
      BindingResult result = ex.getBindingResult();
      List<FieldErrorVM> fieldErrors = result.getFieldErrors().stream()
        .map(f -> new FieldErrorVM(f.getObjectName(), f.getField(), f.getCode()))
        .collect(Collectors.toList());

      Problem problem = Problem.builder()
        .withType(ErrorConstants.CONSTRAINT_VIOLATION_TYPE)
        .withTitle("Method argument not valid")
        .withStatus(defaultConstraintViolationStatus())
        .with("message", ErrorConstants.ERR_VALIDATION)
        .with("fieldErrors", fieldErrors)
        .build();
      return create(ex, problem, request);
  }

  @ExceptionHandler(BadRequestAlertException.class)
  public ResponseEntity<Problem> handleBadRequestAlertException(BadRequestAlertException ex, NativeWebRequest request) {
      return create(ex, request, HeaderUtil.createFailureAlert(ex.getEntityName(), ex.getErrorKey(), ex.getMessage()));
  }

  @ExceptionHandler(ConcurrencyFailureException.class)
  public ResponseEntity<Problem> handleConcurrencyFailure(ConcurrencyFailureException ex, NativeWebRequest request) {
      Problem problem = Problem.builder()
        .withStatus(Status.CONFLICT)
        .with("message", ErrorConstants.ERR_CONCURRENCY_FAILURE)
        .build();
      return create(ex, problem, request);
  }
}

5.统一业务异常抛出

异常统一在service或者controller里面抛出,抛出异常类型为BadRequestAlertException

  • 例子
@PostMapping("/as")
@Timed
public ResponseEntity<A> createA(@Valid @RequestBody A a) throws URISyntaxException {
    log.debug("REST request to save A : {}", a);
    if (a.getId() != null) {
        throw new BadRequestAlertException("A new a cannot already have an ID", ENTITY_NAME, "idexists");
    }
    A result = aService.save(a);
    return ResponseEntity.created(new URI("/api/as/" + result.getId()))
        .headers(HeaderUtil.createEntityCreationAlert(ENTITY_NAME, result.getId().toString()))
        .body(result);
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi阅读 7,279评论 0 10
  • 昨天袁公子打电话给我说,他要结婚了,让我备好礼金。 我很好奇谁,袁公子给我说还是那个她,那个学霸。 说起来袁公子的...
    慕鸿雪阅读 511评论 2 4
  • 昨天青蛙完成两只 约好下班后去拜访祝博士教育的,由于下雨和临时会议,已取消。 今日三只青蛙: 去中原地产天河4战区...
    海岸线2017阅读 164评论 0 0
  • 高考,一年一度,无休无止,但又是一个结束。 它是高中时代的结束,经过它,高中与你绝缘了,你回不去了。社...
    初天晓月阅读 201评论 0 0