美文网首页Django 源码解读
Django源码分析--路由分发

Django源码分析--路由分发

作者: minhelloworld | 来源:发表于2020-02-17 11:05 被阅读0次

    上一节,我们分析了Django的网络服务的启动流程,以及它如何通过网络层接收到数并将请求转发给Django Application。那么这一节,我们主要分析,当Request到达Django Application之后,Django内部的路由规则是如何将其导入到正确的View(视图)方法中的呢?

    1、准备工作

    • Python 3.5.2
    • Django 2.1.2
    • PyCharm 2018.2.1 (Professional Edition)
    • 启动项目
    [min:] ~/Desktop/python/Demo$ python manage.py runserver 0.0.0.0:8000
    

    2、分析流程

    django.core.handlers.base.BaseHandler#get_response

    def get_response(self, request):
        """Return an HttpResponse object for the given HttpRequest."""
        # Setup default url resolver for this thread
        set_urlconf(settings.ROOT_URLCONF)
    
        response = self._middleware_chain(request)  # 重点关注!!
    
        response._closable_objects.append(request)
    
        # If the exception handler returns a TemplateResponse that has not
        # been rendered, force it to be rendered.
        if not getattr(response, 'is_rendered', True) and callable(getattr(response, 'render', None)):
            response = response.render()
    
        if response.status_code >= 400:
            log_response(
                '%s: %s', response.reason_phrase, request.path,
                response=response,
                request=request,
            )
    
        return response
    
    • Django源码分析--服务启动中,我们知道了当请求到达时,最后会调用到WSGIHandler的__call__方法,然后会执行response = self.get_response(request),即调用父类中的get_reponse方法;
    • 在此方法中我们重点关注一下response = self._middleware_chain(request),发现该属性来自于load_middleware方法。

    django.core.handlers.base.BaseHandler#load_middleware

    def load_middleware(self):
        """
        Populate middleware lists from settings.MIDDLEWARE.
    
        Must be called after the environment is fixed (see __call__ in subclasses).
        """
        self._view_middleware = []
        self._template_response_middleware = []
        self._exception_middleware = []
    
        handler = convert_exception_to_response(self._get_response)
        for middleware_path in reversed(settings.MIDDLEWARE):
            middleware = import_string(middleware_path) # 通过类似反射的方法获得对象实例;
            try:
                mw_instance = middleware(handler)   # 调用MiddlewareMixin的__init__方法;
            except MiddlewareNotUsed as exc:
                if settings.DEBUG:
                    if str(exc):
                        logger.debug('MiddlewareNotUsed(%r): %s', middleware_path, exc)
                    else:
                        logger.debug('MiddlewareNotUsed: %r', middleware_path)
                continue
                    ########省 略######
            handler = convert_exception_to_response(mw_instance)
    
        # We only assign to this when initialization is complete as it is used
        # as a flag for initialization being complete.
        self._middleware_chain = handler
    
    class MiddlewareMixin:
        def __init__(self, get_response=None):
            self.get_response = get_response
            super().__init__()
    
        def __call__(self, request):
            response = None
            if hasattr(self, 'process_request'):
                response = self.process_request(request)
            # 这步比较关键,决定了会按照MIDDLEWARE中的顺序依次倒序执行;
            response = response or self.get_response(request) 
            if hasattr(self, 'process_response'):
                response = self.process_response(request, response)
            return response
    
    • 这个方法就很重要,如果大家有看过我们的上一个章节,就会知道这个方法是WSGIHandler的__init__方法中调用的,也就是在执行runserver命令的时候就执行的;

    • convert_exception_to_response是一个装饰器,通过这装饰器和MiddlewareMixin方法结合,使得最后的

      handler实例可以依次倒序执行 settings.MIDDLEWARE中配置的所有的中间件方法,最后执行_get_response方法!

      备注:这块比较考察对装饰器的理解,需要好好琢磨一下,关键点在于理解在于MiddlewareMixin的get_response对应的就是类似于SecurityMiddleware的实例,所以每次__call__方法中response = response or self.get_response(request)就相当于重新调用了新的Middleware的__call__

    django.core.handlers.base.BaseHandler#_get_response

    def _get_response(self, request):
        """
        Resolve and call the view, then apply view, exception, and
        template_response middleware. This method is everything that happens
        inside the request/response middleware.
        """
        response = None
    
        # 获得Django App的Root URLResolver
        if hasattr(request, 'urlconf'):
            urlconf = request.urlconf
            set_urlconf(urlconf)
            resolver = get_resolver(urlconf)
        else:
            resolver = get_resolver()
        
        # 根据URL完成匹配,返回一个ResolverMatch示例
        resolver_match = resolver.resolve(request.path_info)
        # 分解ResolverMatch示例,得到对应的view方法,即callback
        callback, callback_args, callback_kwargs = resolver_match
        request.resolver_match = resolver_match
    
        # Apply view middleware
        for middleware_method in self._view_middleware:
            response = middleware_method(request, callback, callback_args, callback_kwargs)
            if response:
                break
    
        if response is None:
            wrapped_callback = self.make_view_atomic(callback)
            try:
                # 重要,在此处完成了对view方法对执行;
                response = wrapped_callback(request, *callback_args, **callback_kwargs)
            except Exception as e:
                response = self.process_exception_by_middleware(e, request)
    
        # Complain if the view returned None (a common error).
        if response is None:
            if isinstance(callback, types.FunctionType):  # FBV
                view_name = callback.__name__
            else:  # CBV
                view_name = callback.__class__.__name__ + '.__call__'
    
            raise ValueError(
                "The view %s.%s didn't return an HttpResponse object. It "
                "returned None instead." % (callback.__module__, view_name)
            )
    
        # If the response supports deferred rendering, apply template
        # response middleware and then render the response
        elif hasattr(response, 'render') and callable(response.render):
            for middleware_method in self._template_response_middleware:
                response = middleware_method(request, response)
                # Complain if the template response middleware returned None (a common error).
                if response is None:
                    raise ValueError(
                        "%s.process_template_response didn't return an "
                        "HttpResponse object. It returned None instead."
                        % (middleware_method.__self__.__class__.__name__)
                    )
    
            try:
                response = response.render()
            except Exception as e:
                response = self.process_exception_by_middleware(e, request)
    
        return response
    
    • 这个方法是整个路由分发系统的核心,可以看到就是在这个方法,request对应的视图方法被匹配到,执行并返回结果,即URLResolver根据path info匹配到正确的ResolverMatch,然后调用其中的view方法,并返回respone,具体的过程看上述代码注释,接下来我们进入详细分析;
    • 还有一个需要说明的就是在这个方法中判断respone中有没有render,有就调用;
    @functools.lru_cache(maxsize=None)
    def get_resolver(urlconf=None):
        if urlconf is None
            urlconf = settings.ROOT_URLCONF
        return URLResolver(RegexPattern(r'^/'), urlconf)
    

    tutorial/settings.py

    ROOT_URLCONF = 'tutorial.urls'
    

    tutorial/urls.py

    urlpatterns = [
        url(r'^', include('snippets.urls')),
        url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
        url(r'^schema/$', schema_view),
        url(r'^docs/', include_docs_urls(title=API_TITLE, description=API_DESCRIPTION))
    ]
    
    • 通过get_resolver方法,完成了对settings.ROOT_URLCONF关联的整个路由系统的加载,返回一个URLResolver实例;
    • urlpatterns其实就是一个URLPattern和URLResolver的集合列表,包含inclue的为URLResolver,直接跟着一个view方法的则是URLPattern;

    django.urls.conf._path

    def _path(route, view, kwargs=None, name=None, Pattern=None):
        if isinstance(view, (list, tuple)):
            # For include(...) processing.
            pattern = Pattern(route, is_endpoint=False)
            urlconf_module, app_name, namespace = view
            return URLResolver(
                pattern,
                urlconf_module,
                kwargs,
                app_name=app_name,
                namespace=namespace,
            )
        elif callable(view):
            pattern = Pattern(route, name=name, is_endpoint=True)
            return URLPattern(pattern, view, kwargs, name)
        else:
            raise TypeError('view must be a callable or a list/tuple in the case of include().')
    
    • 这个方法其实就是url(r'^schema/$', schema_view)追溯过去的,解释了上面的问题:什么情况是URLPattern和什么情况下是URLResolver;
    class URLPattern:
        def __init__(self, pattern, callback, default_args=None, name=None):
            self.pattern = pattern
            self.callback = callback  # the view
            self.default_args = default_args or {}
            self.name = name
    
        def __repr__(self):
            return '<%s %s>' % (self.__class__.__name__, self.pattern.describe())
          
        ########省 略################################
          
        def resolve(self, path):  # 负责完成path的匹配,并返回匹配的视图方法;
            match = self.pattern.match(path)
            if match:
                new_path, args, kwargs = match
                # Pass any extra_kwargs as **kwargs.
                kwargs.update(self.default_args)
                return ResolverMatch(self.callback, args, kwargs, self.pattern.name)
    
    • 每个URLPattern都需要指定如下几个内容:
      • 一个正则表达式字符串。
      • 一个可调用对象,通常为一个视图函数或一个指定视图函数路径的字符串。
      • 可选的要传递给视图函数的默认参数(字典形式)。
      • 一个可选的name参数。
    • 如果有include,则递归生成上面的模式;
    • resolve(self, path)负责完成path的匹配,并返回匹配的视图方法;
    class URLResolver:
        def __init__(self, pattern, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
            self.pattern = pattern
            # urlconf_name is the dotted Python path to the module defining
            # urlpatterns. It may also be an object with an urlpatterns attribute
            # or urlpatterns itself.
            self.urlconf_name = urlconf_name
            self.callback = None
            self.default_kwargs = default_kwargs or {}
            self.namespace = namespace
            self.app_name = app_name
            self._reverse_dict = {}
            self._namespace_dict = {}
            self._app_dict = {}
            # set of dotted paths to all functions and classes that are used in
            # urlpatterns
            self._callback_strs = set()
            self._populated = False
            self._local = threading.local()
        
        ################################省 略################################
        
       def resolve(self, path):
            path = str(path)  # path may be a reverse_lazy object
            tried = []
            match = self.pattern.match(path)
            if match:
                new_path, args, kwargs = match
                for pattern in self.url_patterns:
                    try:
                        sub_match = pattern.resolve(new_path)
                    except Resolver404 as e:
                        sub_tried = e.args[0].get('tried')
                        if sub_tried is not None:
                            tried.extend([pattern] + t for t in sub_tried)
                        else:
                            tried.append([pattern])
                    else:
                        if sub_match:
                            # Merge captured arguments in match with submatch
                            sub_match_dict = {**kwargs, **self.default_kwargs}
                            # Update the sub_match_dict with the kwargs from the sub_match.
                            sub_match_dict.update(sub_match.kwargs)
                            # If there are *any* named groups, ignore all non-named groups.
                            # Otherwise, pass all non-named arguments as positional arguments.
                            sub_match_args = sub_match.args
                            if not sub_match_dict:
                                sub_match_args = args + sub_match.args
                            return ResolverMatch(
                                sub_match.func,
                                sub_match_args,
                                sub_match_dict,
                                sub_match.url_name,
                                [self.app_name] + sub_match.app_names,
                                [self.namespace] + sub_match.namespaces,
                            )
                        tried.append([pattern])
                raise Resolver404({'tried': tried, 'path': new_path})
            raise Resolver404({'path': path})
    
        @cached_property
        def urlconf_module(self):
            if isinstance(self.urlconf_name, str):
                return import_module(self.urlconf_name)
            else:
                return self.urlconf_name
    
        @cached_property
        def url_patterns(self):
            # urlconf_module might be a valid set of patterns, so we default to it
            patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
            try:
                iter(patterns)
            except TypeError:
                msg = (
                    "The included URLconf '{name}' does not appear to have any "
                    "patterns in it. If you see valid patterns in the file then "
                    "the issue is probably caused by a circular import."
                )
                raise ImproperlyConfigured(msg.format(name=self.urlconf_name))
            return patterns
    
    • 一个URL Resolve可以包含多个URL Pattern,也可以包含多个其他URL Resolve。 通过这种包含结构设计,实现Django对URL的层级解析,也是Django实现app与项目解耦的关键。通常由include方法操作的URL配置模块,最终会被解释成为URL分解器。

    • 每个URL分解器都包含两个重要的变量:

      • 一个正则表达式字符串。URL开始部分是否匹配正则表达式,如匹配,去除成功匹配部分后余下部分匹配包含的URL模式和URL分解器。
      • URL配置模块名或URL配置模块的引用。
    • resolve(self, path)在这个方法开始遍历URLResolve,直到获得正确匹配的URLPattern,并调用其resolve方法

    class ResolverMatch:
        def __init__(self, func, args, kwargs, url_name=None, app_names=None, namespaces=None):
            self.func = func
            self.args = args
            self.kwargs = kwargs
            self.url_name = url_name
    
            # If a URLRegexResolver doesn't have a namespace or app_name, it passes
            # in an empty value.
            self.app_names = [x for x in app_names if x] if app_names else []
            self.app_name = ':'.join(self.app_names)
            self.namespaces = [x for x in namespaces if x] if namespaces else []
            self.namespace = ':'.join(self.namespaces)
    
            if not hasattr(func, '__name__'):
                # A class-based view
                self._func_path = func.__class__.__module__ + '.' + func.__class__.__name__
            else:
                # A function-based view
                self._func_path = func.__module__ + '.' + func.__name__
    
            view_path = url_name or self._func_path
            self.view_name = ':'.join(self.namespaces + [view_path])
    
        def __getitem__(self, index):
            return (self.func, self.args, self.kwargs)[index]
    
        def __repr__(self):
            return "ResolverMatch(func=%s, args=%s, kwargs=%s, url_name=%s, app_names=%s, namespaces=%s)" % (
                self._func_path, self.args, self.kwargs, self.url_name,
                self.app_names, self.namespaces,
            )匹配结果(Resolver Match)
    
    • 匹配结果是指当URL被正确匹配时,需返回的匹配结果,匹配结果需指定以下几个内容:

      • 一个可调用对象。通常是视图函数
      • 视图函数参数。通常是URL模式中正则表达式命名组匹配的值
      • 视图函数关键字参数。通常是url方法中设置传递给视图函数的参数(字典形式)
      • 可选的URL名称参数。
      • 可选的APP名称参数。
      • 可选的命名空间参数。
    • 用来表示匹配结果。ResolverMatch类实现了__getitem__方法,可以同元组操作一样,获取视图函数引用与视图函数参数,从而具备调用视图函数的条件。

    3、总结

    上面所有的过程都是Django的服务启动后,一个新的request达到后,经过怎么的流程到达对应view方法的步骤,简单做一个总结;

    1. 在启动Django服务的时候,会调用load_middleware来加载所有的中间键到BaseHandler的_middleware_chain变量中,可以等同一个多层的装饰器;
    2. 当一个新的请求到达后,会调用WSGIHandler的__call__方法,最终执行到BaseHandler中的get_reponse方法;
    3. get_reponse方法中调用了_middleware_chain方法,即依次倒序执行中间件中的方法,最后执行到_get_response方法;
    4. 在这个方法,request对应的视图方法被匹配到,执行并返回结果,即URLResolver根据path info匹配到正确的ResolverMatch,然后调用其中的view方法,并返回respone。

    4、参考:

    相关文章

      网友评论

        本文标题:Django源码分析--路由分发

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