引言
問(wèn)題提出
先看下面一段代碼
from flask import Flask, jsonify
flask_app = Flask(__name__)
@flask_app.route('/', endpoint="11")
def hello_world():
a = 1 / 0
return jsonify(
code=0,
msg="success",
data=["hello,world!"]
)
if __name__ == '__main__':
flask_app.run()
這段代碼有一個(gè)很顯然的未捕捉的異常限匣,就是1/0
再看flask
在生產(chǎn)環(huán)境中返回了什么惯疙,一個(gè)狀態(tài)是500
、內(nèi)容是html
的返回
然而在用json
格式交互的前后端分離場(chǎng)景下,前端希望后端仍然返回json
格式的數(shù)據(jù)瓶摆,而不是html
再考慮到可能以后還有安卓和ios
凹蜂,人家可能都不需要html
所以我們需要在后端服務(wù)出現(xiàn)未捕獲的異常時(shí)候馍驯,返回自定義的json
格式數(shù)據(jù)
修改代碼如下
from flask import Flask, jsonify
flask_app = Flask(__name__)
@flask_app.route('/', endpoint="11")
def hello_world():
a = 1 / 0
return jsonify(
code=0,
msg="success",
data=["hello,world!"]
)
@flask_app.errorhandler(500)
def handle_500(e):
# 可能還要記錄一下自定義的日志
# 也可能還需要回滾一下數(shù)據(jù)庫(kù)操作
return jsonify(
code=-1,
msg="unknown error"
)
if __name__ == '__main__':
flask_app.run()
重啟服務(wù)
這樣,我們的前端玛痊、ios和安卓只需要再一個(gè)全局的位置汰瘫,判斷一下,如果code
等于 -1
擂煞,就展示unknown error
或者自己換個(gè)名字 服務(wù)器未知異常
就好了
這里我們用到了 flask
提供的 errorhandler
為什么建議用errorhandler
有的人可能會(huì)在沒(méi)有看文檔的情況下混弥,寫(xiě)一個(gè)裝飾器去裝飾視圖函數(shù),來(lái)捕捉未知異常对省,像這樣
from flask import Flask, jsonify
flask_app = Flask(__name__)
def handle_500(func):
def wrapper(*args, **kw):
try:
rv = func(*args, **kw)
except Exception as e:
# 可能還要記錄一下自定義的日志
# 可能還需要回滾數(shù)據(jù)庫(kù)操作
rv = {
"code": -1,
"msg": "unknown error"
}
return rv
return wrapper
@flask_app.route('/', endpoint="11")
@handle_500
def hello_world():
a = 1 / 0
return jsonify(
code=0,
msg="success",
data=["hello,world!"]
)
# @flask_app.errorhandler(500)
# def handle_500(e):
# # 可能還要記錄一下自定義的日志
# return jsonify(
# code=-1,
# msg="unknown error"
# )
if __name__ == '__main__':
flask_app.run()
這樣寫(xiě)我個(gè)人不太建議:
- 官方文檔建議用
errorhandler
的處理方式蝗拿,最好用這種 -
handle_500
這個(gè)裝飾器要在@app.route
這個(gè)裝飾器下面才有作用 - 如果你用的是函數(shù)視圖而不是類(lèi)視圖,那么你每個(gè)函數(shù)都要加這樣一個(gè)裝飾器官辽,產(chǎn)生重復(fù)代碼蛹磺,還可能會(huì)遺忘。如果你用類(lèi)視圖同仆,可以寫(xiě)一個(gè)被裝飾的基類(lèi)
- 這個(gè)裝飾器只能捕捉函數(shù)視圖或者類(lèi)視圖中的異常萤捆,我們?cè)陂_(kāi)發(fā)中還有
@app.before_request
等鉤子函數(shù),里面也會(huì)出現(xiàn)異常俗批,這個(gè)裝飾器無(wú)法捕捉俗或,但errorhandler
可以 - 這樣更加解耦
2345
的問(wèn)題errorhandler
都可以解決
原理
下面是程序執(zhí)行的方法調(diào)用棧,基本原理就是異常在源碼中已經(jīng)被catch
了然后檢查Flask
對(duì)象有沒(méi)有對(duì)應(yīng)的handler
可以處理岁忘,沒(méi)有就上拋辛慰,一直拋
源碼實(shí)現(xiàn)
這里用斷點(diǎn)調(diào)試就很簡(jiǎn)單了
先在 full_dispatch_request
打斷點(diǎn),ctrl+鼠標(biāo)左鍵
進(jìn)入方法內(nèi)部
在 full_dispatch_request
方法內(nèi)部打斷點(diǎn)干像,注意要打兩個(gè)斷點(diǎn)帅腌,然后按f9
讓程序走到這個(gè)斷點(diǎn)
進(jìn)入dispatch_request
內(nèi)部驰弄,在self.view_functions[rule.endpoint](**req.view_args)
處打斷點(diǎn)
self.view_functions[rule.endpoint](**req.view_args)
,這一步其實(shí)就是在執(zhí)行 hello_world()
我們知道 hello_world
這個(gè)函數(shù)會(huì)拋出一個(gè) 零不能被除
的異常
那這個(gè)異常會(huì)被捕捉嗎速客?
繼續(xù)往前調(diào)試戚篙,我們會(huì)回到上一層(因?yàn)槲覀冊(cè)谥?code>full_dispatch_request內(nèi)部打了兩個(gè)斷點(diǎn))
零不能被除
異常在上一層被捕捉到了,這個(gè)e
就是我們的異常
我們?cè)谶@個(gè)地方停留一會(huì)兒溺职,我們發(fā)現(xiàn)這個(gè)try
內(nèi)部除了 dispatch_request
岔擂,還有 rv = self.preprocess_request()
這一句,看名字也知道是預(yù)處理浪耘,什么預(yù)處理呢乱灵?沒(méi)錯(cuò)就是之前提到的 @app.before_request
裝飾的鉤子函數(shù),由此可見(jiàn)七冲,鉤子函數(shù)的異常也會(huì)被捕捉到
繼續(xù)看except
之后的代碼
那么 rv = self.handle_user_exception(e)
會(huì)幫我們處理這個(gè) division by zero
異常嗎痛倚?
其實(shí)也不會(huì),他會(huì)把異常繼續(xù)往上拋癞埠,我們稍后再講他的作用
點(diǎn)擊調(diào)用棧的這個(gè)地方状原,我們要回到上一層,繼續(xù)打上一個(gè)斷點(diǎn)苗踪,注意看圖中 Frames
的位置,選擇箭頭指向的上一層
在error=e
處打斷點(diǎn)
估計(jì)你也知道了削锰,零不能被除
異常被 handle_user_exception(e)
又拋了出來(lái)通铲,在上一層的 wsgi_app
方法中被捕捉到了,并且交給 handle_exception
來(lái)處理
而 handle_exception
做的事情也簡(jiǎn)單
先打日志器贩,把出錯(cuò)類(lèi)型 出錯(cuò)值 出錯(cuò)調(diào)用棧全打出來(lái)
然后再準(zhǔn)備報(bào) InternalServerError
也就是狀態(tài)碼是500
的flask
服務(wù)器異常
但是在正式返回之前颅夺,會(huì)先看一下你有沒(méi)有 處理500
錯(cuò)誤的handler
,而我們是有的,于是調(diào)用你的handle_500
函數(shù)處理服務(wù)器異常
注意這句 if self.propagate_exception
蛹稍,self.propagate_exception
這個(gè)屬性是為了決定是否傳播你的異常吧黄,在開(kāi)發(fā)和測(cè)試環(huán)境下,這個(gè)為真唆姐,異常會(huì)被繼續(xù)拋到上層拗慨,所以我們寫(xiě)的handler
會(huì)不生效,因此奉芦,為了看到我們的500
錯(cuò)誤處理器生效赵抢,記得在生產(chǎn)環(huán)境查看
選中 handler
右鍵在彈出菜單中選擇evaluate
,可以發(fā)現(xiàn)這個(gè)handler
就是我們定義的handle_500
函數(shù),handle_500
返回的就是我們自定義的json
格式
handle_user_exception處理什么異常
比較容易看出声功,這個(gè)方法主要是處理 HTTPException
和用戶(hù)自定義的非500
的異常
看個(gè)例子
from flask import Flask, jsonify
flask_app = Flask(__name__)
class ValidationError(ValueError):
pass
@flask_app.route('/', endpoint="11", methods=["GET", "POST"])
def hello_world():
raise ValidationError("參數(shù)驗(yàn)證失敗")
@flask_app.errorhandler(ValidationError)
def validation_error(e):
return jsonify(
code=1,
msg=e.args
)
if __name__ == '__main__':
flask_app.run()
例如你的代碼里面有多處需要拋出和捕獲 ValidationError
這個(gè)錯(cuò)誤烦却,進(jìn)一步說(shuō),在orm層會(huì)拋出這個(gè)異常先巴,view
層捕捉其爵,那么寫(xiě)多次try
不妨考慮用這種全局異常注冊(cè)的方式
現(xiàn)在我們看看源碼怎么處理的
handle_user_exception
內(nèi)部打上斷點(diǎn)
handler
就是我們的 validation_error
函數(shù)冒冬,最后返回的實(shí)際上是 validation_error
函數(shù)的執(zhí)行結(jié)果
后端api格式
我們都知道rest
是一種風(fēng)格的api
他用http
碼來(lái)表示狀態(tài),但實(shí)際中有時(shí)候是不夠用的摩渺,或者前端简烤、ios
和安卓都希望不用http
的什么400
狀態(tài)碼,
而需要在json
數(shù)據(jù)中再定義自己的狀態(tài)碼证逻,http
狀態(tài)碼統(tǒng)一200
像這樣
{
"code":0,
"msg":"success",
"data":[]
}
這種常見(jiàn)于國(guó)內(nèi)比較大型的項(xiàng)目中
到底是采用rest
風(fēng)格的api
還是自定義狀態(tài)碼乐埠,爭(zhēng)論很多,具體還是看公司要求