DRF之請求執行流程和APIView原始碼分析

ssrheart發表於2024-04-23

DRF之請求執行流程和APIView原始碼分析

【一】路由入口

from django.contrib import admin
from django.urls import path
from book import views

urlpatterns = [
    path('admin/', admin.site.urls),
    # 原來的路由寫法
    # path('test_http/', views.TestHttpResponse),
    # 現在的路由寫法
    path('test/', views.TestView.as_view()),
    path('test_http/', views.TestHttpResponse.as_view()),
]
  • 在檢視類中我們繼承了 APIView
  • 在路由中我們由原來的繼承 View 的檢視函式 TestHttpResponse變成了 繼承 APIView 的檢視函式 TestView,並使用了寫的路由寫法,即TestView.as_view()
  • 因此我們的入口就是在 as_view() 方法上

【二】檢視分析

from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
class TestView(APIView):
    def get(self, request, *args, **kwargs):
        print(request)
        print(type(request))
        print(dir(request))

        return Response('ok')

【三】APIView原始碼分析

【1】執行流程入口

  • 當請求過來時 會觸發
path('test/', views.TestView.as_view())
  • 執行 檢視函式 TestViewas_view 方法
  • 那我們就從 as_view 進去

【2】路由中的 as_view()

class APIView(View):

    # The following policies may be set at either globally, or per-view.
    # 設定用於渲染響應的類,預設使用api_settings.DEFAULT_RENDERER_CLASSES。
    renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
    
    # 設定用於解析請求內容的類,預設使用api_settings.DEFAULT_PARSER_CLASSES。
    parser_classes = api_settings.DEFAULT_PARSER_CLASSES
    
    # 設定用於認證使用者身份的類,預設使用api_settings.DEFAULT_AUTHENTICATION_CLASSES。
    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
    
    # throttle_classes:設定用於限制API訪問頻率的類,預設使用api_settings.DEFAULT_THROTTLE_CLASSES。
    throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
    
    # 設定用於確定使用者許可權的類,預設使用api_settings.DEFAULT_PERMISSION_CLASSES。
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
    
    # 設定用於協商內容的類,預設使用api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS。
    content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
    
    # 設定用於處理後設資料的類,預設使用api_settings.DEFAULT_METADATA_CLASS。
    metadata_class = api_settings.DEFAULT_METADATA_CLASS
    
    # 設定用於API版本控制的類,預設使用api_settings.DEFAULT_VERSIONING_CLASS。
    versioning_class = api_settings.DEFAULT_VERSIONING_CLASS

    # Allow dependency injection of other settings to make testing easier.
    # 允許依賴注入其他設定以方便測試,允許在配置檔案中自定義配置並使用自定義配置
    settings = api_settings
	
    # 引用了DefaultSchema,表示預設的API模式類
    schema = DefaultSchema()
	
    # 包裝成靜態方法
    @classmethod
    def as_view(cls, **initkwargs):
        """
        # 將原始類儲存在檢視函式中
        Store the original class on the view function.
		
		# 這允許我們在執行URL時發現有關檢視的資訊反向查詢
        This allows us to discover information about the view when we do URL
        reverse lookups.  Used for breadcrumb generation.
        """
        
        # 判斷獲取到的屬性值是否為models.query.QuerySet型別
        # cls 檢視類 去檢視類中反射,是否存在 queryset 物件
        # getattr(cls, 'queryset', None) 
        if isinstance(getattr(cls, 'queryset', None), models.query.QuerySet):
            
            # 作用是在直接訪問.queryset屬性時觸發一個執行時錯誤
            def force_evaluation():
                # 不要直接評估.queryset屬性,因為結果會被快取並在請求之間重用
                # 應該使用.all()方法或呼叫.get_queryset()方法來獲取資料集。
                raise RuntimeError(
                    'Do not evaluate the `.queryset` attribute directly, '
                    'as the result will be cached and reused between requests. '
                    'Use `.all()` or call `.get_queryset()` instead.'
                )
            # 將force_evaluation()函式賦值給cls.queryset._fetch_all
            # 當外部程式碼直接訪問.queryset屬性時,會丟擲RuntimeError異常
            # 提醒開發者按照建議的方式來獲取資料集。
            cls.queryset._fetch_all = force_evaluation
		
        # 呼叫父類的 as_view 方法
        view = super().as_view(**initkwargs)
        
        # 將當前檢視類 新增 給 view 方法
        view.cls = cls
        
        # 將所有傳入的引數 新增給 view 方法
        view.initkwargs = initkwargs
        
		# 基於會話的身份驗證是顯式CSRF驗證的
        # Note: session based authentication is explicitly CSRF validated,
        # 所有其他認證都是免除CSRF的
        # all other authentication is CSRF exempt.
        
        # 用 csrf_exempt 包裝了 view 方法,去除了 csrf 認證
        # 這裡返回出去的去除了 csrf 認證的 view 物件就是我們上面的as_view
        # 而我們在上面執行了 as_view() 方法其實就是 這個 view() 方法 對到相應的檢視函式就是 get(request,*args,**kwargs)
        return csrf_exempt(view)

