前言
我們經常在很多項目里面看到用異常來處理業務邏輯,發現不符合預期直接拋出異常,然后在最外面捕獲異常統一處理,這樣使用非常方便。
但是又有很多文章寫著異常處理性能,所以不建議使用異常來做流程控制。甚至在阿里巴巴開發手冊里面明確說明了,不要用來做流程控制。

那么問題來了:
究竟能不能用異常來做流程控制?效率低是低多少?看完這一篇文章你就知道了。
開始測試
先做最簡單的測試
我們循環10萬次,然后棧有5層,然后輸出返回結果。
private static final int RUN_COUNT = 10 * 10000;
/**
* 測試異常耗時
* 輸出異常堆棧&信息
*/
@Test
public void printStack() {
long start1 = System.currentTimeMillis();
for (int i = 0; i < RUN_COUNT; i++) {
log.info(Storey1.test());
}
long start2 = System.currentTimeMillis();
for (int i = 0; i < RUN_COUNT; i++) {
try {
Storey1.testException();
} catch (Exception e) {
log.info(e.getMessage(), e);
}
}
long end = System.currentTimeMillis();
log.info("普通返回耗時:{},異常返回耗時:{}", start2 - start1, end - start1);
}
public static class Storey1 {
public static String test() {
return Storey2.test();
}
public static String testException() {
return Storey2.testException();
}
}
public static class Storey2 {
public static String test() {
return Storey3.test();
}
public static String testException() {
return Storey3.testException();
}
}
public static class Storey3 {
public static String test() {
return Storey4.test();
}
public static String testException() {
return Storey4.testException();
}
}
public static class Storey4 {
public static String test() {
return Storey5.test();
}
public static String testException() {
return Storey5.testException();
}
}
public static class Storey5 {
public static String test() {
return Integer.toString(count++);
}
public static String testException() {
throw new CustomException(Integer.toString(count++));
}
}
public static class CustomException extends RuntimeException {
public CustomException(String message) {
super(message);
}
}
結果差別很大,普通返回只要2137毫秒,而異常卻要75026毫秒,幾十倍的差距。
15:07:59.648 [main] INFO com.alibaba.easytools.test.temp.exception.ExceptionTest - 普通返回耗時:2137,異常返回耗時:75026
不輸出堆棧信息
聰明的同學不難發現,上面有個變量沒控制住,就是使用異常的情況下,輸出了堆棧信息,那我們關閉堆棧輸出試試。會不會是輸出的堆棧信息導致的慢呢?
/**
* 測試異常耗時
* 僅僅輸出信息
*/
@Test
public void print() {
long start1 = System.currentTimeMillis();
for (int i = 0; i < RUN_COUNT; i++) {
log.info(Storey1.test());
}
long start2 = System.currentTimeMillis();
for (int i = 0; i < RUN_COUNT; i++) {
try {
Storey1.testException();
} catch (Exception e) {
log.info(e.getMessage());
}
}
long end = System.currentTimeMillis();
log.info("普通返回耗時:{},異常返回耗時:{}", start2 - start1, end - start1);
}
結果發現普通返回是2053毫秒,而異常卻要4380毫秒,發現差距瞬間變小了。
15:43:54.260 [main] INFO com.alibaba.easytools.test.temp.exception.ExceptionTest - 普通返回耗時:2053,異常返回耗時:4380
不輸出任何信息
顯然我們發現,關閉了日志輸出對執行時間影像很大,那我們關閉了日志輸出會有什么效果了呢?
/**
* 測試異常耗時
* 不輸出信息
*/
@Test
public void noPrint() {
long start1 = System.currentTimeMillis();
for (int i = 0; i < RUN_COUNT; i++) {
Storey1.test();
}
long start2 = System.currentTimeMillis();
for (int i = 0; i < RUN_COUNT; i++) {
try {
Storey1.testException();
} catch (Exception e) {
}
}
long end = System.currentTimeMillis();
log.info("普通返回耗時:{},異常返回耗時:{}", start2 - start1, end - start1);
}
結果發現普通返回是58毫秒,而異常卻要719毫秒,看來性能實際差距就是十幾倍。
15:47:55.901 [main] INFO com.alibaba.easytools.test.temp.exception.ExceptionTest - 普通返回耗時:58,異常返回耗時:719
關閉堆棧
在處理異常的時候,很多時間在封裝異常堆棧,那有沒有辦法可以不要封裝呢?
仔細研究異常類發現,異常類有個參數`writableStackTrace` 可以讓異常不去封裝堆棧信息。
public class RuntimeException extends Exception {
/**
* Constructs a new runtime exception with the specified detail
* message, cause, suppression enabled or disabled, and writable
* stack trace enabled or disabled.
*
* @param message the detail message.
* @param cause the cause. (A {@code null} value is permitted,
* and indicates that the cause is nonexistent or unknown.)
* @param enableSuppression whether or not suppression is enabled
* or disabled
* @param writableStackTrace whether or not the stack trace should
* be writable
*
* @since 1.7
*/
protected RuntimeException(String message, Throwable cause,
boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
我們把拋異常的時候不去封裝異常信息
/**
* 測試異常耗時
* 關閉堆棧 并且不打印
*/
@Test
public void noPrintCloseStackTrace() {
long start1 = System.currentTimeMillis();
for (int i = 0; i < RUN_COUNT; i++) {
Storey1.test();
}
long start2 = System.currentTimeMillis();
for (int i = 0; i < RUN_COUNT; i++) {
try {
Storey1.testException();
} catch (Exception e) {
}
}
long end = System.currentTimeMillis();
log.info("普通返回耗時:{},異常返回耗時:{}", start2 - start1, end - start1);
}
public static class Storey5 {
public static String test() {
return Integer.toString(count++);
}
public static String testException() { throw new CustomException(Integer.toString(count++), null, false, false);
}
}
public static class CustomException extends RuntimeException {
public CustomException(String message) {
super(message);
}
public CustomException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
結果發現普通返回是31毫秒,而異常卻要62毫秒,差距也沒有想象中的大了。差不多是2倍左右。
15:54:26.984 [main] INFO com.alibaba.easytools.test.temp.exception.ExceptionTest - 普通返回耗時:31,異常返回耗時:62
最終結果
我們來看下最終對比結論
|
普通 |
異常 |
普通輸出日志,異常輸出堆棧 |
2137 |
75026 |
普通輸出日志,異常僅輸出日志 |
2053 |
4380 |
都不輸出日志 |
58 |
719 |
關閉堆棧 |
31 |
62 |
結論
所以我們可以總結出以下結論
- 日志輸出堆棧非常耗時
- 哪怕日志只是輸出業務邏輯,耗時和業務處理也不是一個時間維度的
- 排出日志影響,封裝堆棧非常耗時
- 關閉堆棧以后耗時相差不大,大概1萬次相差3毫秒
三種方式優缺點總結下:
|
優點 |
缺點 |
普通 |
|
|
關閉堆棧的異常 |
|
|
不關閉堆棧的異常 |
|
|
如果我們的并發沒有大到必須關閉日志這種情況下,實際上來說異常來控制流程問題不大,影響微乎其微,所以還是怎么方便怎么來。
當然如果項目并發超級高,高到單機1萬次請求要省3毫秒的情況下,建議還是用返回去控制業務流程。