异常的正确使用在微服务架构中的重要性排前三,没什么意见吧异常的正确使用在微服务架构中的重要性排前三,没什么意见吧
curdboy 们好久不见,先祝大家端午节快乐。最近想说说异常,我的思考俨然形成了闭环,希望这套组合拳能对你的业务代码有所帮助。
下面只讨论世界上最好的语言和生态最完整的语言,没什么意见吧。
异常的异同php 在 php7 异常的设计和 java 保持一致了 exception extends throwable ,不过在历史原因和设计理念上还是有一些细微的差别。比如 php 中的异常是有 code 属性的,这样就存在多种异常聚类为同一个异常,然后在catch 区块里根据 code 写不同的业务逻辑代码。
而 java 异常则没有code ,不能这样设计,只能针对不同的情况使用不同的异常。所以我们习惯服务对外暴露的通过包装类来封装,而不是直接依赖异常的透传。
统一异常的处理在 java 代码里,最让人诟病的就是漫山遍野的try catch ,没什么意见吧。随便抓一段代码
@overridepublic 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也是运行时异常。
@overridepublic 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
@overridepublic 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. 直接 newtry { $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:12stack 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视频教程
以上就是在微服务架构中异常如何正确使用的详细内容。
