JavaWeb
中 Service
層異常會(huì)怎么處理?這是個(gè)非常有啟發(fā)意義的問(wèn)題。
一般初學(xué)者學(xué)習(xí)編碼和錯(cuò)誤處理時(shí),先知道編程語(yǔ)言有一種處理錯(cuò)誤的形式或者約定(如Java
就是拋異常),然后就開(kāi)始用這些工具,但是卻反過(guò)來(lái)忽視這個(gè)問(wèn)題的本質(zhì):
處理錯(cuò)誤是為了寫出正確的程序
到底怎么算“正確”呢?是要由解決的問(wèn)題決定的。問(wèn)題不同,解決方案就不同。
比如,一個(gè)web接口
接受用戶的請(qǐng)求,這個(gè)請(qǐng)求需要傳入一個(gè)參數(shù)“年齡”,也許業(yè)務(wù)要求這個(gè)字段應(yīng)該是個(gè)0~150之間的整數(shù)。如果用戶輸入的是個(gè)字符串或者負(fù)數(shù)就肯定不被接受。一般在后端的某個(gè)地方都會(huì)做輸入的合法性檢查,檢查不過(guò)就拋異常。但是歸根到底這個(gè)問(wèn)題的“正確”解決方法總是要以某種形式提示用戶。而提示用戶是某種前端工作,這就要看這個(gè)界面到底是app
,H5 + ajax
還是類似于jsp
那樣的服務(wù)器產(chǎn)生的界面。不管哪種,你需要根據(jù)需求去”設(shè)計(jì)一個(gè)修復(fù)錯(cuò)誤“的流程。比如一個(gè)常見(jiàn)的流程需要后端拋異常,然后一路到某個(gè)集中處理錯(cuò)誤的代碼,將其轉(zhuǎn)換為某個(gè)HTTP
的錯(cuò)誤(某個(gè)特定業(yè)務(wù)錯(cuò)誤碼)提供給前端,前端再去做”提示“。如果用戶輸入了非法的請(qǐng)求,從邏輯上后端都沒(méi)法自己修復(fù),這是個(gè)“正確”的策略。
換一個(gè)例子,比如用戶想上傳一個(gè)頭像,后端將圖片發(fā)給某個(gè)云存儲(chǔ),結(jié)果云存儲(chǔ)報(bào)500錯(cuò)誤。怎么辦呢?你可能想到了重試幾次,因?yàn)橐苍S問(wèn)題僅僅是臨時(shí)的網(wǎng)絡(luò)抖動(dòng)而已,重試就可以正常執(zhí)行。但如果重試多次無(wú)效。如果做系統(tǒng)時(shí)設(shè)計(jì)了某種熱備方案,那么就可能改為發(fā)到另外一個(gè)服務(wù)器上。“重試”和“使用備份的依賴”都是“立刻處理“。
(推薦教程:Java教程)
但如果重試無(wú)效,所有的備份服務(wù)也無(wú)效,那么也許就能像上面那樣把錯(cuò)誤拋給前端,提示用戶“服務(wù)器開(kāi)小差”。從這個(gè)方案很容易看出來(lái),你想把錯(cuò)誤拋到哪里是因?yàn)槟莻€(gè)catch
的地方是處理問(wèn)題最方便的地方。一個(gè)問(wèn)題的解決方案可能要幾個(gè)不同的錯(cuò)誤處理組合起來(lái)才能辦到。
另外一個(gè)例子,你的程序拋了一個(gè)NPE
。這一般就是程序員的bug
——要不就是程序員想要表達(dá)一個(gè)東西”沒(méi)有“,結(jié)果在后續(xù)處理中忘了判斷是否為null
;要不就是在寫代碼時(shí)覺(jué)得100%不可能為null
的地方出現(xiàn)了一個(gè)null
。不管哪種情況,這個(gè)錯(cuò)誤用戶總會(huì)看到一個(gè)很含糊的報(bào)錯(cuò)信息,這遠(yuǎn)遠(yuǎn)不夠?!罢_”的辦法是程序員自己能盡快發(fā)現(xiàn)它,并盡快修復(fù)。要做到這一點(diǎn),需要監(jiān)控系統(tǒng)不斷的爬log
,把問(wèn)題報(bào)警出來(lái)。而不是等到用戶找客服來(lái)吐槽。
再換一個(gè)例子,比如你的后端程序突然OOM
,掛了。掛的程序是沒(méi)法恢復(fù)自己的。要做到“正確”就必須得在服務(wù)之外的容器考慮這個(gè)問(wèn)題。比如你的服務(wù)跑在k8s
上,他們會(huì)監(jiān)控你程序的狀態(tài),然后重新啟動(dòng)新的服務(wù)實(shí)例以彌補(bǔ)掛掉的服務(wù),還得調(diào)整流量,把去往掛掉服務(wù)的流量切掉,重新?lián)Q到新的實(shí)例上。這里的恢復(fù)因?yàn)榭缦到y(tǒng)所以不能僅僅用異常實(shí)現(xiàn),但是道理是一樣的。但光靠重啟就是“正確”的嗎?如果服務(wù)是完全無(wú)狀態(tài)的,問(wèn)題不大。但是如果是有狀態(tài)的,部分用戶數(shù)據(jù)可能就會(huì)被執(zhí)行一半的請(qǐng)求搞亂套。因此重啟時(shí)要留意先“恢復(fù)數(shù)據(jù)到合法狀態(tài)”。這又回到了你需要知道怎么樣才是“正確”的做法。只依靠簡(jiǎn)單的語(yǔ)法功能是不能無(wú)腦解決這個(gè)事的。
- 我們可以推廣下,一個(gè)工作線程的“外部容器“是管理工作線程的“master”。一個(gè)網(wǎng)絡(luò)請(qǐng)求的“外部容器”是一個(gè)web server。一個(gè)用戶進(jìn)程的“外部容器”是操作系統(tǒng)。Erlang把這種supervisor-worker的機(jī)制融入到語(yǔ)言的設(shè)計(jì)中。
(推薦微課:Java微課)
Web程序之所以很大程度上能夠把異常拋給頂層,主要由于3個(gè)原因:
- 請(qǐng)求來(lái)自于前端,對(duì)于因?yàn)橛脩粽?qǐng)求有誤(數(shù)據(jù)合法性、權(quán)限、用戶上下文狀態(tài))造成的問(wèn)題,最終大概率只能告訴用戶。因此拋異常到一個(gè)集中處理錯(cuò)誤的地方,把異常轉(zhuǎn)換為某個(gè)業(yè)務(wù)錯(cuò)誤碼的方法是合理的。
- 后端服務(wù)一般都是無(wú)狀態(tài)的。這也是互聯(lián)網(wǎng)系統(tǒng)設(shè)計(jì)的一般性原則。無(wú)狀態(tài)就意味著可以隨意重啟。對(duì)于用戶的數(shù)據(jù)因?yàn)橄乱粭l一般情況下不會(huì)出問(wèn)題。
- 后端對(duì)數(shù)據(jù)的修改依賴DB的事務(wù)。因此一個(gè)改了一半的沒(méi)提交的事務(wù)是不會(huì)造成副副作用。
但你要清楚上面這3條并不是總是成立的??倳?huì)存在一些處理邏輯并非完全無(wú)狀態(tài),也并不是所有的數(shù)據(jù)修改都能用一個(gè)事務(wù)保護(hù)。尤其要注意對(duì)微服務(wù)的調(diào)用,對(duì)內(nèi)存狀態(tài)的修改是沒(méi)有事務(wù)保護(hù)的,一不留神就會(huì)出現(xiàn)搞亂用戶數(shù)據(jù)的問(wèn)題。比如:
try {
int res1 = doStep1();
this.statusVar1 += res1;
int res2 = doStep2();
this.statusVar2 += res2;
int res3 = doStep3(); // throw an exception
this.statusVar3 = statusVar1 + statusVar2 + res3;
} catch ( ...) {
// ...
}
先假設(shè)this.statusVar1
, this.statusVar2
, this.statusVar3
之間需要維護(hù)某種不變的約束(invariant)。然后執(zhí)行這段代碼時(shí),如果在doStep3
那拋出一個(gè)異常下面對(duì)statusVar3
的賦值就不會(huì)執(zhí)行。這時(shí)如果不能將statusVar1
和statusVar2
的修改rollback
回去,就會(huì)造成數(shù)據(jù)違反約束的問(wèn)題。而程序員一般是很難直接發(fā)現(xiàn)這個(gè)數(shù)據(jù)被改壞了。而壞掉的數(shù)據(jù)可能會(huì)偷偷的導(dǎo)致其他依賴這個(gè)數(shù)據(jù)的代碼邏輯出錯(cuò)(比如原本應(yīng)該給積分的,結(jié)果卻沒(méi)給)。而這種錯(cuò)誤一般非常難調(diào)查,從大量數(shù)據(jù)里找到不正確的那一小撮是相當(dāng)困難的事。
比起上面這段更難搞得定的是這樣的代碼:
// controller
void controllerMethod(/* some params*/) {
try {
return svc.doWorkAndGetResult(/* some params*/);
} catch (Exception e) {
return ErrorJsonObject.of(e);
}
}
// class svc
void doWorkAndGetResult(/* some params*/) {
int res1 = otherSvc1.doStep1(/* some params */);
this.statusVar1 += res1;
int res2 = otherSvc2.doStep2(/* some params */);
this.statusVar2 += res2;
int res3 = otherSvc3.doStep3(/* some params */);
this.statusVar3 = statusVar1 + statusVar2 + res3;
return SomeResult.of(this.statusVar1, this.statusVar2, this.statusVar3);
}
這段代碼的可怕之處在于,你在寫的時(shí)候可能會(huì)以為doStep1~3
這種東西即使拋異常,也能被Controller
里的catch
。在svc
這層是不用處理任何異常的,因此不寫try……catch
是天經(jīng)地義的。但實(shí)際上doStep1
、doStep2
、doStep3
任何一個(gè)拋異常都會(huì)造成svc
的數(shù)據(jù)狀態(tài)不一致。甚至你一開(kāi)始都可以通過(guò)文檔或者其他溝通方式確定doStep1
、doStep2
、doStep3
一開(kāi)始都是必然可以成功,不會(huì)拋錯(cuò)的,因此你寫的代碼一開(kāi)始是對(duì)的。但是你可能無(wú)法控制他們的實(shí)現(xiàn)(比如他們是另外一個(gè)團(tuán)隊(duì)開(kāi)發(fā)的lib提供的),而他們的實(shí)現(xiàn)可能會(huì)改成會(huì)拋錯(cuò)。你的代碼可能在完全不自知的情況下從“不會(huì)出問(wèn)題”變成了“可能出問(wèn)題”…… 更可怕的是類似于這樣的代碼是不能正確工作的:
void doWorkAndGetResult(/* some params*/) {
try {
int res1 = otherSvc1.doStep1(/* some params */);
this.statusVar1 += res1;
int res2 = otherSvc2.doStep2(/* some params */);
this.statusVar2 += res2;
int res3 = otherSvc3.doStep3(/* some params */);
this.statusVar3 = statusVar1 + statusVar2 + res3;
return SomeResult.of(this.statusVar1, this.statusVar2, this.statusVar3);
} catch (Exception e) {
// do rollback
}
}
你可能以為這樣就會(huì)處理好數(shù)據(jù)rollback
了,甚至你會(huì)覺(jué)得這種代碼非常優(yōu)雅。但是實(shí)際上doStep1~3
每一個(gè)地方拋錯(cuò),rollback
的代碼都不一樣。你必須得這么寫:
void doWorkAndGetResult(/* some params*/) {
int res1, res2, res3;
try {
res1 = otherSvc1.doStep1(/* some params */);
this.statusVar1 += res1;
} catch (Exception e) {
throw e;
}
try {
res2 = otherSvc2.doStep2(/* some params */);
this.statusVar2 += res2;
} catch (Exception e) {
// rollback statusVar1
this.statusVar1 -= res1;
throw e;
}
try {
res3 = otherSvc3.doStep3(/* some params */);
this.statusVar3 = statusVar1 + statusVar2 + res3;
} catch (Exception e) {
// rollback statusVar1 & statusVar2
this.statusVar1 -= res1;
this.statusVar2 -= res2;
throw e;
}
}
這才是能得到正確結(jié)果的代碼——在任何地方出現(xiàn)錯(cuò)誤都能維護(hù)數(shù)據(jù)一致性。優(yōu)雅嗎?看起來(lái)很丑。這甚至比go
的if err != nil
還丑。但如果一定要在正確性和優(yōu)雅性上作出取舍,我會(huì)毫不猶豫的選擇前者。作為程序員是不能直接認(rèn)為拋異??梢越鉀Q任何問(wèn)題的,你必須學(xué)會(huì)寫出有正確邏輯的程序,哪怕很難,并且看起來(lái)很丑。為了達(dá)成很高的正確性,你不能總是把自己大部分注意力放在“一切都OK的流程上“,而把錯(cuò)誤看作是可以隨便搞一下的工作,或者簡(jiǎn)單的相信exception
可以自動(dòng)搞定一切。
總結(jié)一下,我希望所有程序員對(duì)錯(cuò)誤處理都要有起碼的敬畏之心。Java
這邊因?yàn)?code>Checked Exception的設(shè)計(jì)問(wèn)題不得不避免使用(見(jiàn)大寬寬 - Java設(shè)計(jì)出checked exception有必要嗎?),而Uncaughted Exception
實(shí)在是太過(guò)于弱雞,是不能給程序員提供更好地幫助的。
因此,程序員在每次拋錯(cuò)或者處理錯(cuò)誤的時(shí)候都要對(duì)自己靈魂三擊:這個(gè)錯(cuò)誤的處理是正確的嗎?會(huì)讓用戶看到什么?會(huì)不會(huì)搞亂數(shù)據(jù)?不要以為自己拋了個(gè)異常就不管了。
此外,在編譯器不能幫上太多忙的時(shí)候,好好寫UT
來(lái)保護(hù)代碼脆弱的正確性。
以上就是w3cschool分享的JavaWeb 中 Service 層異常拋到 Controller 層處理還是直接處理。希望對(duì)大家有所幫助。