這下對(duì)阿里java這幾條規(guī)范有更深理解了
背景
阿里java開發(fā)規(guī)范是阿里巴巴總結(jié)多年來的最佳編程實(shí)踐,其中每一條規(guī)范都經(jīng)過仔細(xì)打磨或踩坑而來,目的是為社區(qū)提供一份最佳編程規(guī)范,提升代碼質(zhì)量,減少bug。
這基本也是java業(yè)界都認(rèn)可的開發(fā)規(guī)范,我們團(tuán)隊(duì)也是以此規(guī)范為基礎(chǔ),在結(jié)合實(shí)際情況,補(bǔ)充完善。最近在團(tuán)隊(duì)遇到的幾個(gè)問題,加深了我對(duì)這份開發(fā)規(guī)范中幾個(gè)點(diǎn)的理解,下面就一一道來。
日志規(guī)約

這條規(guī)范說明了,在異常發(fā)送記錄日志時(shí),要記錄案發(fā)現(xiàn)場信息和異常堆棧信息,不處理要往上throws,切勿吃掉異常。
堆棧信息比較好理解,就是把整個(gè)方法調(diào)用鏈打印出來,方便定位具體是哪個(gè)方法出錯(cuò)。而案發(fā)現(xiàn)場信息我認(rèn)為至少要能說明:“誰發(fā)生了什么錯(cuò)誤”。
例如,哪個(gè)uid下單報(bào)錯(cuò)了,哪個(gè)訂單支付失敗了,原因是什么。否則滿屏打印:“user error”,看到你都無從下手。
在我們這次出現(xiàn)的問題就是有一個(gè)feign,調(diào)用外部接口報(bào)錯(cuò)了,降級(jí)打印了不規(guī)范日志,導(dǎo)致排查問題花了很多時(shí)間。偽代碼如下:
@Slf4j
@Component
class MyClientFallbackFactory implements FallbackFactory<MyClient> {
@Override
public MyClient create(Throwable cause) {
return new MyClient() {
@Override
public Result<DataInfoVo> findDataInfo(Long id) {
log.error("findDataInfo error");
return Result.error(SYS_ERROR);
}
};
}
}
發(fā)版后錯(cuò)誤日志開始告警,打開kibana看到了滿屏了:“findDataInfo error”,然后開始一頓盲查。
因?yàn)檫@個(gè)接口本次并沒有修改,所以猜測(cè)是目標(biāo)服務(wù)出問題,上服務(wù)器curl接口,發(fā)現(xiàn)調(diào)用是正常的。
接著猜測(cè)是不是熔斷器有問題,熔斷后沒有恢復(fù),但重啟服務(wù)后,還是繼續(xù)報(bào)錯(cuò)。開始各種排查,arthas跟蹤,最后實(shí)在沒辦法了,還是老老實(shí)實(shí)把異常打印出來,走發(fā)版流程。
log.error("{} findDataInfo error", id, cause);
有了異常堆棧信息就很清晰了,原來是返回參數(shù)反序列失敗了,接口提供方新增一個(gè)不兼容的參數(shù)導(dǎo)致反序列失敗。(這點(diǎn)在下一個(gè)規(guī)范還會(huì)提到)
可見日志打印不清晰給排查問題帶來多大的麻煩,記住:日志一定要打印關(guān)鍵信息,異常要打印堆棧。
二方庫依賴

上面提到的返回參數(shù)反序列化失敗就是枚舉造成的,原因是這個(gè)接口返回新增一個(gè)枚舉值,這個(gè)枚舉值原本返回給前端使用的,沒想到還有其它服務(wù)也調(diào)用了它,最終在反序列化時(shí)就報(bào)錯(cuò)了,找不到“xxx”枚舉值。
比如如下接口,你提交一個(gè)不認(rèn)得的黑色BLACK,就會(huì)報(bào)反序列錯(cuò)誤:
enum Color {
GREEN, RED
}
@Data
class Test {
private Color color;
}
@PostMapping(value = "/post/info")
public void info(@NotNull Test test) {
}
curl --location 'localhost/post/info' \
--header 'Content-Type: application/json' \
--data '{
"testEnum": "BLACK"
}'
關(guān)于這一點(diǎn)我們看下作者孤盡對(duì)它的闡述:

