【12.0】DRF之全局异常处理

发布时间 2023-07-31 21:56:05作者: Chimengmeng

【一】引入

  • 在前端开发中,为了便于处理后端报错,通常需要后端返回统一的格式。
  • 通过统一的格式,前端可以更方便地处理后端返回的错误信息
    • 比如根据错误码展示不同的提示信息给用户。
{code:999,msg:'系统异常,请联系系统管理员'}

// 其中code表示错误码,msg表示错误信息。
  • 只要三大认证,视图类的方法出了异常,都会执行一个函数:
# 全局异常处理
# 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
  • 在后端开发中,无论是视图类的方法出现异常,还是其他地方出现异常,都可以通过全局异常处理来进行统一的处理。
  • 例如,给定的Python代码片段展示了一个全局异常处理函数,它会处理所有视图类方法出现的异常。
  • 通过配置'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler'
    • 当发生异常时,系统会调用该函数进行异常处理。
  • 在该函数中,可以执行一些自定义的操作来处理异常
    • 比如记录日志、返回统一的错误响应等。
  • 但是不能在源码中修改处理异常的函数
  • 不能直接修改源码中的异常处理函数。
  • 这是因为源码是框架或库的核心部分,更改源码可能导致意想不到的后果,并且对于源码的修改在升级或维护时可能会产生冲突。
  • 因此,在开发过程中应尽量避免修改源码,而是通过配置或继承等方式来实现自定义的异常处理逻辑。

【二】源码分析

from rest_framework.views import exception_handler
  • exception_handler
def exception_handler(exc, context):
    """
    Returns the response that should be used for any given exception.

    By default we handle the REST framework `APIException`, and also
    Django's built-in `Http404` and `PermissionDenied` exceptions.

    Any unhandled exceptions may return `None`, which will cause a 500 error
    to be raised.
    """
    if isinstance(exc, Http404):
        exc = exceptions.NotFound()
    elif isinstance(exc, PermissionDenied):
        exc = exceptions.PermissionDenied()

    if isinstance(exc, exceptions.APIException):
        headers = {}
        if getattr(exc, 'auth_header', None):
            headers['WWW-Authenticate'] = exc.auth_header
        if getattr(exc, 'wait', None):
            headers['Retry-After'] = '%d' % exc.wait

        if isinstance(exc.detail, (list, dict)):
            data = exc.detail
        else:
            data = {'detail': exc.detail}

        set_rollback()
        return Response(data, status=exc.status_code, headers=headers)

    return None
  • rest_framework是一个用于构建Web API的框架

    • views模块提供了与视图相关的功能。
  • exception_handler函数用于处理异常并返回相应的响应。

    • 该函数接受两个参数:
      • exc表示捕获到的异常对象
      • context表示异常上下文信息。
  • 函数首先会检查捕获到的异常类型

    • 如果是Http404类型的异常,则将其转换为NotFound异常。
    • 同样地,如果是PermissionDenied类型的异常,则转换为PermissionDenied异常。
    • 这些转换操作是为了将Django内置的异常转换为REST框架中定义的异常类型。
  • 如果捕获到的异常是继承自APIException的自定义异常

    • 那么函数会根据异常对象的属性来构造响应。
    • 具体来说,函数会考虑异常对象的auth_header属性和wait属性,并在响应头中设置相应的值
    • 。然后,函数会判断异常对象的detail属性的类型
      • 如果是listdict类型,直接将其作为响应数据
      • 否则将detail属性封装为字典数据。
  • 最后

    • 函数会调用set_rollback()函数来设置回滚标志,并返回一个带有相应数据、状态码和响应头的Response对象。
    • 如果无法匹配到合适的异常类型,函数将返回None,导致抛出500错误。

【三】自定义处理异常函数

# 仿写 exception_handler
from rest_framework.views import exception_handler
from rest_framework.response import Response


