关于java:异常的正确使用在微服务架构中的重要性排前三没什么意见吧

40次阅读

共计 5202 个字符,预计需要花费 14 分钟才能阅读完成。

异样的正确应用在微服务架构中的重要性排前三,没什么意见吧

Curdboy 们好久不见,先祝大家端午节高兴。最近想说说异样,我的思考俨然造成了闭环,心愿这套组合拳能对你的业务代码有所帮忙。

上面只探讨世界上最好的语言和生态最残缺的语言,没什么意见吧。

异样的异同

PHP 在 PHP7 异样的设计和 Java 保持一致了 Exception extends Throwable,不过在历史起因和设计理念上还是有一些轻微的差异。比方 PHP 中的异样是有 code 属性的,这样就存在多种异样聚类为同一个异样,而后在catch 区块里依据 code 写不同的业务逻辑代码。

而 Java 异样则没有code,不能这样设计,只能针对不同的状况应用不同的异样。所以咱们习惯服务对外裸露的通过包装类来封装,而不是间接依赖异样的透传。

对立异样的解决

在 Java 代码里,最让人诟病的就是铺天盖地的try catch ,没什么意见吧。轻易抓一段代码

@Override
public DataResult<List<AdsDTO>> getAds(Integer liveId) {
    
    try {List<AdsDTO> adsDTO = new ArrayList<>();
        //... 业务逻辑省略
        DataResult.success(adsDTO);
    } catch (Exception e) {log.error("getAds has Exception:{}", e.getMessage(), e);
        DataResult.failure(ResultCode.CODE_INTERNAL_ERROR, e.getMessage()); // 将异样信息返回给服务端调用方
    }
    
    return dataResult;
}

很多时候都是无脑上来就先写个 try catch 再说,不论外面是否会有非运行时异样。比拟好的形式是应用 aop 的形式来拦挡所有的服务办法的调用,对立接管异样而后做解决。

@Around("recordLog()")
public Object record(ProceedingJoinPoint joinPoint) throws Throwable {
  //... 申请调用起源记录
  
  Object result;

  try {result = joinPoint.proceed(joinPoint.getArgs());
  } catch (Exception e) {
    //... 记录异样日志
    
    DataResult<Object> res = DataResult.failure(ResultCode.CODE_INTERNAL_ERROR, e.getMessage());
    result = res;
  }

    //... 返回值日志记录
  
  return result;
}

有一点小问题,如果间接将 A 服务的异样信息间接返回给调用者 B,可能存在一些潜在的危险,永远不能置信调用者,即便他根正苗红三代贫农也不行。因为不能确定调用者会将该错误信息作何解决,可能就间接作为 json 返回给了前端。

RuntimeException

在 Java 中异样能够分为运行时异样和非运行时异样,运行时异样是不须要捕捉的 ,在办法上也不须要标注 throw Exception,比方咱们在办法里应用 guava 包里的Preconditions 工具类,抛出的 IllegalArgumentException 也是运行时异样。

@Override
public DataResult<List<AdsDTO>> getAds(Integer liveId) {Preconditions.checkArgument(null != liveId, "liveIds not be null");
  
  List<AdsDTO> adsDTOS = new ArrayList<>();
  //... 业务逻辑省略
  return DataResult.success(adsDTOS);
}

咱们也能够应用该个性,自定义本人的业务异样类继承RuntimeException

XXServiceRuntimeException extends RuntimeException 

对于不合乎业务逻辑状况则间接抛出 XXServiceRuntimeException

@Override
public DataResult<List<AdsDTO>> getAds(Integer liveId) {if (null == liveId) {throw new XXServiceRuntimeException("liveId can't be null");
  }
  
  List<AdsDTO> adsDTOS = new ArrayList<>();
  //... 业务逻辑省略
  return DataResult.success(adsDTOS);
}

而后在 aop 做对立解决做相应的优化,对于后面比拟粗犷的做法,应该将除了 XXServiceRuntimeExceptionIllegalArgumentException之外的异样外部记录,不再对外裸露,然而肯定要记得通过 requestId 将分布式链路串起来,在 DataResult 中返回,不便问题的排查。

@Around("recordLog()")
public Object record(ProceedingJoinPoint joinPoint) throws Throwable {
  //... 申请调用起源记录
  
  Object result;

  try {result = joinPoint.proceed(joinPoint.getArgs());
  } catch (Exception e) {
    //... 记录异样日志①
    log.error("{}#{}, exception:{}:", clazzSimpleName, methodName, e.getClass().getSimpleName(), e);
    
    DataResult<Object> res = DataResult.failure(ResultCode.CODE_INTERNAL_ERROR);
    if (e instanceof XXServiceRuntimeException || e instanceof IllegalArgumentException) {res.setMessage(e.getMessage());
    }
 
    result = res;
  }

  if (result instanceof DataResult) {((DataResult) result).setRequestId(EagleEye.getTraceId()); // DMC 
  }

    //... 返回值日志记录
  
  return result;
}

