溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

java在微服務(wù)架構(gòu)中異常怎么用

發(fā)布時(shí)間:2021-06-18 13:41:46 來(lái)源:億速云 閱讀:194 作者:小新 欄目:編程語(yǔ)言

小編給大家分享一下java在微服務(wù)架構(gòu)中異常怎么用,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

異常的正確使用在微服務(wù)架構(gòu)中的重要性排前三,沒(méi)什么意見(jiàn)吧

異常的正確使用在微服務(wù)架構(gòu)中的重要性排前三,沒(méi)什么意見(jiàn)吧

Curdboy 們好久不見(jiàn),先祝大家端午節(jié)快樂(lè)。最近想說(shuō)說(shuō)異常,我的思考儼然形成了閉環(huán),希望這套組合拳能對(duì)你的業(yè)務(wù)代碼有所幫助。

下面只討論世界上最好的語(yǔ)言和生態(tài)最完整的語(yǔ)言,沒(méi)什么意見(jiàn)吧。

異常的異同

PHP 在 PHP7 異常的設(shè)計(jì)和 Java 保持一致了 Exception extends Throwable  ,不過(guò)在歷史原因和設(shè)計(jì)理念上還是有一些細(xì)微的差別。比如 PHP 中的異常是有 code 屬性的,這樣就存在多種異常聚類(lèi)為同一個(gè)異常,然后在catch 區(qū)塊里根據(jù) code 寫(xiě)不同的業(yè)務(wù)邏輯代碼。

而 Java 異常則沒(méi)有code ,不能這樣設(shè)計(jì),只能針對(duì)不同的情況使用不同的異常。所以我們習(xí)慣服務(wù)對(duì)外暴露的通過(guò)包裝類(lèi)來(lái)封裝,而不是直接依賴(lài)異常的透?jìng)鳌?/p>

統(tǒng)一異常的處理

在 Java 代碼里,最讓人詬病的就是漫山遍野的try catch  ,沒(méi)什么意見(jiàn)吧。隨便抓一段代碼

@Override
public DataResult<List<AdsDTO>> getAds(Integer liveId) {
    
    try {
        List<AdsDTO> adsDTO = new ArrayList<>();
        //...業(yè)務(wù)邏輯省略
        DataResult.success(adsDTO);
    } catch (Exception e) {
        log.error("getAds has Exception:{}", e.getMessage(), e);
        DataResult.failure(ResultCode.CODE_INTERNAL_ERROR, e.getMessage()); // 將異常信息返回給服務(wù)端調(diào)用方
    }
    
    return dataResult;
}

很多時(shí)候都是無(wú)腦上來(lái)就先寫(xiě)個(gè) try catch 再說(shuō),不管里面是否會(huì)有非運(yùn)行時(shí)異常。比較好的方式是使用 aop 的方式來(lái)攔截所有的服務(wù)方法的調(diào)用,統(tǒng)一接管異常然后做處理。

@Around("recordLog()")
public Object record(ProceedingJoinPoint joinPoint) throws Throwable {
  //... 請(qǐng)求調(diào)用來(lái)源記錄
  
  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;
}

有一點(diǎn)小問(wèn)題,如果直接將 A 服務(wù)的異常信息直接返回給調(diào)用者 B,可能存在一些潛在的風(fēng)險(xiǎn),永遠(yuǎn)不能相信調(diào)用者,即使他根正苗紅三代貧農(nóng)也不行。因?yàn)椴荒艽_定調(diào)用者會(huì)將該錯(cuò)誤信息作何處理,可能就直接作為 json 返回給了前端。

RuntimeException

在 Java 中異??梢苑譃檫\(yùn)行時(shí)異常和非運(yùn)行時(shí)異常,運(yùn)行時(shí)異常是不需要捕獲的,在方法上也不需要標(biāo)注 throw Exception,比如我們?cè)诜椒ɡ锸褂?guava 包里的Preconditions工具類(lèi),拋出的IllegalArgumentException也是運(yùn)行時(shí)異常。

@Override
public DataResult<List<AdsDTO>> getAds(Integer liveId) {
  Preconditions.checkArgument(null != liveId, "liveIds not be null");
  
  List<AdsDTO> adsDTOS = new ArrayList<>();
  //...業(yè)務(wù)邏輯省略
  return DataResult.success(adsDTOS);
}

我們也可以使用該特性,自定義自己的業(yè)務(wù)異常類(lèi)繼承RuntimeException