def common_exception_handler(exc, context):
    '''
    exception_handler:只处理了 DRF 的异常,其他的异常不会被捕获
    :param exc:
    :param context:
    :return:
    '''
    # 一般出现异常,都会进行日志的记录

    # 定义返回异常格式
    back_dict = {"code": 999, "message": ""}

    # 如果异常对象是 DRF 的 APIException对象,则会返回 Response
    # 如果异常对象不是 DRF 的 APIException对象,则会返回 None

    # 执行原来的 exception_handler 函数会捕获到异常 并且返回 Response 或 None
    result = exception_handler(exc, context)

    if result:
        # 有值 Response 则说明是DRF的异常,已经被捕获和处理了
        back_dict['code'] = 996
        # 异常返回有两种格式,一种是字典,一种是直接的数据
        if isinstance(result.data, dict):
            # 异常是字典格式
            back_dict["detail"] = result.data.get('detail')
        else:
            # 异常是直接的数据
            print(result.data)
            back_dict["detail"] = result.data

        back_dict["message"] = "程序异常,请联系管理员处理异常!"
        # back_dict["message"] = str(exc)  # 这种错误展示会将具体的错误信息展示出来,一般给客户做东西,客户可以提交这个信息便于排查问题
        return Response(back_dict)
    else:
        # 无值 None 则说明是 其他的异常,没有被处理
        back_dict["message"] = "系统异常,请联系管理员处理异常!"
        return Response(back_dict)
  • 定义返回异常格式:
    • 创建一个字典back_dict,用于存储异常相关的信息。
  • 调用exception_handler函数:
    • 首先尝试调用DRF内置的exception_handler函数,它会捕获DRF的APIException对象并返回Response
    • 如果传入的异常不是DRF的APIException对象,则返回None。
  • 处理DRF的异常:
    • 如果result有值(即存在返回的Response)
      • 说明是DRF的异常被捕获和处理了。
      • 此时将back_dictcode设置为996,表示程序异常,将result.data进行判断
    • 如果是字典格式
      • 则取其中的detail作为异常的具体信息
      • 否则直接使用result.data作为异常的具体信息。
    • back_dictmessage设置为"程序异常,请联系管理员处理异常!",最后返回处理后的Response。
  • 处理其他异常:
    • 如果result没有值(None),则说明是其他未被处理的异常。
    • back_dictmessage设置为"系统异常,请联系管理员处理异常!"
    • 然后返回处理后的Response。
  • 配置文件中修改
REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'app01.excepiton.my_excepiton.common_exception_handler',
}
  • 为了使用该自定义异常处理函数,需要在配置文件中进行相应的修改。
  • 在配置文件的REST_FRAMEWORK
    • EXCEPTION_HANDLER的值设置为自定义函数的路径,
    • 即'app01.excepiton.my_excepiton.common_exception_handler'。
  • 异常展示

    • DRF的异常(APIException)

      {
          "code": 996,
          "message": "程序异常,请联系管理员处理异常!",
          "detail": "这是DRF的异常"
      }
      
    • 原生的异常(Exception)

      {
          "code": 999,
          "message": "系统异常,请联系管理员处理异常!"
      }
      

【四】异常处理模板

from rest_framework.views import exception_handler
from rest_framework.response import Response


def common_exception_handler(exc, context):

    # 定义返回异常格式
    back_dict = {"code": 999, "message": ""}
    
    result = exception_handler(exc, context)

    if result:
        back_dict['code'] = 996
        if isinstance(result.data, dict):
            back_dict["detail"] = result.data.get('detail')
        else:
            print(result.data)
            back_dict["detail"] = result.data

        back_dict["message"] = "程序异常,请联系管理员处理异常!"
        # back_dict["message"] = str(exc)  
        return Response(back_dict)
    else:
        back_dict["message"] = "系统异常,请联系管理员处理异常!"
        return Response(back_dict)
  • rest_framework.views.exception_handler 是 Django REST framework 中的一个内置函数,用于处理视图函数中发生的异常。
  • rest_framework.response.Response 是 Django REST framework 提供的用于创建 HTTP 响应的类。
  • common_exception_handler 函数是自定义的异常处理函数,它会被 Django REST framework 在发生异常时自动调用。
  • exc 是捕获到的异常对象。
  • context 是一个包含有关发生异常的上下文信息的字典,可以包含有关请求、视图和其他相关数据的信息。
  • response_data 是用于构建最终响应的字典,包含了错误码(code)和错误消息(message)等字段。
  • result 不为空时,表示有发生异常,并通过 result 获取到异常的详细信息,将其添加到 response_data 中,并将错误码设置为 996。
  • result 为空时,表示没有发生异常,直接将错误码设置为 999,并设置错误消息为系统异常。
  • 返回最终响应,其中包含了处理后的异常信息。