异样监控

说好的闭环呢,应用了自定义异样类之后,对异样日志的监控报警的阈值就能够升高不少,报警更加精准,以阿里云 SLS 的监控为例

* and ERROR not XXServiceRuntimeException not IllegalArgumentException|SELECT COUNT(*) AS count

这里监控的是 记录异样日志① 的日志

PHP 里的异样

下面 Java 里说到的问题在 PHP 里也同样存在,不必 3 种办法来模仿 aop 都不能体现 PHP 是世界上最好的语言

//1. call_user_func_array
//2. 反射
//3. 间接 new
try {$class = new $className();
  $result = $class->$methodName();} catch (\Throwable $e) {//... 略}

相似下面的架构逻辑不再反复编写伪代码,根本保持一致。也是自定义本人的业务异样类继承RuntimeException,而后做对外输入解决。

然而 PHP 里有一些历史包袱,起初设计的时候很多运行时异样都是作为 NoticeWarning 谬误输入的,然而谬误的输入短少调用栈,不利于问题的排查

function foo(){return boo("xxx");
}

function boo($a){return explode($a);
}

foo();
Warning: explode() expects at least 2 parameters, 1 given in /Users/mengkang/Downloads/ab.php on line 8

看不到具体的参数,也看不到调用栈。如果应用 set_error_handler + ErrorException 之后,就十分清晰了。

set_error_handler(function ($severity, $message, $file, $line) {throw new ErrorException($message, 10001, $severity, $file, $line);
});

function foo(){return boo("xxx");
}

function boo($a){return explode($a);
}

try{foo();
}catch(Exception $e){echo $e->getTraceAsString();
}

最初打印进去的信息就是

Fatal error: Uncaught ErrorException: explode() expects at least 2 parameters, 1 given in /Users/mengkang/Downloads/ab.php:12
Stack trace:
#0 [internal function]: {closure}(2, 'explode() expec...', '/Users/mengkang...', 12, Array)
#1 /Users/mengkang/Downloads/ab.php(12): explode('xxx')
#2 /Users/mengkang/Downloads/ab.php(8): boo('xxx')
#3 /Users/mengkang/Downloads/ab.php(15): foo()
#4 {main}
  thrown in /Users/mengkang/Downloads/ab.php on line 12

批改下面的函数

function boo(array $a){return implode(",", $a);
}

则没法捕捉了,因为抛出的是 PHP Fatal error: Uncaught TypeError,PHP7 新增了
class Error implements Throwable,则在 PHP 零碎谬误日志里会有 Stack, 然而不能和整个业务零碎串联起来,这里就又不得不说日志的设计,咱们冀望像 Java 那样通过一个 traceId 将所有的日志串联起来,从 Nginx 日志到 PHP 里的失常 info level 日志以及这些Uncaught TypeError,所以接管默认输入到零碎谬误日志,在 catch 代码块中记录到对立的中央。那么这里就简略批改为

set_error_handler(function ($severity, $message, $file, $line) {throw new ErrorException($message, 10001, $severity, $file, $line);
});

function foo(){return boo("xxx");
}

function boo(array $a){return implode(",", $a);
}

try{foo();
}catch(Throwable $e){echo $e->getTraceAsString();
}

catch Throwable就能承受 ErrorException了。

然而 set_error_handler 没方法解决一些谬误,比方 E_PARSE 的谬误,能够用 register_shutdown_function 来兜底。

值得注意的是 register_shutdown_function 的用意是在脚本失常退出或显示调用 exit 时,执行注册的函数。
是脚本运行 (run-time not parse-time) 出错退出时,能力应用。如果在调用 register_shutdown_function 的同一文件的外面有语法错误,是无奈注册的,然而咱们我的项目个别都是分多个文件的,这样就其余文件里有语法错误,也能捕捉了

register_shutdown_function(function(){$e = error_get_last();
    if ($e){throw new \ErrorException($e["message"], 10002, E_ERROR, $e["file"], $e["line"]);
    }
});

如果你想间接应用这些代码(PHP 的)间接到我的项目可能会有很多坑,因为咱们习惯了零碎中有很多 notice 了,能够将 notice 的谬误转成异样之后被动记录,然而不对外抛出异样即可。

明天先到这里,下次说下日志应该怎么记。

尽管 PHP 大环境的倒退上仿佛不太清朗,然而因为爱所以继续吧。某些场景下还是最佳抉择的。

正文完
 0