XXServiceRuntimeException extends RuntimeException

對(duì)于不符合業(yè)務(wù)邏輯情況則直接拋出 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<>();
  //...業(yè)務(wù)邏輯省略
  return DataResult.success(adsDTOS);
}

然后在 aop 做統(tǒng)一處理做相應(yīng)的優(yōu)化,對(duì)于前面比較粗暴的做法,應(yīng)該將除了XXServiceRuntimeException和IllegalArgumentException之外的異常內(nèi)部記錄,不再對(duì)外暴露,但是一定要記得通過(guò)requestId將分布式鏈路串起來(lái),在DataResult中返回,方便問(wèn)題的排查。

@Around("recordLog()")
public Object record(ProceedingJoinPoint joinPoint) throws Throwable {
  //... 請(qǐng)求調(diào)用來(lái)源記錄
  
  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;
}

異常監(jiān)控

說(shuō)好的閉環(huán)呢,使用了自定義異常類(lèi)之后,對(duì)異常日志的監(jiān)控報(bào)警的閾值就可以降低不少,報(bào)警更加精準(zhǔn),以阿里云 SLS 的監(jiān)控為例

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

這里監(jiān)控的是記錄異常日志① 的日志

PHP 里的異常

上面 Java 里說(shuō)到的問(wèn)題在 PHP 里也同樣存在,不用 3 種方法來(lái)模擬 aop 都不能體現(xiàn) PHP 是世界上最好的語(yǔ)言

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

類(lèi)似上面的架構(gòu)邏輯不再重復(fù)編寫(xiě)偽代碼,基本保持一致。也是自定義自己的業(yè)務(wù)異常類(lèi)繼承RuntimeException,然后做對(duì)外輸出處理。

但是PHP 里有一些歷史包袱,起初設(shè)計(jì)的時(shí)候很多運(yùn)行時(shí)異常都是作為 Notice,Warning 錯(cuò)誤輸出的,但是錯(cuò)誤的輸出缺少調(diào)用棧,不利于問(wèn)題的排查

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

看不到具體的參數(shù),也看不到調(diào)用棧。如果使用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();
}

最后打印出來(lái)的信息就是

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

修改上面的函數(shù)

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

則沒(méi)法捕獲了,因?yàn)閽伋龅氖荘HP Fatal error:  Uncaught TypeError,PHP7 新增了
class Error implements Throwable,則在 PHP 系統(tǒng)錯(cuò)誤日志里會(huì)有 Stack,但是不能和整個(gè)業(yè)務(wù)系統(tǒng)串聯(lián)起來(lái),這里就又不得不說(shuō)日志的設(shè)計(jì),我們期望像 Java 那樣通過(guò)一個(gè) traceId 將所有的日志串聯(lián)起來(lái),從 Nginx 日志到 PHP 里的正常 info level 日志以及這些Uncaught TypeError,所以接管默認(rèn)輸出到系統(tǒng)錯(cuò)誤日志,在 catch 代碼塊中記錄到統(tǒng)一的地方。那么這里就簡(jiǎn)單修改為

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 沒(méi)辦法處理一些錯(cuò)誤,比如E_PARSE的錯(cuò)誤,可以用register_shutdown_function來(lái)兜底。

值得注意的是register_shutdown_function的用意是在腳本正常退出或顯示調(diào)用exit時(shí),執(zhí)行注冊(cè)的函數(shù)。
是腳本運(yùn)行(run-time not parse-time)出錯(cuò)退出時(shí),才能使用。如果在調(diào)用register_shutdown_function的同一文件的里面有語(yǔ)法錯(cuò)誤,是無(wú)法注冊(cè)的,但是我們項(xiàng)目一般都是分多個(gè)文件的,這樣就其他文件里有語(yǔ)法錯(cuò)誤,也能捕獲了
register_shutdown_function(function(){
    $e = error_get_last();
    if ($e){
        throw new \ErrorException($e["message"], 10002, E_ERROR, $e["file"], $e["line"]);
    }
});

如果你想直接使用這些代碼(PHP的)直接到項(xiàng)目可能會(huì)有很多坑,因?yàn)槲覀兞?xí)慣了系統(tǒng)中有很多  notice 了,可以將 notice 的錯(cuò)誤轉(zhuǎn)成異常之后主動(dòng)記錄,但是不對(duì)外拋出異常即可。

以上是“java在微服務(wù)架構(gòu)中異常怎么用”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI