异常的正确使用在微服务架构中的重要性排前三,没什么意见吧
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 做统一处理做相应的优化,对于前面比较粗暴的做法,应该将除了XXServiceRuntimeException
和IllegalArgumentException
之外的异常内部记录,不再对外暴露,但是一定要记得通过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 里有一些历史包袱,起初设计的时候很多运行时异常都是作为 Notice
,Warning
错误输出的,但是错误的输出缺少调用栈,不利于问题的排查
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
就能接受Error
和Exception
了。
但是 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 大环境的发展上似乎不太明朗,但是因为爱所以持续吧。某些场景下还是最佳选择的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。