一、断点调试使用

所谓断点调试就是程序以debug模式运行,可以在任意位置停下,查看停止位置变量的变化情况。

使用步骤

  • 步骤一:打断点

  • 步骤二:以debug模式运行代码

  • 步骤三:这时候我们看pycharm下方的窗口,可以根据不同功能的按钮,执行不同的操作

二、认证,权限,频率源码分析(了解)

2.1 权限类的执行源码

权限的源码执行流程

	-写一个权限类,局部使用,配置在视图类的,就会执行权限类的has_permission方法,完成权限校验

之前我们在学习drf的apiview的源码中,了解到在执行视图类方法之前执行了三大认证,同时两者都是处于dispatch异常捕获结构中的(需要注意这里的dispatch是APIView的方法,不是view中的dispatch方法)。

    -APIView类的497行左右, self.initial(request, *args, **kwargs)---》执行3大认证

# APIView类的399行左右:
def initial(self, request, *args, **kwargs):
# 能够解析的编码,版本控制(知道有这作用就好了,不用管,重点是下面部分)
self.format_kwarg = self.get_format_suffix(**kwargs)
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme # 认证组件的执行位置
self.perform_authentication(request)
# 权限组件 [读它]
self.check_permissions(request)
# 频率组件
self.check_throttles(request)

接着我们点进权限组件的执行源码查看

# APIView的326 左右
def check_permissions(self, request):
# self.get_permissions()----》[CommonPermission(),]
# permission 是我们配置在视图类上权限类的对象
for permission in self.get_permissions():
# 权限类的对象,执行has_permission,这就是为什么我们写的权限类要重写has_permission方法
# self 是视图类的对象,就是咱们自己的的权限类的has_permission的view参数(具体的解释看下面)
if not permission.has_permission(request, self):
# has_permission方法的返回值是True或是False,如果return 的是False,就会走这里,走这里的结果是没有权限的情况
# 如果配了多个权限类,第一个没过,直接不会再执行下一个权限类了
self.permission_denied(
request,
message=getattr(permission, 'message', None),
code=getattr(permission, 'code', None)
)
'在check_permissions中主要是一个for循环,而for循环的对象是get_permissions方法得到的结果,因此我们进来看看他是起了啥作用' # APIView的274行左右 get_permissions
def get_permissions(self):
# self.permission_classes 是咱们配置在视图类上的列表,里面是一个个的权限类,没加括号就是没调用,他这里就相当于把这些权限类都加括号执行,把最后获取到的对象放到列表中返回出来
# permission_classes = [CommonPermission]
# [CommonPermission(),] 本质返回了权限类的对象,放到列表中
return [permission() for permission in self.permission_classes] '分析完之后回到上面' '我们编写的权限类'
# 写权限类,写一个类,继承基类BasePermission,重写has_permission方法,在方法中实现权限认证,如果有权限return True ,如果没有权限,返回False
from rest_framework.permissions import BasePermission class CommonPermission(BasePermission):
def has_permission(self, request, view):
# 实现权限的控制 ---》知道当前登录用户是谁?当前登录用户是 request.user
if request.user.user_type == 1:
return True
else:
# 没有权限,向对象中放一个属性 message
# 如果表模型中,使用了choice,就可以通过 get_字段名_display() 拿到choice对应的中文
self.message = '您是【%s】,您没有权限' % request.user.get_user_type_display()
return False '我们在这里重新定义了has_permission方法,在上面的源码中调用了这个方法,但是他的参数中,第二个参数是self,从我们定义的参数来看,我们可以知道这个self绑定给了view' '看完之后还是回到上面'

总结:

  • APIView中的dispatch方法,dispatch方法内部执行了initial方法进行认证权限频率这三大认证,initial方法的倒数第二行执行了权限校验---》self.check_permissions(request)

  • 里面取出配置在视图类上的权限类,实例化得到对象,一个个执行对象的has_permission方法,如果返回False,就直接结束,不再继续往下执行,权限就认证通过

  • 如果视图类上不配置权限类:permission_classes = [CommonPermission],会使用配置文件的api_settings.DEFAULT_PERMISSION_CLASSES

    使用顺序是优先使用视图类中的局部配置,再优先使用项目配置文件中的配置,其次使用drf内置配置文件中的配置

2.2 认证源码分析

前面部分跟权限类的分析流程一样,不做详细解释了,到执行initial方法处为止。

# 之前读过:drf的apiview,在执行视图类的方法之前,执行了3大认证----》dispatch方法中的
-497行左右, self.initial(request, *args, **kwargs)---》执行3大认证 # APIView类的399行左右:
def initial(self, request, *args, **kwargs):
# 能够解析的编码,版本控制(知道有这作用就好了,不用管,重点是下面部分)
self.format_kwarg = self.get_format_suffix(**kwargs)
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme # 认证组件的执行位置【读它】
self.perform_authentication(request)
# 权限组件
self.check_permissions(request)
# 频率组件
self.check_throttles(request) '我们进入认证组件的源码进行分析'
# APIView的316行左右
def perform_authentication(self, request):
request.user #咱们觉得它是个属性,其实它是个方法,包装成了数据属性 # Request类的user方法(用了APIView或是GenericAPIView后,在dispatch中重新定义了request,用Request产生的对象包装了一些,可以看第三天的博客) Request的源码的219行左右
@property
def user(self):
if not hasattr(self, '_user'):
'咋们先看这里没有找到_user属性时执行的代码,可以看到执行了_authenticate方法,而当前的对象是Request类产生的,因此我们要去Request类中查找这个方法'
with wrap_attributeerrors():
self._authenticate()
return self._user # self 是Request的对象,找Request类的self._authenticate() 373 行
def _authenticate(self):
# self.authenticators 跟我们在权限类的源码分析中见到的形式不一样,但是我们可以猜测两者的作用应该是相似的,就是我们配置在视图类上认证类的一个个对象,放到列表中
# 点进源码我们发现他是在Request类初始化的时候,传入的(看下面)
for authenticator in self.authenticators:
try:
# 而异常捕获内的代码也跟之前一样,是用于获取认证类中的重写的方法的结果返回了两个值。
# 他有两种返回结果,第一种结果时返回两个值,第一个值是当前登录用户,第二个的token,只走这一个认证类,后面的不再走了
# 第二种结果是可以返回None,会继续执行下一个认证类
# 同时我们也可以看到,如果报错了就会直接停止运行,有返回值的时候会把值获取,然后返回出去
user_auth_tuple = authenticator.authenticate(self)
except exceptions.APIException:
self._not_authenticated()
raise if user_auth_tuple is not None:
self._authenticator = authenticator
# 解压赋值(当我们在获取到认证类校验之后的结果后,会到下方的代码这里进行解压赋值):
#self.user=当前登录用户,self是当次请求的新的Request的对象
#self.auth=token
self.user, self.auth = user_auth_tuple
return self._not_authenticated() # self.authenticators 去Request类的init中找 152行左右
def __init__(self, request, parsers=None, authenticators=None,
negotiator=None, parser_context=None):
.....
self.authenticators = authenticators or ()
..... # 什么时候调用Reqeust的__init__?---》APIVIew的dispatch上面的492行的:request = self.initialize_request(request, *args, **kwargs)-----》385行----》
def initialize_request(self, request, *args, **kwargs):
return Request(
request,
parsers=self.get_parsers(),
authenticators=self.get_authenticators(),
negotiator=self.get_content_negotiator(),
parser_context=parser_context
)
'我们可以看到就是在给老的request进行包装的时候传入的这个参数'
'接着我们点进get_authenticators方法中查看源码'
def get_authenticators(self):
return [auth() for auth in self.authentication_classes]
'这里我们可以看到,跟权限类的源码中相似的部分出现了,就是把视图类中使用的认证类进行加括号然后存放到列表中返回'
'回到上面' # 总结:
1 配置在视图类上的认证类,会在执行视图类方法之前执行,在权限认证之前执行
2 自己写的认证类,可以返回两个值或None
3 后续可以从request.user 取出当前登录用户(前提是你要在认证类中返回)

2.3 频率源码分析

前面部分跟权限类的分析流程一样,不做详细解释了,到执行initial方法处为止。

# 之前读过:drf的apiview,在执行视图类的方法之前,执行了3大认证----》dispatch方法中的
-497行左右, self.initial(request, *args, **kwargs)---》执行3大认证 # APIView类的399行左右:
def initial(self, request, *args, **kwargs):
# 能够解析的编码,版本控制(知道有这作用就好了,不用管,重点是下面部分)
self.format_kwarg = self.get_format_suffix(**kwargs)
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme # 认证组件的执行位置
self.perform_authentication(request)
# 权限组件
self.check_permissions(request)
# 频率组件【读它】
self.check_throttles(request) # APIView 的352行
def check_throttles(self, request):
throttle_durations = []
#self.get_throttles() 这个方法我们也不能一下看出他是什么作用的,但是在这个位置,必然跟前面两个的源码中的代码的作用是类似的,因此我们也要点进源码进行查看(见下方)
# 配置在视图类上的频率类的对象,放到列表中 # 每次取出一个频率类的对象,执行allow_request方法,这里allow_request方法是存在的,因为我们继承的是SimpleRateThrottle,他内部已经写了一格allow_request方法。如果我们继承BaseThrottle可以在源码中发现allow_request方法是需要我们自行重写的,不然会报错
# 我们回顾频率类中我们自行重写的方法并没有对频率进行校验,因此我们也可以在源码中看出是allow_request方法进行了校验。
# allow_request方法的返回值如果是False,访问频率超过了限制,不能再走了
# allow_request方法的返回值如果是True,没有超出访问频率限制,可以直接往后
for throttle in self.get_throttles():
if not throttle.allow_request(request, self):
throttle_durations.append(throttle.wait())
'这里的wait()方法和后面的代码的作用可以看接下去的自定义频率类进行对比理解,不展开具体分析了'
if throttle_durations:
# Filter out `None` values which may happen in case of config / rate
# changes, see #1438
durations = [
duration for duration in throttle_durations
if duration is not None
] duration = max(durations, default=None)
self.throttled(request, duration) '在get_throttles这个方法中我们可以看到他确实是跟之前一样的作用,把频率类加上括号然后套在列表中返回出来' def get_throttles(self):
return [throttle() for throttle in self.throttle_classes]
'返回上面' 'allow_request方法(讲解不详细,后文有详细的分析)'
def allow_request(self, request, view):
if self.rate is None:
return True
'这里的两个属性通过英文意思我猜测就是检测是否配置了频率要求'
self.key = self.get_cache_key(request, view)
if self.key is None:
return True self.history = self.cache.get(self.key, [])
self.now = self.timer()
'通过自定义频率类的学习,我们可以得知这里的字典是存储频率的检测的时间和计算频率的参数'
# Drop any requests from the history which have now passed the
# throttle duration
while self.history and self.history[-1] <= self.now - self.duration:
self.history.pop()
if len(self.history) >= self.num_requests:
return self.throttle_failure()
return self.throttle_success()
'这里就是根据时间范畴对字典中的数据值进行检测,并返回检测结果(布尔值)'
# 总结:
-我们写的频率类:继承BaseThrottle,重写allow_request,在内部判断,如果超频了,就返回False,如果没超频率,就返回True

2.4 自定义频率类(了解)

这里咋们了解逻辑,会用代码即可

class SuperThrottle(BaseThrottle):
VISIT_RECORD = {}
'从下面的逻辑中我们发现需要有一个访问字典存储ip和被访问的时间,因此需要在这里定义'
def __init__(self):
self.history = None
'这里的history属性同样是因为下方的代码中需要用到才定义的'
def allow_request(self, request, view):
# 自己写逻辑,判断是否超频
# (1)取出访问者ip
# (2)判断当前ip不在访问字典里,添加进去,并且直接返回True,表示第一次访问,在字典里,继续往下走 {ip地址:[时间1,时间2,时间3,时间4]}
# (3)循环判断当前ip的列表,有值,并且当前时间减去列表的最后一个时间大于60s,把这种数据pop掉,这样列表中只有60s以内的访问时间,
# (4)判断,当列表小于3,说明一分钟以内访问不足三次,把当前时间插入到列表第一个位置,返回True,顺利通过
# (5)当大于等于3,说明一分钟内访问超过三次,返回False验证失败
# (1)取出访问者ip
ip = request.META.get('REMOTE_ADDR')
import time
ctime = time.time()
# (2)判断当前ip不在访问字典里,添加进去,并且直接返回True,表示第一次访问
if ip not in self.VISIT_RECORD:
self.VISIT_RECORD[ip] = [ctime, ]
return True
# self.history = [时间1]
self.history = self.VISIT_RECORD.get(ip,[])
# (3)循环判断当前ip的列表,有值,并且当前时间减去列表的最后一个时间大于60s,把这种数据pop掉,这样列表中只有60s以内的访问时间,
while self.history and ctime - self.history[-1] > 60:
self.history.pop()
# (4)判断,当列表小于3,说明一分钟以内访问不足三次,把当前时间插入到列表第一个位置,返回True,顺利通过
# (5)当大于等于3,说明一分钟内访问超过三次,返回False验证失败
if len(self.history) < 3:
self.history.insert(0, ctime)
return True
else: return False
'当我们不写下方的wait方法时,我们会发现跟SimpleRateThrottle中编写的allow_request方法有点不同,没有那串英文展示还有多久时间可以继续访问,这里的wait方法就是用于展示那句话的'
def wait(self):
import time
ctime = time.time()
return 60 - (ctime - self.history[-1])

2.5 SimpleRateThrottle源码分析

# 写一个频率类,重写allow_request方法,在里面实现频率控制

# SimpleRateThrottle---》allow_request
def allow_request(self, request, view):
# 这里就是通过配置文件和scop取出 频率限制是多少,比如一分钟访问5此
if self.rate is None:
return True # 返回了ip,就以ip做限制
self.key = self.get_cache_key(request, view)
if self.key is None:
return True
# 下面的逻辑,跟咱们写的一样
self.history = self.cache.get(self.key, [])
self.now = self.timer()
while self.history and self.history[-1] <= self.now - self.duration:
self.history.pop()
if len(self.history) >= self.num_requests:
return self.throttle_failure()
return self.throttle_success() # SimpleRateThrottle的init方法
def __init__(self):
if not getattr(self, 'rate', None):
# self.rate= '5、h'
self.rate = self.get_rate() # 这行代码我们不能直接看懂他的意思,因此需要分析源码(接着看下方的代码)
# 5 36000
self.num_requests, self.duration = self.parse_rate(self.rate) '在SimpleRateThrottle的init方法中我们发现rate属性是用get_rate方法获取的'
# SimpleRateThrottle的get_rate() 方法
def get_rate(self):
'这一块代码就是判断我们有没有设置频率限制(scope属性),如果没有有会报错'
if not getattr(self, 'scope', None):
msg = ("You must set either `.scope` or `.rate` for '%s' throttle" %
self.__class__.__name__)
raise ImproperlyConfigured(msg) try:
# self.scope 是 lqz 字符串
# 而scope的相关配置信息是需要我们在配置文件中自行设置的
# 源码就是把他绑定到配置文件中的存有配置信息的字典上:THROTTLE_RATES = api_settings.DEFAULT_THROTTLE_RATES
# return '5/h'
return self.THROTTLE_RATES[self.scope]
except KeyError:
msg = "No default throttle rate set for '%s' scope" % self.scope
raise ImproperlyConfigured(msg)
'看完之后回到上面的双下init先' # SimpleRateThrottle的parse_rate 方法
def parse_rate(self, rate):
# 这里举例rate的值是'5/h' # 这里的if是用于处理有scope配置的时候,但配置内容为空的时候的处理方式
if rate is None:
return (None, None)
# num =5
# period= 'hour'
# 这里很明显就是进行字符串切割
num, period = rate.split('/')
# num_requests=5
# 这里就是获取频率中的访问次数限制
num_requests = int(num)
# 这里就是对频率的时间限制,因为他是用首个字符来进行时分秒的区分的,所以我们之前学习频率组件的时候说时分秒的字符开头对就可以,后面随便写
duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
# (5,36000),这里就是返回频率限制的具体内容了
return (num_requests, duration)

三、基于APIView编写分页

分页功能,只有查询所有才有

ps:编写的时候可以参照ListAPIView中继承的那个list方法进行参考

    def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset()) page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

基于APIView编写的list

