前言

在给 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,有三个原因:

  1. 纯粹是带着猎奇的心态想了解下这么简单的功能人家还能玩出花样来。
  2. 官方推荐 的 Sentry 最后还是会通过 MethodChannel 方式给到对端原生来报这种天生太依赖对端的行为我不太认同我想找一个纯 Dart 实现的库提高异常监控的可移植性。
  3. Catcher 简单读起来可以提高自信心。

Catcher 简介

我的理解 Catcher 有如下特征:

  1. 针对 Flutter 侧异常收集的一个纯 Dart 库,天然支持各种平台包括对 Web 侧的支持。
  2. 支持异常 UI 自定义显示及扩展,默认支持对话框,终端,或者页面形式等。
  3. 支持自定义异常的上报策略,默认支持异常到文件上传到网络,Sentry 等。
  4. 流程清晰简单。

中文介绍详见[译] 使用 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);
}
  1. 通过 CatcherOptions 创建两个配置,一个 debug,一个 release。
  2. 将配置设置到 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 流程图。

如上整个流程:

  1. 应用运行过程中产生了 Error,这些 Error 被 Catcher 捕捉到构造成新的对象 Report。
  2. Report 被发送给了 Reporter,Reporter 会决定对 Report 的处理策略:取消还是接受。
  3. 如果接受 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

优点

  1. 整个流程连贯清晰,reportMode 和 reportHandler,CacherOptions 三个关键对象符合开闭原则,扩展性强。
  2. CatcherOptions 中的字段设计精细,考虑到了不同需求场景,比如支持指定异常的 Handler 处理,支持忽略某些指定异常,支持增加异常日志添加额外信息,支持屏蔽掉设备信息中敏感字段,感觉作者考虑得好细。
  3. 支持异常存储到文件和上传到网络,支持传输到其他知名 flutter 后台,如 Sentry 等。

缺点

  1. 异常处理和上传过程在 main 线程中,对处理和上报操作都做了时间间隔限制进行去重和丢弃处理。是否可以将其放到子线程中。
  2. 超时处理的 report 未序列化到数据库中,以备后续上传,上传都是一次性的。
  3. Report 包装过程太固定无法自定义,比如我需要自定义设备信息的获取过程这样就需要修改源码了。
  4. 没有考虑 Flutter engine 和 Native 异常的扩展处理情况,虽然他们不属于 Flutter Error 的范围。

欢迎搜索公众号:【码里特别有禅】 里面整理收集了最详细的Flutter进阶与优化指南。关注我,获取我的最新文章~

参考链接

Report errors to a service | Flutter

jhomlala/catcher: Flutter error catching & handling plugin. Handles and reports exceptions in your app!

[译] 使用 Catcher 处理 Flutter 错误 - 掘金

本文由mdnice多平台发布

Flutter异常监控 - 贰 | 框架Catcher原理分析的更多相关文章

  1. Shiro框架 (原理分析与简单实现)

    Shiro框架(原理分析与简单实现) 有兴趣的同学也可以阅读我之前分享的:Java权限管理(授权与认证)CRM权限管理   (PS : 这篇博客里面的实现方式没有使用框架,完全是手写的授权与认证,可以 ...

  2. 黑马程序员—创建JDBC框架及原理分析

    对于Java数据库的连接,由最初学习的每次全部手工代码,到后面的不断利用知识简化代码量:这是不断学习的过程,就像人类由原始社会的钻木取火到当代的文明,都是一步步过来的! 本文不从最开始的JDBC入门开 ...

  3. MyBatis框架及原理分析

    MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架,其主要就完成2件事情: 封装JDBC操作 利用反射打通Java类与SQL语句之间的相互转换 MyBatis的主要设计目的就 ...

  4. 组件化框架设计之阿里巴巴开源路由框架——ARouter原理分析(一)

    阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680 背景 当项目的业务越来越复杂,业务线越来越多的时候,就需要按照业 ...

  5. 【源码分享】WPF漂亮界面框架实现原理分析及源码分享

    1 源码下载 2 OSGi.NET插件应用架构概述 3 漂亮界面框架原理概述 4 漂亮界面框架实现  4.1 主程序  4.2 主程序与插件的通讯   4.2.1 主程序获取插件注册的服务   4.2 ...

  6. java集合框架使用原理分析

    集合是我们日常编程中可能用的很多的技术之一 使用频率极高 可能平时就会知道怎么去用 但是集合之间的关系与不同之处都不是很清楚 对它们的底层原理更甚 所以写词文章 让自己有一个更深的认识 集合是一个庞大 ...

  7. spring框架IOC原理分析代码

    模拟ClasspathXmlApplication: package junit.test; import java.beans.Introspector; import java.beans.Pro ...

  8. PHP框架模板原理

           PHP框架现在是一种很流行的东西了,很多朋友开发应用与网站都会选择一个PHP框架或模板了,下面我们来看看PHP框架是如何实现的吧. 本文主要来聊聊框架理论,但不针对任何一款框架,不过任何 ...

  9. Junit 注解 类加载器 .动态代理 jdbc 连接池 DButils 事务 Arraylist Linklist hashset 异常 哈希表的数据结构,存储过程 Map Object String Stringbufere File类 文件过滤器_原理分析 flush方法和close方法 序列号冲突问题

    Junit 注解 3).其它注意事项: 1).@Test运行的方法,不能有形参: 2).@Test运行的方法,不能有返回值: 3).@Test运行的方法,不能是静态方法: 4).在一个类中,可以同时定 ...

  10. SpringMvc框架MockMvc单元测试注解及其原理分析

    来源:https://www.yoodb.com/ 首先简单介绍一下Spring,它是一个轻量级开源框架,简单的来说,Spring是一个分层的JavaSE/EEfull-stack(一站式) 轻量级开 ...

