Flutter异常监控 - 贰 | 框架Catcher原理分析
前言
在给 Flutter 应用做异常监控的时候,一开始我是拒绝滴,如果不考虑 Flutter Engine 和 native 侧的监控,用我另一篇文章中不得不知道的 Flutter 异常捕获知识点 提到的方法基本可以搞定所有 Dart 侧异常,关键代码也不多,复杂不到哪里去。如下(有不清楚原理的可以看下原文,这里就不赘叙了):
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
Zone.current.handleUncaughtError(details.exception, details.stack);//Tag1
//或customerReport(details);
};
//Tag2
Isolate.current.addErrorListener(
RawReceivePort((dynamic pair) async {
final isolateError = pair as List<dynamic>;
customerReport(details);
}).sendPort,
);
runZoned(
() => runApp(MyApp()),
zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
report(line)
},
),
onError: (Object obj, StackTrace stack) {
//Tag3
customerReport(e, stack);
}
);
}
为什么会找到 Catcher,有三个原因:
- 纯粹是带着猎奇的心态想了解下这么简单的功能人家还能玩出花样来。
- 官方推荐 的 Sentry 最后还是会通过 MethodChannel 方式给到对端原生来报这种天生太依赖对端的行为我不太认同我想找一个纯 Dart 实现的库提高异常监控的可移植性。
- Catcher 简单读起来可以提高自信心。
Catcher 简介
我的理解 Catcher 有如下特征:
- 针对 Flutter 侧异常收集的一个纯 Dart 库,天然支持各种平台包括对 Web 侧的支持。
- 支持异常 UI 自定义显示及扩展,默认支持对话框,终端,或者页面形式等。
- 支持自定义异常的上报策略,默认支持异常到文件上传到网络,Sentry 等。
- 流程清晰简单。
中文介绍详见[译] 使用 Catcher 处理 Flutter 错误 - 掘金,这里说下基本使用。
main() {
/// STEP 1. Create catcher configuration.
/// Debug configuration with dialog report mode and console handler. It will show dialog and once user accepts it, error will be shown /// in console.
CatcherOptions debugOptions =
CatcherOptions(DialogReportMode(), [ConsoleHandler()]);
/// Release configuration. Same as above, but once user accepts dialog, user will be prompted to send email with crash to support.
CatcherOptions releaseOptions = CatcherOptions(DialogReportMode(), [
EmailManualHandler(["support@email.com"])
]);
/// STEP 2. Pass your root widget (MyApp) along with Catcher configuration:
Catcher(rootWidget: MyApp(), debugConfig: debugOptions, releaseConfig: releaseOptions);
}
- 通过 CatcherOptions 创建两个配置,一个 debug,一个 release。
- 将配置设置到 Catcher 对象中即可完成异常上报和监控。
效果展示图:
如果设置了 ConsoleHandler , 日志输出如下:
I/flutter ( 7457): [2019-02-09 12:40:21.527271 | ConsoleHandler | INFO] ============================== CATCHER LOG ==============================
I/flutter ( 7457): [2019-02-09 12:40:21.527742 | ConsoleHandler | INFO] Crash occured on 2019-02-09 12:40:20.424286
I/flutter ( 7457): [2019-02-09 12:40:21.527827 | ConsoleHandler | INFO]
I/flutter ( 7457): [2019-02-09 12:40:21.527908 | ConsoleHandler | INFO] ------- DEVICE INFO -------
I/flutter ( 7457): [2019-02-09 12:40:21.528233 | ConsoleHandler | INFO] id: PSR1.180720.061
I/flutter ( 7457): [2019-02-09 12:40:21.528337 | ConsoleHandler | INFO] androidId: 726e4abc58dde277
I/flutter ( 7457): [2019-02-09 12:40:21.528431 | ConsoleHandler | INFO] board: goldfish_x86
I/flutter ( 7457): [2019-02-09 12:40:21.528512 | ConsoleHandler | INFO] bootloader: unknown
I/flutter ( 7457): [2019-02-09 12:40:21.528595 | ConsoleHandler | INFO] brand: google
I/flutter ( 7457): [2019-02-09 12:40:21.528694 | ConsoleHandler | INFO] device: generic_x86
I/flutter ( 7457): [2019-02-09 12:40:21.528774 | ConsoleHandler | INFO] display: sdk_gphone_x86-userdebug 9 PSR1.180720.061 5075414 dev-keys
I/flutter ( 7457): [2019-02-09 12:40:21.528855 | ConsoleHandler | INFO] fingerprint: google/sdk_gphone_x86/generic_x86:9/PSR1.180720.061/5075414:userdebug/dev-keys
I/flutter ( 7457): [2019-02-09 12:40:21.528939 | ConsoleHandler | INFO] hardware: ranchu
I/flutter ( 7457): [2019-02-09 12:40:21.529023 | ConsoleHandler | INFO] host: vped9.mtv.corp.google.com
I/flutter ( 7457): [2019-02-09 12:40:21.529813 | ConsoleHandler | INFO] isPsychicalDevice: false
I/flutter ( 7457): [2019-02-09 12:40:21.530178 | ConsoleHandler | INFO] manufacturer: Google
I/flutter ( 7457): [2019-02-09 12:40:21.530345 | ConsoleHandler | INFO] model: Android SDK built for x86
I/flutter ( 7457): [2019-02-09 12:40:21.530443 | ConsoleHandler | INFO] product: sdk_gphone_x86
I/flutter ( 7457): [2019-02-09 12:40:21.530610 | ConsoleHandler | INFO] tags: dev-keys
I/flutter ( 7457): [2019-02-09 12:40:21.530713 | ConsoleHandler | INFO] type: userdebug
I/flutter ( 7457): [2019-02-09 12:40:21.530825 | ConsoleHandler | INFO] versionBaseOs:
I/flutter ( 7457): [2019-02-09 12:40:21.530922 | ConsoleHandler | INFO] versionCodename: REL
I/flutter ( 7457): [2019-02-09 12:40:21.531074 | ConsoleHandler | INFO] versionIncremental: 5075414
I/flutter ( 7457): [2019-02-09 12:40:21.531573 | ConsoleHandler | INFO] versionPreviewSdk: 0
I/flutter ( 7457): [2019-02-09 12:40:21.531659 | ConsoleHandler | INFO] versionRelase: 9
I/flutter ( 7457): [2019-02-09 12:40:21.531740 | ConsoleHandler | INFO] versionSdk: 28
I/flutter ( 7457): [2019-02-09 12:40:21.531870 | ConsoleHandler | INFO] versionSecurityPatch: 2018-08-05
I/flutter ( 7457): [2019-02-09 12:40:21.532002 | ConsoleHandler | INFO]
I/flutter ( 7457): [2019-02-09 12:40:21.532078 | ConsoleHandler | INFO] ------- APP INFO -------
I/flutter ( 7457): [2019-02-09 12:40:21.532167 | ConsoleHandler | INFO] version: 1.0
I/flutter ( 7457): [2019-02-09 12:40:21.532250 | ConsoleHandler | INFO] appName: catcher_example
I/flutter ( 7457): [2019-02-09 12:40:21.532345 | ConsoleHandler | INFO] buildNumber: 1
I/flutter ( 7457): [2019-02-09 12:40:21.532426 | ConsoleHandler | INFO] packageName: com.jhomlala.catcherexample
I/flutter ( 7457): [2019-02-09 12:40:21.532667 | ConsoleHandler | INFO]
I/flutter ( 7457): [2019-02-09 12:40:21.532944 | ConsoleHandler | INFO] ---------- ERROR ----------
I/flutter ( 7457): [2019-02-09 12:40:21.533096 | ConsoleHandler | INFO] Test exception
I/flutter ( 7457): [2019-02-09 12:40:21.533179 | ConsoleHandler | INFO]
I/flutter ( 7457): [2019-02-09 12:40:21.533257 | ConsoleHandler | INFO] ------- STACK TRACE -------
I/flutter ( 7457): [2019-02-09 12:40:21.533695 | ConsoleHandler | INFO] #0 ChildWidget.generateError (package:catcher_example/file_example.dart:62:5)
I/flutter ( 7457): [2019-02-09 12:40:21.533799 | ConsoleHandler | INFO] <asynchronous suspension>
I/flutter ( 7457): [2019-02-09 12:40:21.533879 | ConsoleHandler | INFO] #1 ChildWidget.build.<anonymous closure> (package:catcher_example/file_example.dart:53:61)
I/flutter ( 7457): [2019-02-09 12:40:21.534149 | ConsoleHandler | INFO] #2 _InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:507:14)
I/flutter ( 7457): [2019-02-09 12:40:21.534230 | ConsoleHandler | INFO] #3 _InkResponseState.build.<anonymous closure> (package:flutter/src/material/ink_well.dart:562:30)
I/flutter ( 7457): [2019-02-09 12:40:21.534321 | ConsoleHandler | INFO] #4 GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:102:24)
I/flutter ( 7457): [2019-02-09 12:40:21.534419 | ConsoleHandler | INFO] #5 TapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:242:9)
I/flutter ( 7457): [2019-02-09 12:40:21.534524 | ConsoleHandler | INFO] #6 TapGestureRecognizer.handlePrimaryPointer (package:flutter/src/gestures/tap.dart:175:7)
I/flutter ( 7457): [2019-02-09 12:40:21.534608 | ConsoleHandler | INFO] #7 PrimaryPointerGestureRecognizer.handleEvent (package:flutter/src/gestures/recognizer.dart:315:9)
I/flutter ( 7457): [2019-02-09 12:40:21.534686 | ConsoleHandler | INFO] #8 PointerRouter._dispatch (package:flutter/src/gestures/pointer_router.dart:73:12)
I/flutter ( 7457): [2019-02-09 12:40:21.534765 | ConsoleHandler | INFO] #9 PointerRouter.route (package:flutter/src/gestures/pointer_router.dart:101:11)
I/flutter ( 7457): [2019-02-09 12:40:21.534843 | ConsoleHandler | INFO] #10 _WidgetsFlutterBinding&BindingBase&GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:180:19)
I/flutter ( 7457): [2019-02-09 12:40:21.534973 | ConsoleHandler | INFO] #11 _WidgetsFlutterBinding&BindingBase&GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:158:22)
I/flutter ( 7457): [2019-02-09 12:40:21.535052 | ConsoleHandler | INFO] #12 _WidgetsFlutterBinding&BindingBase&GestureBinding._handlePointerEvent (package:flutter/src/gestures/binding.dart:138:7)
I/flutter ( 7457): [2019-02-09 12:40:21.535136 | ConsoleHandler | INFO] #13 _WidgetsFlutterBinding&BindingBase&GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:101:7)
I/flutter ( 7457): [2019-02-09 12:40:21.535216 | ConsoleHandler | INFO] #14 _WidgetsFlutterBinding&BindingBase&GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:85:7)
I/flutter ( 7457): [2019-02-09 12:40:21.535600 | ConsoleHandler | INFO] #15 _rootRunUnary (dart:async/zone.dart:1136:13)
I/flutter ( 7457): [2019-02-09 12:40:21.535753 | ConsoleHandler | INFO] #16 _CustomZone.runUnary (dart:async/zone.dart:1029:19)
I/flutter ( 7457): [2019-02-09 12:40:21.536008 | ConsoleHandler | INFO] #17 _CustomZone.runUnaryGuarded (dart:async/zone.dart:931:7)
I/flutter ( 7457): [2019-02-09 12:40:21.536138 | ConsoleHandler | INFO] #18 _invoke1 (dart:ui/hooks.dart:170:10)
I/flutter ( 7457): [2019-02-09 12:40:21.536271 | ConsoleHandler | INFO] #19 _dispatchPointerDataPacket (dart:ui/hooks.dart:122:5)
I/flutter ( 7457): [2019-02-09 12:40:21.536375 | ConsoleHandler | INFO]
I/flutter ( 7457): [2019-02-09 12:40:21.536539 | ConsoleHandler | INFO] ======================================================================
Catcher 设计思路
Catcher 流程图。
如上整个流程:
- 应用运行过程中产生了 Error,这些 Error 被 Catcher 捕捉到构造成新的对象 Report。
- Report 被发送给了 Reporter,Reporter 会决定对 Report 的处理策略:取消还是接受。
- 如果接受 Report,那么 Report 会交给 handers 继续处理直至完成。
1. Catcher 异常捕获时机与 Report 构造
这里可以盲猜下,如上步骤 1 其实相当于前言中的个人基础版本代码,负责收集 Error 过程。看下 Catcher 收集 Error 的代码三个关键点分别如下,基本跟我们代码处理是一样的。
runZonedGuarded
Isolate.current.addErrorListener
FlutterError.onError
Report 构造
void _reportError(
dynamic error,
dynamic stackTrace, {
FlutterErrorDetails? errorDetails,
}) async {
//.....
final Report report = Report(
error,
stackTrace,
//额外添加字段如下:
DateTime.now(),
_deviceParameters,
_applicationParameters,
_currentConfig.customParameters,
errorDetails,
_getPlatformType(),
screenshot,
);
2. Reporter 接收和决策 Report
从上面步骤中我们知道,关心的 error 和 stackTrace 被包装到了 Report 中,我们主要关注 Report 流向即可跟踪主流程。这里说下为啥不直接处理 error 和 stackTrace 搞个包装类 Report。因为将异常保持到本地或者服务器后台中我们免不了要添加额外数据方便定位问题,比如机型信息,应用信息和平台等信息,能更加有效的还原 error 出现的场景。
看源码可以发现找不到一个叫做 Reporter 的对象,那么这个对象为啥要接收和决策 Report 呢?它想干嘛?Reporter 对象其实是 ReportMode 对象及其子类,ReportMode 是具有显示和决策 Report 对象的能力,接收 Report 就是为了显示,决策就是可以取消继续处理 Report 或者继续处理它。说白了就是一个给用户可查看异常的视图接口。
//这个类主要作用
//1. 呈现异常堆栈不同UI给用户操作:比如是以对话框,还是以页面,还是以通知栏,还是以终端日志
//2. 其他设置都是为显示1中UI服务的,比如当前UI是什么语言显示,当前UI出现是否需要上下文等。
abstract class ReportMode {
late ReportModeAction _reportModeAction;
LocalizationOptions? _localizationOptions;
// ignore: use_setters_to_change_properties
/// Set report mode action.
void setReportModeAction(ReportModeAction reportModeAction) {
_reportModeAction = reportModeAction;
}
/// Code which should be triggered if new error has been caught and core
/// creates report about this.
///该方法下就会实现对应的UI,如弹框就会在这里弹出来。
void requestAction(Report report, BuildContext? context);
/// On user has accepted report
///这个会被上述UI中类似”接收”的按钮统一调用
void onActionConfirmed(Report report) {
_reportModeAction.onActionConfirmed(report);
}
/// On user has rejected report
///这个会被上述UI中类似”取消”的按钮统一调用
void onActionRejected(Report report) {
_reportModeAction.onActionRejected(report);
}
/// Check if given report mode requires context to run
///当前模式下UI是否需要上下文支持。即Context
bool isContextRequired() {
return false;
}
///...
}
ReportMode 子类
从上面不难看出,为什么 Catcher 可以支持异常多种 UI 显示效果都是 ReportMode 的功劳,你可以扩展它让它实现你想要的样式。这里涉及一个常规是设计思想,抽象。 因为需求是呈现不一样的 UI,有对话框样式,有通知栏样式,还有页面样式,这几个样式里面相同的就是接收同样的 Report 数据,公共的接收和拒绝按钮。于是相同东西可以被抽到父类中,于是有了 requestAction,onActionConfirmed 和 onActionRejected 的行为。
认识上面 ReportMode 关键的 UI 接口,继续主流程:
void _reportError(
dynamic error,
dynamic stackTrace, {
FlutterErrorDetails? errorDetails,
}) async {
//...
final Report report = Report(
error,
stackTrace,
//....
);
//...
if (reportMode.isContextRequired()) {
if (_isContextValid()) {
reportMode.requestAction(report, _getContext());
} else {
_logger.warning(
"Couldn't use report mode because you didn't provide navigator key. Add navigator key to use this report mode.",
);
}
} else {
reportMode.requestAction(report, null);
}
}
上面 Report 构造完之后流向了 Reporter(也就是 ReportMode), 这里注意下 isContextRequired()和_isContextValid(), 这两个方法的作用:你在 UI 显示的时候是不是需要上下文呢,buildContext,比如 dialog 方式显示的时候,page 显示的时候,有才能显示出来。但是如果你不打算显示在 UI 上,只是显示在终端上,你就不需要 context 了,这就是 ReportMode 设计这两个方法的作用。
那么问题来了,这个 Context 到底如何设置的呢? 答案是通过 Catcher 中可选参数navigatorKey
其中流程比较简单可以自行查看源码。
如果用户设置了 DialogReportMode 之后,呈现出来的就是上面效果,用户点击 Cancel 就没后文了,点击 Accept 就会继续把当前 Report 流传下去。
来看看下一个接力对象。
3. ReportHandler:默默承受下所有的人
@override
void onActionConfirmed(Report report) {
///...
for (final ReportHandler handler in _currentConfig.handlers) {
_handleReport(report, handler);
}
}
void _handleReport(Report report, ReportHandler reportHandler) {
reportHandler
.handle(report, _getContext())
.catchError((dynamic handlerError) {
_logger.warning(
"Error occurred in ${reportHandler.toString()}: ${handlerError.toString()}",
);
}).then((result) {
}).timeout(
);
}
点击了步骤 2 中的接收,最后会到 Catcher 的 onActionConfirmed, 这里 Report 会被 CatcherOptions 中提供的 handlers 列表中每个元素依次处理。Catcher 会日志中打印出相关的处理结果和超时等。
/// Handlers that should be used
final List<ReportHandler> handlers;
/// Builds catcher options instance
CatcherOptions(
this.reportMode,
this.handlers, //...);
这里重点说下 ReportHandler 的设计跟哪个有关? 没错,就是你为所欲为的上报策略,你可以报给后台,也可以只是显示在控制台,也可以存储到文件。
/// 主要作用是用来处理report的,比如这个report是保持到文件还是上传到服务器,还是显示在终端。
abstract class ReportHandler {
///Logger instance
late CatcherLogger logger;
/// Method called when report has been accepted by user
///上报处理结果,比如上传到服务器或者保持到文件,成功会返回true,失败返回false
Future<bool> handle(Report error, BuildContext? context);
/// Get list of supported platforms
List<PlatformType> getSupportedPlatforms();
///Location settings
LocalizationOptions? _localizationOptions;
/// Get currently used localization options
LocalizationOptions get localizationOptions =>
_localizationOptions ?? LocalizationOptions.buildDefaultEnglishOptions();
// ignore: use_setters_to_change_properties
/// Set localization options (translations) to this report mode
void setLocalizationOptions(LocalizationOptions? localizationOptions) {
_localizationOptions = localizationOptions;
}
/// Check if given report mode requires context to run
bool isContextRequired() {
return false;
}
/// Check whether report mode should auto confirm without user confirmation.
bool shouldHandleWhenRejected() {
return false;
}
}
ReportHander 子类
很容易看到,我们可以支持上报 Report 到哪里,你甚至可以通过 SentryHandler 报到 Sentry 后台,通过 HttpHandler 报到自己家后台。从 ReportHandler 定义知道,其实这些上报策略的关键点就在 Future handle(Report error, BuildContext? context) 的不同实现。无非就是对 Report error 参数的一个转换过程不同而已,你想报到 Sentry 就直接把我们的 error 转换成 Sentry Sdk 支持的实体类格式,你想把 Error 报到自己后台就转换成自己后台支持格式用 http 来 post。
总结
读完 Catcher 了解其中核心原理,可以回答前言中几个问题了,Catcher 代码实现确实简单,掰着手指你都知道 Catcher,Reportmode,ReportHander CatcherOption 其他类都可以干掉丝毫不影响整个框架正常运行。对 reportmode 和 reporthandler 的开闭原则设计上堪称无敌。
如果从工作量上来说的话前言里面的个人基础版本只能算完成了监控的 1/3 ,还有 2/3 的工作没做,只能算刚刚开始而已,所以有时候真的是你眼中的完美在大佬面前只是井底视野。。。
设计模式
继承和多态:Reportmode 和它的子类们,reportHandler 和它的子类们 都是通过多态来让程序更有弹性。
遇到的问题
上传到 Sentry 后发现堆栈不打印业务相关的行数。解决办法如下:
https://github.com/jhomlala/catcher/pull/225
优点
- 整个流程连贯清晰,reportMode 和 reportHandler,CacherOptions 三个关键对象符合开闭原则,扩展性强。
- CatcherOptions 中的字段设计精细,考虑到了不同需求场景,比如支持指定异常的 Handler 处理,支持忽略某些指定异常,支持增加异常日志添加额外信息,支持屏蔽掉设备信息中敏感字段,感觉作者考虑得好细。
- 支持异常存储到文件和上传到网络,支持传输到其他知名 flutter 后台,如 Sentry 等。
缺点
- 异常处理和上传过程在 main 线程中,对处理和上报操作都做了时间间隔限制进行去重和丢弃处理。是否可以将其放到子线程中。
- 超时处理的 report 未序列化到数据库中,以备后续上传,上传都是一次性的。
- Report 包装过程太固定无法自定义,比如我需要自定义设备信息的获取过程这样就需要修改源码了。
- 没有考虑 Flutter engine 和 Native 异常的扩展处理情况,虽然他们不属于 Flutter Error 的范围。
欢迎搜索公众号:【码里特别有禅】 里面整理收集了最详细的Flutter进阶与优化指南。关注我,获取我的最新文章~
参考链接
Report errors to a service | Flutter
[译] 使用 Catcher 处理 Flutter 错误 - 掘金
本文由mdnice多平台发布
Flutter异常监控 - 贰 | 框架Catcher原理分析的更多相关文章
- Shiro框架 (原理分析与简单实现)
Shiro框架(原理分析与简单实现) 有兴趣的同学也可以阅读我之前分享的:Java权限管理(授权与认证)CRM权限管理 (PS : 这篇博客里面的实现方式没有使用框架,完全是手写的授权与认证,可以 ...
- 黑马程序员—创建JDBC框架及原理分析
对于Java数据库的连接,由最初学习的每次全部手工代码,到后面的不断利用知识简化代码量:这是不断学习的过程,就像人类由原始社会的钻木取火到当代的文明,都是一步步过来的! 本文不从最开始的JDBC入门开 ...
- MyBatis框架及原理分析
MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架,其主要就完成2件事情: 封装JDBC操作 利用反射打通Java类与SQL语句之间的相互转换 MyBatis的主要设计目的就 ...
- 组件化框架设计之阿里巴巴开源路由框架——ARouter原理分析(一)
阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680 背景 当项目的业务越来越复杂,业务线越来越多的时候,就需要按照业 ...
- 【源码分享】WPF漂亮界面框架实现原理分析及源码分享
1 源码下载 2 OSGi.NET插件应用架构概述 3 漂亮界面框架原理概述 4 漂亮界面框架实现 4.1 主程序 4.2 主程序与插件的通讯 4.2.1 主程序获取插件注册的服务 4.2 ...
- java集合框架使用原理分析
集合是我们日常编程中可能用的很多的技术之一 使用频率极高 可能平时就会知道怎么去用 但是集合之间的关系与不同之处都不是很清楚 对它们的底层原理更甚 所以写词文章 让自己有一个更深的认识 集合是一个庞大 ...
- spring框架IOC原理分析代码
模拟ClasspathXmlApplication: package junit.test; import java.beans.Introspector; import java.beans.Pro ...
- PHP框架模板原理
PHP框架现在是一种很流行的东西了,很多朋友开发应用与网站都会选择一个PHP框架或模板了,下面我们来看看PHP框架是如何实现的吧. 本文主要来聊聊框架理论,但不针对任何一款框架,不过任何 ...
- Junit 注解 类加载器 .动态代理 jdbc 连接池 DButils 事务 Arraylist Linklist hashset 异常 哈希表的数据结构,存储过程 Map Object String Stringbufere File类 文件过滤器_原理分析 flush方法和close方法 序列号冲突问题
Junit 注解 3).其它注意事项: 1).@Test运行的方法,不能有形参: 2).@Test运行的方法,不能有返回值: 3).@Test运行的方法,不能是静态方法: 4).在一个类中,可以同时定 ...
- SpringMvc框架MockMvc单元测试注解及其原理分析
来源:https://www.yoodb.com/ 首先简单介绍一下Spring,它是一个轻量级开源框架,简单的来说,Spring是一个分层的JavaSE/EEfull-stack(一站式) 轻量级开 ...
随机推荐
- k8s 中的 service 如何找到绑定的 Pod 以及如何实现 Pod 负载均衡
k8s 中的 service 如何找到绑定的 Pod 以及如何实现 Pod 负载均衡 前言 endpoint kube-proxy userspace 模式 iptables ipvs kernels ...
- 基于QT和C++实现的翻金币游戏
基于QT和C++的翻金币游戏 声明: QT翻金币项目可以说是每个新学QT的同学都会去写的一个项目,网上的源码也很多,我也是最近刚开始学QT,所以也参考了很多前辈的代码自己重新敲了一遍代码. 游戏介绍: ...
- 18.MongDB系列之批量更新写入Python版
在实际的工作中,难免批量更新的数量极大,pymongo提供了便捷的客户端供使用 假设读者对pandas比较熟悉,下图为事先准备好的dataframe import pandas as pd from ...
- Vue学习之--------绑定样式、条件渲染、v-show和v-if的区别(2022/7/12)
文章目录 1.绑定样式 1.1 基础知识 1.2 代码实例 1.3 测试效果 2.条件渲染 2.1 基本知识 2.2 代码实例 2.3 测试效果 1.绑定样式 没啥好说的.我觉得还没直接引入外部写好的 ...
- Droplet——一款轻量的Golang应用层框架
Github地址 如标题所描述的,Droplet 是一个 轻量 的 中间层框架,何为中间层呢? 通常来说,我们的程序(注意这里我们仅仅讨论程序的范围,而非作为一个系统,因此这里不设计如 LB.Gate ...
- Linux的挖矿木马病毒清除(kswapd0进程)
1.top查看资源使用情况 看到这些进程一直在变化,但是,主要是由于kswapd0进程在作怪,占据了99%以上的CUP,查找资料后,发现它就是挖矿进程 2.排查kswapd0进程 执行命令netsta ...
- numpy常用知识点备忘
常用函数 a.max(axis=0) a.max(axis=1) a.argmax(axis=1) : 每列的最大值(在行方向找最大值).每行的最大值(在列方向找对大致).最大值的坐标 sum()求和 ...
- EBI、DDD及其演变架构史
一.引子 聊架构总离不开"领域驱动架构",大多能聊到DDD(Domain-Driven Design),实际上早期思想EBI架构 1992年就诞生了.核心价值点在于:关注核心业务领 ...
- 2022春每日一题:Day 23
题目:Piotr's Ants 蚂蚁转头走,其实可以看做他们交换灵魂后接着往前走,同样发现,既然他们的速度相同,那么在数轴上相对位置不会改变(碰面会改变方向),那就好办了. 先把初始状态排序,id都记 ...
- IDEA提交任务到spark standalone集群
参考文章: 在idea里面怎么远程提交spark任务到yarn集群 代码 注意setJars,提交的代码,要提前打好包.否则会报找不到类的错误 个人理解就相当于运行的main方法是起了一个spark- ...