class BookView(ViewSetMixin, APIView):
'这里的方法名称必须是list,因为路由在自动自动创建的时候映射也是自动配置的'
def list(self, request):
books = Book.objects.all()
# 使用步骤
# 1 实例化得到一个分页类的对象
paginator = CommonLimitOffsetPagination()
# 2 调用分页类对象的paginate_queryset方法来完成分页(因为源码写了三个参数我们也写三个参数,最后一个参数是view,又因为我们就是在视图配中编写,所以写self就好了),返回的page是 要序列化的数据,分页好的
page = paginator.paginate_queryset(books, request, self)
if page is not None:
serializer = BookSerializer(instance=page, many=True)
# 我们可以发现源码中显示调用了get_serializer方法,这就是调用序列化类,因此这里我们也可以直接调用序列化类 # 3 返回数据,调用paginator的get_paginated_response方法
# return paginator.get_paginated_response(serializer.data)
# 通过查看他的源码我们发现改用Response返回数据的原理(看下方源码和分析)
return Response({
'total': paginator.count,
'next': paginator.get_next_link(),
'previous': paginator.get_previous_link(),
'results': serializer.data
})
'这里的字典中的键,可以自定义命名,一旦我们更改了这些名字,在postman中接收到的数据的键的名称也会更改'

第二步中需要研究的源码

page = self.paginate_queryset(queryset)

'这里的这个paginate_queryset方法是需要去GenericAPIView中查找的,代码如下:'

    @property
def paginator(self):
"""
The paginator instance associated with the view, or `None`.
"""
if not hasattr(self, '_paginator'):
if self.pagination_class is None:
self._paginator = None
else:
self._paginator = self.pagination_class()
return self._paginator '我们不难发现这个被伪装的paginator其实就是产生一个分页类的对象' def paginate_queryset(self, queryset):
if self.paginator is None:
return None
return self.paginator.paginate_queryset(queryset, self.request, view=self) '在最后我们看到了一个熟悉的单词paginate_queryset,这里就相当于是使用分页类产生的对象传入数据进行校验'

第三步中需要研究的源码

    def get_paginated_response(self, data):
assert self.paginator is not None
return self.paginator.get_paginated_response(data)
'这里我们可以看到这里跟第二步其实差不多,是获取序列化类产生的对象,然后返回相应的结果,我们通过之前的学习得知返回的结果肯定是Response封装的,因此肯定就是get_paginated_response方法中进行了封装' def get_paginated_response(self, data):
return Response(OrderedDict([
('count', self.page.paginator.count),
('next', self.get_next_link()),
('previous', self.get_previous_link()),
('results', data)
]))
'然后我们在视图类中写了这个方法之后,点进来会看到上方的这个源码,他就是封装了返回的数据的函数,因此我们可以直接给他进行替换,然后对格式进行自定义'

四、全局异常处理