【3】父類 Viewas_view方法

class View:
    """
    # 為所有檢視建立簡單的父類。僅實現按方法排程和簡單的健全性檢查。
    Intentionally simple parent class for all views. Only implements
    dispatch-by-method and simple sanity checking.
    """
	
    # 定義允許請求的請求方式
    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
	
    # 定義初始化方法
    def __init__(self, **kwargs):
        """
        #在URLconf中呼叫;可以包含有用的額外關鍵字引數和其他內容。
        Constructor. Called in the URLconf; can contain helpful extra
        keyword arguments, and other things.
        """
        # Go through keyword arguments, and either save their values to our
        # instance, or raise an error.
        
        # 遍歷傳入的所有引數
        for key, value in kwargs.items():
            # 將遍歷得到的鍵和值,全部新增到 self 物件中
            setattr(self, key, value)
	
    # 包裝成靜態方法
    @classonlymethod
    # 允許傳入檢視類和其他引數
    def as_view(cls, **initkwargs):
        # 請求-響應過程的主要入口點
        """Main entry point for a request-response process."""
        
        # 遍歷 initkwargs 傳入的引數的鍵
        for key in initkwargs:
            # 判斷當前請求方式是否在上述請求方式列表中存在
            if key in cls.http_method_names:
                
                # 丟擲異常
                # 方法名稱 不被接受為關鍵字引數
                raise TypeError(
                    'The method name %s is not accepted as a keyword argument '
                    'to %s().' % (key, cls.__name__)
                )
                
            # 判斷如果當前檢視類中沒有寫當前請求方式
            if not hasattr(cls, key):
                # 丟擲異常
                # 只能接收存在的請求當時
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))
		
        # 閉包函式
        def view(request, *args, **kwargs):
            
            # 例項化得到物件,並將引數傳入
            self = cls(**initkwargs)
            
            # 呼叫啟動方法,初識化公共類屬性
            self.setup(request, *args, **kwargs)
            
            # 判斷當前物件是否存在 request 屬性
            if not hasattr(self, 'request'):
                # 不存在則丟擲異常
                raise AttributeError(
                    # 不存在 request 屬性,必須提供
                    "%s instance has no 'request' attribute. Did you override "
                    "setup() and forget to call super()?" % cls.__name__
                )
            
            # 返回 dispatch 方法,並將所有引數傳入
            return self.dispatch(request, *args, **kwargs)
        
        # 將 當前類 新增給 view 物件
        view.view_class = cls
        # 將所有引數 新增給 view 物件
        view.view_initkwargs = initkwargs

        # take name and docstring from class
        # 從類中獲取名稱和文件字串
        update_wrapper(view, cls, updated=())

        # and possible attributes set by decorators
        # like csrf_exempt from dispatch
        # 是否存在裝飾器,例如 csrf認證
        update_wrapper(view, cls.dispatch, assigned=())
        
        # 將 view 物件返回
        return view
  • setup