【补充】函数和方法的区别

  • 在编程中,函数和方法是两个相关但又有区别的概念。

【1】函数

  • 函数是一段可以重复使用的代码块,它接受输入参数并产生输出结果。
  • 函数通常独立于任何特定的对象或类,并可以在程序的不同位置被调用和执行。
  • 函数可以有返回值,也可以没有返回值。

【2】方法

  • 方法是与特定对象或类关联的函数。
  • 它是一个属于某个类或对象的函数,用于描述该类或对象的行为。
  • 方法可以读取和修改类或对象的属性,也可以执行与对象相关的操作。
  • 方法与对象或类之间存在着紧密的耦合关系,必须通过对象或类来调用和执行。

【3】函数和方法之间区别

  • 所属关系:

    • 函数可以独立存在

    • 而方法必须依赖于对象或类存在。

  • 调用方式:

    • 函数可以直接调用

    • 而方法必须通过对象或类来调用。

  • 参数传递:

    • 函数的参数是显式传递的

    • 方法的第一个参数通常是隐式传递的,表示调用该方法的对象(通常命名为self)。

  • 数据访问权限:

    • 函数不能直接访问对象的属性

    • 而方法可以通过self参数访问对象的属性。

  • 命名约定:

    • 函数的名称通常用小写字母和下划线
    • 方法的名称通常用驼峰命名法。
  • 下面是一个示例代码,展示了函数和方法的区别:

# 函数示例
def add(a, b):
    return a + b

result = add(2, 3)
print(result)  # 输出:5

# 方法示例
class Calculator:
    def add(self, a, b):
        return a + b

calculator = Calculator()
result = calculator.add(2, 3)
print(result)  # 输出:5
  • 在上述示例中
    • add函数是一个独立的函数,可以直接调用。
    • 而Calculator类中的add方法是一个属于Calculator类的函数,必须通过Calculator对象来调用。

【4】总结起来

  • 函数是一段独立的可重复使用的代码
  • 而方法是与对象或类紧密关联的函数,用于描述对象或类的行为。
  • 它们在调用方式、所属关系、参数传递和数据访问权限等方面存在明显的区别。

【补充】isinstance()/issubclass()

  • isinstance()函数和issubclass()函数是Python中的两个内置函数
  • 用于判断对象与类之间的关系。

【1】isinstance()

  • isinstance()函数用于检查一个对象是否是一个类的实例。
  • 它接受两个参数
    • 第一个参数是待检查的对象
    • 第二个参数是类或类型。
  • 如果对象是第二个参数指定的类或类型的实例
    • 则返回True;
    • 否则返回False。
  • 例如:
class Person:
    pass

person = Person()
print(isinstance(person, Person))  # 输出:True
print(isinstance(person, object))  # 输出:True
print(isinstance(person, str))     # 输出:False
  • 在上面的例子中,通过isinstance()函数可以判断person对象是否是Person类的实例。
  • 同时,由于所有的类都继承自object类,所以person对象也被认为是object类的实例。

【2】issubclass()

  • issubclass()函数用于检查一个类是否是另一个类的子类。
  • 它接受两个参数
    • 第一个参数是待检查的类
    • 第二个参数是父类或超类。
  • 如果第一个参数是第二个参数指定的类或类型的子类
    • 则返回True;
    • 否则返回False。
  • 例如:
class Animal:
    pass

class Dog(Animal):
    pass

print(issubclass(Dog, Animal))   # 输出:True
print(issubclass(Dog, object))   # 输出:True
print(issubclass(Dog, str))      # 输出:False
  • 在上述示例中,通过issubclass()函数可以判断Dog类是否是Animal类的子类。
    • 同时由于所有的类都是object类的子类,所以Dog类也被认为是object类的子类。
  • 这就是isinstance()函数和issubclass()函数的详解。
    • 通过使用这两个函数,我们可以判断对象与类之间的关系,从而进行相应的处理。