# APIView的dispatch方法中运行了三大认证,然后运行了视图类的方法,如果出了异常,会被异常捕获,捕获后统一处理
def dispatch(self, request, *args, **kwargs):
try:
self.initial(request, *args, **kwargs)
# Get the appropriate handler method
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(),
self.http_method_not_allowed)
else:
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 # drf 内置了一个函数,只要上面过程出了异常,就会执行这个函数,这个函数只处理的drf的异常
-主动抛的非drf异常
-程序出错了
都不会被处理
我们的目标,无论主动抛还是程序运行出错,都同意返回规定格式--》能记录日志
公司里一般返回 {code:999,'msg':'系统错误,请联系系统管理员'} # 写一个函数,内部处理异常,在配置文件中配置一下即可 '第一步:首先在drf的配置文件中有一个配置信息如下(这个后面再配置,先挑出来讲而已)'
# Exception handling
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
'这个配置就是处理drf内部异常的配置文件,如果我们想要进行自定义,就需要在dango的配置文件中自行注册进行替换' '第二步:我们查看dispatch中的异常捕获,发现handle_exception处理了这些异常捕获的信息,而他的参数就是错误信息' response = self.handle_exception(exc) '第三步:我们进入他的源码发现他返回的信息是response,并且这个response是由exception_handler方法获得的' def handle_exception(self, exc):
if isinstance(exc, (exceptions.NotAuthenticated,
exceptions.AuthenticationFailed)):
# WWW-Authenticate header for 401 responses, else coerce to 403
auth_header = self.get_authenticate_header(self.request) if auth_header:
exc.auth_header = auth_header
else:
exc.status_code = status.HTTP_403_FORBIDDEN exception_handler = self.get_exception_handler() context = self.get_exception_handler_context()
response = exception_handler(exc, context) if response is None:
self.raise_uncaught_exception(exc) response.exception = True
return response '第四步:我们可以看到他则是来自上面的一行代码' exception_handler = self.get_exception_handler() '第五步:进入这个get_exception_handler方法的源码,我们可以发现他就是拿了配置文件中的配置返回出去' def get_exception_handler(self):
return self.settings.EXCEPTION_HANDLER '而配置信息中对应的是一个函数,因此exception_handler的值就相当于是获取这个函数的结果' def exception_handler(exc, context):
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 '研究了源码之后我们就开始自己编写,因为原本的异常处理函数有两个参数,因此我们也需要上两个参数,而它的源码太多了,我们可以跟面向对象的派生方法一样,在我们自行定义的函数内调用原本的函数,在此基础上进行自定义' def common_exception_handler(exc, context):
# exc 错误对象
# context:上下文,有view:当前出错的视图类的对象,args和kwargs视图类方法分组出来的参数,request:当次请求的request对象
# 只要走到这里,就要记录日志 ,只有错了,才会执行这个函数
# 记录日志尽量详细
print('时间,登录用户id,用户ip,请求方式,请求地址,执行的视图类,错误原因')
res = exception_handler(exc, context)
if res: # 通过观察原来的函数我们发现有值的时候,说明返回了Response 对象,没有值说明返回None
# 如果是Response 对象说明是drf的异常,已经被处理了,如果是None表明没有处理,就是非drf的异常
res = Response(data={'code': 888, 'msg': res.data.get('detail', '请联系系统管理员')}) # 这里就是自定义返回的信息的格式,处理的就是原本的drf中的报错
else:
# res = Response(data={'code': 999, 'msg': str(exc)})
# 记录日志
res = Response(data={'code': 999, 'msg': '系统错误,请联系系统管理员'})
# 这里就是处理非drf报错的代码,如果想看具体报错可以把msg的值换成参数中的exc接收的错误信息 return res # 在配置文件中配置
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'app01.exceptions.common_exception_handler',
}

五、作业

# 1 整理认证,频率,权限源码 执行流程
# 2 自定义频率类,完成一分钟访问5次控制
# 3 整理SimpleRateThrottle源码执行流程
# 4 基于APIView编写分页
# 5 编写全局异常处理,后端报错,返回固定格式
-详细打印错误原因
print('时间,登录用户id(如果没登录就是匿名用户),用户ip,请求方式,请求地址,执行的视图类,错误原因')

最新文章

  1. 深入理解 Java G1 垃圾收集器--转
  2. BigInteger在Java8中的改进
  3. android 多布局
  4. 【转载】C++——CString用法大全
  5. Javascript学习笔记(一)
  6. [ffmpeg 扩展第三方库编译系列] 关于需要用到cmake 创建 mingw32编译环境问题
  7. jsp servelet
  8. Using YARN with Cgroups testing in sparkml cluster
  9. 深入理解Node系列-细说Connect(上)
  10. sleep() 和 wait() 有什么区别?
  11. LightOJ1282 Leading and Trailing
  12. nyoj 复杂度
  13. busybox(三)最小根文件系统
  14. word2vec概述
  15. MYSQL 比较集
  16. 日志打入kafka改造历程-我们到底能走多远系列49
  17. 洛谷P1144最短路计数题解
  18. 20165234 《Java程序设计》第四周学习总结
  19. 『计算机视觉』Mask-RCNN_推断网络终篇:使用detect方法进行推断
  20. 商品和订单中使用MQ

热门文章

  1. 基于docker和cri-dockerd部署kubernetes v1.25.3
  2. VBA---文件操作
  3. mybatis一对多根据条件查询的查条数
  4. yum的$releaser与$basearch
  5. 基于.NET 7 的 WebTransport 实现双向通信
  6. C温故补缺(四):GDB
  7. 多点DMALL &#215; Apache Kyuubi:构建统一SQL Proxy探索实践
  8. ThinkPHP6.0在phpstorm添加查询构造器和模型的代码提示
  9. day21 单列索引与组合索引 &amp; 索引的优点和使用原则 &amp; 视图与函数
  10. day 19 分组查询 &amp; having和where区别