def setup(self, request, *args, **kwargs):
    # 初始化所有檢視方法共享的屬性
    """Initialize attributes shared by all view methods."""
    
    # 判斷當前類物件中存在get方法,並且沒有 head 方法
    if hasattr(self, 'get') and not hasattr(self, 'head'):
        # 將自身的 head 方法替換成 get 方法
        self.head = self.get
        
    # 將傳入的 request 賦值給當前物件
    self.request = request
    # 將傳入的 位置引數 賦值給當前物件
    self.args = args
    # 將傳入的 關鍵字引數 賦值給當前物件
    self.kwargs = kwargs

【4】APIView 的 dispatch 方法

  • 透過上面分析,我們發現在APIView中呼叫了父類的 as_view()方法
    • 在父類 View 中,又呼叫了 dispatch 方法
  • 因為我們是又 APIView 進到的 View ,所以我們當前的 self 其實是 APIView
  • 那 self.dispatch() ,理所應當的就要從自己找,就是在下面所示的 APIView 中的 dispatch

image-20230913192421317

  • 原始碼解析
def dispatch(self, request, *args, **kwargs):
    """
    # 大致意識是和 APIView相似但是新增了新的功能
    `.dispatch()` is pretty much the same as Django's regular dispatch,
    but with extra hooks for startup, finalize, and exception handling.
    """
    
    # 初識化引數,將 位置引數 新增給 self 物件
    self.args = args
    # 初識化引數,將 關鍵字引數 新增給 self 物件
    self.kwargs = kwargs
    
    # 初始化傳入的請求物件,將其封裝為符合Django規範的請求物件
    request = self.initialize_request(request, *args, **kwargs)
    
    # 儲存了初始化後的請求物件
    self.request = request
    
    # 儲存了預設的響應頭部資訊
    self.headers = self.default_response_headers  # deprecate?

    try:
        # 進行的初始化操作,例如驗證使用者身份等
        self.initial(request, *args, **kwargs)

        # Get the appropriate handler method
        # 獲取適當的處理程式方法
        # 將 請求方式小寫 ,並判斷當前請求方式是否在允許的請求方式類表內
        if request.method.lower() in self.http_method_names:
            
            # handler : 當前的請求當時,獲取到當前請求方式
            handler = getattr(self, request.method.lower(),
                              self.http_method_not_allowed)
        else:
            # 如果請求方法不存在,則會呼叫self.http_method_not_allowed方法,返回不允許的HTTP方法的響應
            handler = self.http_method_not_allowed
		
        # 呼叫選擇的處理方法,將請求物件和引數傳遞給它,並獲取返回的響應物件
        response = handler(request, *args, **kwargs)

    except Exception as exc:
        # 如果在處理請求過程中發生任何異常,異常處理方法可以根據實際需求進行自定義,可以返回適當的錯誤響應。
        response = self.handle_exception(exc)
	
    # 對響應進行最後的處理,例如新增額外的響應頭部資訊、修改響應內容等。
    self.response = self.finalize_response(request, response, *args, **kwargs)
    
    # 返回處理好的最終響應物件
    return self.response
  • initialize_request
def initialize_request(self, request, *args, **kwargs):
    """
    # 返回一個例項化的 request 物件
    Returns the initial request object.
    """
    # 拿到解析後的資料字典
    parser_context = self.get_parser_context(request)
	
    # 返回例項化後的Request物件
    return Request(
        # 當前 request 物件
        request,
        # 解析器
        parsers=self.get_parsers(),
        # 認證使用者
        authenticators=self.get_authenticators(),
        negotiator=self.get_content_negotiator(),
        # 解析後的資料
        parser_context=parser_context
    )
  • get_parser_context
def get_parser_context(self, http_request):
    """
    # 返回一個被解析器解析過得資料字典
    Returns a dict that is passed through to Parser.parse(),
    as the `parser_context` keyword argument.
    """
    # Note: Additionally `request` and `encoding` will also be added
    #       to the context by the Request object.
    
    # 返回了 類 物件本身
    return {
        'view': self,
        # 將 位置引數 返回,無則為空
        'args': getattr(self, 'args', ()),
        # 將 關鍵字引數 返回,無則為空
        'kwargs': getattr(self, 'kwargs', {})
    }
  • initial