随机推荐

  1. 自主创建mybtis管理应用,用以横向管理数据源

    这个是我写的第一个随手小记,一晃眼做后端开发也有7年多了,现在也准备将一些杂七杂八的资料整理下.也算是回顾这7年中做的比较有意思的东西了. 这个需求是我17年做的,当时的应用场景是仓储库比较多,随时会 ...

  2. HTTPS实现原理分析

    概述 在上一节中介绍了两种加密方法 对称加密 非对称加密 其中对称加密性能高,但是有泄露密钥的风险,而非对称加密相反,加密性能较差,但是密钥不易泄露,那么能不能把他们进行一下结合呢? HTTPS采用混 ...

  3. SpringBoot 2.5.5整合轻量级的分布式日志标记追踪神器TLog

    TLog能解决什么痛点 随着微服务盛行,很多公司都把系统按照业务边界拆成了很多微服务,在排错查日志的时候.因为业务链路贯穿着很多微服务节点,导致定位某个请求的日志以及上下游业务的日志会变得有些困难. ...

  4. JUC(4)Callable和常用的辅助类

    1.Callable 1.可以有返回值 2.可以抛出异常 3.方法不同.run()/call() future Task 细节: 1.有缓存 2.结果可能需要等待,会阻塞 2.常用的辅助类 2.1 C ...

  5. 齐博x1如何取消禁止跨城市密码登录限制

    为安全起意见,只要用户绑定了手机,或者QQ登录,或者微信登录其中的一项,只要用户的IP所在城市变了,就会禁止密码登录.如下图所示 而必须选择绑定过的手机或QQ或微信其中一种方式登录.以避免密码被盗所带 ...

  6. Educational Codeforces Round 138 (Rated for Div. 2) A-E

    比赛链接 A 题解 知识点:贪心. 注意到 \(m\geq n\) 时,不存在某一行或列空着,于是不能移动. 而 \(m<n\) 时,一定存在,可以移动. 时间复杂度 \(O(1)\) 空间复杂 ...

  7. 复杂场景数据处理的 OLTP 与 OLAP 融合实践

    本文首发于 NebulaGraph 公众号 Dag Controller 介绍 Dag Controller 是 NebulaGraph 企业版的系统,经过反复测试无误后进行了发布,它主要解决的是 O ...

  8. LcdTools如何实现PX01设置不同的画面不同的背光亮度

    背光驱动分两种原理:恒压模式和恒流模式.恒压背光顾名思义提供恒定电压即可,这种屏正常来讲自带背光驱动电路,只需提供背光工作电压.背光使能和背光调光占空比控制.恒流背光指屏的背光只有纯灯串,需外部提供相 ...

  9. Codeforces 1672 E. notepad.exe

    题意 这是一道交互题,有n个字符串,每个字符串长度:0-2000, n :0-2000 有一个机器对他进行排版,你可以给他一个每行的最大宽度w,那么每行只能放长度为w的字符: 每行相邻两个字符串之间至 ...

  10. nrf52——DFU升级OTA升级方式详解(基于SDK开发例程)

    在我们开始前,默认你已经安装好了一些基础工具,如nrfutil,如果你没有安装过请根据官方中文博客去安装好这些基础工具,连接如下:Nordic nRF5 SDK开发环境搭建(nRF51/nRF52芯片 ...