瞧瞧別人家的異常處理,那叫一個(gè)優(yōu)雅
前言
在我們?nèi)粘9ぷ髦?,?jīng)常會(huì)遇到一些異常,比如:NullPointerException、NumberFormatException、ClassCastException等等。
那么問題來了,我們?cè)撊绾翁幚懋惓?,讓代碼變得更優(yōu)雅呢?
1.不要忽略異常
不知道你有沒有遇到過下面這段代碼:
反例:
Long id = null;
try {
id = Long.parseLong(keyword);
} catch(NumberFormatException e) {
//忽略異常
}
用戶輸入的參數(shù),使用Long.parseLong方法轉(zhuǎn)換成Long類型的過程中,如果出現(xiàn)了異常,則使用try/catch直接忽略了異常。
并且也沒有打印任何日志。
如果后面線上代碼出現(xiàn)了問題,有點(diǎn)不太好排查問題。
建議大家不要忽略異常,在后續(xù)的工作中,可能會(huì)帶來很多麻煩。
正例:
Long id = null;
try {
id = Long.parseLong(keyword);
} catch(NumberFormatException e) {
log.info(String.format("keyword:{} 轉(zhuǎn)換成Long類型失敗,原因:{}",keyword , e))
}
后面如果數(shù)據(jù)轉(zhuǎn)換出現(xiàn)問題,從日志中我們一眼就可以查到具體原因了。
2.使用全局異常處理器
有些小伙伴,經(jīng)常喜歡在Service代碼中捕獲異常。
不管是普通異常Exception,還是運(yùn)行時(shí)異常RuntimeException,都使用try/catch把它們捕獲。
反例:
try {
checkParam(param);
} catch (BusinessException e) {
return ApiResultUtil.error(1,"參數(shù)錯(cuò)誤");
}
在每個(gè)Controller類中都捕獲異常。
在UserController、MenuController、RoleController、JobController等等,都有上面的這段代碼。
顯然這種做法會(huì)造成大量重復(fù)的代碼。
我們?cè)贑ontroller、Service等業(yè)務(wù)代碼中,盡可能少捕獲異常。
這種業(yè)務(wù)異常處理,應(yīng)該交給攔截器統(tǒng)一處理。
在SpringBoot中可以使用@RestControllerAdvice注解,定義一個(gè)全局的異常處理handler,然后使用@ExceptionHandler注解在方法上處理異常。
例如:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 統(tǒng)一處理異常
*
* @param e 異常
* @return API請(qǐng)求響應(yīng)實(shí)體
*/
@ExceptionHandler(Exception.class)
public ApiResult handleException(Exception e) {
if (e instanceof BusinessException) {
BusinessException businessException = (BusinessException) e;
log.info("請(qǐng)求出現(xiàn)業(yè)務(wù)異常:", e);
return ApiResultUtil.error(businessException.getCode(), businessException.getMessage());
}
log.error("請(qǐng)求出現(xiàn)系統(tǒng)異常:", e);
return ApiResultUtil.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服務(wù)器內(nèi)部錯(cuò)誤,請(qǐng)聯(lián)系系統(tǒng)管理員!");
}
}
有了這個(gè)全局的異常處理器,之前我們?cè)贑ontroller或者Service中的try/catch代碼可以去掉。
如果在接口中出現(xiàn)異常,全局的異常處理器會(huì)幫我們封裝結(jié)果,返回給用戶。
3.盡可能捕獲具體異常
在你的業(yè)務(wù)邏輯方法中,有可能需要去處理多種不同的異常。
你可能你會(huì)覺得比較麻煩,而直接捕獲Exception。
反例:
try {
doSomething();
} catch(Exception e) {
log.error("doSomething處理失敗,原因:",e);
}
這樣捕獲異常太籠統(tǒng)了。
其實(shí)doSomething方法中,會(huì)拋出FileNotFoundException和IOException。
這種情況我們最好捕獲具體的異常,然后分別做處理。
正例:
try {
doSomething();
} catch(FileNotFoundException e) {
log.error("doSomething處理失敗,文件找不到,原因:",e);
} catch(IOException e) {
log.error("doSomething處理失敗,IO出現(xiàn)了異常,原因:",e);
}
這樣如果后面出現(xiàn)了上面的異常,我們就非常方便知道是什么原因了。
4.在finally中關(guān)閉IO流
我們?cè)谑褂肐O流的時(shí)候,用完了之后,一般需要及時(shí)關(guān)閉,否則會(huì)浪費(fèi)系統(tǒng)資源。
我們需要在try/catch中處理IO流,因?yàn)榭赡軙?huì)出現(xiàn)IO異常。
反例:
try {
File file = new File("/tmp/1.txt");
FileInputStream fis = new FileInputStream(file);
byte[] data = new byte[(int) file.length()];
fis.read(data);
for (byte b : data) {
System.out.println(b);
}
fis.close();
} catch (IOException e) {
log.error("讀取文件失敗,原因:",e)
}
上面的代碼直接在try的代碼塊中關(guān)閉fis。
假如在調(diào)用fis.read方法時(shí),出現(xiàn)了IO異常,則可能會(huì)直接拋異常,進(jìn)入catch代碼塊中,而此時(shí)fis.close方法沒辦法執(zhí)行,也就是說這種情況下,無法正確關(guān)閉IO流。
正例:
FileInputStream fis = null;
try {
File file = new File("/tmp/1.txt");
fis = new FileInputStream(file);
byte[] data = new byte[(int) file.length()];
fis.read(data);
for (byte b : data) {
System.out.println(b);
}
} catch (IOException e) {
log.error("讀取文件失敗,原因:",e)
} finally {
if(fis != null) {
try {
fis.close();
fis = null;
} catch (IOException e) {
log.error("讀取文件后關(guān)閉IO流失敗,原因:",e)
}
}
}
在finally代碼塊中關(guān)閉IO流。
但要先判斷fis不為空,否則在執(zhí)行fis.close()方法時(shí),可能會(huì)出現(xiàn)NullPointerException異常。
需要注意的地方時(shí),在調(diào)用fis.close()方法時(shí),也可能會(huì)拋異常,我們還需要進(jìn)行try/catch處理。
5.多用try-catch-resource
前面在finally代碼塊中關(guān)閉IO流,還是覺得有點(diǎn)麻煩。
因此在JDK7之后,出現(xiàn)了一種新的語法糖try-with-resource。
上面的代碼可以改造成這樣的:
File file = new File("/tmp/1.txt");
try (FileInputStream fis = new FileInputStream(file)) {
byte[] data = new byte[(int) file.length()];
fis.read(data);
for (byte b : data) {
System.out.println(b);
}
} catch (IOException e) {
e.printStackTrace();
log.error("讀取文件失敗,原因:",e)
}
try括號(hào)里頭的FileInputStream實(shí)現(xiàn)了一個(gè)AutoCloseable
接口,所以無論這段代碼是正常執(zhí)行完,還是有異常往外拋,還是內(nèi)部代碼塊發(fā)生異常被截獲,最終都會(huì)自動(dòng)關(guān)閉IO流。
我們盡量多用try-catch-resource的語法關(guān)閉IO流,可以少寫一些finally中的代碼。
而且在finally代碼塊中關(guān)閉IO流,有順序的問題,如果有多種IO,關(guān)閉的順序不對(duì),可能會(huì)導(dǎo)致部分IO關(guān)閉失敗。
而try-catch-resource就沒有這個(gè)問題。
6.不在finally中return
我們?cè)谀硞€(gè)方法中,可能會(huì)有返回?cái)?shù)據(jù)。
反例:
public int divide(int dividend, int divisor) {
try {
return dividend / divisor;
} catch (ArithmeticException e) {
// 異常處理
} finally {
return -1;
}
}
上面的這個(gè)例子中,我們?cè)趂inally代碼塊中返回了數(shù)據(jù)-1。
這樣最后在divide方法返回時(shí),會(huì)將dividend / divisor的值覆蓋成-1,導(dǎo)致正常的結(jié)果也不對(duì)。
我們盡量不要在finally代碼塊中返回?cái)?shù)據(jù)。
正解:
public int divide(int dividend, int divisor) {
try {
return dividend / divisor;
} catch (ArithmeticException e) {
// 異常處理
return -1;
}
}
如果dividend / divisor出現(xiàn)了異常,則在catch代碼塊中返回-1。
7.少用e.printStackTrace()
我們?cè)诒镜亻_發(fā)中,喜歡使用e.printStackTrace()方法,將異常的堆棧跟蹤信息輸出到標(biāo)準(zhǔn)錯(cuò)誤流中。
反例:
try {
doSomething();
} catch(IOException e) {
e.printStackTrace();
}
這種方式在本地確實(shí)容易定位問題。
但如果代碼部署到了生產(chǎn)環(huán)境,可能會(huì)帶來下面的問題:
- 可能會(huì)暴露敏感信息,如文件路徑、用戶名、密碼等。
- 可能會(huì)影響程序的性能和穩(wěn)定性。
正解:
try {
doSomething();
} catch(IOException e) {
log.error("doSomething處理失敗,原因:",e);
}
我們要將異常信息記錄到日志中,而不是保留給用戶。
8.異常打印詳細(xì)一點(diǎn)
我們?cè)诓东@了異常之后,需要把異常的相關(guān)信息記錄到日志當(dāng)中。
反例:
try {
double b = 1/0;
} catch(ArithmeticException e) {
log.error("處理失敗,原因:",e.getMessage());
}
這個(gè)例子中使用e.getMessage()方法返回異常信息。
但執(zhí)行結(jié)果為:
doSomething處理失敗,原因:
這種情況異常信息根本沒有打印出來。
我們應(yīng)該把異常信息和堆棧都打印出來。
正例:
try {
double b = 1/0;
} catch(ArithmeticException e) {
log.error("處理失敗,原因:",e);
}
執(zhí)行結(jié)果:
doSomething處理失敗,原因:
java.lang.ArithmeticException: / by zero
at cn.net.susan.service.Test.main(Test.java:16)
將具體的異常,出現(xiàn)問題的代碼和具體行數(shù)都打印出來。
9.別捕獲了異常又馬上拋出
有時(shí)候,我們?yōu)榱擞涗浫罩?,可能?huì)對(duì)異常進(jìn)行捕獲,然后又拋出。
反例:
try {
doSomething();
} catch(ArithmeticException e) {
log.error("doSomething處理失敗,原因:",e)
throw e;
}
在調(diào)用doSomething方法時(shí),如果出現(xiàn)了ArithmeticException異常,則先使用catch捕獲,記錄到日志中,然后使用throw關(guān)鍵拋出這個(gè)異常。
這個(gè)騷操作純屬是為了記錄日志。
但最后發(fā)現(xiàn)日志記錄兩次。
因?yàn)樵诤罄m(xù)的處理中,可能會(huì)將這個(gè)ArithmeticException異常又記錄一次。
這樣就會(huì)導(dǎo)致日志重復(fù)記錄了。
10.優(yōu)先使用標(biāo)準(zhǔn)異常
在Java中已經(jīng)定義了許多比較常用的標(biāo)準(zhǔn)異常,比如下面這張圖中列出的這些異常:
反例:
public void checkValue(int value) {
if (value < 0) {
throw new MyIllegalArgumentException("值不能為負(fù)");
}
}
自定義了一個(gè)異常表示參數(shù)錯(cuò)誤。
其實(shí),我們可以直接復(fù)用已有的標(biāo)準(zhǔn)異常。
正例:
public void checkValue(int value) {
if (value < 0) {
throw new IllegalArgumentException("值不能為負(fù)");
}
}
11.對(duì)異常進(jìn)行文檔說明
我們?cè)趯懘a的過程中,有一個(gè)好習(xí)慣是給方法、參數(shù)和返回值,增加文檔說明。
反例:
/*
* 處理用戶數(shù)據(jù)
* @param value 用戶輸入?yún)?shù)
* @return 值
*/
public int doSomething(String value)
throws BusinessException {
//業(yè)務(wù)邏輯
return 1;
}
這個(gè)doSomething方法,把方法、參數(shù)、返回值都加了文檔說明,但異常沒有加。
正解:
/*
* 處理用戶數(shù)據(jù)
* @param value 用戶輸入?yún)?shù)
* @return 值
* @throws BusinessException 業(yè)務(wù)異常
*/
public int doSomething(String value)
throws BusinessException {
//業(yè)務(wù)邏輯
return 1;
}
拋出的異常,也需要增加文檔說明。
12.別用異??刂瞥绦虻牧鞒?/span>
我們有時(shí)候,在程序中使用異常來控制了程序的流程,這種做法其實(shí)是不對(duì)的。
反例:
Long id = null;
try {
id = Long.parseLong(idStr);
} catch(NumberFormatException e) {
id = 1001;
}
如果用戶輸入的idStr是Long類型,則將它轉(zhuǎn)換成Long,然后賦值給id,否則id給默認(rèn)值1001。
每次都需要try/catch還是比較影響系統(tǒng)性能的。
正例:
Long id = checkValueType(idStr) ? Long.parseLong(idStr) : 1001;
我們?cè)黾恿艘粋€(gè)checkValueType方法,判斷idStr的值,如果是Long類型,則直接轉(zhuǎn)換成Long,否則給默認(rèn)值1001。
13.自定義異常
如果標(biāo)準(zhǔn)異常無法滿足我們的業(yè)務(wù)需求,我們可以自定義異常。
例如:
/**
* 業(yè)務(wù)異常
*
* @author 蘇三
* @date 2024/1/9 下午1:12
*/
@AllArgsConstructor
@Data
public class BusinessException extends RuntimeException {
public static final long serialVersionUID = -6735897190745766939L;
/**
* 異常碼
*/
private int code;
/**
* 具體異常信息
*/
private String message;
public BusinessException() {
super();
}
public BusinessException(String message) {
this.code = HttpStatus.INTERNAL_SERVER_ERROR.value();
this.message = message;
}
}
對(duì)于這種自定義的業(yè)務(wù)異常,我們可以增加code和message這兩個(gè)字段,code表示異常碼,而message表示具體的異常信息。
BusinessException繼承了RuntimeException運(yùn)行時(shí)異常,后面處理起來更加靈活。
提供了多種構(gòu)造方法。
定義了一個(gè)序列化ID(serialVersionUID)。