def initial(self, request, *args, **kwargs):
    """
    # 在呼叫方法處理程式之前執行任何需要發生的事情。
    Runs anything that needs to occur prior to calling the method handler.
    """
    # 透過get_format_suffix方法獲取到的格式字尾儲存在例項變數self.format_kwarg中
    self.format_kwarg = self.get_format_suffix(**kwargs)

    # Perform content negotiation and store the accepted info on the request
    # 呼叫了perform_content_negotiation方法,並將請求物件request作為引數傳遞進去
    # 執行內容協商,並返回一個包含可接受的渲染器和媒體型別的元組
    neg = self.perform_content_negotiation(request)
    
    # 將內容協商結果中的渲染器和媒體型別儲存在請求物件request的accepted_renderer和accepted_media_type屬性中
    request.accepted_renderer, request.accepted_media_type = neg

    # Determine the API version, if versioning is in use.
    # 呼叫了determine_version方法,並將請求物件request以及其他引數傳遞進去。
    # 該方法用於確定API的版本和版本控制方案,並返回一個包含版本和版本控制方案的元組。
    version, scheme = self.determine_version(request, *args, **kwargs)
    # 將確定的API版本和版本控制方案儲存在請求物件request的version和versioning_scheme屬性中
    request.version, request.versioning_scheme = version, scheme
	
    # 確保允許傳入請求
    # Ensure that the incoming request is permitted

    # 登入認證:呼叫了perform_authentication方法,並將請求物件request作為引數傳遞進去。
    # 該方法用於執行身份驗證,確保傳入的請求是合法的。
    
    self.perform_authentication(request)
    # 許可權認證:呼叫了check_permissions方法,並將請求物件request作為引數傳遞進去。
    # 該方法用於檢查許可權,確保使用者有權訪問該資源
    self.check_permissions(request)
    
    # 頻率認證:呼叫了check_throttles方法,並將請求物件request作為引數傳遞進去。
    # 該方法用於檢查限流,確保請求沒有超過預定的頻率限制。
    self.check_throttles(request)
  • get_format_suffix
def get_format_suffix(self, **kwargs):
    """
    # 確定請求是否包含“.json”樣式的格式字尾
        Determine if the request includes a '.json' style format suffix
        """
    if self.settings.FORMAT_SUFFIX_KWARG:
        return kwargs.get(self.settings.FORMAT_SUFFIX_KWARG)

【四】總結

【1】請求過來的完整執行流程

  • 當請求過來時,觸發路由中的TestView.as_view()方法
    • 也就是 TestView.as_view()(request)

image-20230913201506292

  • APIView中觸發了self.as_view()
    • 但是 APIView 沒有 as_view()
    • 於是呼叫了父類中的 as_view() 方法

image-20230913201952923

  • 在父類的as_view()方法又觸發了dispatch方法
    • 於是又回到了 APIViewdispatch 方法

image-20230913202319757

  • APIViewdispatch 方法中對資料進行處理

image-20230913202624533

  • dispatch方法中有一個initial方法,這個方法完成了三大認證
    • 即 登陸、許可權、頻率認證

image-20230913202810027

  • 三大認證完成後,執行 handler
    • 先到檢視類中對映檢視函式,然後執行檢視函式,獲得響應資料,並返回

image-20230913205517624

  • 所有資料都處理完後接著向下走
  • 對返回的 view 物件去除的 csrf 認證

image-20230913202835740

【2】APIView相較View的大變化

  • 以後只要繼承APIView的所有檢視類的方法,都沒有csrf的校驗了
  • 以後只要繼承APIView的所有檢視類的方法 中的request是新的request了
  • 在執行檢視類的方法之前,執行了三大認證(認證,許可權,頻率)
  • 期間除了各種錯誤,都會被異常捕獲,統一處理

相關文章