美文网首页
Flask源码分析系列(2) -Flask源码分析

Flask源码分析系列(2) -Flask源码分析

作者: VincentWang9 | 来源:发表于2020-06-09 18:05 被阅读0次

    转载请注明出处即可
    源码地址github flask
    主要参考文档为flask
    环境为MacOS, Python 3.7+, IDE Pycharm

    注意:文章中的源码存在删减,主要是为了减少篇幅和去除非核心逻辑,但不会影响对执行流程的理解。

    如果对Werkzeug不是很了解,请先看Flask源码分析系列(1) -Werkzeug源码分析这篇文章

    一、从一个最简单的Demo开始

    Flask是Python语言编写的一个优秀的开源Web框架。我们先从一个最小的Demo开始,逐步来分析Flask是如何实现相关功能的。

    from flask import Flask
    
    app = Flask(__name__)
    
    
    @app.route('/')
    def hello_world():
        return 'Hello, World!'
    
    
    def main():
        app.run(host='0.0.0.0', port=8080, debug=True)
    
    
    if __name__ == '__main__':
        main()
    

    首先app变量或者说Flask类创建的对象,其实是一个WSGI Application,也就是说是一个符合上篇文件中描述的一个符合WSGI规则的一个函数,具体是Flask类的wsgi_app方法来实现。

    # app.py 2366行, Flask类的方法
    def __call__(self, environ, start_response):
        return self.wsgi_app(environ, start_response)
    
    # app.py 2323行, Flask类的方法
    def wsgi_app(self, environ, start_response):
        pass
    

    虽然app.run方法提供了Werkzeug的serving.make_server的实现,但是你依然可以选择其他支持WSGI协议的Server来运行Flask应用,比如gunicorn等。在实践中,我们在开发环境可以选择一些基本的WSGI Server用于本地调试。而在生产环境中在使用gunicorn等来实现多进程运行。当然这都直接取决于你自己根据实际的环境进行选择。以下代码是使用tornado的httpserver的一个例子。

    from tornado.wsgi import WSGIContainer
    from tornado.httpserver import HTTPServer
    from tornado.ioloop import IOLoop
    from demo import app
    
    import sys
    
    reload(sys)
    sys.setdefaultencoding("utf-8")
    
    
    def main():
        http_server = HTTPServer(WSGIContainer(app))
        http_server.listen(8080)
        IOLoop.instance().start()
    
    
    app.config['SESSION_TYPE'] = 'filesystem'
    app.config['APIURL'] = '/api'
    
    if __name__ == "__main__":
        main()
    

    如果使用gunicorn,那么可以通过以下指令来运行Flask应用。

    export FLASK_ENV=development
    THREAD_COUNT=8
    gunicorn -k gevent -w $THREAD_COUNT -b 0.0.0.0:8080 demo:app -t 6000000
    

    扯了一些基本应用,下面开始进入正题。

    二、Route实现原理

    Route的实际作用是将url path和具体要执行的函数进行映射。Flask并没有把这些能力自己实现,而是使用了Werkzeug的Map、Rule和MapAdapter来实现。

    首先先看下@app.route('/')装饰器的实现。
    (Python的装饰器在这里不详细解释,如果不明白请查看廖雪峰的Python教程)

    def route(self, rule, **options):
        def decorator(f):
            endpoint = options.pop("endpoint", None)
            self.add_url_rule(rule, endpoint, f, **options)
            return f
    
        return decorator
    

    代码很简洁,route方法的参数rule是url path,而options则对应着Werkzeug中Rule类的参数,比如endpoint,methods等。除了endpoint做了一些特殊的处理以外,其他的参数原封不动的传到了Rule的__init__
    在decorator函数中的第一行从options dict中pop出了endpoint,这里是因为在add_url_rule进行了一些其他处理(其实就是判断是否是None,然后选择是否使用函数名称而已)。
    add_url_rule方法的第三个参数f,则为被装饰的函数,在Demo的例子中就是hello_world函数。
    Flask默认使用的endpoint是方法的名称,但依然保留了这个参数,方便用户自定义endpoint。

    然后我们来看下Flask.add_url_rule方法的实现。具体源码在app.py的1099行。由于方法略长,我们来拆分即可来分析。方法的参数列表没有什么需要过多解释的。

    def add_url_rule(
        self,
        rule,
        endpoint=None,
        view_func=None,
        provide_automatic_options=None,
        **options,
    ):
      pass
    

    函数的第一段,是处理endpoint,如果用户没有在route中设置endpoint参数的话,则默认使用了view_func.__name__来获取函数的名称。然后获取了methods的参数。

    if endpoint is None:
        endpoint = _endpoint_from_view_func(view_func)
    options["endpoint"] = endpoint
    

    函数的第二段,是对methods参数的处理,如果用户没有设置methods列表(或元组)的话,默认设置为("Get",)。并且对methods进行了是否是字符串的检查, 最后将所有的method都变成大写和去重(放入了Set中)。

    methods = options.pop("methods", None)
    if methods is None:
        methods = getattr(view_func, "methods", None) or ("GET",)
    if isinstance(methods, str):
        raise TypeError(
            "Allowed methods must be a list of strings, for"
            ' example: @app.route(..., methods=["POST"])'
        )
    methods = {item.upper() for item in methods}
    

    函数的第三段是增加了必须要添加的methods的检查,比如在methods中如果没有OPTIONS的话,Flask也会增加默认的OPTIONS到Methods集合中。

    required_methods = set(getattr(view_func, "required_methods", ()))
    
    if provide_automatic_options is None:
        provide_automatic_options = getattr(
            view_func, "provide_automatic_options", None
        )
    
    if provide_automatic_options is None:
        if "OPTIONS" not in methods:
            provide_automatic_options = True
            required_methods.add("OPTIONS")
        else:
            provide_automatic_options = False
    
    methods |= required_methods
    

    函数的第四段,主要与Werkzeug的Rule和Map类有关。其中url_rule_class = Rule,而url_map_class = Map,self.url_map = self.url_map_class()。所以这段的最后一行其实就是在Map的rules列表中添加Rule类的对象。

    rule = self.url_rule_class(rule, methods=methods, **options)
    rule.provide_automatic_options = provide_automatic_options
    
    self.url_map.add(rule)
    

    函数的最后一段的逻辑,如果看过上篇文章的话,也就能猜到还差一个endpoint到view_func的映射关系,在Flask中 self.view_functions = {} 也是通过一个字典来存储的。并且还进行了一个检查,防止一个endpoint映射到多个view_func中。

    if view_func is not None:
        old_func = self.view_functions.get(endpoint)
        if old_func is not None and old_func != view_func:
            raise AssertionError(
                "View function mapping is overwriting an existing"
                f" endpoint function: {endpoint}"
            )
        self.view_functions[endpoint] = view_func
    

    最后我们可以看下dispatch_request方法,在app.py的1830行。函数的最后一行是

    return self.view_functions[rule.endpoint](**req.view_args)
    

    是不是和上篇文章的一个例子很像^_^
    当然只获取到具体的view_func来执行是不够的,还需要通过finalize_request来构造response,还需要符合WSGI的规范。

    三、request、Response、session等对象的实现

    其实除了Response, request和session都使用了Werkzeug中的Context Locals。并且request就是Werkzeug中的Request。globals.py中的部分源码如下。

    from werkzeug.local import LocalProxy
    from werkzeug.local import LocalStack
    
    # context locals
    _request_ctx_stack = LocalStack()
    _app_ctx_stack = LocalStack()
    current_app = LocalProxy(_find_app)
    request = LocalProxy(partial(_lookup_req_object, "request"))
    session = LocalProxy(partial(_lookup_req_object, "session"))
    g = LocalProxy(partial(_lookup_app_object, "g"))
    

    在这里多说下Session,Flask的session默认是客户端session,也就是说session的数据不是存储在内存中的,而是加密后存储在了Cookie中,并且每次请求在解密后还原session。Flask使用的是AES之类的对称加密算法。所以在使用session时,尽量不要将大对象存储在session中,否则后续的每个请求都会携带这些数据。对于session的具体实现,在这里不进行详述,感兴趣的可以看下源码中的sessions.py。
    对于Flask的session的实践,可以在公共缓存中存储一个实际的session对象,而在Flask的session中仅存储用户的id,进而减轻用户请求传输的Cookie的数据量。

    四、一些简单的封装

    (1) 登录校验与拦截

    可以通过装饰器来实现,在需要登录的view_func上增加@login_required即可

    def login_required(f):
        @wraps(f)
        def decorated_function(*args, **kw):
            user_id = session.get('user_id')
            if user_id is None:
                return BaseError.not_login()
    
            return f(*args, **kw)
    
        return decorated_function
    

    (2) 自定义异常与返回值处理

    class BusinessException(Exception):
        def __init__(self, code=None, msg=None, func=None):
            self.code = code
            self.msg = msg
            self.func = func
    
    class Error(BaseError):
        @staticmethod
        def custom_error():
            return return_data(code=REQUEST_FAIL, msg=u'自定义异常')
    
    def request_handler(**data_dict):
        def decorator(func):
            @wraps(func)
            def handle_request_data(*args, **kw):
                try:
                    check_rule = build_check_rule(str(request.url_rule), get_rule_version(),
                                                  list(request.url_rule.methods & set(METHODS)))
                    check_func = check_param.get_check_rules().get(check_rule)
                    if check_func:
                        check_func(*args, **kw)
                except BusinessException as e:
                    if e.func is not None:
                        return e.func()
                    elif e.code is not None and e.msg is not None:
                        logger.error('BusinessException, code: %s, msg: %s' % (e.code, e.msg))
                        return return_data(code=e.code, msg=e.msg)
                    else:
                        return request_fail()
                except Exception:
                    return request_fail()
    
                try:
                    return func(*args, **kw)
                except BusinessException as e:
                    if e.func is not None:
                        return e.func()
                    elif e.code is not None and e.msg is not None:
                        logger.error('BusinessException, code: %s, msg: %s' % (e.code, e.msg))
                        return return_data(code=e.code, msg=e.msg)
                    else:
                        return request_fail()
                except Exception:
                    return request_fail()
    
            return handle_request_data
    
        return decorator
    

    在具体业务逻辑编写时,则无需在每个view_func中对异常进行处理,只需要raise具体的业务异常即可。

    @app.route('/main.json', version=['<=1.3'])
    @request_handler()
    def main_json():
         raise BusinessException(func=Error.custom_error)
    

    request_handler中的对参数检查的相关函数,是因为笔者之前所写的业务逻辑,需要大量的参数校验,并且还存在着一定的校验逻辑复用,所以将参数校验和具体的业务逻辑进行了分离。具体使用时,类似于下面的形式来使用。check_outer和route的路径相同即可进行一一对应。

    @check_outer.check('/main.json', version=versions)
    def main_json(*args, **kw):
       raise BusinessException(func=Error.custom_error)
    

    至于具体的实现,笔者简单抄了下Blueprint的源码。

    class CheckParam(object):
        def __init__(self):
            self.check_rules = dict()
    
        def register_check_param(self, check_param=None, url_prefix=''):
            if not isinstance(check_param, SubCheckParam):
                raise RuntimeError('check_param is not a SubCheckParam object. type: %s' % type(check_param))
    
            check_rules = check_param.get_check_rules()
    
            for check_rule in check_rules:
                url = check_rule.url
                version = check_rule.version
                methods = check_rule.methods
                f = check_rule.f
    
                self.check_rules[
                    str({'url': url_prefix + url, 'version': sorted(version), 'methods': sorted(methods)})] = f
    
        def get_check_rules(self):
            return self.check_rules
    
    
    class CheckRule(object):
        def __init__(self, url, version, methods, f):
            self.url = url
            self.version = version
            self.methods = methods
            self.f = f
    
    
    class SubCheckParam(object):
        def __init__(self):
            self.check_rules = []
    
        def check(self, url=None, version=None, methods=None):
            methods = methods if methods is not None else DEFAULT_METHODS
    
            def decorator(f):
                if not url:
                    raise ValueError('A non-empty url is required.')
                if not methods:
                    raise ValueError('A non-empty method is required.')
    
                self.__add_check_rule(url, version, methods, f)
                return f
    
            return decorator
    
        def __add_check_rule(self, url, version, methods, f):
            if version and isinstance(version, list):
                version = sorted(version)
            else:
                version = []
    
            self.check_rules.append(CheckRule(url=url, version=version, methods=methods, f=f))
    
        def get_check_rules(self):
            return self.check_rules
    
    
    def build_check_rule(url=None, version=None, methods=None):
        if not url:
            raise ValueError('A non-empty url is required.')
        if not methods:
            raise ValueError('A non-empty method is required.')
    
        if version and isinstance(version, list):
            version = sorted(version)
        else:
            version = []
    
        return str({'url': url, 'version': version, 'methods': sorted(methods)})
    

    具体的使用,和前面说的一样,只要url path一致即可。下面的SelfFlask和SelfBlueprint是因为为了支持版本号路由而继承了Flask和Blueprint来进行了扩展。

    app = SelfFlask(__name__)
    app.config.from_object(configs)
    
    check_inner = SubCheckParam()
    check_outer = SubCheckParam()
    check_manager = SubCheckParam()
    check_owner = SubCheckParam()
    check_member = SubCheckParam()
    check_third = SubCheckParam()
    
    inner = SelfBlueprint('inner', __name__)
    outer = SelfBlueprint('outer', __name__)
    manager = SelfBlueprint('manager', __name__)
    owner = SelfBlueprint('owner', __name__)
    member = SelfBlueprint('member', __name__)
    third = SelfBlueprint('third', __name__)
    
    from backend.versions import *
    
    app.register_blueprint(inner, url_prefix='/inner')
    app.register_blueprint(outer, url_prefix='/outer')
    app.register_blueprint(manager, url_prefix='/manager')
    app.register_blueprint(owner, url_prefix='/owner')
    app.register_blueprint(member, url_prefix='/member')
    app.register_blueprint(third, url_prefix='/third')
    
    check_param.register_check_param(check_inner, url_prefix='/inner')
    check_param.register_check_param(check_outer, url_prefix='/outer')
    check_param.register_check_param(check_manager, url_prefix='/manager')
    check_param.register_check_param(check_owner, url_prefix='/owner')
    check_param.register_check_param(check_member, url_prefix='/member')
    check_param.register_check_param(check_third, url_prefix='/third')
    

    比如,对endpoint进行了修改,来支持版本号路由。

    class SelfBlueprint(Blueprint):
        def route(self, rule, **options):
            """Like :meth:`Flask.route` but for a blueprint.  The endpoint for the
            :func:`url_for` function is prefixed with the name of the blueprint.
            """
    
            # set default methods
            methods = options.get('methods')
            if methods is None:
                options['methods'] = DEFAULT_METHODS
    
            def decorator(f):
                endpoint = options.pop("endpoint", f.__name__ + str(options.get('version')).replace('.', '_'))
                self.add_url_rule(rule, endpoint, f, **options)
                return f
    
            return decorator
    

    五、结束语

    在这里就把Flask主要的部分实现分析完成了,但是还有一些如Blueprint、Jinjia2等还没有说,如果读者感兴趣请自行查看源码。但是在生产环境还是建议不要使用模板引擎来渲染页面逻辑。最好还是做到前后端分离。

    相关文章

      网友评论

          本文标题:Flask源码分析系列(2) -Flask源码分析

          本文链接:https://www.haomeiwen.com/subject/fvtetktx.html