異常是面向?qū)ο笳Z言非常重要的一個特性,,良好的異常設(shè)計對程序的可擴展性、可維護(hù)性、健壯性都起到至關(guān)重要,。
JAVA根據(jù)用處的不同,定義了兩類異常
* Checked Exception: Exception的子類,方法簽名上需要顯示的聲明throws,,編譯器迫使調(diào)用者處理這類異?;蛘呗暶鱰hrows繼續(xù)往上拋。
* Unchecked Exception: RuntimeException的子類,,方法簽名不需要聲明throws,,編譯器也不會強制調(diào)用者處理該類異常。
異常的作用和好處:
1. 分離錯誤代碼和正常代碼,,代碼更簡潔,。
2. 保護(hù)數(shù)據(jù)的正確性和完整性,程序更嚴(yán)謹(jǐn)。
3. 便于調(diào)試和排錯,,軟件更好維護(hù),。
……
相信很多JAVA開發(fā)人員都看到或聽到過“不要使用異常來控制流程”,雖然這句話非常易于記憶,,但是它并未給出“流程”的定義,,所以很難理解作者的本意,讓人迷惑不解,。
如果“流程”是包括程序的每一步執(zhí)行,,我認(rèn)為異常就是用來控制流程的,它就是用來區(qū)分程序的正常流程和錯誤流程,,為了更能明確的表達(dá)意思,,上面這
句話應(yīng)改成“不要用異常來控制程序的正常流程”。現(xiàn)在帶來一個新的問題就是如何區(qū)分程序正常流程和異常流程,?我實在想不出一個評判標(biāo)準(zhǔn),,就舉例來說明,大
家思維擴散下,。
為了后面更方便的表達(dá),,我把異常分成兩類,不妥之處請諒解
* 系統(tǒng)異常:軟件的缺陷,,客戶端對此類異常是無能為力的,,通常都是Unchecked Exception。
*業(yè)務(wù)異常:用戶未按正常流程操作導(dǎo)致的異常,,都是Checked Exception
金幣轉(zhuǎn)帳例子
1. 需求規(guī)定金幣一次的轉(zhuǎn)賬范圍是1~500,,如果超過這個額度,就要提示用戶金額超出單筆轉(zhuǎn)賬的限制,,轉(zhuǎn)賬的金額是由用戶在頁面輸入的:
因為值是用戶輸入的,,所以給的值超出限定的范圍肯定是司空見慣。我們當(dāng)然不能把它(輸入的值超出限定的范圍)歸結(jié)于異常流程,,它應(yīng)該屬于正常流程,,應(yīng)該提供驗證數(shù)據(jù)的完整功能。
正確的實現(xiàn)如下:
提供一個判斷轉(zhuǎn)賬金幣數(shù)量是否超出限定范圍的方法
- private static final int MAX_PRE_TRANSFER_COIN = 500;
-
- public boolean isCoinExceedTransferLimits(int coin) {
- return coin > MAX_PRE_TRANSFER_COIN;
- }
private static final int MAX_PRE_TRANSFER_COIN = 500;
public boolean isCoinExceedTransferLimits(int coin) {
return coin > MAX_PRE_TRANSFER_COIN;
}
Action里先對值進(jìn)行校驗,,若不合法,,直接返回并提示用戶
2. 在轉(zhuǎn)賬的過程里,發(fā)些金幣數(shù)量不夠:
我們的程序都是運行在并發(fā)環(huán)境中,,Action無法完全做到判斷金幣是否足夠,。因為在判斷之后和事務(wù)之前的剎那間,有可能產(chǎn)生其他扣費操作導(dǎo)致金幣不夠,。這時我們就需要用業(yè)務(wù)異常(Checked Exception)來控制
正確的實現(xiàn)如下
CoinNotEnoughExcetion .java
-
- public class CoinNotEnoughExcetion extends Exception {
- private static final long serialVersionUID = -7867713004171563795L;
- private int coin;
- public CoinNotEnoughExcetion() {
- }
-
- public CoinNotEnoughExcetion(int coin) {
- this.coin = coin;
- }
-
- public int getCoin() {
- return coin;
- }
-
- @Override
- public String getMessage() {
- return coin + " is exceed transfer limit:500";
- }
- }
//金幣不夠的異常類
public class CoinNotEnoughExcetion extends Exception {
private static final long serialVersionUID = -7867713004171563795L;
private int coin;
public CoinNotEnoughExcetion() {
}
public CoinNotEnoughExcetion(int coin) {
this.coin = coin;
}
public int getCoin() {
return coin;
}
@Override
public String getMessage() {
return coin + " is exceed transfer limit:500";
}
}
//轉(zhuǎn)賬方法
- private static final int MAX_PRE_TRANSFER_COIN = 500;
-
- public void transferCoin(int coin) throws CoinNotEnoughExcetion{
- if (!hasEnoughCoin())
- throw new CoinNotEnoughExcetion(coin);
-
- }
private static final int MAX_PRE_TRANSFER_COIN = 500;
public void transferCoin(int coin) throws CoinNotEnoughExcetion{
if (!hasEnoughCoin())
throw new CoinNotEnoughExcetion(coin);
// do transfering coin
}
3. 接口transferCoin(int coin)的規(guī)范里已經(jīng)定了契約,,調(diào)用transferCoin之前必須要先調(diào)用isCoinExceedTransferLimits判斷值是否合法:
雖然規(guī)范人人都要遵循,,但畢竟只是規(guī)范,編譯器無法強制約束,。此時就需要用系統(tǒng)異常(Unchecked Exception,,用JDK的標(biāo)準(zhǔn)異常)來保證程序的正確性,沒遵守規(guī)范的都當(dāng)做軟件bug處理,。
正確的實現(xiàn)如下:
//轉(zhuǎn)賬方法
- public void transferCoin(int coin){
- if (coin > MAX_PRE_TRANSFER_COIN)
- throw new IllegalArgumentException(coin+" is exceed tranfer limits:500");
-
- }
public void transferCoin(int coin){
if (coin > MAX_PRE_TRANSFER_COIN)
throw new IllegalArgumentException(coin+" is exceed tranfer limits:500");
// do transfering coin
}
至此,,舉例已經(jīng)結(jié)束了,在這里再延伸下—業(yè)務(wù)異常和系統(tǒng)異常類在實現(xiàn)上的區(qū)別,,該區(qū)別的根源來自于調(diào)用者捕獲到異常后是如何處理的
Action對業(yè)務(wù)異常的處理:操作具體的異常類
- public String execute() {
- try {
- userService.transferCoin(coin);
- } catch (CoinExceedTransferLimitExcetion e) {
- e.getCoin();
- }
- return SUCCESS;
- }
public String execute() {
try {
userService.transferCoin(coin);
} catch (CoinExceedTransferLimitExcetion e) {
e.getCoin();
}
return SUCCESS;
}
Action對系統(tǒng)異常的處理:無法知道具體的異常類
- public String execute() {
- try {
- userService.transferCoin(coin);
- } catch (RuntimeException e) {
- LOG.error(e.getMessage());
- }
- return SUCCESS;
- }
public String execute() {
try {
userService.transferCoin(coin);
} catch (RuntimeException e) {
LOG.error(e.getMessage());
}
return SUCCESS;
}
調(diào)用者捕獲業(yè)務(wù)異常(Checked Excetion)之后,,通常不會去調(diào)用getMessage()方法的,而是調(diào)用異常類里特有的方法,,所以業(yè)務(wù)異常類的實現(xiàn)要注重特有的,,跟業(yè)務(wù)相關(guān)的方法,而不是getMessage()方法,。
系統(tǒng)異常類恰恰相反,,捕獲者只會調(diào)用getMessage()獲取異常信息,然后記錄錯誤日志,,所以系統(tǒng)異常類(Uncheck Exception)的實現(xiàn)類對getMessage()方法返回內(nèi)容比較講究,。
不管是業(yè)務(wù)異常還是系統(tǒng)異常,都需要提供豐富的信息,。如:數(shù)據(jù)庫訪問異常,,需要提供查詢sql語句等;HTTP接口調(diào)用異常,,需要給出訪問的URL和參數(shù)列表(如果是post請求,,參數(shù)列表不提供也可以,取決于維護(hù)人員或者開發(fā)人員拿到參數(shù)列表會如何去使用),。
1. Sql語法錯誤異常類(取自spring框架的異常類,,Spring的異常體系很強大,值得一看):
- public class BadSqlGrammarException extends InvalidDataAccessResourceUsageException {
- private String sql;
-
- public BadSqlGrammarException(String task, String sql, SQLException ex) {
- super(task + "; bad SQL grammar [" + sql + "]", ex);
- this.sql = sql;
- }
-
- public SQLException getSQLException() {
- return (SQLException) getCause();
- }
-
- public String getSql() {
- return this.sql;
- }
- }
public class BadSqlGrammarException extends InvalidDataAccessResourceUsageException {
private String sql;
public BadSqlGrammarException(String task, String sql, SQLException ex) {
super(task + "; bad SQL grammar [" + sql + "]", ex);
this.sql = sql;
}
public SQLException getSQLException() {
return (SQLException) getCause();
}
public String getSql() {
return this.sql;
}
}
2. HTTP接口調(diào)用異常類
- public class HttpInvokeException extends RuntimeException {
- private static final long serialVersionUID = -6477873547070785173L;
-
- public HttpInvokeException(String url, String message) {
- super("http interface unavailable [" + url + "];" + message);
- }
- }
public class HttpInvokeException extends RuntimeException {
private static final long serialVersionUID = -6477873547070785173L;
public HttpInvokeException(String url, String message) {
super("http interface unavailable [" + url + "];" + message);
}
}
如何選擇用Unchecked Exception和Checked Exception
1.是軟件bug還是業(yè)務(wù)異常,,軟件bug是Unchecked Exception,否則是Checked Exception
2.如果把該異常拋給用戶,,用戶能否做出補救。如果客戶端無能為力,,則用Unchecked Exception,,否則拋Checked Exception
結(jié)合這兩點,兩類異常就不會混淆使用了,。
異常設(shè)計的幾個原則:
1.如果方法遭遇了一個無法處理的意外情況,那么拋出一個異常,。
2.避免使用異常來指出可以視為方法的常用功能的情況,。
3.如果發(fā)現(xiàn)客戶違反了契約(例如,,傳入非法輸入?yún)?shù)),那么拋出非檢查型異常,。
4.如果方法無法履型契約,,那么拋出檢查型異常,也可以拋出非檢查型異常,。
5.如果你認(rèn)為客戶程序員需要有意識地采取措施,,那么拋出檢查型異常。
6.異常類應(yīng)該給客戶提供豐富的信息,,異常類跟其它類一樣,,允許定義自己的屬性和方法。
7.異常類名和方法遵循JAVA類名規(guī)范和方法名規(guī)范
8.跟JAVA其它類一樣,,不要定義多余的方法和變量,。(不會使用的變量,就不要定義,spring的BadSqlGrammarException.getSql() 就是多余的)
以下是我工作當(dāng)中碰到的一些我認(rèn)為不是很好的寫法,,我之前也犯過此類的錯誤
A. 整個業(yè)務(wù)層只定義了一個異常類
- public class ServiceException extends RuntimeException {
- private static final long serialVersionUID = 8670135969660230761L;
-
- public ServiceException(Exception e) {
- super(e);
- }
-
- public ServiceException(String message) {
- super(message);
- }
- }
public class ServiceException extends RuntimeException {
private static final long serialVersionUID = 8670135969660230761L;
public ServiceException(Exception e) {
super(e);
}
public ServiceException(String message) {
super(message);
}
}
理由:
1.業(yè)務(wù)異常不應(yīng)該是Unchecked Exception,。
2.不存在一個具體的異常類名稱是“ServiceException”。
解決方法:定義一個抽象的業(yè)務(wù)異常“ServiceException”
- public abstract class ServiceException extends Exception {
- private static final long serialVersionUID = -8411541817140551506L;
- }
public abstract class ServiceException extends Exception {
private static final long serialVersionUID = -8411541817140551506L;
}
B. 忽略異常
- try {
- new String(source.getBytes("UTF-8"), "GBK");
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- }
try {
new String(source.getBytes("UTF-8"), "GBK");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
理由:
1.環(huán)境不支持UTF-8或者GBK很顯然是一個非常嚴(yán)重的bug,,不能置之不理
2.堆棧的方式記錄錯誤信息不合理,,若產(chǎn)品環(huán)境是不記錄標(biāo)準(zhǔn)輸出,這個錯誤信息就會丟失掉,。若產(chǎn)品環(huán)境是記錄標(biāo)準(zhǔn)輸出,,萬一這段程序被while循環(huán)的線程調(diào)用,有可能引起硬盤容量溢出,,最終導(dǎo)致程序的運行不正常,,甚至數(shù)據(jù)的丟失。
解決方法:捕獲UnsupportedEncodingException,,封裝成Unchecked Exception,,往上拋,中斷程序的執(zhí)行,。
- try {
- new String(source.getBytes("UTF-8"), "GBK");
- } catch (UnsupportedEncodingException e) {
- throw new IllegalStateException("the base runtime environment does not support 'UTF-8' or 'GBK'");
- }
try {
new String(source.getBytes("UTF-8"), "GBK");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("the base runtime environment does not support 'UTF-8' or 'GBK'");
}
C. 捕獲頂層的異?!狤xception
- public void transferCoin(int outUid, int inUserUid, int coin) throws CoinNotEnoughException {
- final User outUser = userDao.getUser(outUid);
- final User inUser = userDao.getUser(inUserUid);
- outUser.decreaseCoin(coin);
- inUser.increaseCoin(coin);
- try {
-
- userDao.save(outUser);
- userDao.save(inUser);
-
-
- } catch (Exception e) {
- throw new ServiceException(e);
- }
- }
public void transferCoin(int outUid, int inUserUid, int coin) throws CoinNotEnoughException {
final User outUser = userDao.getUser(outUid);
final User inUser = userDao.getUser(inUserUid);
outUser.decreaseCoin(coin);
inUser.increaseCoin(coin);
try {
// BEGIN TRANSACTION
userDao.save(outUser);
userDao.save(inUser);
// END TRANSACTION
// log transfer operate
} catch (Exception e) {
throw new ServiceException(e);
}
}
理由:
1. Service并不是只能拋出業(yè)務(wù)異常,Service也可以拋出其他異常
如IllegalArgumentException,、ArrayIndexOutOfBoundsException或者spring框架的DataAccessException
2. 多數(shù)情況下,,Dao不會拋Checked Exception給Service,假如所有代碼都非常規(guī)范,,Service類里不應(yīng)該出現(xiàn)try{}catch代碼,。
解決方法:刪除try{}catch代碼
- public void transferCoin(int outUid, int inUserUid, int coin) throws CoinNotEnoughException {
- final User outUser = userDao.getUser(outUid);
- final User inUser = userDao.getUser(inUserUid);
- outUser.decreaseCoin(coin);
- inUser.increaseCoin(coin);
-
- userDao.save(outUser);
- userDao.save(inUser);
-
-
- }
public void transferCoin(int outUid, int inUserUid, int coin) throws CoinNotEnoughException {
final User outUser = userDao.getUser(outUid);
final User inUser = userDao.getUser(inUserUid);
outUser.decreaseCoin(coin);
inUser.increaseCoin(coin);
// BEGIN TRANSACTION
userDao.save(outUser);
userDao.save(inUser);
// END TRANSACTION
// log transfer operate
}
D. 創(chuàng)建沒有意義的異常
- public class DuplicateUsernameException extends Exception {
- }
public class DuplicateUsernameException extends Exception {
}
理由
1. 它除了有一個"意義明確"的名字以外沒有任何有用的信息了。不要忘記Exception跟其他的Java類一樣,,客戶端可以調(diào)用其中的方法來得到更多的信息,。
解決方案:定義上捕獲者需要用到的信息
- public class DuplicateUsernameException extends Exception {
- private static final long serialVersionUID = -6113064394525919823L;
- private String username = null;
- private String[] availableNames = new String[0];
-
- public DuplicateUsernameException(String username) {
- this.username = username;
- }
-
- public DuplicateUsernameException(String username, String[] availableNames) {
- this(username);
- this.availableNames = availableNames;
- }
-
- public String requestedUsername() {
- return this.username;
- }
-
- public String[] availableNames() {
- return this.availableNames;
- }
- }
public class DuplicateUsernameException extends Exception {
private static final long serialVersionUID = -6113064394525919823L;
private String username = null;
private String[] availableNames = new String[0];
public DuplicateUsernameException(String username) {
this.username = username;
}
public DuplicateUsernameException(String username, String[] availableNames) {
this(username);
this.availableNames = availableNames;
}
public String requestedUsername() {
return this.username;
}
public String[] availableNames() {
return this.availableNames;
}
}
E. 把展示給用戶的信息直接放在異常信息里,。
- public class CoinNotEnoughException2 extends Exception {
- private static final long serialVersionUID = 4724424650547006411L;
-
- public CoinNotEnoughException2(String message) {
- super(message);
- }
- }
-
- public void decreaseCoin(int forTransferCoin) throws CoinNotEnoughException2 {
- if (this.coin < forTransferCoin)
- throw new CoinNotEnoughException2("金幣數(shù)量不夠");
- this.coin -= forTransferCoin;
- }
public class CoinNotEnoughException2 extends Exception {
private static final long serialVersionUID = 4724424650547006411L;
public CoinNotEnoughException2(String message) {
super(message);
}
}
public void decreaseCoin(int forTransferCoin) throws CoinNotEnoughException2 {
if (this.coin < forTransferCoin)
throw new CoinNotEnoughException2("金幣數(shù)量不夠");
this.coin -= forTransferCoin;
}
理由:展示給用戶錯誤提示信息屬于文案范疇,文案易變動,,最好不要跟程序混淆一起,。
解決方法:
錯誤提示的文案統(tǒng)一放在一個配置文件里,根據(jù)異常類型獲取對應(yīng)的錯誤提示信息,,若需要支持國際化還可以提供多個語言的版本,。
F. 方法簽名聲明了多余的throws
理由:代碼不夠精簡,調(diào)用者不得不加上try{}catch代碼
解決方案:若方法不可能會拋出該異常,,那就刪除多余的throws
G. 給每一個異常類都定義一個不會用到ErrorCode
理由:多一個功能就多一個維護(hù)成本
解決方法:不要無謂的定義ErrorCode,,除非真的需要(如給別人提供接口調(diào)用的,這時,,最好對異常進(jìn)行規(guī)劃和分類,。如1xx代表金幣相關(guān)的異常,2xx代表用戶相關(guān)的異常…
最后推薦幾篇關(guān)于異常設(shè)計原則
1.異常設(shè)計
http://www.cnblogs.com/JavaVillage/articles/384483.html(翻譯)
http://www./jw-07-1998/jw-07-techniques.html(原文)
2. 異常處理最佳實踐
http://tech./articles/2009/79/1247105040929_1.html(翻譯)
http:///pub/a/onjava/2003/11/19/exceptions.html(原文)
|