這就是我們出問題的場景,提供方新增了一個(gè)枚舉值,而使用方?jīng)]有升級(jí),導(dǎo)致錯(cuò)誤。可能有的同學(xué)說那通知使用方升級(jí)不就可以了?是的,但這出現(xiàn)了依賴問題,如果使用方有成百上千個(gè),你會(huì)非常頭痛。
那又為什么說不要使用枚舉作為返回值,而可以作為輸入?yún)?shù)呢?
我的理解是:作為枚舉的提供者,不得隨意新增/修改內(nèi)容,或者說修改前要同步到所有枚舉使用者,讓大家知道,否則使用者就可能因?yàn)椴徽J(rèn)識(shí)這個(gè)枚舉而報(bào)錯(cuò),這是不可接受的。
但反過來,枚舉提供者是可以將它作為輸入?yún)?shù)的,如果調(diào)用者傳了一個(gè)不存在的值就會(huì)報(bào)錯(cuò),這是合理的,因?yàn)樘峁┱卟]有說支持這個(gè)值,調(diào)用者正常就不應(yīng)該傳遞這個(gè)值,所以這種報(bào)錯(cuò)是合理的。
ORM映射

以下是規(guī)范里的說明:
1)增加查詢分析器解析成本。
2)增減字段容易與 resultMap 配置不一致。
3)無用字段增加網(wǎng)絡(luò)消耗,尤其是 text 類型的字段。
這都很好理解,就不過多說明。
在我們開發(fā)中,有的同學(xué)為了方便,還是使用了select *,一直以來也風(fēng)平浪靜,運(yùn)行得好好的,直到有一天對(duì)該表加了個(gè)字段,代碼沒更新,報(bào)錯(cuò)了~,你沒看錯(cuò),代碼沒動(dòng),加個(gè)字段程序就報(bào)錯(cuò)了。
報(bào)錯(cuò)信息如下:

數(shù)組越界!問題可以在本地穩(wěn)定復(fù)現(xiàn),先把程序跑起來,執(zhí)行 select * 的sql,再add column給表新增一個(gè)字段,再次執(zhí)行相同的sql,報(bào)錯(cuò)。

具體原因是我們程序使用了sharding-jdbc做分表(5.1.2版本),它會(huì)在服務(wù)啟動(dòng)時(shí),加載字段信息緩存,在查詢后做字段匹配,出錯(cuò)就在匹配時(shí)。
具體代碼位置在:com.mysql.cj.protocol.a.MergingColumnDefinitionFactory#createFromFields

這個(gè)緩存是跟數(shù)據(jù)庫鏈接相關(guān)的,只有鏈接失效時(shí),才會(huì)重新加載。主要有兩個(gè)參數(shù)和它相關(guān):
spring.shardingsphere.datasource.master.idle-timeout 默認(rèn)10min
spring.shardingsphere.datasource.master.max-lifetime 默認(rèn)30min
默認(rèn)緩存時(shí)間都比較長,你只能趕緊重啟服務(wù)解決,而如果服務(wù)數(shù)量非常多,又是一個(gè)生產(chǎn)事故。
我在sharding sphere github搜了一圈,沒有好的處理方案,相關(guān)鏈接如:
https://github.com/apache/shardingsphere/issues/21728
https://github.com/apache/shardingsphere/issues/22824
大體意思是如果真想這么做,數(shù)據(jù)庫ddl需要通過sharding proxy,它會(huì)負(fù)責(zé)刷新客戶端的緩存,但我們使用的是sharding jdbc模式,那只能老老實(shí)實(shí)遵循規(guī)范,不要select * 了。如果select具體字段,那新增的字段也不會(huì)被select出來,和緩存的就能對(duì)應(yīng)上。
那么以后面試除了上面規(guī)范說到的,把這一點(diǎn)親身經(jīng)歷也擺出來,應(yīng)該可以加分吧。
總結(jié)
每條開發(fā)規(guī)范都有其背后的含義,都是經(jīng)驗(yàn)總結(jié)和踩坑教訓(xùn),對(duì)于團(tuán)隊(duì)的開發(fā)規(guī)范我們都要仔細(xì)閱讀,嚴(yán)格遵守。可以看到上面每個(gè)小問題都可能導(dǎo)致不小的生產(chǎn)事故,保持敬畏之心,大概就是這個(gè)意思了吧。
更多分享,歡迎關(guān)注我的github:https://github.com/jmilktea/jtea

浙公網(wǎng)安備 33010602011771號(hào)