flask源码剖析--请求流程
想了解这篇里面的内容,请先去了解我另外一篇博客Flask上下文
在了解flask之前,我们需要了解两个小知识点
- 偏函数
import functools def func(a1,a2): print(a1) print(a2) #重新封装成一个 给前面参数加默认值 的函数 new_func = functools.partial(func, 666) new_func(777)
- 面向对象 对象 + 会执行__add__方法
class Foo(object): def __init__(self, num): self.num = num def __add__(self, other): data = self.num + other.num return Foo(data) obj1 = Foo(11) obj2 = Foo(22) v = obj1 + obj2 print(v.num)
- 拼接列表中的值
from itertools import chain v1 = [11,22,33] v2 = [44,55,66] new = chain(v1, v2) for item in new: print(item) def f1(x): return x + 1 func1_list = [f1, lambda x:x-1] def f2(x): return x + 10 new_func_list = chain([f2], func1_list) for func in new_func_list: print(func)
- 强制调用私有变量 _类名__私有变量名
class Foo(object): def __init__(self): self.name = 'alex' self.__age = 18 def get_age(self): return self.__age obj = Foo() # print(obj.name) # print(obj.get_age()) # 强制获取私有字段 print(obj._Foo__age)
为什么要了解这个几个点呢?这边先按下不说,分析过程中,自然就明白了
从哪里开始,那就要留心我们平时写代码了,flask程序运行起来主要app.run实现的
进入run函数中,代码最终执行了run_simple(host, port, self, **options),而run_simple执行时,第三参数加括号执行,也就是会执行self(flask对象)__calll__方法
__call__方法里,执行了self.wsgi_app(environ, start_response),而这里面的代码可以说是flask处理请求的核心代码
ctx = self.request_context(environ) ctx.push() error = None try: try: response = self.full_dispatch_request() except Exception as e: error = e response = self.handle_exception(e) except: error = sys.exc_info()[1] raise return response(environ, start_response) finally: if self.should_ignore_error(error): error = None ctx.auto_pop(error)
请求到来时
第一步
ctx = self.request_context(environ) 函数里执行了RequestContext(self, environ)
-
执行RequestContext类里的__init__方法
def __init__(self, app, environ, request=None): self.app = app #app为flask对象 if request is None: request = app.request_class(environ) self.request = request #请求相关 self.url_adapter = app.create_url_adapter(self.request) #url映射 self.flashes = None #闪现相关 self.session = None #session相关
- 注意此时self为RequestContext对象,也就是赋值给ctx变量的对象
- __init__方法主要把flask对象,生成请求对象并把请求和闪现,session...相关的东西封装在ctx里
第二步
- ctx.push() 执行RequestContext里的push方法,函数里最终执行了_request_ctx_stack.push(self)
-
执行的是LocalStack的push方法,并把RequestContext(ctx)对象为参数传了进来
rv = getattr(self._local, 'stack', None) if rv is None: self._local.stack = rv = [] rv.append(obj) return r
- 刚开始进入stack是没有值的,所以会执行self._local.stack = rv = [],等同把空列表同时赋值给stack和rv
- self._local.stack = [],本质上会执行self._local对象里的__setattr__方法,也就是Local类的
#其中name为stack,value为[] def __setattr__(self, name, value): ident = self.__ident_func__() #线程或协程唯一标识 storage = self.__storage__ #__init__方法赋值为{} try: storage[ident][name] = value except KeyError: storage[ident] = {name: value}
3. 等同把RequestContext对象放到Local的这么一个字典中
storage = { 唯一ID:{ stack:[RequestContext对象,] }, 唯一ID:{ stack:[RequestContext对象,] } }
请求结束时
- 在wsgi_app函数中,在try代码中无非就是url映射,找到视图函数并执行,执行完后就需要到存储字典里把相关request数据移除掉,而在finally最终会执行这么一句代码ctx.auto_pop(error)--error有错误时就是错误信息没有就是None,也就是执行RequestContext类里的auto_pop方法
- else中执行了self.pop(exc) self为RequestContext对象,pop里最终执行_request_ctx_stack.pop(),而_request_ctx_stack则是LocalStack对象,也就是执行了LocalStack类的pop方法
stack = getattr(self._local, 'stack', None) #stack 类似这么个列表[RequestContext对象,] if stack is None: return None elif len(stack) == 1: release_local(self._local) return stack[-1] else: return stack.pop() #列表pop进行删除
请求处理中
- 找到视图函数并执行请求
- print(request),打印对象,会执行类里的__str__方法,那这个request是哪个类的呢,要知道这个,就需要知道在导入时,是从哪导的
from flask import Flask,request
- 也就是执行了下面这句代码,也许你会觉得这个partial怎么似曾相识啊,这个就是偏函数,把request字符串当默认值传入,并且还是返回函数,所以这句代码就是实例化LocalProxy对象,并把一个偏函数传了进去,此时会去执行LocalProxy的__init__,所以说在导入的request对象,本质是LocalProxy对象
request = LocalProxy(partial(_lookup_req_object, 'request'))
- 执行LocalProxy的__init__方法时,有这么一句代码,其中_LocalProxy__local是不是感觉有在哪见过,对没错,这就是强制调用私有变量,本质上就是做了__local = 偏函数,local是传进来的偏函数
object.__setattr__(self, '_LocalProxy__local', local)
- 回到视图函数进行打印request则会执行LocalProxy里的__str__方法,也就是下面这句
__str__ = lambda x: str(x._get_current_object())
- 在_get_current_object函数里,进入if中执行__local加括号,也就是执行偏函数
if not hasattr(self.__local, '__release_local__'): #刚开始时 偏函数中肯定没辙玩意 return self.__local() try: return getattr(self.__local, self.__name__) except AttributeError: raise RuntimeError('no object bound to %s' % self.__name__)
- 而在_get_current_object主要做了这么一件事,提取偏函数并执行,也就是执行下面这个函数
def _lookup_req_object(name): #此时的name为request字符串 top = _request_ctx_stack.top if top is None: raise RuntimeError(_request_ctx_err_msg) return getattr(top, name)
- _request_ctx_stack.top,执行LocalStack的top,最终返回就是字典存储的RequestContext对象
try: return self._local.stack[-1] except (AttributeError, IndexError): return None
- 所以最终getattr(top, name),是去RequestContext中获取request数据的
- request.method,执行LocalProxy的__getattr__方法
- 还是执行_get_current_object方法,获取偏函数_lookup_req_object执行,提取request数据
上下文总结:
threading.Local和Flask自定义Local对象
--请求到来
- ctx = 封装RequestContext(request, session)
- ctx放到Local中
--执行视图时
- 导入request
- print(request) --> LocalProxy对象的__str__
- request.method --> LocalProxy对象的__getattr__
- request + 1 --> LocalProxy对象的__add__
- 调用 _lookup_req_object函数:去local中将requestContext想获取到,再去requestContext中获取request或session
-- 请求结束
- ctx.auto_pop()
- ctx从local中移除
了解整个请求流程源码后,其实你也可以这么做
from flask.globals import _request_ctx_stack from functools import partial def _lookup_req_object(name): # name = request # top= ctx top = _request_ctx_stack.top if top is None: raise RuntimeError('不存在') # return ctx.request return getattr(top, name) class Foo(object): def __init__(self): self.xxx = 123 self.ooo = 888 req = partial(_lookup_req_object,'xxx') xxx = partial(_lookup_req_object,'ooo') # 当前求刚进来时 _request_ctx_stack.push(Foo()) # 使用 # obj = _request_ctx_stack.top # obj.xxx v1 = req() print(v1) v2 = xxx() print(v2) # 请求终止,将local中的值移除 _request_ctx_stack.pop()
APP上下文
由于flask版本的区别,有一部分数据被分离到APP上下文中,而其原理和请求上下文是一样的
- 我们再次看到app.wsgi_app源码中这句ctx.push(),函数里会有这么几句
app_ctx = _app_ctx_stack.top if app_ctx is None or app_ctx.app != self.app: app_ctx = self.app.app_context() app_ctx.push()
- 这里self是RequestContext对象,即ctx,self.app就是封装在里面的flask对象,再看下app_context方法中干了啥
- 返回的是一个AppContext,并把flask对象传入了进去,并执行AppContext的__init__方法,封装了flask对象给app,还封装了一个g对象,对于这个对象,你就可以理解为一个存储数据的字典
def __init__(self, app): self.app = app self.url_adapter = app.create_url_adapter(None) self.g = app.app_ctx_globals_class()
为了方便你理解这个g中主要存储什么数据,这里不妨举个例子:比如你在请求前有个数据要传递给视图函数使用,你会怎么搞了,你可能会想,直接通过request.xxx=yyy赋值就可以了,这样做是可以的,但是为了更好避免名字重了,你用这个g来进行存储,这里需要注意的是:它主要保存的是一个请求周期的值
from flask import Flask,request,g app = Flask(__name__) @app.before_request def before(): g.permission_code_list = ['list','add'] @app.route('/',methods=['GET',"POST"]) def index(): print(g.permission_code_list) return "index" if __name__ == '__main__': app.run()
- app_ctx.push()-->_app_ctx_stack.push(self),其中_app_ctx_stack又是一个LocalStack对象,执行它里面的push,会把app_ctx(AppContext对象)放到_app_ctx_stack下的local,这里会注意到:无论多少个线程,会创建请求上下文和APP上下文两个local对象进行存储数据
- 上面是请求到来时,请求结束时,也会进行删除,和请求上下文是一样,可以通过ctx.auto_pop(error)一步一步看下去,最终会执行_app_ctx_stack.pop(),也就是LocalStack的pop
- 请求过程中,比如打印g对象,还是执行LocalProxy的__str__方法,最终还是执行偏函数,不过这里偏函数换成了_lookup_app_object,也是去local中获取到AppContext对象,并获取到g对象
多APP应用
我们已经学过了蓝图,url过来,经过app分发给蓝图进行处理
接下来的内容,则可以通过不同的APP来处理不同的url
from werkzeug.wsgi import DispatcherMiddleware from werkzeug.serving import run_simple from flask import Flask, current_app app1 = Flask('app01') app2 = Flask('app02') @app1.route('/index') def index(): return "app01" @app2.route('/index2') def index2(): return "app2" # http://www.oldboyedu.com/index # http://www.oldboyedu.com/sec/index2 dm = DispatcherMiddleware(app1, { '/sec': app2, }) if __name__ == "__main__": app2.__call__ run_simple('localhost', 5000, dm)
对上面这段代码的实现原理,看了源码后,你才会发现,原来如此简单
- 先看到run_simple,最终会执行第三参数+(),也就是dm()
- dm是一个DispatcherMiddleware对象,里面封装了起始app以及其他app和url的映射关系
def __init__(self, app, mounts=None): self.app = app self.mounts = mounts or {}
- dm()会执行DispatcherMiddleware的__call__方法
def __call__(self, environ, start_response): script = environ.get('PATH_INFO', '') #获取url信息,比如/sec/index2 path_info = '' while '/' in script: if script in self.mounts: #判断当前url在不在映射关系里 app = self.mounts[script] #在就获取url对应的app对象,退出循环 break
#分割后 script='/sec' last_itme='index2' script, last_item = script.rsplit('/', 1) #如果不在映射关系里,从右进行分割一次 path_info = '/%s%s' % (last_item, path_info) #path_info="/index2" 用于去APP下面找视图函数 else: app = self.mounts.get(script, self.app) #如果都没有匹配到,那就获取 起始app #最后把处理好的路径信息重新封装到environ中
original_script_name = environ.get('SCRIPT_NAME', '') environ['SCRIPT_NAME'] = original_script_name + script environ['PATH_INFO'] = path_info return app(environ, start_response)
- 源码最后执行了app(environ, start_response),如果是/sec/index2,那么此时的app就是app2,是一个flask对象,加括号,会执行里面的__call__,从这里开始又和单app情况下执行流程是一样的
最后,多app情况下,local下存储结构是咋样的呢?
- 和单app的场景是一样的,同时通过线程和协程唯一标识进行存储的
- 在local下获取值和设置值的,不存在通过类似app唯一标识进行操作,只通过线程和协程唯一标识
- 在上述代码中,如果导入request,两个app是暴露在同一全局变量下的,request不对app进行区分
def __getattr__(self, name): try: return self.__storage__[self.__ident_func__()][name] except KeyError: raise AttributeError(name) def __setattr__(self, name, value): ident = self.__ident_func__() #线程或协程唯一标识 storage = self.__storage__ #{} try: storage[ident][name] = value #name -->'stack' except KeyError: storage[ident] = {name: value}
可能大家一直有个疑问,那就是local存储中存储APPcontent对象为什么要用栈呢?
def push(self, obj): """Pushes a new item to the stack""" rv = getattr(self._local, 'stack', None) if rv is None: self._local.stack = rv = [] rv.append(obj) return rv
正常请求流程中,是不会出现栈里有两个对象的,但是测试在进行离线脚本测试时,就有可能有两个呢?
想了解清楚,这里还是了解一下面向对象的一个知识点
class SQLHelper(object): def open(self): pass def fetch(self,sql): pass def close(self): pass def __enter__(self): self.open() return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() # obj = SQLHelper() # obj.open() # obj.fetch('select ....') # obj.close() with SQLHelper() as obj: # 自动调用类中的__enter__方法, obj就是__enter__返回值 obj.fetch('xxxx') # 当执行完毕后,自动调用类 __exit__ 方法
当测试脚本中,存在with嵌套时,栈中就可能有多个对象
from flask import Flask,current_app,globals,_app_ctx_stack app1 = Flask('app01') app1.debug = False # 用户/密码/邮箱 # app_ctx = AppContext(self): # app_ctx.app # app_ctx.g app2 = Flask('app02') app2.debug = True # 用户/密码/邮箱 # app_ctx = AppContext(self): # app_ctx.app # app_ctx.g with app1.app_context():# __enter__方法 -> push -> app_ctx添加到_app_ctx_stack.local # {<greenlet.greenlet object at 0x00000000036E2340>: {'stack': [<flask.ctx.AppContext object at 0x00000000037CA438>]}} print(_app_ctx_stack._local.__storage__) print(current_app.config['DEBUG']) with app2.app_context(): # {<greenlet.greenlet object at 0x00000000036E2340>: {'stack': [<flask.ctx.AppContext object at 0x00000000037CA438> ]}} print(_app_ctx_stack._local.__storage__) print(current_app.config['DEBUG']) #退出时,执行__exit__方法,执行pop,移除栈里最后的一个对象 print(current_app.config['DEBUG'])