[{"content":"前言 在阅读开源项目的源码时，我们经常会对当前代码的写法感到困惑。这时，我们需要查阅原始 issue 以了解更多相关的讨论。\n人工跳转 在不借助插件的情况下，当我们想要访问一个提交的 issue 地址。\n只能先打开对应项目任意一个 issue 页面，修改后面的 issue id 跳转过去，非常的繁琐。\n插件效果预览 如果我们借助一个插件，就能实现如下效果：\n通过提交记录，直接跳转到对应的 issue 地址，可以说是非常的方便了。\n插件配置 配置路径：\nSettings \u0026ndash;\u0026gt; Version Control \u0026ndash;\u0026gt; Issue Navigation\n点击加号，选择 Add Issue Navigation Link.\n来到配置页面\n第一个是匹配 Issue Id 的正则表达式, 比如提交记录中标注 Issue 的格式是 (#4272), 就写提取中间 4 位数字的正则。\n第二个是最终要跳转的 Issue 地址, $1 是 Issue Id 的占位符。以 RocketMQ 的 Issue 地址 https://github.com/apache/rocketmq/issues/4272 为例, 用 $1 替换掉后面的 Issue Id，我们需要输入 https://github.com/apache/rocketmq/issues/$1。\n后面的 Example 主要是用于测试了, 随便输入一个示例, 就能在下方看到拼接后的结果，可以复制跳转看看是否正确。\n配置好后，当我们再打开 Git 的提交记录时, 就能看到标注了 Issue Id 的部分可以点击跳转啦。\n剩下的就是享受阅读源码的乐趣了～～\n","date":"2024-08-21T22:31:32+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/looker-with-her-hair-up.webp","permalink":"https://opoa.top/post/idea-plugin-issue-navigation/","title":"谁懂这款 Idea 插件的含金量啊"},{"content":"前言 突然心血来潮，想试一下玩玩英雄联盟美服。\n主要原因有：\n学英语，全英文的游戏界面，还有机会和外国人交流（打着学习的幌子😏） 体验下美服的游戏环境（众所周知，国服的环境比较差） 游戏中常见的那些英文缩写 lol Laugh Out Loud\n大声地笑，表示觉得某事很好笑。\nlmao Laugh My Ass Off\n表面意思是笑掉我的屁股，表示非常地好笑。\nafk Away From Keyboard\n离开键盘，表示人暂时不在或者挂机。\ncs Creep Score\n补刀数，比如 40 cs 表示 40 刀。\nff Forfeit\nForfeit 的缩写，表示放弃，投降。\n15 ff，15 投。\ngg Good Game\n通常在游戏结束时，用来表示队友或对手打得好。\n不过在国服里，游戏还没到快结束时，如果有人打出 GG，表示他觉得这局已经输定了。\nthx Thanks\n谢谢的缩写，通常会搭配些文字表情，比如：thx :D\nwp Well Played\n打得漂亮，用于在队友打出精彩操作时，向队友表示认可和赞扬。\ngj Good Job\n称赞队友打得好。\nmb My Bad\n我的错，我的锅，表示歉意。\n常用短语 dive into the turret 越塔\n当队友有越塔的行为时，可以说 \u0026ldquo;dont dive into the turret guys\u0026rdquo;, 提醒队友别太激进。\n一些感受 模式 美服有个模式还挺好的，叫快速游戏。\n先自己选好两个英雄，分别在两个位置上，但是有一个要求，两个位置钟至少要有一个是优先级高的位置，我的理解是这些位置当前时间段选取的人较少，需要更多人进来，才能更快地开始游戏。\n比如上面这张图，我预选的位置里至少得有一个是打野或者辅助，才能进行匹配。\n选好之后，等着匹配就行了，匹配确认之后，就能直接使用你选的两个英雄之一，进入游戏。\n但是我专门看了国服，没有这个模式，不知道为啥阉割了。\n战利品 英雄啊皮肤啥的爆率，我都不想说，国服完全不能比。\n这些都是免费开出来的，感觉已经不想回国服了。\n环境 游戏环境是比国服好上不少，但也不代表没有吵架的情况。\n有，但是很少，我快打到 30 级了，遇到有吵架的对局应该不超过 5 把，也有那种两个人激情对喷的，可惜他们打字太快，我看不太懂。希望有一天能也能看懂他们的对喷，或者能参与也不错，说明我的英语水平挺可以的了哈哈哈。\n玩家 国人感觉不是很多，偶尔能遇见一两个，游戏名称是中文的，但也有可能因为很多国人不说话或者说话少。\n有遇到过那种游戏里一直点信号的，问他 \u0026ldquo;what do u want to say?\u0026quot;，又不回，最后快结束来一句 \u0026ldquo;tank gap\u0026rdquo;，挺搞笑的。\n延迟 可能是因为家里的网不是很快，买了加速器，延迟还是挺高，通常是 180ms ~ 220ms 左右，这个延迟，感知已经非常明显了，技能啥的都慢 1 秒。\n看后续有没有机会，升级下网络。\n学习笔记 2024 年 7 月 1 日 记录下这有趣的一局，前期下路有点优势，我中路劣势，这个泽拉斯一直 BB，点信号，喷队友，发起投降。后来看我死得最多，中路最惨，就一直逮着我骂，我都懒得理他。\n后来太烦了，给他一句 \u0026ldquo;can u shut up?\u0026quot;，直接把他屏蔽了。\n他还一直在发起投降，不管局势怎么样。\n后期我在下路河道带线，对面来抓我，赏金一个 TP 过来救我。非常 nice🤞🏼\n游戏结束后，我在结算界面说\n\u0026ldquo;Although you played well, you are totally a grumpy idiot.\u0026rdquo;\n提莫跟上（友情提示，千万不要查他的名字是什么意思）\n\u0026ldquo;facts\u0026rdquo;\n\u0026ldquo;people like kids\u0026rdquo;\n至于这个泽拉斯的 id，是不是国服玩家，不作评判。\n经过这件事我总算是明白了\ngrumpy\nadj.\t脾气坏的；性情暴躁的\n","date":"2024-07-02T19:25:59+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/truth-dragon-yasuo.webp","permalink":"https://opoa.top/post/adventures-in-lol-na/","title":"【英雄联盟】美服历险记"},{"content":"简介 一款可以将应用打包成 Windows 服务的开源工具。\nwinsw public A wrapper executable that can run any executable as a Windows service, in a permissive license. C# 使用流程 以运行 Java 应用举例\n下载软件和准备配置文件 把他们的命名修改统一，exe 自动加载同名配置文件\nXml 配置 \u0026lt;service\u0026gt; \u0026lt;!-- 服务 id --\u0026gt; \u0026lt;id\u0026gt;Api-Gateway\u0026lt;/id\u0026gt; \u0026lt;!-- 服务名称 --\u0026gt; \u0026lt;name\u0026gt;Api-Gateway\u0026lt;/name\u0026gt; \u0026lt;!-- 服务描述 --\u0026gt; \u0026lt;description\u0026gt;Api 网关应用 \u0026lt;/description\u0026gt; \u0026lt;!-- 启动模式 --\u0026gt; \u0026lt;startmode\u0026gt;Automatic\u0026lt;/startmode\u0026gt; \u0026lt;!-- 可执行命令 --\u0026gt; \u0026lt;executable\u0026gt;java\u0026lt;/executable\u0026gt; \u0026lt;!-- 命令参数 --\u0026gt; \u0026lt;arguments\u0026gt;-Xms256m -Xmx256m -Dspring.profiles.active=dev -Dfile.encoding=UTF-8 -jar api-gateway.jar\u0026lt;/arguments\u0026gt; \u0026lt;!-- 日志路径 --\u0026gt; \u0026lt;logpath\u0026gt;logs\u0026lt;/logpath\u0026gt; \u0026lt;!-- 日志模式 --\u0026gt; \u0026lt;log mode=\u0026#34;roll\u0026#34;\u0026gt;\u0026lt;/log\u0026gt; \u0026lt;/service\u0026gt; 安装启动 以 管理员身份 打开命令窗口\n显示服务安装成功\n命令启动服务\nnet start api-gateway Windows 服务中查看\n应用日志查看 同目录下的 logs 文件夹下\n其他命令\n# 启动服务 net start xxx # 停止服务 net stop xxx # 查看所有服务 sc query # 查看指定服务 sc query xxx # 删除指定服务 sc delete xxx 其他实践 Nacos 打包成 Windows 服务 ","date":"2023-09-25T21:06:18+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/rem-loli-yukata.webp","permalink":"https://opoa.top/post/winsw-usage/","title":"WinSW 使用"},{"content":"前言 有同事反馈 ApiEs 集群的 fielddata 占用过高\n我的心情 be like：\nfielddata 是个啥\nElasticsearch 的 fielddata 是用于聚合和排序文本类型字段的机制。它将字段数据加载到内存中，提供更灵活的查询和聚合操作。但需要注意 fielddata 可能占用大量内存，严重时会导致整个集群不可用。\n排查过程 Day 1 先在 Es 中查询各个索引的 fielddata 占用情况\nindices: 查看集群中所有 index 的详细信息 GET _cat/indices?v\u0026amp;h=index,fielddata.memory_size\u0026amp;s=fielddata.memory_size:desc GET _cat/indices：获取索引相关的信息 ?v：verbose 返回详细的输出信息，包括表头信息 \u0026amp;h：header 指定返回结果中所需的字段信息 \u0026amp;s：sort 按特定字段对结果进行排序 也可使用简称\nGET _cat/indices?v\u0026amp;h=i,fm\u0026amp;s=fm:desc 结果：\n发现并没有特别异常的情况，考虑到可能是被清理了。\n联想到最近新上的查询计费索引的业务，可能是计费导出接口引起的。 于是现场提交了一个较大的导出任务，持续观察 fielddata 占用情况\n那再观察观察\nDay 2 早上一来，又出现了。必须得好好排查一下\n首先在官方文档里面寻找解决方案\n如何处理 fielddata 内存使用率高问题 - 阿里云帮助中心 从中可以看到一些常见的场景和关键信息\nElasticsearch 官方文档 中指出禁止对_id 进行聚合、排序和脚本操作 fielddata 会占用大量的堆内存，并且堆内存占用是永久的 根据文档一一排查\n\u0026hellip;\n获取占用 fielddata 内存占用高的字段，分析和业务的哪类查询相关。\nGET _cat/fielddata?v\u0026amp;s=size:desc 确认是使用了_id 进行聚合、排序和脚本操作引起的\n接下来是定位异常的语句\n对近 30 天 fielddata 的使用情况排查，发现两个明显的异常节点，7 月 11 日和 7 月 18 日，针对这两个时间段进行日志查询\n由于日志查询仅支持查询近 7 天的数据，只能选择 7 月 18 日的时间段\n占用陡增时间段在 00:50 至 01:10 之间\nuri: api_custom_biz_log 排查该段时间计费日志索引的查询语句\n发现了使用 _id 排序的语句\n精简后部分语句\n{ \u0026#34;size\u0026#34;: 1000, \u0026#34;query\u0026#34;: { \u0026#34;bool\u0026#34;: { \u0026#34;filter\u0026#34;: [ { \u0026#34;term\u0026#34;: { \u0026#34;api_id\u0026#34;: { \u0026#34;value\u0026#34;: \u0026#34;XXXXXX\u0026#34;, \u0026#34;boost\u0026#34;: 1.0 } } } ] } }, \u0026#34;sort\u0026#34;: [ { \u0026#34;_id\u0026#34;: { \u0026#34;order\u0026#34;: \u0026#34;asc\u0026#34; } } ] } 通过查询语句找到对应的项目和平台，定位到代码处，反馈给负责人进行修改。\n解决方案 _id 取自 request_id，字段中有保存并且是 keyword 类型\n将 _id 替换为 request_id\n总结 先从阿里云官方文档中寻找解决方案 借助性能监控，定位到异常发生的时间段，缩小排查范围 参考资料 一文带你彻底弄懂 ES 中的 doc_values 和 fielddata 白话 Elasticsearch51 - 深入聚合数据分析之 text field 聚合以及 fielddata 原理 ","date":"2023-07-20T13:40:21+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/raingirl.webp","permalink":"https://opoa.top/post/resolve-es-fielddata-issue/","title":"Es fielddata 占用过高问题排查"},{"content":" 超时异常，指一个操作未能在规定的时间内完成，抛出的异常。\n常见超时异常 Connection timeout 当一个客户端尝试连接到一个服务器时，等待服务器响应的时间超过了预设的最长时间，导致建立连接失败。通常是由于网络拥塞或服务器繁忙引起的。\nSocket timeout 当客户端和服务器建立连接后，一段时间内没接收到数据，则抛出该异常。\n比如设置超时 100ms，服务器向客户端发送数据，每个数据包的发送间隔不能超过 100ms，否则报错。\nRead timeout 当一个客户端从服务器端读取数据时，在一定时间内没有接收到任何数据，则会出现读取超时异常。 通常是由于网络连接中断、服务器响应慢或网络防火墙设置问题引起的。\n请求链路 以一个简单的请求举例，SLB 和 Gateway 的超时时间都是 60 秒（其实是 Gateway 向 SLB 看齐），Nginx 由于历史遗留原因超时为 300 秒，但整体还是 60 秒，不管后端设置的请求超时时间是多少，处理需要多久，都需要在 60 秒之内返回，否则网关会把该请求以超时处理。即：\n处理流程之 Read timeout 检查处理过程中是否调用了慢接口 请求工具是否设置了超时时间 如设置了超时时间，仍超时，需要检查超时时间是否生效 是否接口异常，且设置了过大的重试次数 如何设置超时时间及重试次数\n比如一个接口，平均耗时为 3s，3s 可以作为参考，建议手动请求 10 次或者更多，取耗时最长的一次再稍微向上取整，假设最长耗时 8s，则设置超时时间为 10s，已经能够覆盖这个接口大部分的响应耗时。10s 的情况能够重试 5 次，还在超时范围之内，重试 5 次都不能成功，则将异常返回给用户，让用户决定是否继续重试。\n","date":"2023-04-21T11:38:30+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/pretty-butterfly-outfit-red-eyes.webp","permalink":"https://opoa.top/post/guide-to-handling-timeout-exception/","title":"超时异常处理指南"},{"content":"自定义数据流 我们可以通过定义相应的事件（onNext、onError 和 onComplete）创建一个 Flux 或 Mono。Reactort 提供了 generate、create、push 和 handle 等方法，所有这些方法都使用 sink（池）来生成数据流。\nsink，顾名思义，就是池子，可以想象一下厨房水池的样子。如图所示：\nsink 通常至少会暴露三个方法给我们，next、error 和 complete。next 和 error 相当于两个下水口，我们不断将自定义的数据放到 next 口，Reactor 就会帮我们串成一个 publisher 数据流，直到有一个错误数据放到 error 口，或者按了一下 compelete 按钮，数据流就会终止了。\nFlux Create create 方法接受一个 FluxSink\u0026lt;T\u0026gt; 消费者，也就是说，你需要提供一个 FluxSink 的实例用来给下游的订阅者们发送 0 到 N 个元素。每个订阅者都会获得一个 FluxSink 的实例发送元素。\n这个例子用 Flux.create 创建了一个序列。\nFlux\u0026lt;Integer\u0026gt; integerFlux = Flux.create((FluxSink\u0026lt;Integer\u0026gt; fluxSink) -\u0026gt; { IntStream.range(0, 5) .peek(i -\u0026gt; System.out.println(\u0026#34;going to emit - \u0026#34; + i)) .forEach(fluxSink::next); }); 我们有两个下游的订阅者\n//First observer. takes 1 ms to process each element integerFlux.delayElements(Duration.ofMillis(1)).subscribe(i -\u0026gt; System.out.println(\u0026#34;First: \u0026#34; + i)); //Second observer. takes 2 ms to process each element integerFlux.delayElements(Duration.ofMillis(2)).subscribe(i -\u0026gt; System.out.println(\u0026#34;Second: \u0026#34; + i)); 输出：\ngoing to emit - 0 going to emit - 1 going to emit - 2 going to emit - 3 going to emit - 4 going to emit - 0 going to emit - 1 going to emit - 2 going to emit - 3 going to emit - 4 First: 0 Second: 0 First: 1 Second: 1 First: 2 Second: 2 First: 3 Second: 3 Second: 4 First: 4 通过输出我们可以发现\n每个订阅者都有自己的 FluxSink 实例，正如我们期望的创建了一个冷的发布者。 create 不会等待订阅者处理元素，它甚至可能在订阅者开始处理前就发送元素。 如果订阅者的处理速度跟不上怎么办？create 能够额外接受一个定义数据溢出时处理策略的参数，默认的策略是 buffer。\n具体能选择的策略有：\nBUFFER：缓存下游没来得及处理的元素（如果缓存不限大小可能导致 OOM）。 DROP：当下游没有准备好接收新的元素时丢弃这个元素。 ERROR：当下游来不及处理时抛出 IllegalStateException。 IGNORE：完全忽略下游的背压请求。 LATEST：下游只会获得上游最新的元素。 如果需要的话，我们也可以在 create 方法之外获取 FluxSink 实例的引用并且发送元素，它不是必须要发生在 create 方法内。\n一个简易的 FluxSink 消费者实现：\npublic class FluxSinkImpl implements Consumer\u0026lt;FluxSink\u0026lt;Integer\u0026gt;\u0026gt; { private FluxSink\u0026lt;Integer\u0026gt; fluxSink; @Override public void accept(FluxSink\u0026lt;Integer\u0026gt; integerFluxSink) { this.fluxSink = integerFluxSink; } public void publishEvent(int event) { this.fluxSink.next(event); } } 发送元素：\n// 创建一个 FluxSink 实例 FluxSinkImpl fluxSinkConsumer = new FluxSinkImpl(); //create 方法能够传入这个实例 Flux\u0026lt;Integer\u0026gt; integerFlux = Flux.create(fluxSinkConsumer) .delayElements(Duration.ofMillis(1)).share(); integerFlux.delayElements(Duration.ofMillis(1)).subscribe(i -\u0026gt; System.out.println(\u0026#34;First: \u0026#34; + i)); integerFlux.delayElements(Duration.ofMillis(2)).subscribe(i -\u0026gt; System.out.println(\u0026#34;Second: \u0026#34; + i)); // 在这里发送元素 IntStream.range(0, 5) .forEach(fluxSinkConsumer::publishEvent); 输出：\nSecond: 0 First: 0 Second: 1 First: 1 First: 2 Second: 2 First: 3 Second: 3 First: 4 Second: 4 Flux Generate generate 与 create 稍微有些不同，它接收一个 SynchronousSink\u0026lt;T\u0026gt; 的消费者，在上面的 create 方法，我们可以通过消费者发送 0 到 N 个元素，但是在 generate 方法，我们只能发送一个元素。\n是否意味着这个 flux 最多只能发送一个元素？\n并不是，generate 方法也能发送 无限数量的元素。generate 方法能够基于下游的请求一个接一个地发送元素。消费者自己不能循环地发送元素，如果订阅者对后续的元素不敢兴趣，那么 generate 也不会发送元素。\n为了更好地便于理解它们的行为，试着运行以下代码。\nCreate Flux\u0026lt;Integer\u0026gt; integerFlux = Flux.create((FluxSink\u0026lt;Integer\u0026gt; fluxSink) -\u0026gt; { System.out.println(\u0026#34;Flux create\u0026#34;); IntStream.range(0, 100) .peek(i -\u0026gt; System.out.println(\u0026#34;going to emit - \u0026#34; + i)) .forEach(fluxSink::next); }); integerFlux.delayElements(Duration.ofMillis(50)) .subscribe(i -\u0026gt; System.out.println(\u0026#34;First consumed: \u0026#34; + i)); 输出：\nFlux create going to emit - 0 going to emit - 1 going to emit - 2 going to emit - 3 going to emit - 4 going to emit - 5 going to emit - 6 going to emit - 7 going to emit - 8 going to emit - 9 going to emit - 10 going to emit - 11 going to emit - 12 ........ going to emit - 91 going to emit - 92 going to emit - 93 going to emit - 94 going to emit - 95 going to emit - 96 going to emit - 97 going to emit - 98 going to emit - 99 First consumed: 0 First consumed: 1 First consumed: 2 First consumed: 3 First consumed: 4 First consumed: 5 First consumed: 6 First consumed: 7 First consumed: 8 First consumed: 9 First consumed: 10 ........ First consumed: 94 First consumed: 95 First consumed: 96 First consumed: 97 First consumed: 98 First consumed: 99 在上面的消费者 FluxSink\u0026lt;Integer\u0026gt;，它有一个循环持续地发送元素。 \u0026ldquo;Flux create\u0026rdquo; 只执行了一次。 \u0026ldquo;going to emit\u0026rdquo; 语句执行了 100 次。 然后所有的 \u0026ldquo;First consumed\u0026rdquo; 一个接一个地执行。 Generate AtomicInteger atomicInteger = new AtomicInteger(); // Flux generate sequence Flux\u0026lt;Integer\u0026gt; integerFlux = Flux.generate((SynchronousSink\u0026lt;Integer\u0026gt; synchronousSink) -\u0026gt; { System.out.println(\u0026#34;Flux generate\u0026#34;); synchronousSink.next(atomicInteger.getAndIncrement()); }); integerFlux.delayElements(Duration.ofMillis(100)) .subscribe(i -\u0026gt; System.out.println(\u0026#34;First consumed: \u0026#34; + i)); 输出 Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate First consumed: 0 First consumed: 1 First consumed: 2 First consumed: 3 First consumed: 4 First consumed: 5 First consumed: 6 First consumed: 7 First consumed: 8 First consumed: 9 First consumed: 10 First consumed: 11 First consumed: 12 First consumed: 13 First consumed: 14 First consumed: 15 First consumed: 16 First consumed: 17 First consumed: 18 First consumed: 19 First consumed: 20 First consumed: 21 First consumed: 22 Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate First consumed: 23 First consumed: 24 First consumed: 25 First consumed: 26 First consumed: 27 First consumed: 28 First consumed: 29 First consumed: 30 First consumed: 31 First consumed: 32 First consumed: 33 First consumed: 34 First consumed: 35 First consumed: 36 First consumed: 37 First consumed: 38 First consumed: 39 First consumed: 40 First consumed: 41 First consumed: 42 First consumed: 43 First consumed: 44 First consumed: 45 First consumed: 46 Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate Flux generate ......... \u0026ldquo;Flux generate\u0026rdquo; 执行了 32 次。 \u0026ldquo;First consumed\u0026rdquo; 执行了 23 次。 又是一堆 \u0026ldquo;Flux generate\u0026rdquo;。 然后又是一堆 \u0026ldquo;First consumed\u0026rdquo;。 generate 方法是基于下游的请求发送元素，它首先生成了 32 个元素并且缓存，当下游开始处理元素并且缓冲区的容量小于阈值时，它会再发一些元素。这个过程会一直重复，但有一点要注意，如果订阅者停止处理元素，Flux.generate 也会停止发送元素。所以 generate 方法能知道下游订阅者的处理速度。\n我们试一下同时发送两个元素\nAtomicInteger atomicInteger = new AtomicInteger(); Flux\u0026lt;Integer\u0026gt; integerFlux = Flux.generate((SynchronousSink\u0026lt;Integer\u0026gt; synchronousSink) -\u0026gt; { System.out.println(\u0026#34;Flux generate\u0026#34;); synchronousSink.next(atomicInteger.getAndIncrement()); synchronousSink.next(atomicInteger.getAndIncrement()); }); 输出：\nFlux generate 00:13:56.698 [main] ERROR reactor.core.publisher.Operators - Operator called default onErrorDropped reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalStateException: More than one call to onNext Caused by: java.lang.IllegalStateException: More than one call to onNext 使用 SynchronousSink 发送超过一个元素是非法的。 因为我们不能发送多个元素，所以就算我们获取到 SynchronousSink 的引用也是没有意义的。\n如果需要的话，generate 方法可以维护一个状态。\n// initial state Callable\u0026lt;Integer\u0026gt; initialState = () -\u0026gt; 65; BiFunction\u0026lt;Integer, SynchronousSink\u0026lt;Character\u0026gt;, Integer\u0026gt; generator = (state, sink) -\u0026gt; { char value = (char) state.intValue(); sink.next(value); if (value == \u0026#39;Z\u0026#39;) { sink.complete(); } return state + 1; }; // Flux which accepts initialState and bifunction as arg Flux\u0026lt;Character\u0026gt; charFlux = Flux.generate(initialState, generator); charFlux.delayElements(Duration.ofMillis(50)) .subscribe(i -\u0026gt; System.out.println(\u0026#34;Consumed: \u0026#34; + i)); 输出：\nConsumed: A Consumed: B Consumed: C Consumed: D Consumed: E Consumed: F Consumed: G Consumed: H Consumed: I Consumed: J Consumed: K Consumed: L Consumed: M Consumed: N Consumed: O Consumed: P Consumed: Q Consumed: R Consumed: S Consumed: T Consumed: U Consumed: V Consumed: W Consumed: X Consumed: Y Consumed: Z Flux Create\n入参为 Consumer\u0026lt;FluxSink\u0026lt;T\u0026gt;\u0026gt; Consumer 只能被调用一次 Consumer 马上发送 0 到 N 个元素 发布者不清楚下游的状态，所以我们需要提供一个 OverflowStrategy 作为额外的参数 我们可以拿到 FluxSink 的引用持续地发送元素，必要的话多线程也可以 Flux Generate\n入参为 Consumer\u0026lt;SynchronousSink\u0026lt;T\u0026gt;\u0026gt; Consumer 可以基于下游的请求一遍一遍被调用 Consumer 只能发送一个元素 发布者基于下游的请求生成元素 我们能拿到 SynchronousSink 的引用，但没啥用，因为我们只能发送一个元素 背压机制 什么是背压 在响应式数据流中，背压定义了如何调节数据流元素的传输。换句话说，控制消费者能接收到的数据量。 我们用一个例子来说明：\n这个系统包含发布者，消费者，GUI 发布者每秒发送 10000 个数据给消费者 消费者处理数据并把结果发送给 GUI GUI 对用户展示结果 消费者每秒只能处理 7500 个数据 在这个速率下，消费者没有控制数据发送（背压），因此，这个系统会崩溃，用户也不会看到结果。\n背压预防系统故障 这里我们可以用一些背压策略来预防系统故障，来管理额外接收到的数据：\n首先我们会想到 控制数据流的发送 ，发布者需要减缓发布数据的速度。这样，消费者就不会过载，不过，这并不总是可行的，我们得找到其他的选项。 缓存额外的数据元素 。用这种方式，消费者缓存剩下的数据元素直到它能够处理为止。主要的缺点是可能会导致内存崩溃。 丢弃额外的元素 。虽然这个解决办法也不是很理想，不过这样至少系统不会崩溃。 背压控制 我们应该专注于让发布者控制数据的发送。有以下策略：\n当订阅者请求时才发送新的数据 。这是一个拉取的策略。 限制客户端能够接收到元素的数量 。作为限制的推送策略，发布者每次只能向客户端发送不超过指定数量的元素。 当消费者不能处理更多的数据时取消数据流 。这种情况下，接收者能在任何给定的时间终止数据传输并在稍后重新订阅流。 背压机制的实现 Request @Test public void testRequest() { Flux\u0026lt;Integer\u0026gt; intRange = Flux.range(1, 100); //gets only 10 elements intRange.subscribe( System.out::println, Throwable::printStackTrace, () -\u0026gt; System.out.println(\u0026#34;==== Completed ====\u0026#34;), subscription -\u0026gt; subscription.request(10) ); } 输出：\n1 2 3 4 5 6 7 8 9 10 用这种方式，消费者永远不会被发布者的数据压垮。换句话说，消费者在控制它能处理的数据元素。\nLimit 先看下不做限制默认的情况\nFlux.range(1, 100) .log() .delayElements(Duration.ofMillis(100)) .subscribe(System.out::println); 输出：\n00:05:21.718 [main] INFO reactor.Flux.Range.1 - | onSubscribe([Synchronous Fuseable] FluxRange.RangeSubscription) 00:05:21.720 [main] INFO reactor.Flux.Range.1 - | request(32) 00:05:21.720 [main] INFO reactor.Flux.Range.1 - | onNext(1) 00:05:21.755 [main] INFO reactor.Flux.Range.1 - | onNext(2) 00:05:21.756 [main] INFO reactor.Flux.Range.1 - | onNext(3) ...... 00:05:21.756 [main] INFO reactor.Flux.Range.1 - | onNext(32) 1 2 3 ...... 23 00:05:23.198 [parallel-3] INFO reactor.Flux.Range.1 - | request(24) 00:05:23.199 [parallel-3] INFO reactor.Flux.Range.1 - | onNext(33) ...... 00:05:23.204 [parallel-3] INFO reactor.Flux.Range.1 - | onNext(56) 24 25 26 ...... 47 00:05:24.715 [parallel-7] INFO reactor.Flux.Range.1 - | request(24) 00:05:24.715 [parallel-7] INFO reactor.Flux.Range.1 - | onNext(57) ...... 00:05:24.721 [parallel-7] INFO reactor.Flux.Range.1 - | onNext(79) 00:05:24.721 [parallel-7] INFO reactor.Flux.Range.1 - | onNext(80) 48 ...... 71 00:05:26.232 [parallel-11] INFO reactor.Flux.Range.1 - | request(24) 00:05:26.232 [parallel-11] INFO reactor.Flux.Range.1 - | onNext(81) ...... 00:05:26.233 [parallel-11] INFO reactor.Flux.Range.1 - | onNext(98) 00:05:26.233 [parallel-11] INFO reactor.Flux.Range.1 - | onNext(99) 00:05:26.233 [parallel-11] INFO reactor.Flux.Range.1 - | onNext(100) 00:05:26.235 [parallel-11] INFO reactor.Flux.Range.1 - | onComplete() 72 ...... 95 00:05:27.751 [parallel-15] INFO reactor.Flux.Range.1 - | request(24) 96 97 98 99 100 默认情况下，上游会收到一个获取 32 个元素的请求，32 个元素生产并发布给下游 一旦下游的 24 个元素被耗尽（日志可能显示 23，但它应该是 24。因为第 24 个元素被 delayElement 操作符延迟），上游收到另一个获取 24 个元素的请求 它会一直重复直到上游发送完成或者错误信号 限制每次只能请求 10 个元素\nFlux.range(1, 100) .log() .limitRate(10) .delayElements(Duration.ofMillis(100)) .subscribe(System.out::println); 输出：\n00:17:08.045 [main] INFO reactor.Flux.Range.1 - | onSubscribe([Synchronous Fuseable] FluxRange.RangeSubscription) 00:17:08.048 [main] INFO reactor.Flux.Range.1 - | request(10) 00:17:08.048 [main] INFO reactor.Flux.Range.1 - | onNext(1) ...... 00:17:08.085 [main] INFO reactor.Flux.Range.1 - | onNext(10) 1 2 3 4 5 6 7 00:17:08.514 [parallel-7] INFO reactor.Flux.Range.1 - | request(8) 00:17:08.514 [parallel-7] INFO reactor.Flux.Range.1 - | onNext(11) ...... ...... ...... ...... 00:17:13.623 [parallel-7] INFO reactor.Flux.Range.1 - | onNext(98) 88 89 90 91 92 93 94 95 00:17:14.126 [parallel-15] INFO reactor.Flux.Range.1 - | request(8) 00:17:14.127 [parallel-15] INFO reactor.Flux.Range.1 - | onNext(99) 00:17:14.127 [parallel-15] INFO reactor.Flux.Range.1 - | onNext(100) 00:17:14.130 [parallel-15] INFO reactor.Flux.Range.1 - | onComplete() 96 97 98 99 100 一旦有 75% 的数据被发出，它就会自动请求并重新装满该数额。这就是为什么我们首先看到请求 10 个元素，之后每次都会看到请求 8 个元素。\nCancel 更好地控制数据流的方式是在需要的时候取消订阅。你可以用 BaseSubscriber::hookOnNext 订阅生产者。 参考下面的例子，生产者只有在订阅者发送 request(1) 时才会发送下一个元素。实际上，这个生产者可以是一个数据库，而订阅者可以是一个 I/O 设备。为了配置运算的速度，I/O 设备可以请求一批数据并处理，然后再请求下一批数据，以此类推。\n@Test public void cancelCallback() { Flux\u0026lt;Integer\u0026gt; intRange = Flux.range(1, 100).log(); intRange.doOnCancel(() -\u0026gt; System.out.println(\u0026#34;===== Cancel method invoked =======\u0026#34;)) .doOnComplete(() -\u0026gt; System.out.println(\u0026#34;==== Completed ====\u0026#34;)) .subscribe(new BaseSubscriber\u0026lt;Integer\u0026gt;() { @Override protected void hookOnNext(Integer value) { try { Thread.sleep(500); //request next element request(1); System.out.println(value); if (value == 5) { cancel(); } } catch (InterruptedException e) { e.printStackTrace(); } } }); } 输出：\n00:11:07.921 [main] INFO reactor.Flux.Range.1 - | onSubscribe([Synchronous Fuseable] FluxRange.RangeSubscription) 00:11:07.922 [main] INFO reactor.Flux.Range.1 - | request(unbounded) 00:11:07.923 [main] INFO reactor.Flux.Range.1 - | onNext(1) 00:11:08.431 [main] INFO reactor.Flux.Range.1 - | request(1) 1 00:11:08.431 [main] INFO reactor.Flux.Range.1 - | onNext(2) 00:11:08.935 [main] INFO reactor.Flux.Range.1 - | request(1) 2 00:11:08.936 [main] INFO reactor.Flux.Range.1 - | onNext(3) 00:11:09.450 [main] INFO reactor.Flux.Range.1 - | request(1) 3 00:11:09.450 [main] INFO reactor.Flux.Range.1 - | onNext(4) 00:11:09.954 [main] INFO reactor.Flux.Range.1 - | request(1) 4 00:11:09.954 [main] INFO reactor.Flux.Range.1 - | onNext(5) 00:11:10.460 [main] INFO reactor.Flux.Range.1 - | request(1) 5 ===== Cancel method invoked ======= 00:11:10.463 [main] INFO reactor.Flux.Range.1 - | cancel() 00:11:10.484 [main] INFO reactor.Flux.Range.1 - | onSubscribe([Synchronous Fuseable] FluxRange.RangeSubscription) 00:11:10.486 [main] INFO reactor.Flux.Range.1 - | request(unbounded) 00:11:10.486 [main] INFO reactor.Flux.Range.1 - | onNext(1) 00:11:10.486 [main] INFO reactor.Flux.Range.1 - | onNext(2) 00:11:10.486 [main] INFO reactor.Flux.Range.1 - | onNext(3) 00:11:10.486 [main] INFO reactor.Flux.Range.1 - | onNext(4) 00:11:10.486 [main] INFO reactor.Flux.Range.1 - | onNext(5) 00:11:10.486 [main] INFO reactor.Flux.Range.1 - | cancel() 深入理解响应式流 响应式流规范 2013 年末的时候，Netflix、Pivotal、Typesafe 等公司的工程师们共同发起了关于制定 响应流规范（Reactive Stream Specification） 的倡议和讨论，并在 Github 上创建了 reactive-streams-jvm 项目。项目 README 就是规范正文。\n了解这一规范对我们理解和使用开发库也是很有帮助的，因为规范的内容都是对响应式编程思想精髓的呈现。其中包括定义的响应式流的特点：\n具有处理无限数量元素的能力 按序处理 异步地传递元素 必须实现非阻塞的背压 响应式流接口 响应式流规范定义了四个接口：\nPublisher 是能够发出元素的发布者。 public interface Publisher\u0026lt;T\u0026gt; { public void subscribe(Subscriber\u0026lt;? super T\u0026gt; s); } Subscriber 是接收元素并响应的订阅者 public interface Subscriber\u0026lt;T\u0026gt; { public void onSubscribe(Subscription s); public void onNext(T t); public void onError(Throwable t); public void onComplete(); } 当执行 subscribe 方法时，发布者会回调订阅者的 onSubscribe 方法，这个方法中，通常订阅者会借助传入的 Subscription 向发布者请求 n 个数据。然后发布者通过不断调用订阅者的 onNext 方法向订阅者发出最多 n 个数据。如果数据全部发送完，则会调用 onComplete 信号告知订阅者流已经发完；如果有错误发生，则通过 onError 发出错误信号并终止数据流。\nSubscription 是 Publisher 和 Subscriber 的 \u0026ldquo;中间人\u0026rdquo;。 public interface Subscription { public void request(long n); public void cancel(); } 当发布者调用 subscribe 方法注册订阅者时，会通过订阅者的 onSubscribe 传入 Subscription 对象，之后订阅者就可以使用这个 Subscription 对象的 request 方法向发布者请求元素。背压机制也正是基于此来实现的，因此第 4 个特点也实现了。\n订阅之后发生了什么 在 Reactor 中，我们最先接触的生成 Publisher 的方法就是 Flux.just()，下面我们来手写代码模拟一下 Reactor 的实现方式，帮助理解。\n首先，确保项目引入了响应流规范的四个接口定义。\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.reactivestreams\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;reactive-streams\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.4\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 先创建一个最最基础的类 Flux，它是一个 Publisher。\npackage com.opoa.reactive.core; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; public abstract class Flux\u0026lt;T\u0026gt; implements Publisher\u0026lt;T\u0026gt; { @Override public abstract void subscribe(Subscriber\u0026lt;? super T\u0026gt; s); } 在 Reactor 中，Flux 既是一个发布者，又充当工具类的角色，当我们使用 Flux.just()、 Flux.range() 等工厂方法生成 Flux 时，会 new 一个新的 Flux，比如 Flux.just() 会返回一个 FluxArray 对象。\npublic static \u0026lt;T\u0026gt; Flux\u0026lt;T\u0026gt; just(T... data) { return new FluxArray\u0026lt;\u0026gt;(data); } 返回的 FluxArray 对象是 Flux.just 生成的 Publisher，它继承自 Flux，并实现了 subscribe 方法。\npublic class FluxArray\u0026lt;T\u0026gt; extends Flux\u0026lt;T\u0026gt; { private T[] array; // 1 public FluxArray(T[] data) { this.array = data; } @Override public void subscribe(Subscriber\u0026lt;? super T\u0026gt; actual) { actual.onSubscribe(new ArraySubscription\u0026lt;\u0026gt;(actual, array)); // 2 } } FluxArray 内部使用一个数组来保存数据 subscribe 方法通常会回调 Subscriber 的 onSubscribe 方法，该方法需要传入一个 Subscription 对象，从而订阅者之后可以通过回调传回的 Subscription 的 request 方法跟 FluxArray 请求数据。 继续编写 ArraySubscription：\npublic class FluxArray\u0026lt;T\u0026gt; extends Flux\u0026lt;T\u0026gt; { ...... static class ArraySubscription\u0026lt;T\u0026gt; implements Subscription { // 1 final Subscriber\u0026lt;? super T\u0026gt; actual; final T[] array; // 2 int index; boolean canceled; public ArraySubscription(Subscriber\u0026lt;? super T\u0026gt; actual, T[] array) { this.actual = actual; this.array = array; } @Override public void request(long n) { if (canceled) { return; } int length = array.length; for (int i = 0; i \u0026lt; n \u0026amp;\u0026amp; index \u0026lt; length; i++) { actual.onNext(array[index++]); // 3 } if (index == length) { actual.onComplete(); // 4 } } @Override public void cancel() { // 5 this.canceled = true; } } } ArraySubscription 是一个静态内部类。我们可以把它当成普通的类，只不过恰好定义在其他类的内部 Subscription 中也保存有一份数据 当有可以发出的元素时，回调订阅者的 onNext 方法传递元素 当所有的元素都发送完后，回调订阅者的 onComplete 方法 订阅者可以使用 Subscription 取消订阅 至此，发布者就开发完了。我们来测试一下：\n@Test public void fluxArrayTest() { Flux.just(1, 2, 3, 4).subscribe(new Subscriber\u0026lt;Integer\u0026gt;() { // 1 @Override public void onSubscribe(Subscription s) { System.out.println(\u0026#34;onSubscribe\u0026#34;); s.request(5); // 2 } @Override public void onNext(Integer integer) { System.out.println(\u0026#34;onNext: \u0026#34; + integer); } @Override public void onError(Throwable t) { } @Override public void onComplete() { System.out.println(\u0026#34;onComplete\u0026#34;); } }); } Subscriber 通过匿名内部类定义，需要实现接口的四个方法 订阅时请求 5 个元素 输出：\nonSubscribe onNext: 1 onNext: 2 onNext: 3 onNext: 4 onComplete 请求 3 个元素的时候，输出如下：\nonSubscribe onNext: 1 onNext: 2 onNext: 3 没有完成事件，至此，一个简单的 Flux.just 就完成了，通过这个例子我们能初步总结一下：\n工厂方法返回的是 Flux 子类的实例，如 FluxArray FluxArray 的 subscribe 方法会返回给订阅者一个 Subscription 实现类的对象，这个 ArraySubscription 是 FluxArray 的静态内部类，它定义了 \u0026ldquo;如何发布元素\u0026rdquo; 的逻辑 订阅者可以通过这个 ArraySubscription 对象向发布者请求 n 个数据，发布者也可以借助这个 ArraySubscription 对象向订阅者传递数据元素（onNext/onError/onComplete）。 上图的这个过程基本适用于大多数的用于生成 Flux/Mono 的静态工厂方法，如 Flux.just、Flux.range 等等。\n首先，使用类似 Flux.just 的方法创建发布者后，会创建一个具体的发布者（Publisher），如 FluxArray。\n当使用 .subscribe 订阅这个发布者时，首先会 new 一个具有相应逻辑的 Subscription（如 ArraySubscription，这个 Subscription 定义了如何处理下游的 request，以及如何发出元素） 然后发布者将这个 Subscription 通过订阅者的 onSubscribe 方法传给订阅者 在订阅者的.onSubscribe 方法中，需要通过 Subscription 发起第一次请求 request Subscription 收到请求，就通过回调订阅者的 onNext 方法发出元素，有多少发多少，但不能超过请求的个数 订阅者在 onNext 中定义对元素的处理逻辑，处理完成之后，可以继续发起请求 发布者根据需求继续满足订阅者的请求 直至发布者的序列全部结束，通过订阅者的 onComplete 予以告知，当然如果序列在发送过程中有错误产生，则通过订阅者的 onError 告知错误信号，这两种情况都将终止序列。 操作符 \u0026ldquo;流水线\u0026rdquo; 响应式开发库的一个很赞的特性就是可以像组装流水线一样将操作符串起来，用来声明复杂的处理逻辑。比如：\nFlux.just(1, 2, 3, 4, 5) .map(i -\u0026gt; i * i) .filter(i -\u0026gt; (i % 2) == 0) .subscribe(System.out::println); 通过源码，我们可以了解这种 \u0026ldquo;流水线\u0026rdquo; 的实现机制。下面我们来模拟一下 Reactor 中 Flux.map 的实现方式。 Flux.map 用于实现转换，转换后的元素类型可能会发生变化，转换的逻辑由 Function 决定。方法本身返回的是一个转换后的 Flux，基于此，实现如下：\npublic abstract class Flux\u0026lt;T\u0026gt; implements Publisher\u0026lt;T\u0026gt; { ...... public \u0026lt;V\u0026gt; Flux\u0026lt;V\u0026gt; map(Function\u0026lt;? super T, ? extends V\u0026gt; mapper) { // 1 return new FluxMap\u0026lt;\u0026gt;(this, mapper); // 2 } } 泛型方法，通过泛型表示可能出现的类型变换（T —\u0026gt; V） FluxMap 就是新的 Flux 既然 FluxMap 是一个新的 Flux，那么与 FluxArray 类似，其内部定义有 MapSubscription，这是一个 Subscription，能够根据其订阅者的请求发出数据。\npublic class FluxMap\u0026lt;T, R\u0026gt; extends Flux\u0026lt;R\u0026gt; { private final Flux\u0026lt;? extends T\u0026gt; source; private final Function\u0026lt;? super T, ? extends R\u0026gt; mapper; public FluxMap(Flux\u0026lt;? extends T\u0026gt; source, Function\u0026lt;? super T, ? extends R\u0026gt; mapper) { this.source = source; this.mapper = mapper; } @Override public void subscribe(Subscriber\u0026lt;? super R\u0026gt; actual) { } static final class MapSubscriber\u0026lt;T, R\u0026gt; implements Subscription { private final Subscriber\u0026lt;? super R\u0026gt; actual; private final Function\u0026lt;? super T, ? extends R\u0026gt; mapper; MapSubscriber(Subscriber\u0026lt;? super R\u0026gt; actual, Function\u0026lt;? super T, ? extends R\u0026gt; mapper) { this.actual = actual; this.mapper = mapper; } @Override public void request(long n) { // 1 //todo 收到请求 发出元素 } @Override public void cancel() { //todo 取消订阅 } } } map 操作符并不产生数据，只是数据的搬运工。收到 request 后要发出的数据来自上游。 所以 MapSubscriber 同时也应该是一个订阅者，它订阅上游的发布者，并将数据传递给下游的订阅者。\n对下游作为发布者，传递上游的数据到下游，对上游是订阅者，传递下游的请求到上游。\nstatic final class MapSubscriber\u0026lt;T, R\u0026gt; implements Subscriber\u0026lt;T\u0026gt;, Subscription { // 1 ... } 所以我们既需要实现 Subscription，也需要实现 Subscriber。 这样，总共有 6 个方法要实现：来自 Subscriber 接口的 onSubscribe、onNext、onError、onComplete，来自 Subscription 接口的 request 和 cancel。下面我们将实现这几个方法。\n@Override public void subscribe(Subscriber\u0026lt;? super R\u0026gt; actual) { source.subscribe(new MapSubscriber\u0026lt;\u0026gt;(actual, mapper)); } static final class MapSubscriber\u0026lt;T, R\u0026gt; implements Subscriber\u0026lt;T\u0026gt;, Subscription { private final Subscriber\u0026lt;? super R\u0026gt; actual; private final Function\u0026lt;? super T, ? extends R\u0026gt; mapper; boolean done; Subscription subscriptionOfUpstream; MapSubscriber(Subscriber\u0026lt;? super R\u0026gt; actual, Function\u0026lt;? super T, ? extends R\u0026gt; mapper) { this.actual = actual; this.mapper = mapper; } @Override public void onSubscribe(Subscription s) { this.subscriptionOfUpstream = s; // 1 actual.onSubscribe(this); // 2 } @Override public void onNext(T t) { if (done) { return; } actual.onNext(mapper.apply(t)); // 3 } @Override public void onError(Throwable t) { if (done) { return; } done = true; actual.onError(t); // 4 } @Override public void onComplete() { if (done) { return; } done = true; actual.onComplete(); // 5 } @Override public void request(long n) { this.subscriptionOfUpstream.request(n); // 6 } @Override public void cancel() { this.subscriptionOfUpstream.cancel(); // 7 } } 拿到来自上游的 Subscription。 回调下游的 onSubscribe，将自身作为 Subscription 传递过去 收到上游发出的数据后，将其用 mapper 进行转换，然后发给下游 将上游的错误信号原样发给下游 将上游的完成信号原样发给下游 将下游的请求传递给上游 将下游的取消操作传递给上游 从这个对源码的模仿，可以体会到，当有多个操作符串成 \u0026ldquo;链\u0026rdquo; 的时候：\n向下：数据和信号（onSubscribe、onNext、onError、onComplete）是通过每一个操作符向下传递的，传递的过程中进行相应的操作处理。 向上：有一个自下而上的 \u0026ldquo;订阅链\u0026rdquo;，这个订阅链可以用来传递 request，因此背压（backpressure）可以实现从下游向上游传递。 这一节最开头的那一段代码执行过程如下：\nLambdaSubscriber subscribe 方法有几个不同的变种：\nsubscribe(Consumer\u0026lt;? super T\u0026gt; consumer) subscribe(@Nullable Consumer\u0026lt;? super T\u0026gt; consumer, Consumer\u0026lt;? super Throwable\u0026gt; errorConsumer) subscribe(@Nullable Consumer\u0026lt;? super T\u0026gt; consumer, @Nullable Consumer\u0026lt;? super Throwable\u0026gt; errorConsumer, @Nullable Runnable completeConsumer) subscribe(@Nullable Consumer\u0026lt;? super T\u0026gt; consumer, @Nullable Consumer\u0026lt;? super Throwable\u0026gt; errorConsumer, @Nullable Runnable completeConsumer, @Nullable Consumer\u0026lt;? super Subscription\u0026gt; subscriptionConsumer) 用起来很方便，但响应式流规范中只定义了一个订阅方法 subscribe(Subscriber subscriber)。实际上，这几个方法最终都是调用的 subscribe(LambdaSubscriber subscriber)，并通过 LambdaSubscriber 实现了对不同个数参数的组装。如图所示：\n因此，flux.subscribe(System.out::println, System.err::println); 是调用的 flux.subscribe(new LambdaSubscriber(System.out::println, System.err::println, null, null));\n🕳🕳🕳 在使用 Reactor 的开发过程中，很容易因为对于操作符的不熟悉，使得程序没有按照预期执行。 而有些预期之外的行为，并不会对整体流程造成什么影响，但是在线上环境中，它们就像是一个个 \u0026ldquo;不定时炸弹\u0026rdquo;。所以，尽可能深入地熟悉你使用的操作符，是很有必要的。\nSwitchIfEmpty 总是执行 示例 首先准备一个 switchIfEmpty 时执行的方法\npublic Mono\u0026lt;Integer\u0026gt; emptyBackup() { System.out.println(\u0026#34;switchIfEmpty 执行\u0026#34;); return Mono.just(5); } Mono.just(1) .filter(i -\u0026gt; i == 2) .switchIfEmpty(emptyBackup()) .doOnNext(i -\u0026gt; System.out.println(\u0026#34;onNext: \u0026#34; + i)) .subscribe(); 输出：\nswitchIfEmpty 执行 onNext: 5 这段代码是没什么问题的，因为 filter 使得元素必须为 2 才能通过，而发布者只发出了一个值为 1 的元素值，执行到 switchIfEmpty 时数据流已经为空，便切换到 emptyBackup，后续的消费者执行时输出元素为 5。\n我们稍微修改一下代码\nMono.just(1) .filter(i -\u0026gt; i == 1) .switchIfEmpty(emptyBackup()) .doOnNext(i -\u0026gt; System.out.println(\u0026#34;onNext: \u0026#34; + i)) .subscribe(); 把 filter 中的为 2 时才通过改为 1。\n输出：\nswitchIfEmpty 执行 onNext: 1 奇怪，switchIfEmpty 应该不满足才对，后续输出也为 1，而不是 emptyBackup 里面的 5。但它确实执行了。\n我们再修改一下代码\nMono.just(1) .filter(i -\u0026gt; i == 1) .switchIfEmpty(emptyBackup()) .doOnNext(i -\u0026gt; System.out.println(\u0026#34;onNext: \u0026#34; + i)); 输出：\nswitchIfEmpty 执行 这次，我们去掉了 subscribe，总所周知，数据流在订阅之前什么都不会发生。但 switchIfEmpty 仍然执行了！说明它是在装配时期就执行了。\n为什么 也许这并不是关于 Reactor 的问题，而是 Java 语言本身以及它如何解析参数的问题。我们试着把上面的代码用命令式编程的方式拆开：\nMono\u0026lt;Integer\u0026gt; just = Mono.just(1); Mono\u0026lt;Integer\u0026gt; filter = just.filter(i -\u0026gt; i == 1); Mono\u0026lt;Integer\u0026gt; emptyBackup = emptyBackup(); Mono\u0026lt;Integer\u0026gt; switchIfEmpty = filter.switchIfEmpty(emptyBackup); Mono\u0026lt;Integer\u0026gt; doOnNext = switchIfEmpty.doOnNext(i -\u0026gt; System.out.println(\u0026#34;onNext: \u0026#34; + i)); 可以看到，在我们装配 Mono 类型的数据时，emptyBackup 已经被触发了。\n解决办法 在之前介绍操作符的时候有提到过，我们可以使用 Mono.defer，让它被请求时才会触发。\nMono.just(1) .filter(i -\u0026gt; i == 1) .switchIfEmpty(Mono.defer(() -\u0026gt; emptyBackup())) .doOnNext(i -\u0026gt; System.out.println(\u0026#34;onNext: \u0026#34; + i)) .subscribe(); 输出：\nonNext: 1 参考资料 100% 弄明白 5 种 IO 模型 响应式 Spring 的道法术器（Spring WebFlux 快速上手 + 全面介绍） Reactor 3 参考文档 Mono(reactor-core 3.4.24) map vs flatMap in reactor Difference between Mono/Flux.fromCallable and Mono.defer Mono switchIfEmpty() is always called How to handle errors in Reactive Spring webflux Reactor Repeat vs Retry | Vinsguru Project Reactor - Using repeat and repeatWhen Examples Reactor Hot Publisher vs Cold Publisher | Vinsguru Backpressure Mechanism in Spring WebFlux Backpressure in Project reactor - Reactive Programming | Jstobigdata Reactor LimitRate Example | Vinsguru ","date":"2022-10-02T23:13:17+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/kimi-no-na-wa.webp","permalink":"https://opoa.top/post/dive-into-spring-webflux-5/","title":"响应式编程 Spring Webflux 详解（五）"},{"content":"测试与调试 添加测试需要的依赖：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.projectreactor\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;reactor-test\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.1.4.RELEASE\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.12\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; 在命令式编程中，调试通常都是非常直观的：直接看栈调用能定位到问题位置，是否自己代码实现有问题，还是调用的第三方库报错等等。\n响应式的异步代码测试和调试起来会比命令式编程麻烦的多，不过我们先了解一个基本的单元测试工具 —— StepVerifier。\n当测试时关注于每一个数据元素的时候，就非常适合 StepVerifier 的使用场景：下一个期望的数据或信号是什么？是否想要某一个特别的值等等。\n以发出 1-6 的 Flux 为例：\nprivate Flux\u0026lt;Integer\u0026gt; generateFlux() { return Flux.just(1, 2, 3, 4, 5, 6); } @Test public void testVerify() { StepVerifier.create(generateFlux()) .expectNext(1, 2, 3, 4, 5, 6) .expectComplete() .verify(); } expectNext 用于测试下一个期望的数据元素。 expectComplete 用于测试下一个元素是否为完成信号。 以一个错误的数据流为例：\nprivate Mono\u0026lt;Void\u0026gt; generateError() { return Mono.error(new RuntimeException(\u0026#34;some error\u0026#34;)); } @Test public void testVerify() { StepVerifier.create(generateError()) .expectErrorMessage(\u0026#34;some error\u0026#34;) .verify(); } expectErrorMessage 用于测试是否有指定信息的异常信号。 对于终止事件，相应的期望方法（比如 expectComplete()、expectError()，及其所有的变体方法）使用之后就不能再继续增加别的期望方法，最后只能对 StepVerifier 触发校验（verify()）。\n再看一个例子：\nprivate Mono\u0026lt;Integer\u0026gt; getMonoWithException() { return Flux.range(1, 5) .map(i -\u0026gt; i * i) .filter(i -\u0026gt; (i % 2) == 0) .single(); // 1 } single 方法必须且只能接收一个元素，没有和多了都会导致异常。 @Test public void testBug() { getMonoWithException() .subscribe(); } 运行用例，异常信息：\n能看到异常内容大部分都是 Reactor 库内部的调用，上边 stack trace 的问题还是出自 .subcribe() 那一行。 在命令式编程的方式中比较容易使用 IDEA 的工具进行调试，但在异步编程方式下，就不太好使了。所以还是得使用响应式编程库本身提供的调试工具。\n开启调试模式 Hooks.onOperatorDebug();\n调试模式能在抛出异常时打印一些有用的信息，把这一行加上：\n@Test public void testBug() { Hooks.onOperatorDebug(); getMonoWithException() .subscribe(); } 这时再运行，增加了以下内容：\nSuppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Assembly trace from producer [reactor.core.publisher.MonoSingle] : reactor.core.publisher.Flux.single(Flux.java:7989) com.opoa.WebfluxTest.getMonoWithException(WebfluxTest.java:21) Error has been observed at the following site(s): *__Flux.single ⇢ at com.opoa.WebfluxTest.getMonoWithException(WebfluxTest.java:21) 这样就能定位到问题根源。\nHooks.onOperatorDebug() 是一种全局性的 Hook，会影响到应用中所有的操作符，所以带来的性能成本也是比较大的。如果我们大概知道问题可能在哪，而对整个应用开启调试模式，也容易被茫茫多的调试信息淹没。这时，我们需要一种更精准的定位方式。\n使用 checkpoint() 定位 如果我们知道问题出在哪个链上，就可以针对这个链使用 checkpoint() 进行问题定位。 checkpoint() 操作符就像一个 Hook，但它的作用范围仅限于这个链上。\n@Test public void testBug() { getMonoWithException() .checkpoint() .subscribe(); } 出现异常时仍然可以打印出调试信息：\nSuppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Assembly trace from producer [reactor.core.publisher.MonoSingle] : reactor.core.publisher.Mono.checkpoint(Mono.java:2177) com.opoa.WebfluxTest.testBug(WebfluxTest.java:27) Error has been observed at the following site(s): *__checkpoint() ⇢ at com.opoa.WebfluxTest.testBug(WebfluxTest.java:27) 调度器与线程模型 在以往的多线程开放场景中，使用 Executors 可以创建四种线程池：\nnewCachedThreadPool 创建一个弹性大小缓存线程池，如果线程池长度超过处理需要，可灵活回收空闲线程，若无可回收线程，则创建线程； newFixedThreadPool 创建一个大小固定的线程池，可控制线程最大并发数，超出的线程会在队列中等待； newScheduledThreadPool 创建一个大小固定的线程池，支持定时及周期性的任务执行； newSingleThreadExecutor 创建一个单线程化的线程池，它只会用唯一的工作线程来执行任务，保证所有任务按照指定顺序执行。 Reactor 让线程管理和任务调度更加简单 —— 调度器（Scheduler） 帮我们搞定这件事。 Scheduler 是一个拥有多个实现类的抽象接口。Schedulers 类提供的静态方法可以搭建以下几种线程执行环境：\n当前线程（Schedulers.immediate()）； 可重用的单线程（Schedulers.single()）。这个方法对所有调用者都提供同一个线程使用，直到该调度器被废弃。如果想使用独占的线程，请使用 Schedulers.newSingle()； 弹性线程池（Schedulers.elastic()）。它根据需要创建一个线程池，重用空闲线程。线程如果空闲时间过长（默认为 60s）就会被销毁。对于 I/O 阻塞的场景比较适用。Schedulers.elastic() 能够方便地给一个阻塞的任务分配它自己的线程，而不会妨碍其他任务和资源。 固定大小线程池（Schedulers.parallel()），所创建的线程池的大小与 CPU 个数相同。 Schedulers 类已经预先创建了几种常用的线程池：使用 single()、elastic() 和 parallel() 方法可以分别使用内置的单线程、弹性线程池和固定大小线程池。如果想创建新的线程池，可以使用 newSingle()、newElastic() 和 newParallel() 方法。\nExecutors 提供的几种线程池在 Reactor 中都支持：\nSchedulers.single() 和 Schedulers.newSingle() 对应 Executors.newSingleThreadExecutor(); Schedulers.elastic() 和 Schedulers.newElastic() 对应 Executors.newCachedThreadPool(); Schedulers.parallel() 和 Schedulers.newParallel() 对应 Executors.newFixedThreadPool(); 包装一个同步阻塞的调用 很多时候，信息源是同步和阻塞。在 Reactor 中，我们可以用以下方式处理：\nMono.fromCallable(() -\u0026gt; { // 1 return /* make a remote synchronous call */ // 2 }) .subscribeOn(Schedulers.boundedElastic()); // 3 使用 fromCallable 生成一个 Mono; 返回同步阻塞的资源；（比如通过 http 请求查询一个数据） 使用 Schedulers.elastic() 确保每个订阅运行在一个专门的线程上。 切换调度器 Reactor 提供了两种在响应式链中调整调度器 Scheduler 的方法：publishOn 和 subcribeOn。它们都接受一个 Scheduler 作为参数，能够改变调度器。但是 publishOn 在链中出现的位置是有讲究的，而 subscribeOn 就无所谓。\n@Test public void testScheduling() throws InterruptedException { Flux.range(1, 10) .map(i -\u0026gt; i + 1) .log() // 1 .publishOn(Schedulers.elastic()) .filter(i -\u0026gt; (i % 2) == 0) // .log() // 2 .publishOn(Schedulers.parallel()) .doOnNext(i -\u0026gt; System.out.println(\u0026#34;int: \u0026#34; + i)) // .log() // 3 .subscribeOn(Schedulers.single()) .subscribe(); Thread.sleep(5000); } 只保留这个 log() 的话，源头数据流是执行在 single 线程池上的。 只保留这个 log() 的话，可以看到，publishOn 之后的数据流是在 elastic 线程池上执行的。 只保留这个 log() 的话，可以看到，publishOn 之后的数据流切换到了 parallel 线程池上执行。 通过以上 log() 的输出，能总结出以下操作链：\npublishOn 会影响链中之后的操作符，比如第一个 publishOn 调整调度器为 elastic，则 filter 的处理是在弹性线程池中执行的；同理，doOnNext 是在 parallel 线程池中执行的； subscribeOn 无论出现在什么位置，只影响源头的执行环境，也就是 map 方法是执行在单线程中的，直至被第一个 publishOn 切换调度器。 并行执行 对于一些能够在一个线程中顺序处理的任务，即使调度到 ParallelScheduler 上，通常也只由一个 Worker 来执行， 比如：\n@Test public void testParallelFlux() throws InterruptedException { Flux.range(1, 10) .publishOn(Schedulers.parallel()) .log() .subscribe(); TimeUnit.SECONDS.sleep(1); } 输出：\n12:20:51.189 [main] INFO reactor.Flux.PublishOn.1 - | onSubscribe([Fuseable] FluxPublishOn.PublishOnSubscriber) 12:20:51.191 [main] INFO reactor.Flux.PublishOn.1 - | request(unbounded) 12:20:51.205 [parallel-1] INFO reactor.Flux.PublishOn.1 - | onNext(1) 12:20:51.205 [parallel-1] INFO reactor.Flux.PublishOn.1 - | onNext(2) 12:20:51.205 [parallel-1] INFO reactor.Flux.PublishOn.1 - | onNext(3) 12:20:51.205 [parallel-1] INFO reactor.Flux.PublishOn.1 - | onNext(4) 12:20:51.205 [parallel-1] INFO reactor.Flux.PublishOn.1 - | onNext(5) 12:20:51.205 [parallel-1] INFO reactor.Flux.PublishOn.1 - | onNext(6) 12:20:51.205 [parallel-1] INFO reactor.Flux.PublishOn.1 - | onNext(7) 12:20:51.205 [parallel-1] INFO reactor.Flux.PublishOn.1 - | onNext(8) 12:20:51.205 [parallel-1] INFO reactor.Flux.PublishOn.1 - | onNext(9) 12:20:51.205 [parallel-1] INFO reactor.Flux.PublishOn.1 - | onNext(10) 12:20:51.205 [parallel-1] INFO reactor.Flux.PublishOn.1 - | onComplete() 如果我们想要一些任务能够 \u0026ldquo;均匀\u0026rdquo; 分布在不同的工作线程中执行，就需要用到 ParallelFlux。 你可以对任务 Flux 使用 parallel() 操作符来得到一个 ParallelFlux。不过它本身并不会进行并行处理，而是将负载划分到多个执行 \u0026ldquo;轨道\u0026rdquo; 上（默认情况下，轨道个数与 CPU 核心数相同）。\n为了配置 ParallelFlux 如何并行地执行每一个轨道，需要使用 runOn(Scheduler)，Schedulers.parallel 是比较推荐的专门用于并行处理的调度器。\n@Test public void testParallelFlux() throws InterruptedException { Flux.range(1, 10) .parallel(3) .runOn(Schedulers.parallel()) .log() .subscribe(); TimeUnit.SECONDS.sleep(1); } 输出：\n12:32:57.483 [main] INFO reactor.Parallel.RunOn.1 - onSubscribe([Fuseable] FluxPublishOn.PublishOnSubscriber) 12:32:57.485 [main] INFO reactor.Parallel.RunOn.1 - request(unbounded) 12:32:57.497 [main] INFO reactor.Parallel.RunOn.1 - onSubscribe([Fuseable] FluxPublishOn.PublishOnSubscriber) 12:32:57.497 [main] INFO reactor.Parallel.RunOn.1 - request(unbounded) 12:32:57.497 [main] INFO reactor.Parallel.RunOn.1 - onSubscribe([Fuseable] FluxPublishOn.PublishOnSubscriber) 12:32:57.497 [main] INFO reactor.Parallel.RunOn.1 - request(unbounded) 12:32:57.498 [parallel-1] INFO reactor.Parallel.RunOn.1 - onNext(1) 12:32:57.498 [parallel-2] INFO reactor.Parallel.RunOn.1 - onNext(2) 12:32:57.498 [parallel-1] INFO reactor.Parallel.RunOn.1 - onNext(4) 12:32:57.498 [parallel-2] INFO reactor.Parallel.RunOn.1 - onNext(5) 12:32:57.498 [parallel-3] INFO reactor.Parallel.RunOn.1 - onNext(3) 12:32:57.498 [parallel-2] INFO reactor.Parallel.RunOn.1 - onNext(8) 12:32:57.498 [parallel-1] INFO reactor.Parallel.RunOn.1 - onNext(7) 12:32:57.498 [parallel-3] INFO reactor.Parallel.RunOn.1 - onNext(6) 12:32:57.498 [parallel-1] INFO reactor.Parallel.RunOn.1 - onNext(10) 12:32:57.498 [parallel-3] INFO reactor.Parallel.RunOn.1 - onNext(9) 12:32:57.498 [parallel-1] INFO reactor.Parallel.RunOn.1 - onComplete() 12:32:57.498 [parallel-2] INFO reactor.Parallel.RunOn.1 - onComplete() 12:32:57.498 [parallel-3] INFO reactor.Parallel.RunOn.1 - onComplete() 可以看到，各个元素的 OnNext\u0026quot; 均匀 \u0026ldquo;分布执行在三个线程上，最后每个线程都有独立的onComplete 事件。\nSchedulers.elastic() 存在可能会创建过多线程的问题，不应该在生产环境中使用。该方法已经被标记为过时，并且会在 reactor-core3.5 版本中删除。取而代之应该使用 Schedulers.boundedElastic() 有界弹性线程池，最大线程数为 CPU 可用核心数 * 10。\nHot vs Cold 无论是 Flux 还是 Mono，都有一个特点：订阅前什么都不会发生。当我们 \u0026ldquo;创建\u0026rdquo; 了一个 Flux 的时候，我们只是 \u0026ldquo;声明\u0026rdquo;/\u0026ldquo;组装\u0026rdquo; 了它，但是如果不调用 subscribe 来订阅它，它就不会开始发出元素。\n我们前面常用的 Mono.just 就属于 \u0026ldquo;冷\u0026rdquo; 的发布者。\n冷序列 想象一下，你登陆 B 站打开一个喜欢的视频，你可以在任何时间开始观看，与此同时，可能有几百个人在不同的地方和你看同一个视频，它们是不同的数据流。可能我已经看到一半了，别人才刚刚开始看。B 站的视频播放就像是一个 \u0026ldquo;冷的发布者\u0026rdquo;。\n冷发布者不会产生任何数据除非至少有一个人订阅它。并且它会为每个订阅者创建新的数据。\n我们把这个方法当作获取视频的流数据\nprivate Stream\u0026lt;String\u0026gt; getVideo() { System.out.println(\u0026#34;Got the video streaming request\u0026#34;); return Stream.of( \u0026#34;scene 1\u0026#34;, \u0026#34;scene 2\u0026#34;, \u0026#34;scene 3\u0026#34;, \u0026#34;scene 4\u0026#34;, \u0026#34;scene 5\u0026#34; ); } videoFlux 实现如下：\n// 每间隔一秒播放一个画面 Flux\u0026lt;String\u0026gt; videoFlux = Flux.fromStream(() -\u0026gt; getVideo()) .delayElements(Duration.ofSeconds(1)); videoFlux.subscribe(scene -\u0026gt; System.out.println(\u0026#34;Tom are watching \u0026#34; + scene)); Thread.sleep(4000); videoFlux.subscribe(scene -\u0026gt; System.out.println(\u0026#34;Jerry are watching \u0026#34; + scene)); 输出：\nGot the video streaming request Tom are watching scene 1 Tom are watching scene 2 Tom are watching scene 3 Got the video streaming request Tom are watching scene 4 Jerry are watching scene 1 Tom are watching scene 5 Jerry are watching scene 2 Jerry are watching scene 3 Jerry are watching scene 4 Jerry are watching scene 5 我们发现，\u0026ldquo;Got the video streaming request\u0026rdquo; 输出了两次，说明每次新的订阅都会触发 getVideo 请求，Tom 比 Jerry 先开始看视频，当 Jerry 开始看视频时，Tom 已经看完了 3 个画面，Tom 和 Jerry 在并行地看同一个视频，但是在不同的画面。Jerry 并不会因为开始时间晚而错过任何画面。\n热序列 想象有一个直播间，它不关心是否有人真的在看，一直持续地播放视频流，用户可以在它开播期间任意时间打开，并且直播间内的所有用户在同一时刻都看到同样的画面（数据），如果有人进来晚了，就只能错过之前的片段。直播间就像一个 \u0026ldquo;热的发布者\u0026rdquo;。\nshare 还是之前的 videoFlux，我们只需要添加一个 share，让这个 Flux\u0026rdquo; 变 \u0026ldquo;成一个直播间。share 把冷序列转换成了对多个订阅者广播数据的热序列。\n// 每间隔一秒播放一个画面 Flux\u0026lt;String\u0026gt; videoFlux = Flux.fromStream(() -\u0026gt; getVideo()) .delayElements(Duration.ofSeconds(1)) .share(); videoFlux.subscribe(scene -\u0026gt; System.out.println(\u0026#34;Tom are watching \u0026#34; + scene)); Thread.sleep(4000); videoFlux.subscribe(scene -\u0026gt; System.out.println(\u0026#34;Jerry are watching \u0026#34; + scene)); 输出：\nGot the video streaming request Tom are watching scene 1 Tom are watching scene 2 Tom are watching scene 3 Tom are watching scene 4 Jerry are watching scene 4 Tom are watching scene 5 Jerry are watching scene 5 从输出来看，Jerry 因为来晚了，错过了前面 3 秒的画面，但他能和 Tom 一样观看最新的画面。\n如果第二个订阅者加入时，数据已经发送完成了，那么第二个订阅会让序列重复发送流程，就像电影院中，一部影片放映完了，还会有后续的放映。\n// 每间隔一秒播放一个画面 Flux\u0026lt;String\u0026gt; videoFlux = Flux.fromStream(() -\u0026gt; getVideo()) .delayElements(Duration.ofSeconds(1)) .share(); videoFlux.subscribe(scene -\u0026gt; System.out.println(\u0026#34;Tom are watching \u0026#34; + scene)); // Tom 看完电影 Thread.sleep(6000); videoFlux.subscribe(scene -\u0026gt; System.out.println(\u0026#34;Jerry are watching \u0026#34; + scene)); 输出：\nGot the video streaming request Tom are watching scene 1 Tom are watching scene 2 Tom are watching scene 3 Tom are watching scene 4 Tom are watching scene 5 Got the video streaming request Jerry are watching scene 1 Jerry are watching scene 2 Jerry are watching scene 3 Jerry are watching scene 4 Jerry are watching scene 5 cache 如果我们不想让序列重复，可以使用 cache，它会缓存历史数据并广播给多个订阅者。\n// 每间隔一秒播放一个画面 Flux\u0026lt;String\u0026gt; videoFlux = Flux.fromStream(() -\u0026gt; getVideo()) .delayElements(Duration.ofSeconds(1)) .cache(); videoFlux.subscribe(scene -\u0026gt; System.out.println(\u0026#34;Tom are watching \u0026#34; + scene)); Thread.sleep(6000); videoFlux.subscribe(scene -\u0026gt; System.out.println(\u0026#34;Jerry are watching \u0026#34; + scene)); 输出：\nGot the video streaming request Tom are watching scene 1 Tom are watching scene 2 Tom are watching scene 3 Tom are watching scene 4 Tom are watching scene 5 Jerry are watching scene 1 Jerry are watching scene 2 Jerry are watching scene 3 Jerry are watching scene 4 Jerry are watching scene 5 可以看到，getVideo() 只调用了一次，但是 Jerry 仍然可以观看的所有的画面，cache 为以后的订阅者缓存了所有的数据。\n如果不想缓存，可以使用 cache(0)。\n输出：\nGot the video streaming request Tom are watching scene 1 Tom are watching scene 2 Tom are watching scene 3 Tom are watching scene 4 Tom are watching scene 5 此时的 Jerry 总结 冷序列：不管订阅者在何时订阅，都能收到数据流中的全部数据。 热序列：热序列持续产生数据，订阅者只能获取到其订阅之后的数据。 ","date":"2022-09-12T22:40:24+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/pond-tea-rem.webp","permalink":"https://opoa.top/post/dive-into-spring-webflux-4/","title":"响应式编程 Spring Webflux 详解（四）"},{"content":"操作符 通常情况下，我们需要对发布者发出的原始数据进行多个阶段的处理，并最终得到我们需要的数据。这种感觉就像是一条流水线，从流水线的源头进入传送带的是原料，经过流水线上各个工位的处理，逐渐从原料变成半成品、零件、组件、成品，最终成为消费者需要的包装品。这其中，流水线源头的下料机就相当于发布者，消费者就相当于订阅者，流水线上的一道道工序就相当于一个个操作符。\n下面介绍一些常用的操作符。\nmap map 操作可以将元素进行转换 / 映射，得到一个新的元素。\npublic final \u0026lt;V\u0026gt; Flux\u0026lt;V\u0026gt; map(Function\u0026lt;? super T, ? extends V\u0026gt; mapper) public final \u0026lt;R\u0026gt; Mono\u0026lt;R\u0026gt; map(Function\u0026lt;? super T, ? extends R\u0026gt; mapper) 上面是 Flux 的 map 操作示意图，上方的箭头是原始序列的时间轴，下方箭头是经过 map 处理后的数据序列时间轴。 map 接受一个 Function 的函数式接口作为参数，这个函数定义了转换操作的策略，可以把它理解为 Java8 流式编程的 map 方法。举例说明：\nFlux.range(1, 6) // 1 .map(i -\u0026gt; i * 2) // 2 .subscribe(System.out::println); // 3 输出如下：\n2 4 6 8 8 10 12 Flux.range(1, 6) 用于生成从 \u0026ldquo;1\u0026rdquo; 开始，自增为 1 的 \u0026ldquo;6\u0026rdquo; 个整型数据 map 接受 Lambda i -\u0026gt; i * 2 为参数，对每个数据进行乘 2 操作 订阅数据流并输出每个元素值 flatMap flatMap 操作可以将每个数据元素转换 / 映射为一个流，然后将这些流合并为一个大的数据流。\n流的合并是异步的，先到先得，并非是按照原始序列的顺序（途中绿色和黄色方块是交叉的）。\npublic final \u0026lt;R\u0026gt; Flux\u0026lt;R\u0026gt; flatMap(Function\u0026lt;? super T, ? extends Publisher\u0026lt;? extends R\u0026gt;\u0026gt; mapper) public final \u0026lt;R\u0026gt; Mono\u0026lt;R\u0026gt; flatMap(Function\u0026lt;? super T, ? extends Mono\u0026lt;? extends R\u0026gt;\u0026gt; transformer) flatMap 也是接收一个 Function 的函数接口为参数，这个函数输入为一个 T 类型的数据值，对于 Flux 来说输出可以是 Flux 和 Mono，对于 Mono 来说输出只能是 Mono。 举例说明：\nFlux.just(\u0026#34;flux\u0026#34;, \u0026#34;mono\u0026#34;) .flatMap(word -\u0026gt; Flux.fromArray(word.split(\u0026#34;\\\\s*\u0026#34;)) // 1 .delayElements(Duration.ofMillis(100)) // 2 ) .doOnNext(System.out::print) // 3 .subscribe(); // 4 TimeUnit.SECONDS.sleep(1); 对于每一个字符串 word, 将其拆分为包含一个字符的字符串流 每个元素延迟 100ms 对每个元素进行打印 (doOnNext 是 \u0026ldquo;偷窥式\u0026rdquo; 的方法，类似于流式编程中的 peek，不会消费数据流) 订阅数据流 打印结果为 mfolnuox，原因在于各个拆分后的字符都是间隔 100ms 发出的，因此会交叉。 如果项目的顺序很重要，可以考虑改用 flatMapSequential 运算符。 flatMap 通常用于每个元素又会引入数据流的情况，比如我们有一串用户 id 数据流，需要通过 id 信息查询用户数据，假设响应式的请求方法如下：\nMono\u0026lt;User\u0026gt; getUser(String userId) {...} 而用户 id 数据流为一个 Flux\u0026lt;String\u0026gt; ids，为了获取所有的用户信息，需要用到 flatMap\nids.flatMap(id -\u0026gt; getUser(id)); 其返回内容为 Flux\u0026lt;User\u0026gt; 类型的 User 流。\nmap 与 flatMap 的区别 map 是同步的，非阻塞，一对一的转换。 flatMap 是异步的，非阻塞，一对多的转换。 /** * Transform the items emitted by this {@link Flux} by applying a synchronous function * to each item. */ public final \u0026lt;V\u0026gt; Flux\u0026lt;V\u0026gt; map(Function\u0026lt;? super T, ? extends V\u0026gt; mapper) /** * Transform the elements emitted by this {@link Flux} asynchronously into Publishers, * then flatten these inner publishers into a single {@link Flux} through merging, * which allow them to interleave. */ public final \u0026lt;R\u0026gt; Flux\u0026lt;R\u0026gt; flatMap(Function\u0026lt;? super T, ? extends Publisher\u0026lt;? extends R\u0026gt;\u0026gt; mapper) 当流被订阅后，映射器对输入流中的元素执行必要的转换（执行上述 mapper 操作）。这些元素中的每一个都可以转换为多个数据，然后用于创建新的流。\n那么如何判断什么时候用 map，什么时候用 flatMap 呢？\n凡是涉及需要异步处理的方法，都用 flatMap，比如请求接口，读取数据库数据，读取缓存等等。 其他同步方法，比如转换对象，对已有数据做计算等等，都用 map。 filter filter 操作可以对数据元素进行筛选\npublic final Flux\u0026lt;T\u0026gt; filter(Predicate\u0026lt;? super T\u0026gt; p) public final Mono\u0026lt;T\u0026gt; filter(final Predicate\u0026lt;? super T\u0026gt; tester) filter 接受一个 Predicate 的函数式接口为参数，这个函数式的作用是进行判断并返回 Boolean。 举例说明：\nFlux.range(1, 10) // 1 .filter(i -\u0026gt; i % 3 == 0) // 2 .doOnNext(System.out::println) .subscribe(); 生成从 \u0026ldquo;1\u0026rdquo; 开始，自增为 1 的 \u0026ldquo;10\u0026rdquo; 个整型数据 filter 的 Lambda 参数表示过滤操作保留 3 的倍数 输出：\n3 6 9 switchIfEmpty switchIfEmpty 操作在一个序列元素为空时，替换为另一个元素\n举例说明：\n@Data @AllArgsConstructor @NoArgsConstructor class User { private String name; } public Mono\u0026lt;User\u0026gt; getUserFromCache() { return Mono.just(new Random().nextBoolean()) .filter(b -\u0026gt; b) .map(u -\u0026gt; new User(\u0026#34;user from cache\u0026#34;)); } 从缓存中获取用户信息，可能返回为空。\npublic Mono\u0026lt;User\u0026gt; getUserFromConsole() { return Mono.just(new Random().nextBoolean()) .filter(b -\u0026gt; b) .map(u -\u0026gt; new User(\u0026#34;user from console\u0026#34;)); } 从控制台中获取用户信息，可能返回为空。\n@GetMapping(\u0026#34;/user/get\u0026#34;) public Mono\u0026lt;User\u0026gt; getUser() { return getUserFromCache() // 1 .switchIfEmpty(Mono.defer(() -\u0026gt; getUserFromConsole())) //2 .switchIfEmpty(Mono.defer(() -\u0026gt; Mono.error(new RuntimeException(\u0026#34;用户不存在\u0026#34;)))); //3 } 从缓存中获取用户信息 如果缓存中数据为空，则从控制台获取用户信息 如果控制台中数据仍为空，则抛出用户不存在异常终止序列 输出：\n# 结果 1 { \u0026#34;name\u0026#34;: \u0026#34;user from cache\u0026#34; } # 结果 2 { \u0026#34;name\u0026#34;: \u0026#34;user from console\u0026#34; } # 结果 3 { \u0026#34;timestamp\u0026#34;: \u0026#34;2022-10-05T04:34:39.274+00:00\u0026#34;, \u0026#34;path\u0026#34;: \u0026#34;/reactive/user/get\u0026#34;, \u0026#34;status\u0026#34;: 500, \u0026#34;error\u0026#34;: \u0026#34;Internal Server Error\u0026#34;, \u0026#34;requestId\u0026#34;: \u0026#34;653d3049-14\u0026#34; } defer defer 把元素加入流中，但是延时加载，直到 subsribe() 时才会加载\n举例说明：\nMono\u0026lt;String\u0026gt; dateTime = Mono.just(LocalDateTime.now().toString()); System.out.println(\u0026#34;t0: \u0026#34; + dateTime.block()); Thread.sleep(10_000); System.out.println(\u0026#34;t1: \u0026#34; + dateTime.block()); Thread.sleep(5_000); System.out.println(\u0026#34;t2: \u0026#34; + dateTime.block()); 输出：\nt0: 2022-10-05T13:23:31.482 t1: 2022-10-05T13:23:31.482 t2: 2022-10-05T13:23:31.482 调用 Mono.just(LocalDateTime.now().toString()) 时会立即执行 LocalDateTime.now().toString() 并且获取到结果，通过订阅 Mono 时只会将结果发出，所以订阅多次并不会改变原来的值。\nMono\u0026lt;String\u0026gt; dateTime = Mono.defer(() -\u0026gt; Mono.just(LocalDateTime.now().toString())); System.out.println(\u0026#34;t0: \u0026#34; + dateTime.block()); Thread.sleep(10_000); System.out.println(\u0026#34;t1: \u0026#34; + dateTime.block()); Thread.sleep(5_000); System.out.println(\u0026#34;t2: \u0026#34; + dateTime.block()); 输出：\nt0: 2022-10-05T13:24:40.500 t1: 2022-10-05T13:24:50.515 t2: 2022-10-05T13:24:55.519 defer 操作符会在每次订阅时重新评估 Lambda 表达式中的内容。\n⭐Tips\n有一个用法是，当需要定义异常 Mono.error() 时，并不需要每次都创建，而是在出现异常时才加载，这样可以节省内存。\nMono.defer(() -\u0026gt; Mono.error(new RuntimeException(\u0026#34;异常\u0026#34;))); then then 操作符会忽略前一个流的结果，并将序列转换为另一个 Mono\u0026lt;V\u0026gt; 或者 Mono\u0026lt;Void\u0026gt;。\npublic final \u0026lt;V\u0026gt; Mono\u0026lt;V\u0026gt; then(Mono\u0026lt;V\u0026gt; other) public final Mono\u0026lt;Void\u0026gt; then() Mono.just(1) .then() // 1 .doOnNext(item -\u0026gt; System.out.println(item)) // 2 .subscribe(); 忽略掉 Mono.just 中的元素，返回 Mono\u0026lt;Void\u0026gt;。 输出序列中的值（因为前一个序列已经替换为空，所以此处不会有任何输出） Mono.just(1) .then(Mono.just(\u0026#34;other item\u0026#34;)) // 1 .doOnNext(item -\u0026gt; System.out.println(item)) .subscribe(); 忽略掉 Mono.just 中的元素，返回新的 Mono 序列。 输出序列中的值 输出：\nother item 使用场景：\npublic class SaveSessionGatewayFilterFactory extends AbstractGatewayFilterFactory\u0026lt;Object\u0026gt; { @Override public GatewayFilter apply(Object config) { return new GatewayFilter() { @Override public Mono\u0026lt;Void\u0026gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) { return exchange.getSession().map(WebSession::save).then(chain.filter(exchange)); } @Override public String toString() { return filterToStringCreator(SaveSessionGatewayFilterFactory.this).toString(); } }; } } 参考自保存 Session 的网关过滤器 SaveSessionGatewayFilterFactory，filter 方法中，map 调用了 save()，该方法返回值是 Mono\u0026lt;Void\u0026gt;，该序列会因为没有元素发出而终止。这里使用 then，不管上游序列的输出是 Mono\u0026lt;Void\u0026gt; 还是其他元素，将其忽略，并向下继续调用其他过滤器的方法。\n如果你关心前一个流的结果，可以使用 map, flatMap 或者其他 map 的变种方法，如果你只想要前一个流完成，不关心流的数据，则使用 then。\ndoOnNext doOnNext 操作符，当一个可用且存在的数据成功发出时，触发添加的无副作用行为。\npublic final Mono\u0026lt;T\u0026gt; doOnNext(Consumer\u0026lt;? super T\u0026gt; onNext) public final Flux\u0026lt;T\u0026gt; doOnNext(Consumer\u0026lt;? super T\u0026gt; onNext) doOnNext 接收一个 Consumer 函数为参数，仅有入参，没有出参，并不会对原序列的数据类型造成影响。\n举例说明：\nMono.just(new User(\u0026#34;Alex\u0026#34;)) .doOnNext(u -\u0026gt; System.out.println(u.getName())) .doOnNext(u -\u0026gt; u.setName(\u0026#34;Bob\u0026#34;)) .doOnNext(u -\u0026gt; System.out.println(u.getName())) .subscribe(); 输出：\nAlex Bob doOnSuccess doOnSuccess 操作符，在 Mono 序列成功完成时触发，不管结果是 T 还是 null。\n举例说明：\nMono.empty() .doOnSuccess(i -\u0026gt; System.out.println(\u0026#34;on success: \u0026#34; + i)) .subscribe(); Mono.just(\u0026#34;hello\u0026#34;) .doOnSuccess(i -\u0026gt; System.out.println(\u0026#34;on success: \u0026#34; + i)) .subscribe(); 输出：\non success: null on success: hello doOnSuccess 仅可作用在 Mono 序列中。\ndoOnNext 与 doOnSuccess 的区别 doOnNext 是可有用数据是才触发。 doOnSuccess 只要上游执行的序列没有报错，都可以判断为成功，不管结果是什么。 doFinally doFinally 操作符，会添加一个行为，当 Mono 序列因为任何原因终止时触发。\npublic final Mono\u0026lt;T\u0026gt; doFinally(Consumer\u0026lt;SignalType\u0026gt; onFinally) public final Flux\u0026lt;T\u0026gt; doFinally(Consumer\u0026lt;SignalType\u0026gt; onFinally) doFinally 接收一个 Consumer 函数，入参为 SignalType 信号类型，包括取消、完成、错误信号等等。\n举例说明：\n完成信号 Mono.just(\u0026#34;hello\u0026#34;) .doOnNext(i -\u0026gt; System.out.println(\u0026#34;next: \u0026#34; + i)) .doFinally(signalType -\u0026gt; System.out.println(\u0026#34;finally, signalType: \u0026#34; + signalType.name())) .subscribe(); 输出：\nnext: hello finally, signalType: ON_COMPLETE 异常信号 Mono.just(\u0026#34;hello\u0026#34;) .then(Mono.error(new RuntimeException(\u0026#34;exception\u0026#34;))) .doOnNext(i -\u0026gt; System.out.println(\u0026#34;next: \u0026#34; + i)) .doFinally(signalType -\u0026gt; System.out.println(\u0026#34;finally, signalType: \u0026#34; + signalType.name())) .subscribe(); 输出：\n23:15:12.203 [main] ERROR reactor.core.publisher.Operators - Operator called default onErrorDropped reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.RuntimeException: exception Caused by: java.lang.RuntimeException: exception at com.opoa.controller.ReactiveController.main(ReactiveController.java:69) finally, signalType: ON_ERROR 可以看到，在 Mono 序列中抛出了一个异常，终止了 doOnNext 的正常执行，但 doFinally 仍能触发，并且信号类型为错误，所以 doFinally 始终能够执行。\n如果有连续的 doFinally 在执行，其顺序是倒序\nFlux.just(\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;) .doFinally(f -\u0026gt; System.out.println(\u0026#34;finally - 1\u0026#34;)) .doOnNext(w -\u0026gt; System.out.println(\u0026#34;on next 1: \u0026#34; + w)) .doFinally(f -\u0026gt; System.out.println(\u0026#34;finally - 2\u0026#34;)) .doOnNext(w -\u0026gt; System.out.println(\u0026#34;on next 2: \u0026#34; + w)) .doFinally(f -\u0026gt; System.out.println(\u0026#34;finally - 3\u0026#34;)) .subscribe(); 输出\non next 1: hello on next 2: hello on next 1: world on next 2: world finally - 3 finally - 2 finally - 1 doOnError doOnError 操作符，会添加一个行为，当序列因为异常终止时触发。\npublic final Mono\u0026lt;T\u0026gt; doOnError(Consumer\u0026lt;? super Throwable\u0026gt; onError) public final \u0026lt;E extends Throwable\u0026gt; Mono\u0026lt;T\u0026gt; doOnError(Class\u0026lt;E\u0026gt; exceptionType, final Consumer\u0026lt;? super E\u0026gt; onError) public final Mono\u0026lt;T\u0026gt; doOnError(Predicate\u0026lt;? super Throwable\u0026gt; predicate, final Consumer\u0026lt;? super Throwable\u0026gt; onError) doOnError 有三个重载方法：\n一个异常类消费者，出现异常时调用。 传参为一个异常类的 Class 和一个对应消费者，当出现该类型的异常时才调用。 传参为一个断言函数和一个对应消费者，当断言结果为 true 时才调用。 举例说明：\nMono.just(9) .map(i -\u0026gt; i / 0) .doOnError(e -\u0026gt; log.error(\u0026#34;an error occurred\u0026#34;)) .subscribe(); 输出：\n22:08:01.282 [main] ERROR com.opoa.controller.ReactiveController - an error occurred 22:08:01.284 [main] ERROR reactor.core.publisher.Operators - Operator called default onErrorDropped reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.ArithmeticException: / by zero Caused by: java.lang.ArithmeticException: / by zero ...(略) onErrorResume onErrorResume，当序列发生错误时，订阅一个备用的发布者 Mono/Flux。\npublic final Mono\u0026lt;T\u0026gt; onErrorResume(Function\u0026lt;? super Throwable, ? extends Mono\u0026lt;? extends T\u0026gt;\u0026gt; fallback) public final \u0026lt;E extends Throwable\u0026gt; Mono\u0026lt;T\u0026gt; onErrorResume(Class\u0026lt;E\u0026gt; type, Function\u0026lt;? super E, ? extends Mono\u0026lt;? extends T\u0026gt;\u0026gt; fallback) public final Mono\u0026lt;T\u0026gt; onErrorResume(Predicate\u0026lt;? super Throwable\u0026gt; predicate, Function\u0026lt;? super Throwable, ? extends Mono\u0026lt;? extends T\u0026gt;\u0026gt; fallback) onErrorResume 有三个重载方法：\n单独一个备用函数作为参数 传参为一个异常类的 Class 和一个备用函数，当出现该类型的异常时才调用。 传参为一个断言函数和一个备用函数，当断言结果为 true 时才调用。 举例说明：\nMono.just(9) .map(i -\u0026gt; i / 0) .onErrorResume(e -\u0026gt; { log.error(\u0026#34;an error occurred: {}\u0026#34;, e.getMessage()); return Mono.just(5); }) .subscribe(System.out::println); 输出：\n22:08:16.636 [main] ERROR com.opoa.controller.ReactiveController - an error occurred: / by zero 5 repeat repeat 会在当前序列发送完成信号时，重新订阅。\n举例说明：\nMono.just(5) .doOnNext(s -\u0026gt; System.out.println(\u0026#34;next 1\u0026#34;)) .doOnNext(s -\u0026gt; System.out.println(\u0026#34;next 2\u0026#34;)) .doOnNext(s -\u0026gt; System.out.println(\u0026#34;next 3\u0026#34;)) .repeat(2) .subscribe(); 输出\nnext 1 next 2 next 3 next 1 next 2 next 3 next 1 next 2 next 3 repeatWhen repeatWhen 当满足特定条件时，会重复订阅这个序列。\n举例说明：\nMono.just(5) .doOnNext(s -\u0026gt; System.out.println(\u0026#34;next 1\u0026#34;)) .doOnNext(s -\u0026gt; System.out.println(\u0026#34;next 2\u0026#34;)) .doOnNext(s -\u0026gt; System.out.println(\u0026#34;next 3\u0026#34;)) .repeatWhen(Repeat.onlyIf(s -\u0026gt; new Random().nextBoolean()).repeatMax(3)) .subscribe(); 这个序列会随机进行重复订阅，当 new Random().nextBoolean() 为 false 或者达到最大重复次数 3 时，停止订阅。\n输出：\nnext 1 next 2 next 3 22:18:09.620 [main] DEBUG reactor.retry.DefaultRepeat - Scheduling repeat attempt, retry context: iteration=1 repeatCompanionValue=1 backoff={0ms} next 1 next 2 next 3 22:18:09.621 [main] DEBUG reactor.retry.DefaultRepeat - Stopping repeats since predicate returned false, retry context: iteration=2 repeatCompanionValue=1 backoff={0ms} 我们可以通过 repeatWhen 能使用更多高级选项\n.repeatWhen(Repeat.times(3).randomBackoff(Duration.ofSeconds(1), Duration.ofSeconds(3)))\n重试 3 次，每次重试间隔在 1 秒到 3 秒之间。\n输出\nnext 1 next 2 next 3 22:22:33.025 [main] DEBUG reactor.retry.DefaultRepeat - Scheduling repeat attempt, retry context: iteration=1 repeatCompanionValue=1 backoff={1184ms/3000ms} next 1 next 2 next 3 22:22:34.227 [parallel-1] DEBUG reactor.retry.DefaultRepeat - Scheduling repeat attempt, retry context: iteration=2 repeatCompanionValue=1 backoff={1939ms/3000ms} next 1 next 2 next 3 22:22:36.170 [parallel-2] DEBUG reactor.retry.DefaultRepeat - Scheduling repeat attempt, retry context: iteration=3 repeatCompanionValue=1 backoff={1647ms/3000ms} next 1 next 2 next 3 22:22:37.832 [parallel-3] DEBUG reactor.retry.DefaultRepeat - Repeats exhausted, retry context: iteration=4 repeatCompanionValue=1 backoff={EXHAUSTED} 如果你在返回 Mono 或者 Flux 的方法中使用了 repeat 或 repeatWhen，该方法会在满足条件时被重复订阅。方法本身只会执行一次，repeat 或 repeatWhen 只是让这个方法被再次订阅。\n示例：\npublic Mono\u0026lt;Boolean\u0026gt; tryLock() { log.info(\u0026#34;尝试获取锁，时间点：\u0026#34; + System.currentTimeMillis()); return Mono.just(new Random().nextBoolean()); } 获取锁的方法，该方法在执行时打印当前时间点，并且随机返回 Boolean。\n@GetMapping(\u0026#34;/getLock\u0026#34;) public Mono\u0026lt;Boolean\u0026gt; lock() { AtomicBoolean locked = new AtomicBoolean(false); // 1 return tryLock() // 2 .doOnNext(locked::set) // 3 .doOnNext(l -\u0026gt; System.out.println(\u0026#34;是否获取到锁：\u0026#34; + locked.get())) // 4 .repeatWhen(Repeat.onlyIf(r -\u0026gt; !locked.get()).randomBackoff(Duration.ofSeconds(2), Duration.ofSeconds(3))) // 5 .then(Mono.defer(() -\u0026gt; Mono.just(locked.get()))) // 6 .doFinally(f -\u0026gt; System.out.println(\u0026#34;释放锁\u0026#34;)); // 7 } 模拟一段获取锁的代码，需要获取锁成功时才返回结果。\n创建原子布尔类型变量。 尝试获取锁，并输出时间节点。 将获取到的值赋给 locked 对象。 输出 locked 当前的变量值。 当未获取到锁时，一直进行重试，并且每次重试间隔 2 到 3 秒。 返回 locked 的变量。 最后，释放锁。 输出： 当第一次获取锁结果为 true 时\n当第一次获取锁结果为 false 时\n当第一次获取锁失败时，repeatWhen 触发，但 tryLock 只会执行一次，所以这段代码会一直拿不到锁，导致卡死。\n为了确保 tryLock 每次重复时都执行，需要把它放到 defer 操作符中。\n@GetMapping(\u0026#34;/getLock\u0026#34;) public Mono\u0026lt;Boolean\u0026gt; lock() { AtomicBoolean locked = new AtomicBoolean(false); return Mono.defer(() -\u0026gt; tryLock()) .doOnNext(locked::set) .doOnNext(l -\u0026gt; System.out.println(\u0026#34;是否获取到锁：\u0026#34; + locked.get())) .repeatWhen(Repeat.onlyIf(r -\u0026gt; !locked.get()).randomBackoff(Duration.ofSeconds(2), Duration.ofSeconds(3))) .then(Mono.defer(() -\u0026gt; Mono.just(locked.get()))) .doFinally(f -\u0026gt; System.out.println(\u0026#34;释放锁\u0026#34;)); } 输出：\n当获取锁失败时，能正确执行 tryLock，每次都去重新获取。\nretry retry 会在当前序列发送任何异常信号时，重新订阅。如果不指定重试次数时，默认重试次数为 Long 的最大值。\n举例：\nFlux.range(1, 5) .doOnNext(i -\u0026gt; System.out.println(\u0026#34;emitted: \u0026#34; + i)) .map(i -\u0026gt; { if (i \u0026gt; 3) { throw new RuntimeException(\u0026#34;can not process \u0026gt; 3\u0026#34;); } return i; }) .subscribe(i -\u0026gt; System.out.println(\u0026#34;received: \u0026#34; + i), error -\u0026gt; System.out.println(\u0026#34;error: \u0026#34; + error)); 该序列会发出数字 1 到 5，但我们的流不能处理大于 3 的数字，否则会抛异常终止。\n输出：\nemitted: 1 received: 1 emitted: 2 received: 2 emitted: 3 received: 3 emitted: 4 error: java.lang.RuntimeException: can not process \u0026gt; 3 我们添加 retry(1)，当收到异常信号时，重订阅一次当前序列。\nFlux.range(1, 5) .doOnNext(i -\u0026gt; System.out.println(\u0026#34;emitted: \u0026#34; + i)) .map(i -\u0026gt; { if (i \u0026gt; 3) { throw new RuntimeException(\u0026#34;can not process \u0026gt; 3\u0026#34;); } return i; }) .retry(1) .subscribe(i -\u0026gt; System.out.println(\u0026#34;received: \u0026#34; + i), error -\u0026gt; System.out.println(\u0026#34;error: \u0026#34; + error)); 输出：\nemitted: 1 received: 1 emitted: 2 received: 2 emitted: 3 received: 3 emitted: 4 // will retry emitted: 1 received: 1 emitted: 2 received: 2 emitted: 3 received: 3 emitted: 4 error: java.lang.RuntimeException: can not process \u0026gt; 3 上面简单介绍了一些常用的操作符，但那也只是冰山一角，如果想要了解更多的操作符，可以通过以下途径：\n想要实战的话，强烈推荐 Reactor 官方的 lite-rx-api-hands-on 项目，拿到项目后，你需要使用操作符，完成 \u0026ldquo;Todo\u0026rdquo; 的代码，让所有的 @Test 绿灯就 Ok 了。完成这些测试后，对常见的操作符就能了然于胸了。 在日常开发中，也可以通过 IDEA 进行查询，比如： ","date":"2022-09-01T23:10:50+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/pink-girl.webp","permalink":"https://opoa.top/post/dive-into-spring-webflux-3/","title":"响应式编程 Spring Webflux 详解（三）"},{"content":"Hello Reactive World 引入依赖 在新建项目的时候引入 Spring Reactive Web，为了方便也引入了 Lombok。\n编写 Controller @RestController @RequestMapping(\u0026#34;/reactive\u0026#34;) public class ReactiveController { @GetMapping(\u0026#34;/hello\u0026#34;) public Mono\u0026lt;String\u0026gt; hello() { return Mono.just(\u0026#34;Hello Reactive World\u0026#34;); } } [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port 8080 启动服务，可以看到程序是运行在 Netty 服务上的。\n请求 WebFlux 提供了与之前 WebMVC 相同的一套注解来定义请求的处理，使得 Spring 使用者迁移到响应式开发方式的过程变得异常轻松。\nFlux 与 Mono Reactor 中的发布者 (Publisher) 由 Flux 和 Mono 两个类定义，它们都提供了丰富的操作符 (operator)。\nFlux: 代表一个包含 0-N 个元素的响应式序列。 Mono: 代表一个包含 0/1 个元素的响应式序列。 可以暂时简单地把 Mono 理解成单个对象，Flux 理解成 List 列表对象。 作为 \u0026ldquo;数据流\u0026rdquo; 的发布者，Flux 和 Mono 都可以发出三种 \u0026ldquo;数据信号\u0026rdquo;：元素值、错误信号、完成信号，错误信号和完成信号都是终止信号，完成信号用于告知下游订阅者该数据流正常结束，错误信号终止数据流的同时将错误传递给下游订阅者。\n下图是一个 Flux 类型的数据流，黑色箭头是时间轴。它连续发出 1 - 6 共 6 个元素值，以及一个完成新信号（图中 6 后边的加粗竖线）, 完成信号告知订阅者数据流已经结束。\n用代码声明：\nFlux.just(1, 2, 3, 4, 5, 6); 下图是一个 Mono 类型的数据流，它发出一个元素值后，又发出一个完成信号。\n用代码声明：\nMono.just(1); Flux 和 Mono 提供了多种创建数据流的方法，just 是一种比较直接的声明数据流的方式，其参数就是数据元素。 还可以通过如下方式声明\n// 基于数组 Integer[] array = {1, 2, 3, 4, 5, 6}; Flux.fromArray(array); // 基于集合 List\u0026lt;Integer\u0026gt; list = Arrays.asList(array); Flux.fromIterable(list); // 基于 Stream Stream\u0026lt;Integer\u0026gt; stream = list.stream(); Flux.fromStream(stream); 不过，这三种信号都不是一定要具备的：\n错误信号和完成信号都是终止信号，二者不可能同时存在。 如果没有发出任何一个元素值，而是直接发出完成 / 错误信号，表示这是一个空数据流。 如果没有错误信号和完成信号，那么就是一个无限数据流。 比如，只有完成 / 错误信号的数据流：\n// 只有完成信号的空数据流 Flux.just(); Flux.empty(); Mono.empty(); // 只有错误信号的数据流 Flux.error(new RuntimeException(\u0026#34;some error\u0026#34;)); Mono.error(new RuntimeException(\u0026#34;some error\u0026#34;)); 空数据流有什么用呢？举个例子，当我们从响应式 DB 中获取结果的时候，就有可能为空：\nFlux\u0026lt;User\u0026gt; findAll(); Mono\u0026lt;User\u0026gt; findById(long id); 无论是结果为空还是发生异常，都需要通过完成 / 错误信号告知订阅者，已经查询完毕，但是没有获取到值。\n订阅前什么都不会发生\n数据流有了，假设我们想把每个数据元素打印出来\nFlux.just(1, 2, 3, 4, 4, 5, 6).subscribe(System.out::println); 输出如下\n1 2 3 4 4 5 6 可见，subscribe 方法中的 Lambda 表达式作用在每一个元素上。Flux 和 Mono 提供了多个 subscribe 的重载方法\n// 订阅并触发数据流 subscribe(); // 订阅并指定对正常数据元素如何处理 subscribe(Consumer\u0026lt;? super T\u0026gt; consumer); // 订阅并定义对正常数据元素和错误信号的处理 subscribe(Consumer\u0026lt;? super T\u0026gt; consumer, Consumer\u0026lt;? super Throwable\u0026gt; errorConsumer); // 订阅并定义对正常数据元素、错误信号和完成信号的处理 subscribe(Consumer\u0026lt;? super T\u0026gt; consumer, Consumer\u0026lt;? super Throwable\u0026gt; errorConsumer, Runnable completeConsumer); 如果是订阅上边声明的 Flux Flux.just(1, 2, 3, 4, 4, 5, 6).subscribe( System.out::println, System.err::println, () -\u0026gt; System.out.println(\u0026#34;已完成\u0026#34;) ); 输出如下\n1 2 3 4 4 5 6 已完成 举一个有错误信号的例子 Mono.error(new RuntimeException(\u0026#34;业务异常\u0026#34;)).subscribe( System.out::println, System.err::println, () -\u0026gt; System.out.println(\u0026#34;已完成\u0026#34;) ); 输出如下\njava.lang.RuntimeException: 业务异常 打印出了错误信号，没有输出已完成，表示没有发出完成信号。\n注意，Flux.just(1, 2, 3, 4, 4, 5, 6) 仅仅声明了数据流，此时数据元素并未发出，只有 subcriber() 方法调用时才会触发数据流。所以，订阅前什么都不会发生。\n","date":"2022-08-27T16:22:43+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/rem-face-portrait-close.webp","permalink":"https://opoa.top/post/dive-into-spring-webflux-2/","title":"响应式编程 Spring Webflux 详解（二）"},{"content":"简介 Spring WebFlux 是 Spring 在 5.0 版本后提供的一套 非阻塞异步 开发框架，它的核心是基于 Reactor 相关 API 实现的，能够运行在 Netty、Undertow 以及 3.1 + 版本的的 Servlet 容器上。\nSpring WebFlux 与 Spring MVC WebFlux 是异步非阻塞 IO 模型，只需少量的工作线程就能够处理并响应请求，无需阻塞等待方法返回，提高了并发处理请求的能力，即系统吞吐量。\nWebMVC 的实现是阻塞 IO，其容器维护一个线程池来处理每一个用户请求，线程池有限的连接数和请求阻塞的处理过程，形成了系统吞吐量的瓶颈。\nTips: 并不是说项目使用了 WebFlux 进行开发，就能发挥出非阻塞的优势，还需要第三方库的支持，比如上面的 MongoDB，Redis，需要使用他们提供的 ReactiveAPI 进行开发。\n五种 IO 模型 为了便于理解 IO 模型的基本概念，我们以应用之间的消息通信举例。\n应用 A 把消息发送到 TCP 发送缓冲区。 TCP 发送缓冲区再把消息发送出去，经过网络传递后，消息会发送到 B 服务器的 TCP 接收缓冲区。 应用 B 从 TCP 接收缓冲区中读取属于自己的数据。 阻塞 IO 模型 阻塞 IO 是当应用 B 发起读取数据申请时，在内核数据没有准备好之前，应用 B 会一直处于等待数据状态，直到内核把数据准备好了交给应用 B 才结束。\n非阻塞 IO 模型 非阻塞 IO 是当应用 B 发起读取数据申请时，如果内核数据没有准备好会立即告诉应用 B，不会让 B 在这里等待。 但是需要应用 B 不断发起读取数据申请，直到读取到需要的数据为止。这样会造成一定的资源浪费。\n复用 IO 模型 思考一个问题：\n如果在并发环境下，有 N 个人向应用 B 发送消息，应用 B 就需要创建多个线程去读取数据，情况如下图：\n并发情况下服务器很可能一瞬间收到几十万的请求，应用 B 就需要创建几十万个线程去读取数据，同时因为线程不知道数据什么时候准备好，为了确保消息能即使读取到，那么这些线程自己会不断请求获取数据。\n先不说服务器能不能扛得住这么多线程，就算扛得住，这种方式也非常浪费资源，大量的线程用于读取数据，意味着能做其它事情的线程就会变少。\n那能不能提供一种方式，可以由一个线程监控多个网络请求，当有数据准备就绪之后再分配对应的线程去读取数据，这样就能节省出大量的线程资源，这个就是 IO 复用模型的思路。\n正如上图，IO 复用模型的思路就是提供了一种函数可以同时监控多个 fd 的操作，这个函数正是我们常说到的 select、poll、epoll 函数，select 函数监控的 fd 中只要有任何一个数据状态准备就绪，select 就会返回可读状态，这时询问线程再去通知处理数据的线程，对应线程再去发起请求读取数据。\n总结： 复用 IO 的基本思路就是通过 slect 或 poll、epoll 来监控多 fd ，来达到不必为每个 fd 创建一个对应的监控线程，从而减少线程资源创建的目的。\n信号驱动 IO 模型 复用 IO 模型可以通过一个线程监控多个 fd，但由于是采用轮询的方式，大部分情况下的轮询都是无效的，而且随着监控的 fd 越来越多，效率也直线下降。那么能不能不要我总是去询问你数据有没有准备好，而是你数据就绪之后就通知后，由此衍生了信号驱动 IO 模型。\n信号驱动 IO 是在调用 sigaction 的时候建立一个 SIGIO 信号联系，当数据准备好之后通过 SIGIO 信号通知线程，线程收到数据就绪的状态后，再发起读取数据的请求，因为信号驱动 IO 模型下的应用线程在发出信号监控后可立即返回，不会阻塞，所以这样的方式下，一个应用线程也可以监控多个 fd。\n总结： 信号驱动 IO 模型通过这种建立信号关联的方式，实现了发出请求后只需要等待数据就绪的通知，这样就可以避免大量无效的数据状态轮询操作。\n异步 IO 模型 通过观察发现，不管是 IO 复用还是信号驱动，我们要获取数据总是要发起两个阶段的请求，第一次发送请求询问数据状态是否准备好，第二次发送请求读取数据。\n于是有人设计了一种方案，应用只需要发送一个获取数据请求，告诉内核它要读取数据后立刻返回，内核收到请求后建立一个信号联系，当数据准备就绪后，内核会主动把数据复制到用户空间，等所有操作完成之后，内核会发起一个通知告诉应用，这种一劳永逸的模式就是异步 IO 模型。\n总结： 在异步 IO 的模型下，只需要发送一次读取请求就可以完成状态询问和数据拷贝的所有操作。\n","date":"2022-08-22T15:36:26+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/miku-miko-twintails-aqua-eyes-mouse.webp","permalink":"https://opoa.top/post/dive-into-spring-webflux-1/","title":"响应式编程 Spring Webflux 详解（一）"},{"content":"定义 模板方法模式在一个方法中定义一个算法骨架，并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下，重新定义算法中的某些步骤。\n简单来说模板方法定义了一套流程，流程中有哪些操作，操作的具体顺序是什么。而操作的具体实现则交给其子类去实现，模板方法不需要关心。\n示例 现有需要对接支付宝和微信支付，部分方法是公用的，另外的需要不同的实现。这里可以使用模板方法，支付流程简单定义为\n打开 App 输入支付金额 （公用） 用户校验 显示支付结果 AbstractPaymentProcess public abstract class AbstractPaymentProcess { /** * 支付流程 */ public void pay(double amount) { launchApp(); inputMoney(amount); inputPassword(); faceId(); paymentResult(); } /** * 打开 App */ protected abstract void launchApp(); /** * 输入支付金额 * * @param amount 支付金额 */ private final void inputMoney(double amount) { System.out.println(\u0026#34;当前待支付金额:\u0026#34; + amount + \u0026#34;元。\u0026#34;); } /** * 输入密码 */ protected void inputPassword() {} /** * 面部识别支付 */ protected void faceId() {} /** * 支付结果 */ protected abstract void paymentResult(); } 打开 App 和显示支付结果为子类必须实现的方法，使用 abstract 修饰。 输入密码校验还是面部识别，选择一种实现即可，不定义为抽象方法。 输入支付金额为公用方法，已经内部实现。\nWeChatPayment public class WeChatPayment extends AbstractPaymentProcess { @Override protected void launchApp() { System.out.println(\u0026#34;打开微信\u0026#34;); } @Override protected void inputPassword() { System.out.println(\u0026#34;----- 微信支付界面 -----\u0026#34;); System.out.println(\u0026#34;密码输入中...\u0026#34;); System.out.println(\u0026#34;密码校验中...\u0026#34;); } @Override protected void paymentResult() { boolean success = new Random().nextBoolean(); System.out.println(\u0026#34;微信支付结果:\u0026#34;); if (success) { System.out.println(\u0026#34;支付成功！\u0026#34;); } else { System.out.println(\u0026#34;支付失败！\u0026#34;); } } } AliPayment public class AliPayment extends AbstractPaymentProcess { @Override protected void launchApp() { System.out.println(\u0026#34;打开支付宝\u0026#34;); } @Override protected void faceId() { System.out.println(\u0026#34;----- 支付宝界面 -----\u0026#34;); System.out.println(\u0026#34;面容 ID 识别中...\u0026#34;); } @Override protected void paymentResult() { boolean success = new Random().nextBoolean(); System.out.println(\u0026#34;支付宝支付结果:\u0026#34;); if (success) { System.out.println(\u0026#34;支付成功！\u0026#34;); } else { System.out.println(\u0026#34;支付失败！\u0026#34;); } } } TestTemplateMethod @Test public void test() { AbstractPaymentProcess aliPay = new AliPayment(); aliPay.pay(12); System.out.println(\u0026#34;---------------------------------\u0026#34;); AbstractPaymentProcess weChatPay = new WeChatPayment(); weChatPay.pay(6); } 输出结果 打开支付宝 当前待支付金额: 12.0 元。 ----- 支付宝界面 ----- 面容 ID 识别中... 支付宝支付结果: 支付成功！ --------------------------------- 打开微信 当前待支付金额: 6.0 元。 ----- 微信支付界面 ----- 密码输入中... 密码校验中... 微信支付结果: 支付失败！ 可以看到，模板方法的优点是提升了代码的复用性和可拓展性。增加一种实现只需要增加一个子类，根据自己的业务去实现抽象方法即可。 缺点是后续的话类的数量可能会很多，不便于维护，如果修改了抽象类，可能需要修改的类数量很多。\n总结 模板方法的 核心思想 是：父类定义骨架，子类实现 某些 细节。\n参考资料 模板方法 （宝，我输液了，输的想你的夜） 模板方法 - 廖雪峰的官方网站 ","date":"2021-07-02T17:03:46+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/whitegirl.webp","permalink":"https://opoa.top/post/design-pattern-template-method/","title":"[设计模式] 模板方法"},{"content":"什么是双链表 双链表也叫双向链表，是链表的一种，它的每个数据结点中都有两个指针，分别指向直接后继和直接前驱。所以，从双链表中的任意一个结点开始，都可以很方便地访问它的前驱结点和后继结点。\n单链表与双链表的区别 单链表 每个数据结点有一个指针，只能单向检索，不够方便。\n双链表 每个数据结点有两个指针，能够更方便访问任意结点的前驱结点和后继结点。效率更高。\n双链表结构 public class DoubleLinkedList\u0026lt;T\u0026gt; { /** * 头结点 */ private Node head; /** * 尾结点 */ private Node tail; /** * 链表元素个数 */ private int size; /** * 结点 */ class Node { /** * 结点数据 */ private T t; /** * 上一个结点 */ private Node prev; /** * 下一个结点 */ private Node next; Node() {} Node(Node prev, T t, Node next) { this.prev = prev; this.t = t; this.next = next; } @Override public String toString() { return \u0026#34;Node{\u0026#34; + t + \u0026#39;}\u0026#39;; } } } 可以看作是在单链表结构的基础上，增加一片指向前驱结点的地址存放空间。 因为双链表既能向后检索结点，也能向前检索，所以对于链表尾结点的记录还是很有必要的。\n完整代码 /** * @program: DoubleLinkedList * @description: 自定义双链表 * @author: opoa * @create: 2021-04-05 16:25 **/ public class DoubleLinkedList\u0026lt;T\u0026gt; { /** * 头结点 */ private Node head; /** * 尾结点 */ private Node tail; /** * 链表元素个数 */ private int size; /** * 结点 */ class Node { /** * 结点数据 */ private T t; /** * 上一个结点 */ private Node prev; /** * 下一个结点 */ private Node next; Node() {} Node(Node prev, T t, Node next) { this.prev = prev; this.t = t; this.next = next; } @Override public String toString() { return \u0026#34;Node{\u0026#34; + t + \u0026#39;}\u0026#39;; } } public DoubleLinkedList() { this.head = null; this.size = 0; } /** * 根据下标获取结点 * * @param index 下标参数 * @return 对应结点 */ public Node getNode(int index) { checkIndex(index); Node curNode = this.head; // 下标为 0 直接返回头结点 if (index == 0) { return head; } for (int i = 0; i \u0026lt; index; i++) { curNode = curNode.next; } return curNode; } /** * 根据元素值返回对应结点 * * @param t 要查找的元素值 * @return 对应结点 */ public Node getNode(T t) { // 从头结点开始 Node curNode = head; // 当前结点不为空且传入结点数据不等于传入元素 while (null != curNode \u0026amp;\u0026amp; curNode.t != t) { curNode = curNode.next; } return curNode; } /** * 新增一个结点 * * @param t 要插入的元素值 */ public void add(T t) { // 如果头结点为空，则链表为空 新增结点既是头节点也是尾结点 if (null == head) { this.head = new Node(null, t, null); this.tail = head; this.size ++; return; } // 新增的结点 以之前的尾结点作为前结点 Node node = new Node(tail, t, null); // 把当前结点的地址赋值给尾结点的 next tail.next = node; // 当前结点作为新的尾结点 this.tail = node; this.size ++; } /** * 在链表尾部插入一个结点 * * @param t 要插入的元素 */ private void insertLast(T t) { // 记录之前的尾结点 final Node l = tail; // 新增一个结点 Node node = new Node(l, t, null); // 该结点作为新的尾结点 this.tail = node; // 如果之前的尾结点为空 if (null == l) { // 则新插入的结点是头结点 this.head = node; } else { // 不为空 则把当前结点的引用赋值给之前尾结点的 next l.next = node; } size ++; } /** * 在某个结点前插一个结点 * * @param t 要插入的结点 * @param old 要插入结点的后驱结点 */ private void insertBefore(T t, Node old) { // 之前结点的前驱结点 也是即将插入结点的前驱结点 Node prev = old.prev; // 新增一个结点 Node node = new Node(prev, t, old); // 当前结点的引用赋值给旧结点的 prev old.prev = node; // 如果之前的前驱结点为空 if (null == prev) { // 则新插入的结点是头结点 this.head = node; } else { // 不为空 则把当前结点的引用赋值给前驱结点的 next prev.next = node; } size ++; } /** * 在指定下标处插入一个结点 * * @param t 要插入的元素 * @param index 要插入的下标 */ public void insert(T t, int index) { checkIndex(index); // 判断是否新增尾结点 if (index == size) { insertLast(t); } else { insertBefore(t, getNode(index)); } } /** * 根据元素删除某个结点 * * @param t 要删除的元素 * @return 删除的元素 */ public T remove(T t) { Node node = getNode(t); if (null == node) { throw new NoSuchElementException(); } return unlink(node); } /** * 根据下标删除某个结点 * * @param index 要删除结点的下标 * @return 删除的元素 */ public T remove(int index) { checkIndex(index); return unlink(getNode(index)); } /** * 删除某个结点 * * @param node 要删除的结点 * @return 删除结点的数据 */ private T unlink(Node node) { // 保存原结点数据 T oldVal = node.t; // 要删除结点的前驱结点 Node prev = node.prev; // 要删除结点的后驱结点 Node next = node.next; // 如果前驱结点为空 if (null == prev) { // 表明要删除的结点是头结点 // 把后驱结点设置为头结点 head = next; } else { // 把后驱结点地址赋值给前驱结点的 next prev.next = next; // 把要删除结点的 prev 设置为空 node.prev = null; } // 如果后驱结点为空 if (null == next) { // 表明要删除的结点是尾结点 // 把前驱结点设置为尾结点 tail = prev; } else { // 把前驱结点地址赋值给后驱结点的 prev next.prev = prev; // 把要删除结点的 next 设置为空 node.next = null; } // 把要删除结点的数据设置为 null 帮助 GC node.t = null; size --; return oldVal; } /** * 给指定下标结点设置数据 * * @param t 要设置的元素 * @param index 要设置结点的下标 */ public T set(int index, T t) { checkIndex(index); // 获取要设置的结点 Node old = getNode(index); // 保存结点中旧的数据 T oldVal = old.t; // 为结点数据赋值 old.t = t; // 返回旧值 return oldVal; } /** * 检查下标参数合法性 * * @param index 要检查的下标参数 */ private void checkIndex(int index) { if (index \u0026lt; 0 || index \u0026gt; size) { throw new IllegalArgumentException(\u0026#34;非法下标参数\u0026#34;); } } /** * 返回链表长度 * * @return 链表长度 */ public int length() { return size; } /** * 打印输出双链表 * * @return 整个双链表 */ @Override public String toString() { if (size == 0) { return null; } Node curNode = head; StringBuilder sb = new StringBuilder(); while (null != curNode) { sb.append(curNode.t.toString()) .append(\u0026#34; -\u0026gt; \u0026#34;); curNode = curNode.next; } sb.append(\u0026#34;null\u0026#34;); return sb.toString(); } } 参考资料 Java8 LinkedList 源码\n","date":"2021-04-10T11:25:27+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/horns-three-eyes-girl.webp","permalink":"https://opoa.top/post/double-linked-list-in-java/","title":"[Java] 双链表"},{"content":"什么是单例模式 单例模式是一种常用的软件设计模式。定义是单例的类只能有一个实例对象，并向外提供一个全局访问点。\n优点 只有一个实例对象，节省内存空间 避免频繁的创建销毁对象，提高性能 避免对共享资源的多重占用 能够全局访问 实现思路 私有化构造方法，让外部不能 new 对象。 在类的内部创建好实例。【静态变量的目的是为了类加载的时候创建实例】 向外提供一个公有的静态方法，返回该唯一实例。 实现写法 1. 饿汉式 /** * @description: 饿汉式 * @author: opoa * @create: 2021-03-14 20:29 **/ /** * 饿汉式 */ public class Singleton1 { /** * 私有化构造方法 */ private Singleton1() {} /** * 在类的内部创建好实例。【静态变量的目的是为了类加载的时候创建实例】 */ private final static Singleton1 instance = new Singleton1(); /** * 向外提供一个公有的静态方法，返回该唯一实例。 * @return 实例 */ public static Singleton1 getInstance() { return instance; } } 优点：写法简单，在类加载的时候就创建了实例，避免了线程同步问题，线程安全。 缺点：如果实例一直没有被使用，则会造成内存空间的浪费。\n2. 懒汉式 非线程安全 public class Singleton2 { private Singleton2() {} private static Singleton2 instance; public static Singleton2 getInstance() { // 如果实例为空 则初始化一个赋给 instance if (null == instance) { instance = new Singleton2(); } return instance; } } 懒汉式，顾名思义，采用懒加载的方式，当需要用到的时候才初始化实例。 这种写法在多线程运行时存在安全问题，可能有多个线程同时进入判断条件，生成多个实例。不推荐使用。\n线程安全 public class Singleton3 { private Singleton3() {} private static Singleton3 instance; public static synchronized Singleton3 getInstance() { if (null == instance) { instance = new Singleton3(); } return instance; } } 优点：线程安全。 缺点：效率低。\n3. 双重检查锁 public class Singleton4 { private Singleton4() {} // volatile 对于 instance 所有的写将先于读 private static volatile Singleton4 instance; public static Singleton4 getInstance() { // 第一次检查 只在第一次创建实例的时候同步，之后不再同步 if (null == instance) { synchronized (Singleton4.class) { // 第二次检查 确保只有一个线程能进入 if (null == instance) { instance = new Singleton4(); } } } return instance; } } volatile 关键字：线程始终能读取到 volatile 修饰的变量的最新值。 这种写法线程安全，懒加载，效率高，推荐使用。\n4. 枚举类 public enum Singleton5 { /** * 唯一实例 */ INSTANCE; public void doSomething() { System.out.println(\u0026#34;doSomething\u0026#34;); } } 测试代码:\npublic class TestSingleton5 { @Test public void test() { Singleton5 instance = Singleton5.INSTANCE; Singleton5 instance1 = Singleton5.INSTANCE; // 比较内存地址 System.out.println(instance == instance1); // 比较 hashCode System.out.println(instance.hashCode()); System.out.println(instance1.hashCode()); instance.doSomething(); } } 运行结果：\ntrue 1645995473 1645995473 doSomething 这种写法简单高效，充分利用了枚举类的特性。是绝对的单例，即使反序列化也不会生成多个实例。推荐使用。\nTips 如何区分懒汉式和饿汉式\n懒汉式：顾名思义，它比较懒，只有需要用到的时候才会去生产，毕竟 Deadline 才是唯一生产力嘛。这里也可以理解为懒加载的形式。\n饿汉式：不愿意让自己多饿一分钟，宁愿先把饭做好了，饿了的话就可以直接吃饭。对应提前初始化好实例对象。\n推荐等级 枚举类 \u0026gt; 双重检查锁 \u0026gt; 饿汉式\n参考资料 【设计模式】单例模式的八种写法分析 ","date":"2021-03-13T23:11:16+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/little-rem.webp","permalink":"https://opoa.top/post/design-pattern-singleton-pattern/","title":"[设计模式] 单例模式实现"},{"content":"什么是单链表 单链表是链表的其中一种基本结构。它是构成单链表的基本结点结构。在结点中数据域用来存储数据元素，指针域用于指向下一个具有相同结构的结点。 因为只有一个指针结点，称为单链表。\n顺序表与单链表的区别 顺序表 (顺序存储)\n优点：可随机存取，存储密度高 缺点：要求大片连续空间，改变容量不方便\n单链表 (链式存储)\n优点：不要求大片连续空间，改变容量方便 缺点：不可随机存取，要耗费一定空间存放指针（地址）\n单链表结构 public class SinglyLinkedList \u0026lt;T\u0026gt; { /** * 头结点 */ private Node head; /** * 链表长度 */ private int size; /** * 结点 */ class Node { /** * 结点数据 */ private T t; /** * 下一个结点 */ private Node next; Node(T t, Node next) { this.t = t; this.next = next; } public Node(T t) { this(t, null); } @Override public String toString() { return \u0026#34;Node{\u0026#34; + t + \u0026#39;}\u0026#39;; } } } 单链表基本操作 插入 /** * 在指定下标处插入一个结点 * @param t * @param index * @return */ public boolean insert(T t, int index) { if (index \u0026lt; 0 || index \u0026gt; size) { throw new IllegalArgumentException(\u0026#34;非法下标参数\u0026#34;); } Node node = new Node(t); // 链表头部插入结点 if (index == 0) { node.next = this.head; this.head = node; this.size++; return true; } // 找到要插入结点的前一个结点 Node preNode = getNode(index - 1); // 前一个结点的下一个结点地址赋给当前结点的 next node.next = preNode.next; // 当前结点的地址赋给前一个结点的 next preNode.next = node; this.size++; return true; } 这里的插入是一个后插操作，既在某个结点之后插入一个结点，因为结点中存有下一个结点的地址，所以其后面的结点都很好寻找和操作了。 对应的还有前插操作，既在某个结点之前插入一个结点，这要求得拿到前一个结点，才能改变前结点的 next 指向。 要拿到前一个结点，可以从头结点开始，依次寻找，有了头结点就等于有了整个单链表。 如果没有头结点，就无法拿到前一个结点。此时只能对当前结点使用后插操作，在交换两个结点当中的数据，变相地实现了前插操作。\n删除 /** * 根据下标删除结点 * * @param index * @return */ public Node remove(int index) { if (index \u0026lt; 0 || index \u0026gt; size) { throw new IllegalArgumentException(\u0026#34;非法下标参数\u0026#34;); } Node removeNode = null; // 删除头结点 if (index == 0) { removeNode = this.head; // 把头结点的下一结点作为头节点 this.head = head.next; this.size--; return removeNode; } // 找到删除结点的前一结点 Node preNode = getNode(index - 1); // 把要删除的结点保存和返回 removeNode = preNode.next; // 把前一结点的下下个结点地址赋值给前一结点的 next preNode.next = preNode.next.next; this.size--; return removeNode; } /** * 根据数据删除结点 * * @param t * @return */ public Node remove(T t) { // 链表或元素为空 直接返回 if (size == 0 || null == t) { return null; } Node removeNode = null; for (int i = 0; i \u0026lt; size; i++) { Node node = getNode(i); if (t == node.t) { removeNode = remove(i); break; } } return removeNode; } 查找 /** * 根据下标获取一个结点 * * @param index * @return */ public Node getNode(int index) { if (index \u0026lt; 0 || index \u0026gt; size) { throw new IllegalArgumentException(\u0026#34;非法下标参数\u0026#34;); } Node curNode = this.head; // 下标为 0 直接返回头结点 if (index == 0) { return curNode; } for (int i = 0; i \u0026lt; index; i++) { curNode = curNode.next; } return curNode; } /** * 根据元素值返回对应下标 * * @param t * @return */ public Node getNode(T t) { // 从头结点开始 Node curNode = this.head; // 当前结点不为空且结点数据不等于传入元素 while (null != curNode \u0026amp;\u0026amp; curNode.t != t) { // 当前结点后移 curNode = curNode.next; } return curNode; } 完整代码 /** * @description: 自定义单链表 * @author: opoa * @create: 2021-02-28 20:21 **/ public class SinglyLinkedList \u0026lt;T\u0026gt; { /** * 头结点 */ private Node head; /** * 链表元素个数 */ private int size; /** * 结点 */ class Node { /** * 结点数据 */ private T t; /** * 下一个结点 */ private Node next; Node(T t, Node next) { this.t = t; this.next = next; } public Node(T t) { this(t, null); } @Override public String toString() { return \u0026#34;Node{\u0026#34; + t + \u0026#39;}\u0026#39;; } } public SinglyLinkedList() { this.head = null; this.size = 0; } /** * 根据下标获取一个结点 * * @param index * @return */ public Node getNode(int index) { if (index \u0026lt; 0 || index \u0026gt; size) { throw new IllegalArgumentException(\u0026#34;非法下标参数\u0026#34;); } Node curNode = this.head; // 下标为 0 直接返回头结点 if (index == 0) { return curNode; } for (int i = 0; i \u0026lt; index; i++) { curNode = curNode.next; } return curNode; } /** * 根据元素值返回对应下标 * * @param t * @return */ public Node getNode(T t) { // 从头结点开始 Node curNode = this.head; // 当前结点不为空且结点数据不等于传入元素 while (null != curNode \u0026amp;\u0026amp; curNode.t != t) { // 当前结点后移 curNode = curNode.next; } return curNode; } /** * 在指定下标处插入一个结点 * * @param t * @param index * @return */ public boolean insert(T t, int index) { if (index \u0026lt; 0 || index \u0026gt; size) { throw new IllegalArgumentException(\u0026#34;非法下标参数\u0026#34;); } Node node = new Node(t); // 链表头部插入结点 if (index == 0) { node.next = this.head; this.head = node; this.size++; return true; } // 找到要插入结点的前一个结点 Node preNode = getNode(index - 1); // 前一个结点的下一个结点地址赋给当前结点的 next node.next = preNode.next; // 当前结点的地址赋给前一个结点的 next preNode.next = node; this.size++; return true; } /** * 使用头插法插入一个元素 * * @param t * @return */ public boolean insertFirst(T t) { return insert(t, 0); } /** * 使用尾插法插入一个元素 * * @param t * @return */ public boolean insertLast(T t) { return insert(t, size); } /** * 根据下标删除结点 * * @param index * @return */ public Node remove(int index) { if (index \u0026lt; 0 || index \u0026gt; size) { throw new IllegalArgumentException(\u0026#34;非法下标参数\u0026#34;); } Node removeNode = null; // 删除头结点 if (index == 0) { removeNode = this.head; // 把头结点的下一结点作为头节点 this.head = head.next; this.size--; return removeNode; } // 找到删除结点的前一结点 Node preNode = getNode(index - 1); // 把要删除的结点保存和返回 removeNode = preNode.next; // 把前一结点的下下个结点地址赋值给前一结点的 next preNode.next = preNode.next.next; this.size--; return removeNode; } /** * 根据数据删除结点 * * @param t * @return */ public Node remove(T t) { // 链表或元素为空 直接返回 if (size == 0 || null == t) { return null; } Node removeNode = null; for (int i = 0; i \u0026lt; size; i++) { Node node = getNode(i); if (t == node.t) { removeNode = remove(i); break; } } return removeNode; } /** * 返回当前链表长度 * * @return */ public int length() { return this.size; } /** * 打印输出单链表 * * @return */ @Override public String toString() { if (size == 0) { return null; } Node curNode = this.head; StringBuilder sb = new StringBuilder(); while (null != curNode) { sb.append(curNode.t.toString()) .append(\u0026#34; -\u0026gt; \u0026#34;); curNode = curNode.next; } sb.append(\u0026#34;null\u0026#34;); return sb.toString(); } } 参考资料 2020 王道考研 数据结构【单链表的定义】 2020 王道考研 数据结构【单链表的插入删除】 2020 王道考研 数据结构【单链表的查找】 Java 基础 \u0026ndash; 单链表的实现 ","date":"2021-02-25T00:01:57+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/rem-horn-dark.webp","permalink":"https://opoa.top/post/single-linked-list-in-java/","title":"[Java] 单链表"},{"content":"定义 线性表 线性表是具有 相同 数据类型的 n (n ≥ 0) 个 数据元素 的有限序列\n顺序表 用 顺序存储 的方式实现线性表顺序存储。把 逻辑上相邻 的元素存储在 物理位置上也相邻 的存储单元中，元素之间的关系由存储单元的邻接关系来体现。\nJava 代码实现 import java.util.Arrays; /** * @description: 自定义顺序表 * @author: opoa * @create: 2021-02-17 14:53 **/ public class MyStringArray { /** * 数据 */ private String[] data; /** * 当前长度 */ private int length = 0; /** * 最大容量 默认为 10 */ private int capacity = 10; public MyStringArray() { this.data = new String[capacity]; } public MyStringArray(int capacity) { this.capacity = capacity; this.data = new String[capacity]; } public int getLength() { return length; } public int getCapacity() { return capacity; } /** * 在指定下标处插入一个元素 * * @param index 下标 * @param str 新增的字符串 * @return 插入是否成功 */ public boolean insert(int index, String str) { // 下标不能超过数组最大容量 if (index \u0026gt; capacity) { throw new IndexOutOfBoundsException(\u0026#34;数组下标越界\u0026#34;); } // 下标不能超过数组当前长度 if (index \u0026gt; length) { throw new IllegalArgumentException(\u0026#34;下标参数异常\u0026#34;); } // 从后往前移动每个元素 for (int i = length; index \u0026lt; i; i--) { data[i] = data[i-1]; } data[index] = str; ++ length; return true; } /** * 在数组末尾插入一个元素 * * @param str 插入的字符串 * @return 插入是否成功 */ public boolean insert(String str) { // 如果数组长度等于最大容量，表示已经装满，直接返回 if (length == capacity) { return false; } data[length] = str; ++ length; return true; } /** * 根据下标删除一个元素 * * @param index 数组下标 * @return 被删除的元素 */ public String remove(int index) { if (index \u0026lt; 0 || index \u0026gt; length - 1) { throw new IllegalArgumentException(\u0026#34;下标参数异常\u0026#34;); } String temp = data[index]; for (int i = index; i \u0026lt; length; i++) { data[i] = data[i + 1]; } -- length; return temp; } /** * 根据下标获取一个元素 * * @param index 数组下标 * @return 返回的元素 */ public String getElement(int index) { if (index \u0026lt; 0 || index \u0026gt; length - 1) { throw new IllegalArgumentException(\u0026#34;下标参数异常\u0026#34;); } return data[index]; } /** * 根据字符串的值返回其元素所在的下标 * * @param value 要查找的字符串 * @return 该字符串在数组中的下标 */ public int getElement(String value) throws Exception { for (int i = 0; i \u0026lt; length; i++) { if (value.equals(data[i])) { return i; } } throw new Exception(\u0026#34;字符串不存在\u0026#34;); } /** * 对数组进行 2 倍扩容 */ public void enlarge() { String[] strs = new String[capacity * 2]; for (int i = 0; i \u0026lt; length; i++){ strs[i] = data[i]; } this.data = strs; this.capacity = capacity * 2; } @Override public String toString() { return Arrays.toString(data); } } 总结 参考资料 2020 王道考研 数据结构【顺序表的定义】 2020 王道考研 数据结构【顺序表的插入删除】 2020 王道考研 数据结构【顺序表的查找】 ","date":"2021-02-15T09:35:35+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/lost-in-space-floating-astronaut-galaxy.webp","permalink":"https://opoa.top/post/sequential-list-in-java/","title":"[Java] 顺序表"},{"content":"题目 描述 给定一个整数数组 nums 和一个整数目标值 target，请你在该数组中找出 和为目标值 的那 两个 整数，并返回它们的数组下标。\n你可以假设每种输入只会对应一个答案。但是，数组中同一个元素不能使用两遍。\n你可以按任意顺序返回答案。\n示例 1 输入：nums = [2, 7, 11, 15], target = 9 输出：[0, 1] 解释：因为 nums [0] + nums [1] == 9 ，返回 [0, 1] 。\n示例 2 输入：nums = [3, 2, 4], target = 6 输出：[1, 2]\n示例 3 输入：nums = [3, 3], target = 6 输出：[0, 1]\n难度 简单\n解法 1 /** * 暴力穷举法 */ public static int[] solution1(int[] nums, int target) { for (int i = 0; i \u0026lt; nums.length; ++i) { for (int j = i+1; j \u0026lt; nums.length; ++j) { if (nums[i] + nums[j] == target) { return new int[] {i, j}; } } } return new int[0]; } 使用双重 for 循环，判断数组元素两两相加的值是否等于目标值，如果等于，则返回两个元素各自的下标，无匹配的情况返回空数组。 外层循环从左到右依次遍历元素，首轮内层循环遍历完成后若未找到正确答案数组，则第一个元素不属于正确答案数组元素之一，进行下一轮遍历，直至找到正确答案为止。\n解法 2 /** * 哈希法 */ public static int[] solution2(int[] nums, int target) { HashMap\u0026lt;Integer, Integer\u0026gt; tempMap = new HashMap\u0026lt;Integer, Integer\u0026gt;(nums.length); for (int i = 0; i \u0026lt; nums.length; ++i) { if (tempMap.containsKey(target - nums[i])) { return new int[] {tempMap.get(target - nums[i]), i}; } tempMap.put(nums[i], i); } return new int[0]; } 利用哈希表仅一层 for 循环就可得出答案。\n先创建一个 Integer 类型同时作为 key 和 value 的 HashMap, key 是当前元素的值，value 是当前元素在数组中的下标。比如 target = 18, int[] nums = [1, 5, 7, 11], 循环三次的 map 结果是 {1 = 0, 5 = 1, 7 = 2} , 判断当前 map 是否存在正确值之一的 key 存在，不存在则添加到 map 中，存在则找到答案数组，直接返回。\n上面的例子答案是 [2, 3], 循环遍历到 7 的时候，18 - 7 = 11, 此时 map 中并没有 11 作为 key 存在，存入一个元素 key = 7, value = 2 到 map 中，遍历到 11 的时候， 18 - 11 = 7, 由于此前存入过 7 为 key 的键值对，可得 11 和 7 为正确答案，在返回他们对应下标的数组即可。\n","date":"2021-02-08T17:04:36+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/screen-code.webp","permalink":"https://opoa.top/post/leetcode-two-sum/","title":"[LeetCode 题解] 001 - 两数之和"},{"content":"搭建环境 Vue 2.9.6 Element-UI 2.13.2 准备工作 打开 百度地图开放平台 官网，来到页面最下方进行注册，已有百度账号可直接登录。\n从导航栏处点击进入到控制台\n左侧菜单 —\u0026gt; 应用管理 —\u0026gt; 我的应用 —\u0026gt; 创建应用\n应用名称可随意填写，应用类型选择浏览器端。\n创建完成之后我的应用中就可以看到，AK (Access Key) 待会儿需要用到。\n接入项目 新建一个单独的 js 文件，作用是传入 ak 的值，在页面的 Header 动态拼接一个加载百度地图 js 的 Script 标签。文件名称和位置随意，我这里文件的名称是 gis.js。\nexport default function loadBMap(ak) { return new Promise(function(resolve, reject) { if (typeof BMap !== \u0026#39;undefined\u0026#39;) { resolve(BMap) return true } window.onBMapCallback = function() { resolve(BMap) } let script = document.createElement(\u0026#39;script\u0026#39;) script.type = \u0026#39;text/javascript\u0026#39; script.src = \u0026#39;http://api.map.baidu.com/api?v=2.0\u0026amp;ak=\u0026#39; + ak + \u0026#39;\u0026amp;callback=onBMapCallback\u0026#39; script.onerror = reject document.head.appendChild(script) }) } 在需要的页面引入该 js 即可\n完整代码\n\u0026lt;template\u0026gt; \u0026lt;div class=\u0026#34;app-container\u0026#34;\u0026gt; \u0026lt;el-card class=\u0026#34;box-card\u0026#34; :body-style=\u0026#34;{ padding: \u0026#39;0px\u0026#39; }\u0026#34;\u0026gt; \u0026lt;div slot=\u0026#34;header\u0026#34; class=\u0026#34;clearfix\u0026#34;\u0026gt; \u0026lt;span\u0026gt;地图\u0026lt;/span\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;map-area\u0026#34; :id=\u0026#34;mapId\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/el-card\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import loadBMap from \u0026#39;@/api/dataFocus/gis.js\u0026#39; export default { data() { return { mapId: \u0026#39;BMap-\u0026#39; + parseInt(Date.now() + Math.random()), myMap: null, siteName: undefined, } }, created() { this.siteName = \u0026#39;成都\u0026#39;; }, mounted() { this.initMap() }, methods: { initMap() { // 填入应用的 AK loadBMap(\u0026#39;xxxxxxxxxx\u0026#39;) .then(() =\u0026gt; { // 百度地图 API 功能 this.myMap = new BMap.Map(this.mapId) // 创建 Map 实例 this.myMap.centerAndZoom(this.siteName, 11) // 初始化地图，设置中心点坐标和地图级别 // 添加地图类型控件 this.myMap.addControl( new BMap.MapTypeControl({ mapTypes: [BMAP_NORMAL_MAP, BMAP_HYBRID_MAP] }) ); this.myMap.addControl(new BMap.ScaleControl({ anchor: BMAP_ANCHOR_TOP_LEFT })); this.myMap.setCurrentCity(this.siteName) // 设置地图显示的城市 此项是必须设置的 this.myMap.enableScrollWheelZoom(true) // 开启鼠标滚轮缩放 }) .catch(err =\u0026gt; { console.log(\u0026#39;地图加载失败\u0026#39;) }) } } } \u0026lt;/script\u0026gt; \u0026lt;style scoped\u0026gt; .map-area { width: 100%; height: 500px; } \u0026lt;/style\u0026gt; 效果图 参考资料 记录 Vue 异步加载百度地图 ","date":"2021-02-01T13:56:49+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/city-draw.webp","permalink":"https://opoa.top/post/how-to-integrate-baidu-maps-with-vue/","title":"记 Vue 接入百度地图"},{"content":"概念 通过 事前预估 算法 时间开销 T (n) 与 问题规模 n 的关系 (T 表示 time) 函数，来描述该算法的运行时间。\n示例 1 // 逐步递增型爱你 public static void main(String[] args) { // n 为问题规模 loveYou(3000); } private static void loveYou(int n) { int i = 1; // ------------------------------------- (1) while (i \u0026lt;= n) { // ------------------------------- (2) i++; // --------------------------------------- (3) System.out.println(\u0026#34;I Love You \u0026#34; + i); // ----- (4) } System.out.println(\u0026#34;I Love You More Than \u0026#34; + n); // (5) } 结果\nI Love You 2995 I Love You 2996 I Love You 2997 I Love You 2998 I Love You 2999 I Love You 3000 I Love You 3001 I Love You More Than 3000 语句 频度 :\n(1) 执行 1 次\n(2) 执行 3001 次\n(3)(4) 执行 3000 次\n(5)\t执行 1 次\nT(3000) = 1 + 3001 + 2 * 3000 + 1\n时间开销与问题规模 n 的关系：\nT(n) = 3n + 3 ≈ 3n\n时间复杂度表达式可以只考虑阶数高的部分\n上述表达式可以简化为 T (n) = O (n)\n大 O 表示\u0026quot;同阶\u0026quot;，同等数量级。即：当 n→∞时，二者之比为常数\n计算技巧 （a）加法规则\nT(n) = T1(n) + T2(n) = O(f(n)) + O(g(n)) = O(max(f(n), g(n)))\n↓\u0026mdash;↓\u0026mdash;↓\u0026mdash;↓\n多项相加，只保留最高阶的项，且系数变为 1\n（b）乘法规则\nT(n) = T1(n) * T2(n) = O(f(n)) * O(g(n)) = O(f(n) * g(n))\n↓\u0026mdash;↓\u0026mdash;↓\u0026mdash;↓\n多项相乘，都保留 时间复杂度大小关系\n示例 2 // 嵌套循环型爱你 public static void loveYou(int n) { int i = 1; while (i \u0026lt;= n) { i++; System.out.println(\u0026#34;I Love You \u0026#34; + i); for (int j = 1; j \u0026lt;= n; j++) { System.out.println(\u0026#34;I\u0026#39;m Iron Man\u0026#34;); } } System.out.println(\u0026#34;I Love You More Than \u0026#34; + n); } 时间开销与问题规模 n 的关系： T(n) = O(n)+O(n2) = O(n2)\n示例 3 // 指数递增型爱你 public static void loveYou(int n) { int i = 1; while (i \u0026lt;= n) { i = i * 2; // 每次翻倍 System.out.println(\u0026#34;I Love You \u0026#34; + i); } System.out.println(\u0026#34;I Love You More Than \u0026#34; + n); } 结果\nI Love You 2 I Love You 4 I Love You 8 I Love You 16 I Love You 32 I Love You 64 I Love You 128 I Love You 256 I Love You 512 I Love You 1024 I Love You 2048 I Love You 4096 I Love You More Than 3000 计算上述算法的时间复杂度 T (n):\n设最深层循环的语句频度 (总共循环次数) 为 x, 则由循环条件可知，循环结束时刚好满足 2x\u0026gt;n\nx = log2n + 1\nT(n) = O(x) = O(log2n)+O(1) = O(log2n)\n示例 4 // flag 数组中乱序存放了 1~n 这些数 int[] flag = {1...n}; loveYou(flag, 3000); public static void loveYou(int[] flag, int n) { System.out.println(\u0026#34;I\u0026#39;m Iron Man\u0026#34;); for (int i = 0; i \u0026lt; n; i++) { // 从第一个元素开始查找 if (flag[i] == n) { // 找到元素 n System.out.println(\u0026#34;I Love You \u0026#34; + n); break; // 跳出循环 } } } 计算上述算法的时间复杂度 T (n):\n最好 情况： 元素 n 在第一个位置 =\u0026gt; T (n) = O (1) 最坏 情况： 元素 n 在最后一个位置 =\u0026gt; T (n) = O (n) 平均 情况： 假设元素 n 在任意位置的概率相同为 1 / n =\u0026gt; T (n) = O (n)\n总结 参考资料 2020 王道考研 数据结构【算法的时间复杂度】 ","date":"2021-01-31T23:32:56+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/cup-rem.webp","permalink":"https://opoa.top/post/how-long-an-algorithm-takes-to-run/","title":"算法的时间复杂度"},{"content":"有些人仅仅是活着，就已经拼尽了全力。\n直到今天，我才真真切切理解了这句话。\n命运，数次无情地摧残着这个热爱生活又努力的普通人，乃至最后埋葬。\n他的奶奶生病，因病致贫，父母躲债跑路，留下他一人高中辍学自生自灭，出来打工遭遇黑心公司，一个月薪资八百元，除去房租每月仅剩三百元，就这样公司还欠薪不发，前去讨薪被踩断身份证。无奈只能回家，后来听说家里房子要拆迁，家人们也回来了，生活好像就要迎来曙光。然而，他那群有血缘关系的家伙回来只是为了把他赶出去……\n墨茶生平(转自 B 站)\n黑茶他家住大凉山，原来家庭也算可以，不是很富裕也不是很贫困，他说他高中学习成绩很好。直到他奶奶生病，家里借了很多钱，最后人没救回来，钱欠了一屁股。于是他父母就跑走躲债去了，只留下黑茶一个人面对生活。为了生存，他去成都打工，装卸工，一个月八百，房租就要五百，三百元的生活费，吃了上顿没有下顿，长期的饥饿与劳作的摧残下，他得了胃部疾病，胃痛到彻夜难眠。 2018 年四五月份，因雇主欠薪不发，他前去讨薪，但是却被老板踩断了身份证，赶了出去。失去生活来源的他，又遭遇诈骗，用花呗五百元额度换了两百元现金。房租眼看就要到期，身体又不能支撑体力劳作，他决定回家。这大概是我们第一次援助他，他没有路费，无奈之下向我们求助，这才凑足了路费，最后得以回家。 他想去医院看病，群里也有学医的人建议他去看病，但是等到了医院才发现他没有医保，具体的原因已经记不清了，好像是欠缴费用，导致不能享受。最后无奈在当地医院用他仅剩的一点存款加上一些援助，在当地医院治病，诊断为胃溃疡和胃炎，但由于不能承受长期住院治疗的费用，输了几天液之后又匆匆回家。好在村里人善良，他回家之后并没有被要求承担家中的债务。否则他可能连片刻安稳都不可得了。 后来他又有了希望，据说他家房子要拆迁了，能拿三十多万，只要能熬到房子拆迁，人生就算破局了。于是虽然被胃病折磨的难以正常生活，他还是决定做些事，赚些钱。这时候我们也零零碎碎资助了一些钱，但是因为群友多是些学生，援助只能让黑茶勉强度日。然后有人提议，让他做 vup 吧，多少能赚些钱。接着群友有的把一些退下来的电脑零件给他寄过去，有的给他画了一张皮，有的给他做了模型。 就这样，墨茶出道了。虽然人气低迷，直播只有群友和一两个老观众看，但他还是坚持着这份事业。 后来，他的家属回家了。他本以为苦日子就要到头了，可现实又给了他当头一棒：他的家属只是来抢走他仅有的遮风挡雨的屋子的。他的家属当天就找人殴打他，把他扫地出门了。在这种情况下，他只能去县城租一间小房，边打工边直播，以此度日。 即使是如此荒诞的人生都没能击垮他的心灵。20 年，他人生中最后的冬天格外寒冷。缺衣少食，疾病缠身的他看到了在寒风中瑟瑟发抖的小猫。尽管他自己买几粒 1.8 元的药片都心疼不已，他仍然为小猫买了舒化奶，拿着破箱子和破衣服为小猫御寒取暖。最后，与他相熟的小卖部老板抚养了这只小猫。 但是谁来关爱他呢？贫病交加，饥寒交迫的他最后还是死在了 2021 年 1 月 4 日或这之后的某个冬日。 酮症酸中毒的死因是一直跟踪他身体状况的我个人的推测，并不代表已经确认其死因为酮症酸中毒，望周知。 作者：御坂伊里奇_Official\n即使命运是这样的造化弄人，他也并没有抱怨什么，更多的只是调侃，和对生活的热爱，哪怕只是一点小小的幸福。\n为了喜爱的事情熬夜\n默默努力地直播着，小心经营着自己的两百多位粉丝\n这张图看着真的很揪心，可能这就是墨茶活着的时候，内心中孤独和无助的真实写照吧。\n最后的日子里，说很想很想吃草莓，却因为草莓太贵了吃不起。太贵了，太贵了\u0026hellip; 一盒草莓十多二十块，可对于一年生活费只有不足 2500 的他来说，得几天的饭钱，才能够吃上一次草莓啊。\n2021 年 1 月 4 日或之后的某个冬日，在饥寒交迫和病痛的折磨下，墨茶离开了。\n你好傻啊！明明可以活下来的，大家也都愿意帮助你，你也只是平凡地努力着，相信总有一天一切都会变好。群友援助给你的钱，说以后赚了钱一定还上。 那现在你有了好多好多的粉丝，终于能赚好多好多的钱了，那承诺呢？可以回来兑现吗，求求你了。\n唉，我今天一整天都处于破防的状态，早上红着眼睛，午休睡不着，泪一直流，下班坐地铁红着眼睛，晚上写这个好几次写不下去，被自己打断。我想也许很多人和我一样，想做点什么，但又改变不了什么，因为我们来晚了。\n很抱歉以这样的方式认识你。\n墨茶，你住在大凉山，我以后会来看你的，一定。\n晚安🌙，墨茶。\n","date":"2021-01-22T21:20:10+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/post/mocha9.webp","permalink":"https://opoa.top/post/dedicate-to-mocha-official/","title":"献给墨茶"},{"content":"转自 https://zhuanlan.zhihu.com/p/68556316 前言 这是我很久前写的一篇文章，当时是一时有感而发就写了出来。过去了这么多年后，我依然觉得这篇文章写的适用于现在的很多年轻人。所以我决定再把它翻出来，写在这里。下文中提到的 “几天前” 等时间词汇都是距今大概十来年的事情了。\n请注意，虽然我将标题写为 “不要从大众平台找到学习之路”，但是不代表我反对关注和浏览大众平台的文字。\n正文 曾经 (很多很多年前) 我试图在不同的平台上建立交流群，以便对计算机科学有兴趣的人可以交流。但是由于我规定问问题前至少先 Google（至少百度）一下，至少先了解一下自己的问题是什么，再发言。当然我还规定了问问题应该提供的信息以及提问方式，结果导致发言的人越来越少，最后变得一片寂静，不得不解散。\n最开始我不明白为什么会这样，明明每个进来的人都是怀着一颗想获得知识的心而来，却在我简单的一条规则下变的沉默了。明明这条规则应该是每一个试图从别人那里获得知识的人都最低限度应该保证的前提。试想如果我下一次往 linux-xfs 邮件列表或 #xfs IRC channel 里发一句：“有人在吗？我有个问题。” 或者 “谁知道我为什么用不了我的 U 盘了？”。 我相信这样久了 Dave 他们一定会把我拉到黑名单里过滤掉的，因为和这样的人说话简直就是浪费宝贵的生命。\n可是看到很多其它的学习群，比如冠以 linux 学习为名义的 QQ 群或公众号都在良好的运行着。我不明白他们是怎么做到的，所以尝试我加入了几个这样的 QQ 群和公众号，并尝试为里面的人解答各种问题，以便寻找原委。但是很快我便受不了了，受不了的不是问题太简单，而是里面讨论问题的过程简直不堪入目。那种感觉就像是你在看着一个懵懂的孩子，正在用无比崇拜的心情，试图从她的那群整天只会在村口嚼舌头的七大姑八大姨那里了解怎么做一个有价值的人。看得让人炸舌，让你觉得你只能眼睁睁的看着一个个充满渴望的眼神，前仆后继的走上只能村口嚼舌头的道路。\n举几个例子，也是我这几天在那几个 QQ 群里司空见惯的事情（下文中每个单独事例的 ABCD… 都是重新定义的个人，不是上文的延续）：\n事件 1 A 发了一个截图显示他尝试在已有一个 windows 的基础上再安装一个 Linux 系统，到分区那步不知道怎么弄了，于是问道 “哪位大神知道下面该干什么？”。\n然后一个好心的 B 出来说（原话）：“后面带 ntfs 的（windows 占用的分区）别动，不带的删了重新分区。”\nA 听了赶忙问：“大神，这怎么分啊？”。\nB 听到别人叫他大神开始高兴了，侃侃而谈到 “先分一个 /，然后 /boot 分个 500M，再分一个 swap 一般是内存的一或二倍。”\nA 听的一头雾水，因为 A 根本连分区是什么都不太懂。我嘴欠，说了一句：“我劝你先找个没用的虚拟机，在上面练习一下 linux 的分区和挂载，理解一下分区的意思，再回来弄。要不你一知半解的弄对已经存在的数据很危险。”\n结果没有人回应我。B 继续说：“点那个 + 号就分区了”。\nA 听他的瞎捣鼓了一会，截图显示他把整个 sda 分成了一个分区挂在了 / 分区上，然后点开挂载点处的下拉菜单问 B 道：“怎么找不到 swap 啊？”\nB 竟然没看出问题的严重性，继续说：“据我了解好像后来的 linux 安装时都不用分 swap 了，没事你不分也行，装好系统会自动给你分出一个 swap 的。“\n然后又跳出一个 C，C 信誓旦旦的说：” 你直接手写 swap 进去就行”。\nA 连忙谢 C 大神，然后把刚才挂载在 / 上的分区挂载到了 /swap 上…… 当然后果肯定是不行，在不断有更多的人加入讨论，而且尝试了无数次类似这样的 “瞎猫碰死耗子” 的行为后，A 终于把系统搞乱了。最后这些人信誓旦旦的把锅推给 A，说他小白自己把系统搞乱了，然后还一副迁就的样子劝他：“看你能力也有限，你就只分一个根分区就得了，swap 不分也行。”\nA 照做后点继续按钮弹出一个错误信息，大致意思是说 \u0026ldquo;设备忙，不能分区\u0026rdquo;。A 截了这个错误信息的图给这些人，这些人像是没看懂一样说：“那还是得分 swap，看来不分不让继续”（天啊，睁着两眼说瞎话）……\n我实在看不下去了，就没再看他们聊什么，只知道最后这个 A 还没装上 linux 就已经不小心把他的 windows 系统给删了，还很难过的说 “我的游戏啊，我的记录啊”……\n事件 2 A (不上面的 ABC 了) 先来一句：“有人在吗？我想问个问题”。\n等了好久没人回，又问：“没人吗？”\nB 答：“当然有了” 。\nCDEF 等也起哄的说 “有人，我们不是人吗？”。\n然后 A 说道（原话）：“我在一个目录下有一个文件夹，这个文件夹下还有文件夹，以此类推有大概四五层。里面不同的位置放着一些不同的 .txt 文件。我现在想把这些 .txt 文件全拿出来放到另外一个单层目录里。只拿出 .txt 文件，不要原来的文件夹。各位大神有没有办法啊？”\nBCDEF 销声了，这时一个 G 跳出来写了一条 find 命令，大概是这样写的\n\u0026ldquo;find ./ -type f -name *.txt -exec mv {} /tmp/ \u0026quot; 对，你没有看错，结尾就是少一个分号。我以为他就是不小心漏打了，心想这还算是一个知道点什么的人。然后 A 把他这句命令直接贴到命令行执行，然后说：\u0026ldquo;我刚执行你给的命令，文件没有被挪走啊？还报错了（他也不说也不看是什么错）\u0026rdquo;\n我刚要插嘴说 A 没理解 G 的意思，结果 G 的一句话直接把我也给噎住了，G 说：“我也是从百度上查来的，这个命令好像就是操作当前目录的 txt 文件，然后把文件挪到 /tmp 里，要不你这样，你在你要操作的目录里把这行写到一个 move.sh 的脚本里，然后执行那个脚本。然后再到 /tmp 下把 .txt 文件弄到你想拷贝到的目录下。”\n我已彻底无语，我说：“你有个问题，别人给你指明用 find 命令，那你至少应该去看一下 find 命令的文档啊。在这像没头苍蝇一样乱撞不是办法的。而且你这个命令结尾少一个分号。”\nG 连忙说：“啊，那个分号有用啊？我以为就是个结束符，没用。呵呵”。\n我还解释了一下 find 的那几个参数的意思，结果 A 看到我又回答他了，赶忙像饿了三天的人看到面包一样，扑过来就就说：“那大神你告诉我一下应该怎么写这个命令吧”。 还发了一个非常可怜的表情。\n我当时心里只有两个想法：一、我刚才的话白说白劝了。二、可怜之人必有可恨之处。于是不再接下面的话。\n事件 3 A 说：“我忘了我机器的启动密码，只能重装系统了吗？大神救我！”\n我不解的问他：“你给你的机器哪个启动阶段设置了密码？”。\nA 说 “就是 root 密码。”\n我无语了半天，心说我真是嘴欠，但还是好心的告诉他：“你得说是登录口令。你不能说启动密码，因为启动过程很复杂，没人知道你指什么启动密码。”\nA 听后似懂非懂，继续说：“那大神有办法挽救吗？”\n我很诚恳的跟他说：“方法有很多，但是从你刚才的提问看，我说了你不一定知道怎么做。比如在 init 的时候直接获取一个 bash 交互，不经过登录认证。或者用另一个 linux 系统挂载你忘了密码的系统的跟分区和必要分区，然后 chroot 过去修改 root 口令。等等”。\n他说：“好的，我去尝试一下第二种方法”。\n结果这时候一个人跳出来发了一个从百度上搜索出来的结果的截图，并告诉 A 照这个做就行。A 一下子高兴了，连连道谢，谢谢大神指点，然后去执行上面说的每一个他都不理解的步骤去了。我大概看了一眼，知道他肯定不可能按照上面说的做成功，因为出入还是不小的，直接照着敲肯定不行。但是我寄希望于他能去求一下甚解，尝试理解每个步骤背后的意思之后把一些步骤上的偏差修正。结果当天下午我又看到他可怜巴巴的在群里恳求大神指点他那个问题，他果然一点进展都没有。\n事件 4 A 发了一张截图（看似很高端），问道 “有人知道这是什么吗？”\nBCDEF 等几个人附和道:“不知道，这什么啊？还望大神指点。”(现在真是 “大神” 泛滥，“大神” 这个词总能让我联想到过去人们经常叫 “大仙” 的场景)\nA 特骄傲地对 B 的说：“我也不知道，我一哥们刚按照百度上的方法黑进了一个防火墙，说是以色列还是哪的，问我黑进去现在能干什么？ 我也不知道，所以来群里问问。”\n一众人等赶忙膜拜，大呼 “黑客啊！牛 B。 我以前也学过一点，后来不练都忘了。”\n还有人说：“我以前搞过防火墙，几年不弄了，忘了怎么搞了”。\nA 似乎觉得很得意，说：“这都黑进去了，我就是不太了解黑进去后该怎么做，要不早就搞破坏了。”\n一众人还回应说：“是啊，是啊，不做点什么多可惜，有人知道防火墙能干什么吗？”\n看着他们各吹各牛的样子，让我想起了搞笑版华山论贱。。。\n类似上面的例子太多太多了，简直不胜枚举。我至今还没有在这几个群里碰到一个 “合格” 的问题，大部分都是截一个图（图上有明显的错误信息或提示信息） ，然后发上来问 “哪位大神知道上图怎么回事？” 这种情况是最多的。更有甚者连他做了什么得到什么错误都不说，比如直接问 “我 U 盘用不了了，哪位大神知道怎么回事？” 问问题的都是这样的。\n我并不认为问低难度的问题就是愚蠢的表现，衡量的标准不是问题的难易，而是问问题的态度。这个态度不是你叫不叫别人一声 “大神” 的事，而是你作为一个寻求帮助的人，要让别人看到你身上有两个亮点：一、你在问问题前已经尝试研究和解决这个问题，现在你碰到瓶颈了。二、你现在的知识水平足够让别人给你讲明白你的问题（就像一个连四则运算都还不会的人，怎么也没法给他讲明白微积分的问题。）\n问问题的人有问问题的义务，那回答问题的人也有回答问题时的义务，但是我这几天的感觉却是大部分接问题说下去的人简直在 “毁” 人不倦。每次有人问我问题，除了有明确答案的，我一般都以提示和引导的方式引导提问者按照某一思路或给出我知道的一些关键字让他顺着去自己研究。因为谁也不一定比谁多知道多少，任何一个人在一个问题上多研究一点，他可能就比你知道的多了，而且片面的固定步骤往往把人引入深渊。而最可怕的事情就是，一些本来自己就一瓶子不满半瓶子逛当的人，却在以 “大神” 的视角向后来者传播着他们充满错误理解的知识以及学习方法上的陋习。这简直太可怕了。我一直强调读第一手材料的重要性，因为这就像咬耳朵传话一样，一段话被传的层数越多，越容易背离它原来的意思。相信很多人都看过类似的笑话或节目。更可怕的是一群以为自己拜了真菩萨的人正在跟着这样一群泥菩萨们过河。而那些真菩萨其实都没有把自己当作大神，越是接近神的人，越觉得自己什么都不懂，越不敢轻易把话说死，怕自己已有的思想束缚或破坏了后来者更上一层楼的理解和思维。\n我一直的观点就是 “永远不要寄希望于能从大众娱乐平台上找到真正的学习之路”，如 QQ、微信、校内（以前火，现在已经没什么人用了）、微博、贴吧等等地方，聚集的都不是真正想要潜心研究知识的人，而是为了娱乐、为了利益、为了名声等而来。大众娱乐平台可以传播科普知识等，因为像科普知识、生活妙招、人文评论等也有娱乐的效果。但是对于严谨的学习，这些地方绝对不是合适的场所。不光是学不学的到真东西的问题，而且它还会影响你的思维和做事方式，让你陷入 “吹牛”、“浮躁”、“闲扯”、“懒惰”、“虚荣” 和 “盲目” 等的泥潭，而不自知。我自己虽然也在知乎、CSDN 上写写东西，但是我自认为自己就是来发表发表纯主观看法闲聊的，顶多是写一些半技术的文章给自己做做笔记（有机会和人闲谈几句），仅此而已，根本就没有通过大众平台传播理论知识这种 “崇高” 的想法。\n真正理科性（文科我不提是因为我不懂）专业知识的学习永远是小众的。既不是下里巴人，也不是阳春白雪，它就是枯燥的苛刻的研究世界，它的关键字是：个人、潜心、忍耐、缓慢、艰难、重复、无趣、严谨等等。而大众世界追求的是关键字是：激情、搞笑、心动、放松、可吐槽、故事性等等。这根本就是两条基本不相交的路，妄图通过大众平台学习理论知识，真的跟过去经常有人问 “有没有又轻松又能学到真本事的方法？” 一样。\n（下面的话可能会引起一些人的不适，请谨慎阅读。当时写的时候有点小激动了）\n……\n……\n很多崇拜大神的人以为自己在崇拜的路上与大神越走越近，他们可以说出很多大神的名字和事迹，甚至用过大神的产品，读过大神的软文。而实际上是那些你真正崇拜的 “真神”，他们在和另外一群 “真神” 们正在辛勤的打造着通往未知世界的道路，在他们的脚下有数不清的跟随者支撑着，做的好的跟随者可以被他们带到超越他们的地方，做的一般的也在为这一建设填砖加瓦。\n而更多的自认为的崇拜者，实际上根本就是 “怀春的粉丝” 在 “真神” 们不知道的遥远的一片泥潭里扑通而已。那是另一片更容易进入的世界，新跳进去的人和在岸边观望的人都被先于他们跳进去的人带领着。而那些先跳进泥潭的人不一定比后来的多知道多少，但是有一点可以肯定的就是先跳进的一定染的更黑，所以更黑的人就是更厉害的 “大神”，跟随他们就能早日超脱成神，这种观念在这片远离 “真神” 的世界上传播着。\n补充一 我题目写的是不要试图从大众平台找到学习之路，注意是 “学习之路”，也就是说我的观点是不能希望依靠大众平台学有所成，理由我上面已经说了。但是这不代表你绝对不能从大众平台去搜索 “经验之谈”。\n然而会学习的人和不会学习的人的其中一个区别就是，会学习的人有自己的判断或者说追求客观的判断，而不会盲从。不会学习的人很容易受到大众平台上各种言语的 “蛊惑”，轻易的被别人带跑。会学习的人在看过一个回答或一个文章后，会想办法理解来龙去脉，至少应该可以看出里面论述的错误或片面或模糊的地方，会客观性的评估内容的可参考性，会通过更客观的资料来分析和认证自己认为 “可能错误” 的地方，从而将这部分只是真正化为己有，而不是单纯的被别人带着走。不会学习的人到死都只会发出两种感叹：“竟然不工作，求大神救救孩子！” 或者 “竟然能工作了，好神奇！”。\n大众平台上有没有学习之路？我认为没有，因为它不符合学习所需要的各种条件因素。大众平台上有没有有参考性的文字？我认为有，只是你要有足够的知识水平和端正的学习态度才能较游刃有余的在正式学习之余穿梭于大众平台之中。否则我认为就会像我正文描述的那样，陷入泥沼，走上另一条远离大路的道路。\n","date":"2021-01-20T22:37:52+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/pc-gaming-keyboard-monitor.webp","permalink":"https://opoa.top/post/dont-count-on-mainstream-platforms-to-show-you-the-way-to-learn/","title":"请永远不要寄希望于从大众平台上找到学习之路"},{"content":" 前言 2020，转眼即将逝去。回顾这一年，我们见证了太多，经历了太多。新冠疫情、战争、巨星陨落… 好在最黑暗的时候已然过去，一切都在慢慢变好。希望如此。\n2020 年初，搭建了这个小站。希望可以记录自己的生活，整理一些学习笔记之类的。毕竟，有输出才有输入嘛。 然而中间还是有好长时间没更新，不管是因为忙还是懒🤣，明年一定要高产起来🏳‍🌈。\n来看看年初立的那些 flag: 愿望 完成情况 完善博客 ✅ 一台新电脑 + 双屏显示器 ✅ 过 CET-6 ❌ 读完两本 Java 相关的书籍 ❌ 拥有一段甜甜的恋爱o(TヘTo) ❌ 📃Detail\n完善博客 外观界面：现在博客的主题是 Sakura，从 Hexo 转到 Halo 也一直在用，个人非常喜欢。衷心感谢博客系统和主题的开发者们💗 内容：小站现在只有十来篇文章，大部分还是关于以前 Hexo 博客使用的。来年要多多输出，质量上也要加油呐💪。\n一台新电脑+双屏显示器 服役了四年左右的游戏本扛不住微服务多开，我被迫换了新笔记本 Yoga 14S,头一次选择 AMD 处理器，用了快半年感觉就俩字儿： AMD，YES! 馋了很久双屏的使用体验，于是几百从网上淘了个副屏。虽然有点小瑕疵，但不细看也还可以接受。所以现在我能同时看两集猫和老鼠，效率直接翻倍（雾。\n过CET-6 今年由于疫情影响，两次考试时间比较接近，9 月和 12 月，9 月的考试准备了两个星期，本来之前有些事决定裸考的，其实后来发现，两个星期好像跟裸考区别也不大。最有把握的长篇阅读翻了车，作文和翻译都没写完。理所当然，考的很差，400 分不到。 12 月的准备了一个月左右，这次作文和翻译都写完了，哎哟容我叉会儿腰。但到底还是得看听力和阅读，写作不拖后腿就行了😢。听力果然还是得持续练才有效果。 希望明年 2 月出成绩的时候我能回来把这里勾上。\n读完两本 Java 相关的书籍 趁着双十一促销，买了一堆书，这不，明年的 flag 又有了。 好吧，这年底冲业绩的时候还是读完了一本《Head First Java》，Java 语言入门书籍，但不得不说，收获颇丰。（也可能是我太菜了）在 Java 中有许多东西我都是知其然不知其所以然，看完这本书让我对它们有了一些新的理解。 现在在啃 Java核心技术卷Ⅰ，感觉里面很多东西需要过代码，不然很可能看完也就完了。所以进度会比第一本慢得多。\n拥有一段甜甜的恋爱o(TヘTo) 又是一年，365 天，8760 小时，525600 分钟，31536000 秒完完整整的单身狗🐕。总是羡慕🍋别人成双成对，自己圈子小，又不愿去拓展社交圈，还幻想着能有甜甜的恋爱。俗称：又菜又爱玩! 明年要有所改变啊🤦‍♂️。多去结交朋友，扩大圈子，说不定呢。 所以这个 flag 继续挂，挂到哪天勾上了为止。\n2021 整点新的 flag: 愿望 完成情况 博客写 12 篇文章 ❌ 养一只小主子🐈 ❌ 过 CET-6 ❌ 拿到学位证 ❌ 读完 6 本书（4 本 Java 相关） ❌ 练好英语听力，能脱离字幕看电影 ❌ 拥有一段甜甜的恋爱o(TヘTo) ❌ 最后，祝大家在新的一年都能实现自己的愿望或目标。 2021，冲鸭！\n","date":"2020-12-31T17:59:57+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/colorful-mask-girl.webp","permalink":"https://opoa.top/post/2020-year-end-summary/","title":"2020 年终总结"},{"content":"前言 最近在给公司的大数据项目写操作手册，又不得不涉及到数仓分层的一些专有名词和概念。这次专门整理学习一下，加深理解记忆。\n为什么要分层 清晰数据结构： 每一个数据分层都有它的作用域，这样我们在使用表的时候能更方便地定位和理解。 数据血缘追踪： 我们最终给业务呈现的是一张能直接使用的业务表，但它的来源有很多，如果一张来源表出了问题，能够快速准确地定位到问题，并清楚它的危害范围。 复杂问题简单化： 将一个复杂的任务分解成多个步骤来完成，每一层只处理单一的步骤，比较简单和容易理解。而且便于维护数据的准确性，当数据出现问题后，可以不用修复所有数据，可以从有问题的步骤开始修复。 提高数据的复用性： 规范数据分层，通过的中间层数据，能够极大减少重复计算，增加一次计算结果的复用性。 隔离原始数据： 不论是数据的异常还是数据的敏感性，使真实数据和统计数据解耦开。 数仓分层结构 ODS 层 （Operational Data Store） 原始数据层 ODS 层存放业务系统获取的最原始的数据，是其他上层数据的源数据。 DWD 层 （Data Warehouse Detail） 明细数据层 对 ODS 层的数据进行清洗转换（去除空值，脏数据和异常数据），数据脱敏（对一些个人信息进行处理，比如电话号码和账户密码）等操作。该层的数据易用性较高，是可以直接给上层使用的。 DWS 层 (Data Warehouse Service) 服务数据层 把 DWD 层的数据进行轻度汇总，一般聚集到以用户当日，商品当日的粒度。比如一个商品当日的浏览量，收藏数，评论数等等。 DWT 层 (Data Warehouse Topic) 主题数据层 把 DWS 层的数据按主题进行汇总，比如用户一个月的活跃天数，某个商品一个月的成交金额等等。 ADS 层 (Application Data Store) 应用数据层 面向实际的数据需求，以 DWD 层、DWS 层或 DWT 层的数据为基础，组成的各种统计报表。 愚见 由于是第一次接触大数据方面的知识，脑补了一个例子来帮助理解记忆，如果存在不准确的地方，还请见谅，欢迎指正。\n以网易云音乐举例：\n我们每日在使用网易云的时候，都会产生很多数据。比如说你今天听了哪些歌，你喜欢的歌，你创建的歌单，你收藏了哪些歌单等等。这时，数据还保存在网易云的数据库里，也就是业务端。 晚上 12 点，网抑云时间到了。生而为人…… 噢不是，新的一天了。意味着昨天的数据记录不会再更改了，你今天听的歌不会算到昨日听歌总时长里。在凌晨这种服务器压力比较小的时间段里，他们会把这一天的数据从业务端采集到 ODS 层。 这里的数据还不具备可用性，在经过清洗转换等等操作之后，数据来到了 DWD 层。 DWD 层的数据经过轻度汇总，以日为粒度来到 DWS 层，可以通过一张表就查询出某个用户当天听歌的总时长，最晚听歌时间等等，而不用去庞大的数据库中费力地搜索。 DWT 层在 DWS 层的基础上，汇总出一个月的数据。当然也可以根据业务需求，创建其他的主题。 最后，由 ADS 层进行报表统计或展示。 到了年末，你的网易云就会推送一份年度使用报告，告诉你今年听了 4213 首歌，听歌的总时长达到 269 小时，以及最常听的 top10，在 9 月 31 日这天，你把《好心分手》循环了 12 遍。 参考资料 数仓分层 数据仓库系列（三）数仓分层的意义价值及如何设计数据分层 电商数仓 -(数仓分层概念 + 数仓理论) ","date":"2020-12-10T14:05:51+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn@master/cover/yellow-leaves-orange-hair.webp","permalink":"https://opoa.top/post/getting-to-know-data-warehouse-layering/","title":"初识数仓分层"},{"content":" 注意：初始化设置的时候先不要联网，否则系统会自动激活，就不能七天无理由退换货了 准备工作 这里需要先准备一个 U 盘，因为需要在断网的环境下完成对电脑的检测，推荐一个软件：\n1.图吧工具箱\t下载地址 集成了很多 DIY 爱好者常用的硬件检测软件，这些软件功能强大，但使用起来需要一定的学习成本，不过对我们来说，其中几款常用的工具就能满足大部分的测试需求了。图吧工具箱本身只是一个启动器，完全免费。\n验机流程 1. 检查外包装 快递到手之后，先仔细检查一下外包装，如果包装上有破损，凹陷，那么就要小心了。很可能是运输过程中造成的，如果还在取件点，可以当着快递员的面开箱，确认里面的机器没有损坏。如果回到了家才发现，那么开箱的过程一定要用录像的方式记录下来。（我以前就吃过这个亏）\n2. 开箱过程 开箱的过程，条件允许的话，最好能用手机录下来，自己不方便可以找别人帮你录。录的时候先清晰地对着包装上的寄件信息，确认无误之后，再记录开箱的过程。\n3. 外观 接下来就是对外观的检查，看 USB 接口是否有使用过的痕迹，闭合的情况下四边是否对齐，外观有无划痕，键盘和触控板是否正常，有没有缝隙比较大的情况等等，总之就是好好地检查一下。毕竟，有一个好看的外观用着也舒心些。\n4. 开机 在接通电源的情况下试着按一下开机键，这时可能会没有反应，因为有些品牌的新电脑，第一次开机需要接通电源来激活电池，如果你正好也是这样，那么恭喜，你大概率是这台机器的第一个主人。\n5. 初始化设置 这里根据自己的喜好设置即可，需要特别注意的是先 不要联网。\n6. 软件检测 开机之后就需要用到之前准备好的 U 盘了。\n（1）屏幕：可以使用图吧工具箱下的坏点与漏光测试，把屏幕切换成纯色背景，看看有没有坏点，一般来说，坏点在 3 个以内都是正常的。\n（2）硬盘：推荐图吧工具箱下的 DiskInfo (64 位)，很容易就能看到硬盘的通电次数，通电时间。通电次数新机都在个位数，通电时间少于 12 小时。\n（3）其他硬件：CPU，显卡等的测试可以根据自己的喜好来，比如单烤，双烤之类的。\n结语 以上的检测都没什么问题了的话，那么恭喜，你的新电脑很健康，可以开始装软件啦。\nEnjoy yourself!\n","date":"2020-07-05T22:40:16+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn@latest/cover/wink-cherry-blossom-cute-smiling.webp","permalink":"https://opoa.top/post/how-to-inspect-a-new-computer/","title":"新电脑如何验机"},{"content":"前言 私密文章参考: Hexo（sakura）设置文章置顶 + 私密文章 为 Hexo 加入了私密文章功能后，密码输入错误之后弹出的浏览器自带提示框，在整个博客中显得很不协调。 而接下来要添加的 SweetAlert ，感官上就舒服得多，能给人更好的交互体验。\n安装 在 themes/Sakura/layout/_partial/footer.ejs 文件中加入以下代码\n\u0026lt;!-- sweetalert --\u0026gt; \u0026lt;script src=\u0026#34;https://unpkg.com/sweetalert/dist/sweetalert.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 修改 node_modules/hexo-blog-encrypt/lib/blog-encrypt.js 文件\n搜索 alert(WrongPassMessage)，找到弹出错误密码信息的 js 代码段\nreturn await verifyContent(hmacKey, decoded); }).catch((e) =\u0026gt; { alert(wrongPassMessage); console.log(e); return false; }); 把弹框替换为 SweetAlert\nreturn await verifyContent(hmacKey, decoded); }).catch((e) =\u0026gt; { // alert(wrongPassMessage); swal({ text: \u0026#34;密码错误！\u0026#34;, icon: \u0026#34;error\u0026#34;, className: \u0026#34;password-error\u0026#34;, button: \u0026#39;OK\u0026#39; }); console.log(e); return false; }); 在 themes/Sakura/source/css/style.css 文件中调整弹窗的样式\n/* 密码错误 sweetalert 弹框样式修改 */ .swal-overlay { background-color: transparent; } .swal-footer { text-align: center; } .password-error { box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15); border-radius: 4px; } 注意：由于修改了依赖库中的代码，一旦修改或更新依赖都会覆盖掉我们的修改，需要重新修改。\n参考文章 Hexo 搭建个人博客系列：进阶设置篇 ","date":"2020-04-27T12:55:04+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/honebami-toushirou-catanime.webp","permalink":"https://opoa.top/post/add-sweetalert-in-hexo/","title":"Hexo 使用 SweetAlert 美化弹窗"},{"content":"前言 一直想给博客装款在线聊天软件，综合对比下来后，选择了 Tidio ，其界面优雅简洁，支持多渠道回复，网页、Windows、安卓、IOS，关键是免费功能对普通用户完全够用，所以理论上来说是完全免费的！(白嫖使我快乐)\n正文 安装起来还是比较简单的，但因为是国外的网站，访问起来会有一些慢，需要点耐心。\n注册账号 访问 Tidio 注册账号，一个邮箱就能搞定，非常方便。\n自定义 Tidio 进入账户界面后，可以根据自己的喜好定制，可选颜色，语言，布局，侧边栏等等。\n找到账户面板的 Public Key 安装 在主题配置文件 _config.yml 添加如下代码\n# Tidio online chat tidio: enable: true key: # 替换为你的 Public Key 在 themes/Sakura/layout/_partial/footer.ejs 中添加如下代码\n\u0026lt;!-- 在线通讯 Tidio --\u0026gt; \u0026lt;% if (theme.tidio.enable){ %\u0026gt; \u0026lt;script src=\u0026#34;//code.tidio.co/\u0026lt;%- theme.tidio.key %\u0026gt;.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;% } %\u0026gt; 到此 Tidio 的安装就完成了，Hexo 三连后刷新页面，在右下角就能看到效果。\nhexo clean \u0026amp;\u0026amp; hexo g \u0026amp;\u0026amp; hexo s 参考文章 Hexo 搭建个人博客系列：进阶设置篇 ","date":"2020-04-26T20:30:45+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/flower-water-drops.webp","permalink":"https://opoa.top/post/add-tidio-in-hexo-sakura-theme/","title":"Hexo (Sakura) 添加在线聊天 Tidio"},{"content":"前言 重做博客时新发现的问题，loop 按钮不显示，且循环播放功能失效。回看才发现，其实一直都存在的问题，处理了站内播放 bug 之后就忽略音乐插件了。\n原因 查看源代码，发现 autoplay 和 loop 都没能被正确赋值。\n感觉可能是 neat 压缩 html 出错了，毕竟也是有过前科的。(被换行注释坑惨了)\n关掉 neat 的 html 压缩，然后\n查看源代码\n解决办法 neat 压缩静态资源很重要，又不想顶着残缺的 aplayer 插件，目前最简单的方式就是把主题配置文件 _config.yml 下出错的地方注释掉，让它采用默认值。既然 neat 你看不惯，那我帮你摁死好了。\n嗯\u0026hellip; 目前双方情绪都很稳定。\n","date":"2020-04-19T15:08:06+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/beauty.webp","permalink":"https://opoa.top/post/music-plugin-playback-error-in-hexo-sakura-theme/","title":"Hexo (Sakura) 音乐插件播放异常"},{"content":"本来打算给网站做个体检的，结果\n网站是可以访问的，却报了 404 的错误。\n博客是部署在 Coding 和 Github 上的，国内走 Coding，国外走 Github，我用的是 谷歌测速中文版 ，可能是指向了 Github。\n打开阿里云的域名解析设置，直接停掉 Github 的两条解析记录。\n等待 10 分钟左右 DNS 服务器刷新。\n🤔emmm\u0026hellip;\n没救了，等死吧，告辞\n","date":"2020-04-09T19:29:26+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/school-girl-shattered-glass-beautiful.webp","permalink":"https://opoa.top/post/fix-the-404-error-in-google-page-speed-analysis/","title":"谷歌 Page Speed 分析 404 的处理"},{"content":"前言 折腾了两天，软件和环境装的差不多了，接下来把我的 Hexo 博客转移过来，也是第一次弄，记录一下。\nGit Git 之前已经装过了，但它对 Hexo 来说太重要，这里还是提一下，要先安装 Git。\nNode 版本：Node v12.16.1\n官网下载 安装 等待安装完成 Finish。\n测试 打开 cmd\nnode -v npm -v 环境配置 配置安装模块的路径，默认会安装到 C 盘下\n在安装目录下新建两个文件夹\nnode_cache node_global 打开 cmd\nnpm config set prefix \u0026#34;E:\\Node\\node_global\u0026#34; npm config set cache \u0026#34;E:\\Node\\node_cache\u0026#34; 配置系统环境变量：\nE:\\Node\\node_global\\node_modules 修改用户变量 Path\nE:\\Node\\node_global 测试 打开 cmd\nnpm install express -g 等待安装完成后到配置的路径下查看\n配置国内淘宝镜像 npm config set registry https://registry.npm.taobao.org 验证配置\nnpm config get registry Hexo 安装 打开 cmd\nnpm install -g hexo-cli 进入事先准备好的博客文件根目录\n右键 \u0026ndash;\u0026gt; Git Bash Here\nnpm i 这个命令会跟据你的 package.json 依赖来新增或删除一些模块，一般不会有太大的变化。\n最后 Hexo 三连\nhexo clean \u0026amp;\u0026amp; hexo g \u0026amp;\u0026amp; hexo s 测试 浏览器访问: http://localhost:4000/\n到这里就迁移成功啦\n","date":"2020-04-06T14:22:35+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/school-girl-reading-a-book.webp","permalink":"https://opoa.top/post/migrate-hexo-blog/","title":"Hexo 博客迁移"},{"content":"前言 2 月份的时候电脑出了些问题，开不了机，但因为在特殊时期，前几天才有机会修好。结果太久没开机，一直卡在修复界面，得，直接重装吧。每次重装又需要的一堆环境配置，实在繁琐，就想在这整理一下，再用的时候也好参考。\n以下安装都是在 Win10 系统进行的\nJava 版本：JDK8\n官网下载 网盘下载 提取码：fmjy\n安装 这里可以选择安装的路径，我采用的默认，点下一步，下一步，等待安装完成即可。\n配置环境 我的电脑右键 \u0026ndash;\u0026gt; 属性 \u0026ndash;\u0026gt; 高级系统设置 \u0026ndash;\u0026gt; 环境变量\n在系统变量一栏操作\n新建：\n变量名：\nJAVA_HOME 变量值:\nC:\\Program Files\\Java\\jdk1.8.0_231 根据自己的路径配置 变量名：\nCLASSPATH 变量值：\n.;%JAVA_HOME%\\lib\\dt.jar;%JAVA_HOME%\\lib\\tools.jar !! 注意前面有个 \u0026ldquo;.\u0026rdquo;!!\n编辑:\n变量名：Path\nPath 变量值：\n%JAVA_HOME%\\bin %JAVA_HOME%\\jre\\bin 都配好之后点确定，在命令行窗口进行测试。\n测试 Win+R 键 \u0026mdash;\u0026gt; 输入 cmd \u0026mdash;\u0026gt; 回车 弹出一个黑窗口\n输入 java\n显示如下:\n用法: java [-options] class [args...] (执行类) 或 java [-options] -jar jarfile [args...] (执行 jar 文件) 其中选项包括: -d32 使用 32 位数据模型 (如果可用) -d64 使用 64 位数据模型 (如果可用) -server 选择 \u0026#34;server\u0026#34; VM 默认 VM 是 server. -cp \u0026lt;目录和 zip/jar 文件的类搜索路径\u0026gt; -classpath \u0026lt;目录和 zip/jar 文件的类搜索路径\u0026gt; 用；分隔的目录，JAR 档案 和 ZIP 档案列表，用于搜索类文件。 -D \u0026lt;名称\u0026gt;=\u0026lt; 值 \u0026gt; 设置系统属性 -verbose:[class|gc|jni] 启用详细输出 -version 输出产品版本并退出 -version:\u0026lt;值\u0026gt; (截取了一部分显示) ...... 输入 javac\n显示如下:\n用法: javac \u0026lt;options\u0026gt; \u0026lt;source files\u0026gt; 其中，可能的选项包括: -g 生成所有调试信息 -g:none 不生成任何调试信息 -g:{lines,vars,source} 只生成某些调试信息 -nowarn 不生成任何警告 -verbose 输出有关编译器正在执行的操作的消息 -deprecation 输出使用已过时的 API 的源位置 -classpath \u0026lt;路径\u0026gt; 指定查找用户类文件和注释处理程序的位置 -cp \u0026lt;路径\u0026gt; 指定查找用户类文件和注释处理程序的位置 -sourcepath \u0026lt;路径\u0026gt; 指定查找输入源文件的位置 -bootclasspath \u0026lt;路径\u0026gt; 覆盖引导类文件的位置 -extdirs \u0026lt;目录\u0026gt; 覆盖所安装扩展的位置 -endorseddirs \u0026lt;目录\u0026gt; 覆盖签名的标准路径的位置 -proc:{none,only} 控制是否执行注释处理和 / 或编译。 -processor \u0026lt;class1\u0026gt;[,\u0026lt;class2\u0026gt;,\u0026lt;class3\u0026gt;...] 要运行的注释处理程序的名称；绕过默认的搜索进程 (截取了一部分显示) ...... 输入 java -version\n显示如下：\njava version \u0026#34;1.8.0_231\u0026#34; Java(TM) SE Runtime Environment (build 1.8.0_231-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.231-b11, mixed mode) 到这里 JDK 就安装配置好了。\nMySQL 版本：MySQL5.7\n官网下载 网盘下载 提取码: rt7r\n安装 等待安装完成，点击 Next\n完成之后点击 Finish\n最后 Finish\n配置环境 C:\\Program Files\\MySQL\\MySQL Server 5.7 编辑 Path：\n%MYSQL_HOME%\\bin 测试 mysql -uroot -p 数据库编码格式 在数据库里查看数据库编码格式:\nshow variables like \u0026#39;%character%\u0026#39;; 这里需要把数据库编码格式设置成 utf8，不然后期很容易出现乱码问题。\n添加 / 修改以下内容\n[mysqld] character-set-server=utf8 [client] default-character-set=utf8 [mysql] default-character-set=utf8 继续修改另一个 my.ini 配置文件\n打开任务管理器 选择服务 找到 MySQL 的服务\n右键 \u0026ndash;\u0026gt; 重新启动\n再进到数据库里 查看编码格式\nshow variables like \u0026#39;%character%\u0026#39;; Tomcat 版本: Tomcat8.5\n官网下载 安装 解压之后就算安装完成了。\n配置环境 E:\\JavaTools\\apache-tomcat-8.5.14 编辑 Path:\n%CATALINA_HOME%\\bin 测试 打开 cmd\n输入 startup.bat 开启服务器\n浏览器访问: http://localhost:8080/\n显示这只三脚猫就说明 Tomcat 装好了。\nMaven 版本: Maven3.6\n官网下载 安装 配置环境 E:\\JavaTools\\Maven3.6\\apache-maven-3.6.2 编辑 Path：\n%MAVEN_HOME%\\bin 测试 打开 cmd\nmvn -v 能看到 Maven 版本就表示安装好了。\n配置本地仓库和云仓库 打开安装目录下 conf/settings.xml 文件\n\u0026lt;localRepository\u0026gt;本地仓库路径\u0026lt;/localRepository\u0026gt; 配置阿里云仓库\n\u0026lt;mirror\u0026gt; \u0026lt;id\u0026gt;alimaven\u0026lt;/id\u0026gt; \u0026lt;name\u0026gt;aliyun maven\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;http://maven.aliyun.com/nexus/content/groups/public/\u0026lt;/url\u0026gt; \u0026lt;mirrorOf\u0026gt;central\u0026lt;/mirrorOf\u0026gt; \u0026lt;/mirror\u0026gt; Git 版本: Git-2.25\n官网下载 安装 默认安装即可\n测试 打开 cmd\ngit 桌面右键\n能看到这两个选项，安装成功。\n","date":"2020-04-05T14:36:45+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/close-up-heterochromia-brown-hair-scarf.webp","permalink":"https://opoa.top/post/personal-development-environment-installation/","title":"个人开发环境安装"},{"content":"前言 由于文章的头图宽高比例不是 1:1，缩略图强行将它放入圆形容器中，挤压了宽度，导致了变形。\n正文 可以大致定位在分类页的部分：\n\u0026lt;article class=\u0026#34;post post-list\u0026#34; itemscope=\u0026#34;\u0026#34; itemtype=\u0026#34;http://schema.org/BlogPosting\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;post-entry\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;feature\u0026#34;\u0026gt; \u0026lt;a href=\u0026#34;\u0026lt;%- url_for(post.path) %\u0026gt;\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;overlay\u0026#34;\u0026gt; \u0026lt;i class=\u0026#34;iconfont icon-text\u0026#34;\u0026gt; \u0026lt;/i\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;img width=\u0026#34;150\u0026#34; height=\u0026#34;150\u0026#34; src=\u0026#34;\u0026lt;%= post.photos[0] %\u0026gt;\u0026#34; class=\u0026#34;attachment-post-thumbnail size-post-thumbnail wp-post-image\u0026#34; alt=\u0026#34;\u0026#34; \u0026gt; \u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; 这里要用到 CSS 属性 object-fit，关键字选择 cover ，让图片填满整个内容框，如有超出的部分则裁剪掉。\n\u0026lt;img width=\u0026#34;150\u0026#34; height=\u0026#34;150\u0026#34; style=\u0026#34;object-fit: cover;\u0026#34; src=\u0026#34;\u0026lt;%= post.photos[0] %\u0026gt;\u0026#34; class=\u0026#34;attachment-post-thumbnail size-post-thumbnail wp-post-image\u0026#34; alt=\u0026#34;\u0026#34; \u0026gt; 注意：图片一定要设置宽高，否则 object-fit 将无效。\n效果 改之前：\n改之后：\n参考资料 用 CSS 解决前端图片变形问题 ","date":"2020-03-26T21:36:45+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/black-hair-purple-eyes-shiny.webp","permalink":"https://opoa.top/post/thumbnail-display-issue-in-hexo-sakura/","title":"Hexo (Sakura) 缩略图显示异常"},{"content":"前言 偶然发现的一个问题，全局的 aplayer 插件在站内跳转的时候会重新加载，让人感觉非常难受。\n正文 回去跑了几天前的代码，发现是 ok 的，可这几天改动的地方很多，很难定位问题在哪里。这里先记上一笔:\n版本控制太重要了！！\n版本控制太重要了！！\n版本控制太重要了！！\n初步判断问题出在 博客根目录 \\themes\\Sakura\\layout\\_partial\\footer.ejs, debug 过程对我这个前端小白来说有些残忍，直接上代码:\n错误代码 \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;/js/lib.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;/js/InsightSearch.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;!-- fancybox 大图查看 需 jq --\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;/css/jquery.fancybox.min.css\u0026#34; media=\u0026#34;all\u0026#34;\u0026gt; \u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;/js/jquery.fancybox.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 这里需要将第 7 行的 jquery 插件引入代码提前，防止第 1 行的 lib.min.js 文件需要用到 jquery 语法的时候加载出现错误。\n修复完成的代码 \u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;/js/lib.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;/js/InsightSearch.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;!-- fancybox 大图查看 需 jq --\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;/css/jquery.fancybox.min.css\u0026#34; media=\u0026#34;all\u0026#34;\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;/js/jquery.fancybox.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 最后运行，搞定。\n总结 在导入 js 和 jquery 插件的时候，一定要先导入 jquery 插件，防止浏览器无法解析 js 中需要用到的 jquery 语法。按照 html 的解析顺序，js 也应该是放在最后导入的。\n参考资料 jquery 导入方法及注意问题 ","date":"2020-03-23T21:22:35+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/apple-candy-yukata-festival-look.webp","permalink":"https://opoa.top/post/music-plugin-issue-of-hexo-sakura-theme/","title":"Hexo (Sakura) 音乐插件问题"},{"content":"转自: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way 提问的智慧 How To Ask Questions The Smart Way\nCopyright © 2001,2006,2014 Eric S. Raymond, Rick Moen\n本指南英文版版权为 Eric S. Raymond, Rick Moen 所有。\n原文网址：http://www.catb.org/~esr/faqs/smart-questions.html Copyleft 2001 by D.H.Grand(nOBODY/Ginux), 2010 by Gasolin, 2015 by Ryan Wu\n本中文指南是基于原文 3.10 版以及 2010 年由 Gasolin 所翻译版本的最新翻译；\n协助指出翻译问题，请发 Issue ，或直接发 Pull Request 给我。\n本文另有 繁體中文版 。\n原文版本历史 目录 [提问的智慧](# 提问的智慧) [原文版本历史](# 原文版本历史) [目录](# 目录) [声明](# 声明) [简介](# 简介) [在提问之前](# 在提问之前) [当你提问时](# 当你提问时) [慎选提问的论坛](# 慎选提问的论坛) Stack Overflow [网站和 IRC 论坛](# 网站和 - irc - 论坛) [第二步，使用项目邮件列表](# 第二步使用项目邮件列表) [使用有意义且描述明确的标题](# 使用有意义且描述明确的标题) [使问题容易回复](# 使问题容易回复) [用清晰、正确、精准并语法正确的语句](# 用清晰正确精准并语法正确的语句) [使用易于读取且标准的文件格式发送问题](# 使用易于读取且标准的文件格式发送问题) [精确地描述问题并言之有物](# 精确地描述问题并言之有物) [话不在多而在精](# 话不在多而在精) [别动辄声称找到 Bug](# 别动辄声称找到 - bug) [低声下气不能代替你的功课](# 低声下气不能代替你的功课) [描述问题症状而非你的猜测](# 描述问题症状而非你的猜测) [按发生时间先后列出问题症状](# 按发生时间先后列出问题症状) [描述目标而不是过程](# 描述目标而不是过程) [别要求使用私人电邮回复](# 别要求使用私人电邮回复) [清楚明确的表达你的问题以及需求](# 清楚明确的表达你的问题以及需求) [询问有关代码的问题时](# 询问有关代码的问题时) [别把自己家庭作业的问题贴上来](# 别把自己家庭作业的问题贴上来) [去掉无意义的提问句](# 去掉无意义的提问句) [即使你很急也不要在标题写 紧急](# 即使你很急也不要在标题写紧急) [礼多人不怪，而且有时还很有帮助](# 礼多人不怪而且有时还很有帮助) [问题解决后，加个简短的补充说明](# 问题解决后加个简短的补充说明) [如何解读答案](# 如何解读答案) [RTFM 和 STFW：如何知道你已完全搞砸了](#rtfm - 和 - stfw 如何知道你已完全搞砸了) [如果还是搞不懂](# 如果还是搞不懂) [处理无礼的回应](# 处理无礼的回应) [如何避免扮演失败者](# 如何避免扮演失败者) [不该问的问题](# 不该问的问题) [好问题与蠢问题](# 好问题与蠢问题) [如果得不到回答](# 如果得不到回答) [如何更好地回答问题](# 如何更好地回答问题) [相关资源](# 相关资源) [鸣谢](# 鸣谢) 声明 许多项目在他们的使用协助 / 说明网页中链接了本指南，这么做很好，我们也鼓励大家都这么做。但如果你是负责管理这个项目网页的人，请在超链接附近的显著位置上注明：\n本指南不提供此项目的实际支持服务！\n我们已经深刻领教到少了上述声明所带来的痛苦。因为少了这点声明，我们不停地被一些白痴纠缠。这些白痴认为既然我们发布了这本指南，那么我们就有责任解决世上所有的技术问题。\n如果你是因为需要某些协助而正在阅读这本指南，并且最后离开是因为发现从本指南作者们身上得不到直接的协助，那么你就是我们所说的那些白痴之一。别问我们问题，我们只会忽略你。我们在这本指南中是教你如何从那些真正懂得你所遇到软件或硬件问题的人取得协助，而 99% 的情况下那不会是我们。除非你确定本指南的作者之一刚好是你所遇到的问题领域的专家，否则请不要打扰我们，这样大家都会开心一点。\n简介 在 黑客 的世界里，当你拋出一个技术问题时，最终是否能得到有用的回答，往往取决于你所提问和追问的方式。本指南将教你如何正确的提问以获得你满意的答案。\n不只是黑客，现在开源（Open Source）软件已经相当盛行，你常常也可以由其他有经验的使用者身上得到好答案，这是件 好事；使用者比起黑客来，往往对那些新手常遇到的问题更宽容一些。然而，将有经验的使用者视为黑客，并采用本指南所提的方法与他们沟通，同样也是能从他们身上得到满意回答的最有效方式。\n首先你应该明白，黑客们喜爱有挑战性的问题，或者能激发他们思维的好问题。如果我们并非如此，那我们也不会成为你想询问的对象。如果你给了我们一个值得反复咀嚼玩味的好问题，我们自会对你感激不尽。好问题是激励，是厚礼。好问题可以提高我们的理解力，而且通常会暴露我们以前从没意识到或者思考过的问题。对黑客而言，\u0026ldquo;好问题！\u0026rdquo; 是诚挚的大力称赞。\n尽管如此，黑客们有着蔑视或傲慢面对简单问题的坏名声，这有时让我们看起来对新手、无知者似乎较有敌意，但其实不是那样的。\n我们不讳言我们对那些不愿思考、或者在发问前不做他们该做的事的人的蔑视。那些人是时间杀手 —— 他们只想索取，从不付出，消耗我们可用在更有趣的问题或更值得回答的人身上的时间。我们称这样的人为 失败者（撸瑟） （由于历史原因，我们有时把它拼作 lusers）。\n我们意识到许多人只是想使用我们写的软件，他们对学习技术细节没有兴趣。对大多数人而言，电脑只是种工具，是种达到目的的手段而已。他们有自己的生活并且有更要紧的事要做。我们了解这点，也从不指望每个人都对这些让我们着迷的技术问题感兴趣。尽管如此，我们回答问题的风格是指向那些真正对此有兴趣并愿意主动参与解决问题的人，这一点不会变，也不该变。如果连这都变了，我们就是在降低做自己最擅长的事情上的效率。\n我们（在很大程度上）是自愿的，从繁忙的生活中抽出时间来解答疑惑，而且时常被提问淹没。所以我们无情的滤掉一些话题，特别是拋弃那些看起来像失败者的家伙，以便更高效的利用时间来回答 赢家（winner） 的问题。\n如果你厌恶我们的态度，高高在上，或过于傲慢，不妨也设身处地想想。我们并没有要求你向我们屈服 —— 事实上，我们大多数人非常乐意与你平等地交流，只要你付出小小努力来满足基本要求，我们就会欢迎你加入我们的文化。但让我们帮助那些不愿意帮助自己的人是没有效率的。无知没有关系，但装白痴就是不行。\n所以，你不必在技术上很在行才能吸引我们的注意，但你必须表现出能引导你变得在行的特质 \u0026ndash; 机敏、有想法、善于观察、乐于主动参与解决问题。如果你做不到这些使你与众不同的事情，我们建议你花点钱找家商业公司签个技术支持服务合同，而不是要求黑客个人无偿地帮助你。\n如果你决定向我们求助，当然你也不希望被视为失败者，更不愿成为失败者中的一员。能立刻得到快速并有效答案的最好方法，就是像赢家那样提问 \u0026ndash; 聪明、自信、有解决问题的思路，只是偶尔在特定的问题上需要获得一点帮助。\n（欢迎对本指南提出改进意见。你可以 email 你的建议至 esr@thyrsus.com 或 respond-auto@linuxmafia.com 。然而请注意，本文并非 网络礼节 的通用指南，而我们通常会拒绝无助于在技术论坛得到有用答案的建议）。\n在提问之前 在你准备要通过电子邮件、新闻群组或者聊天室提出技术问题前，请先做到以下事情：\n尝试在你准备提问的论坛的旧文章中搜索答案。 尝试上网搜索以找到答案。 尝试阅读手册以找到答案。 尝试阅读常见问题文件（FAQ）以找到答案。 尝试自己检查或试验以找到答案。 向你身边的强者朋友打听以找到答案。 如果你是程序开发者，请尝试阅读源代码以找到答案。 当你提出问题的时候，请先表明你已经做了上述的努力；这将有助于树立你并不是一个不劳而获且浪费别人的时间的提问者。如果你能一并表达在做了上述努力的过程中所 学到 的东西会更好，因为我们更乐于回答那些表现出能从答案中学习的人的问题。\n运用某些策略，比如先用 Google 搜索你所遇到的各种错误信息（既搜索 Google 论坛 ，也搜索网页），这样很可能直接就找到了能解决问题的文件或邮件列表线索。即使没有结果，在邮件列表或新闻组寻求帮助时加上一句 我在 Google 中搜过下列句子但没有找到什么有用的东西 也是件好事，即使它只是表明了搜索引擎不能提供哪些帮助。这么做（加上搜索过的字串）也让遇到相似问题的其他人能被搜索引擎引导到你的提问来。\n别着急，不要指望几秒钟的 Google 搜索就能解决一个复杂的问题。在向专家求助之前，再阅读一下常见问题文件（FAQ）、放轻松、坐舒服一些，再花点时间思考一下这个问题。相信我们，他们能从你的提问看出你做了多少阅读与思考，如果你是有备而来，将更有可能得到解答。不要将所有问题一股脑拋出，只因你的第一次搜索没有找到答案（或者找到太多答案）。\n准备好你的问题，再将问题仔细的思考过一遍，因为草率的发问只能得到草率的回答，或者根本得不到任何答案。越是能表现出在寻求帮助前你为解决问题所付出的努力，你越有可能得到实质性的帮助。\n小心别问错了问题。如果你的问题基于错误的假设，某个普通黑客（J. Random Hacker）多半会一边在心里想着 蠢问题…， 一边用无意义的字面解释来答复你，希望着你会从问题的回答（而非你想得到的答案）中汲取教训。\n绝不要自以为 够格 得到答案，你没有；你并没有。毕竟你没有为这种服务支付任何报酬。你将会是自己去 挣到 一个答案，靠提出有内涵的、有趣的、有思维激励作用的问题 —— 一个有潜力能贡献社区经验的问题，而不仅仅是被动的从他人处索取知识。\n另一方面，表明你愿意在找答案的过程中做点什么是一个非常好的开端。谁能给点提示？、我的这个例子里缺了什么？ 以及 我应该检查什么地方 比 请把我需要的确切的过程贴出来 更容易得到答复。因为你表现出只要有人能指个正确方向，你就有完成它的能力和决心。\n当你提问时 慎选提问的论坛 小心选择你要提问的场合。如果你做了下述的事情，你很可能被忽略掉或者被看作失败者：\n在与主题不合的论坛上贴出你的问题。 在探讨进阶技术问题的论坛张贴非常初级的问题；反之亦然。 在太多的不同新闻群组上重复转贴同样的问题（cross-post）。 向既非熟人也没有义务解决你问题的人发送私人电邮。 黑客会剔除掉那些搞错场合的问题，以保护他们沟通的渠道不被无关的东西淹没。你不会想让这种事发生在自己身上的。\n因此，第一步是找到对的论坛。再说一次，Google 和其它搜索引擎还是你的朋友，用它们来找到与你遭遇到困难的软硬件问题最相关的网站。通常那儿都有常见问题（FAQ）、邮件列表及相关说明文件的链接。如果你的努力（包括 阅读 FAQ）都没有结果，网站上也许还有报告 Bug（Bug-reporting）的流程或链接，如果是这样，链过去看看。\n向陌生的人或论坛发送邮件最可能是风险最大的事情。举例来说，别假设一个提供丰富内容的网页的作者会想充当你的免费顾问。不要对你的问题是否会受到欢迎做太乐观的估计 \u0026ndash; 如果你不确定，那就向别处发送，或者压根别发。\n在选择论坛、新闻群组或邮件列表时，别太相信名字，先看看 FAQ 或者许可书以弄清楚你的问题是否切题。发文前先翻翻已有的话题，这样可以让你感受一下那里的文化。事实上，事先在新闻组或邮件列表的历史记录中搜索与你问题相关的关键词是个极好的主意，也许这样就找到答案了。即使没有，也能帮助你归纳出更好的问题。\n别像机关枪似的一次 \u0026ldquo;扫射\u0026rdquo; 所有的帮助渠道，这就像大喊大叫一样会使人不快。要一个一个地来。\n搞清楚你的主题！最典型的错误之一是在某种致力于跨平台可移植的语言、套件或工具的论坛中提关于 Unix 或 Windows 操作系统程序界面的问题。如果你不明白为什么这是大错，最好在搞清楚这之间差异之前什么也别问。\n一般来说，在仔细挑选的公共论坛中提问，会比在私有论坛中提同样的问题更容易得到有用的回答。有几个理由可以支持这点，一是看潜在的回复者有多少，二是看观众有多少。黑客较愿意回答那些能帮助到许多人的问题。\n可以理解的是，老练的黑客和一些热门软件的作者正在接受过多的错发信息。就像那根最后压垮骆驼背的稻草一样，你的加入也有可能使情况走向极端 —— 已经好几次了，一些热门软件的作者从自己软件的支持中抽身出来，因为伴随而来涌入其私人邮箱的无用邮件变得无法忍受。\nStack Overflow 搜索，然后 在 Stack Exchange 问。\n近年来，Stack Exchange community 社区已经成为回答技术及其他问题的主要渠道，尤其是那些开放源码的项目。\n因为 Google 索引是即时的，在看 Stack Exchange 之前先在 Google 搜索。有很高的机率某人已经问了一个类似的问题，而且 Stack Exchange 网站们往往会是搜索结果中最前面几个。如果你在 Google 上没有找到任何答案，你再到特定相关主题的网站去找。用标签（Tag）搜索能让你更缩小你的搜索结果。\nStack Exchange 已经成长到 超过一百个网站 ，以下是最常用的几个站：\nSuper User 是问一些通用的电脑问题，如果你的问题跟代码或是写程序无关，只是一些网络连线之类的，请到这里。 Stack Overflow 是问写程序有关的问题。 Server Fault 是问服务器和网管相关的问题。 网站和 IRC 论坛 本地的使用者群组（user group），或者你所用的 Linux 发行版本也许正在宣传他们的网页论坛或 IRC 频道，并提供新手帮助（在一些非英语国家，新手论坛很可能还是邮件列表）， 这些地方是开始提问的好首选，特别是当你觉得遇到的也许只是相对简单或者很普通的问题时。有广告赞助的 IRC 频道是公开欢迎提问的地方，通常可以即时得到回应。\n事实上，如果程序出的问题只发生在特定 Linux 发行版提供的版本（这很常见），最好先去该发行版的论坛或邮件列表中提问，再到程序本身的论坛或邮件列表提问。（否则）该项目的黑客可能仅仅回复 \u0026ldquo;用 我们的 版本\u0026rdquo;。\n在任何论坛发文以前，先确认一下有没有搜索功能。如果有，就试着搜索一下问题的几个关键词，也许这会有帮助。如果在此之前你已做过通用的网页搜索（你也该这样做），还是再搜索一下论坛，搜索引擎有可能没来得及索引此论坛的全部内容。\n通过论坛或 IRC 频道来提供使用者支持服务有增长的趋势，电子邮件则大多为项目开发者间的交流而保留。所以最好先在论坛或 IRC 中寻求与该项目相关的协助。\n在使用 IRC 的时候，首先最好不要发布很长的问题描述，有些人称之为频道洪水。最好通过一句话的问题描述来开始聊天。\n第二步，使用项目邮件列表 当某个项目提供开发者邮件列表时，要向列表而不是其中的个别成员提问，即使你确信他能最好地回答你的问题。查一查项目的文件和首页，找到项目的邮件列表并使用它。有几个很好的理由支持我们采用这种办法：\n任何好到需要向个别开发者提出的问题，也将对整个项目群组有益。反之，如果你认为自己的问题对整个项目群组来说太愚蠢，也不能成为骚扰个别开发者的理由。 向列表提问可以分散开发者的负担，个别开发者（尤其是项目领导人）也许太忙以至于没法回答你的问题。 大多数邮件列表都会被存档，那些被存档的内容将被搜索引擎索引。如果你向列表提问并得到解答，将来其它人可以通过网页搜索找到你的问题和答案，也就不用再次发问了。 如果某些问题经常被问到，开发者可以利用此信息来改进说明文件或软件本身，以使其更清楚。如果只是私下提问，就没有人能看到最常见问题的完整场景。 如果一个项目既有 \u0026ldquo;使用者\u0026rdquo; 也有 \u0026ldquo;开发者\u0026rdquo;（或 \u0026ldquo;黑客\u0026rdquo;）邮件列表或论坛，而你又不会动到那些源代码，那么就向 \u0026ldquo;使用者\u0026rdquo; 列表或论坛提问。不要假设自己会在开发者列表中受到欢迎，那些人多半会将你的提问视为干扰他们开发的噪音。\n然而，如果你 确信 你的问题很特别，而且在 \u0026ldquo;使用者\u0026rdquo; 列表或论坛中几天都没有回复，可以试试前往 \u0026ldquo;开发者\u0026rdquo; 列表或论坛发问。建议你在张贴前最好先暗地里观察几天以了解那里的行事方式（事实上这是参与任何私有或半私有列表的好主意）\n如果你找不到一个项目的邮件列表，而只能查到项目维护者的电子邮件地址，尽管向他发信。即使是在这种情况下，也别假设（项目）邮件列表不存在。在你的电子邮件中，请陈述你已经试过但没有找到合适的邮件列表，也提及你不反对将自己的邮件转发给他人（许多人认为，即使没什么秘密，私人电子邮件也不应该被公开。通过允许将你的电子邮件转发他人，你给了相应人员处置你邮件的选择）。\n使用有意义且描述明确的标题 在邮件列表、新闻群组或论坛中，大约 50 字以内的标题是抓住资深专家注意力的好机会。别用喋喋不休的 帮帮忙、跪求、急（更别说 救命啊！！！！ 这样让人反感的话，用这种标题会被条件反射式地忽略）来浪费这个机会。不要妄想用你的痛苦程度来打动我们，而应该是在这点空间中使用极简单扼要的描述方式来提出问题。\n一个好标题范例是 目标 —— 差异 式的描述，许多技术支持组织就是这样做的。在 目标 部分指出是哪一个或哪一组东西有问题，在 差异 部分则描述与期望的行为不一致的地方。\n蠢问题：救命啊！我的笔记本电脑不能正常显示了！\n聪明问题：X.org 6.8.1 的鼠标光标会变形，某牌显卡 MV1005 芯片组。\n更聪明问题：X.org 6.8.1 的鼠标光标，在某牌显卡 MV1005 芯片组环境下 - 会变形。\n编写 目标 —— 差异 式描述的过程有助于你组织对问题的细致思考。是什么被影响了？ 仅仅是鼠标光标或者还有其它图形？只在 X.org 的 X 版中出现？或只是出现在 6.8.1 版中？ 是针对某牌显卡芯片组？或者只是其中的 MV1005 型号？ 一个黑客只需瞄一眼就能够立即明白你的环境 和 你遇到的问题。\n总而言之，请想像一下你正在一个只显示标题的存档讨论串（Thread）索引中查寻。让你的标题更好地反映问题，可使下一个搜索类似问题的人能够关注这个讨论串，而不用再次提问相同的问题。\n如果你想在回复中提出问题，记得要修改内容标题，以表明你是在问一个问题， 一个看起来像 Re: 测试 或者 Re: 新 bug 的标题很难引起足够重视。另外，在不影响连贯性之下，适当引用并删减前文的内容，能给新来的读者留下线索。\n对于讨论串，不要直接点击回复来开始一个全新的讨论串，这将限制你的观众。因为有些邮件阅读程序，比如 mutt ，允许使用者按讨论串排序并通过折叠讨论串来隐藏消息，这样做的人永远看不到你发的消息。\n仅仅改变标题还不够。mutt 和其它一些邮件阅读程序还会检查邮件标题以外的其它信息，以便为其指定讨论串。所以宁可发一个全新的邮件。\n在网页论坛上，好的提问方式稍有不同，因为讨论串与特定的信息紧密结合，并且通常在讨论串外就看不到里面的内容，故通过回复提问，而非改变标题是可接受的。不是所有论坛都允许在回复中出现分离的标题，而且这样做了基本上没有人会去看。不过，通过回复提问，这本身就是暧昧的做法，因为它们只会被正在查看该标题的人读到。所以，除非你 只想 在该讨论串当前活跃的人群中提问，不然还是另起炉灶比较好。\n使问题容易回复 以 请将你的回复发送到…… 来结束你的问题多半会使你得不到回答。如果你觉得花几秒钟在邮件客户端设置一下回复地址都麻烦，我们也觉得花几秒钟思考你的问题更麻烦。如果你的邮件程序不支持这样做，换个好点的 ；如果是操作系统不支持这种邮件程序，也换个好点的。\n在论坛，要求通过电子邮件回复是非常无礼的，除非你认为回复的信息可能比较敏感（有人会为了某些未知的原因，只让你而不是整个论坛知道答案）。如果你只是想在有人回复讨论串时得到电子邮件提醒，可以要求网页论坛发送给你。几乎所有论坛都支持诸如 追踪此讨论串、有回复时发送邮件提醒 等功能。\n用清晰、正确、精准并语法正确的语句 我们从经验中发现，粗心的提问者通常也会粗心的写程序与思考（我敢打包票）。回答粗心大意者的问题很不值得，我们宁愿把时间耗在别处。\n正确的拼写、标点符号和大小写是很重要的。一般来说，如果你觉得这样做很麻烦，不想在乎这些，那我们也觉得麻烦，不想在乎你的提问。花点额外的精力斟酌一下字句，用不着太僵硬与正式 —— 事实上，黑客文化很看重能准确地使用非正式、俚语和幽默的语句。但它 必须很 准确，而且有迹象表明你是在思考和关注问题。\n正确地拼写、使用标点和大小写，不要将 its 混淆为 it's，loose 搞成 lose 或者将 discrete 弄成 discreet。不要 全部用大写，这会被视为无礼的大声嚷嚷（全部小写也好不到哪去，因为不易阅读。Alan Cox 也许可以这样做，但你不行）。\n更白话的说，如果你写得像是个半文盲 [译注：[小白](http://zh.wikipedia.org/wiki/ 小白)]，那多半得不到理睬。也不要使用即时通信中的简写或 [火星文](http://zh.wikipedia.org/wiki/ 火星文)，如将 的 简化为 d 会使你看起来像一个为了少打几个键而省字的小白。更糟的是，如果像个小孩似地鬼画符那绝对是在找死，可以肯定没人会理你（或者最多是给你一大堆指责与挖苦）。\n如果在使用非母语的论坛提问，你可以犯点拼写和语法上的小错，但决不能在思考上马虎（没错，我们通常能弄清两者的分别）。同时，除非你知道回复者使用的语言，否则请使用英语书写。繁忙的黑客一般会直接删除用他们看不懂语言写的消息。在网络上英语是通用语言，用英语书写可以将你的问题在尚未被阅读就被直接删除的可能性降到最低。\n如果英文是你的外语（Second language），提示潜在回复者你有潜在的语言困难是很好的： [译注：以下附上原文以供使用]\nEnglish is not my native language; please excuse typing errors.\n英文不是我的母语，请原谅我的错字或语法。 If you speak $LANGUAGE, please email/PM me; I may need assistance translating my question.\n如果你说某语言，请寄信 / 私讯给我；我需要有人协助我翻译我的问题。 I am familiar with the technical terms, but some slang expressions and idioms are difficult for me.\n我对技术名词很熟悉，但对于俗语或是特别用法比较不甚了解。 I\u0026rsquo;ve posted my question in $LANGUAGE and English. I\u0026rsquo;ll be glad to translate responses, if you only use one or the other.\n我把我的问题用某语言 和英文写出来，如果你只用一种语言回答，我会乐意将其翻译成另一种。 使用易于读取且标准的文件格式发送问题 如果你人为地将问题搞得难以阅读，它多半会被忽略，人们更愿读易懂的问题，所以：\n使用纯文字而不是 HTML (关闭 HTML 并不难）。 使用 MIME 附件通常是可以的，前提是真正有内容（譬如附带的源代码或 patch），而不仅仅是邮件程序生成的模板（譬如只是信件内容的拷贝）。 不要发送一段文字只是一行句子但自动换行后会变成多行的邮件（这使得回复部分内容非常困难）。设想你的读者是在 80 个字符宽的终端机上阅读邮件，最好设置你的换行分割点小于 80 字。 但是，对一些特殊的文件不要 设置固定宽度（譬如日志档案拷贝或会话记录）。数据应该原样包含，让回复者有信心他们看到的是和你看到的一样的东西。 在英语论坛中，不要使用 Quoted-Printable MIME 编码发送消息。这种编码对于张贴非 ASCII 语言可能是必须的，但很多邮件程序并不支持这种编码。当它们处理换行时，那些文本中四处散布的 =20 符号既难看也分散注意力，甚至有可能破坏内容的语意。 绝对，永远 不要指望黑客们阅读使用封闭格式编写的文档，像微软公司的 Word 或 Excel 文件等。大多数黑客对此的反应就像有人将还在冒热气的猪粪倒在你家门口时你的反应一样。即便他们能够处理，他们也很厌恶这么做。 如果你从使用 Windows 的电脑发送电子邮件，关闭微软愚蠢的 智能引号 功能 （从 [选项] \u0026gt; [校订] \u0026gt; [自动校正选项]，勾选掉 智能引号 单选框），以免在你的邮件中到处散布垃圾字符。 在论坛，勿滥用 表情符号 和 HTML 功能（当它们提供时）。一两个表情符号通常没有问题，但花哨的彩色文本倾向于使人认为你是个无能之辈。过滥地使用表情符号、色彩和字体会使你看来像个傻笑的小姑娘。这通常不是个好主意，除非你只是对性而不是对答案感兴趣。 如果你使用图形用户界面的邮件程序（如微软公司的 Outlook 或者其它类似的），注意它们的默认设置不一定满足这些要求。大多数这类程序有基于选单的 查看源代码 命令，用它来检查发送文件夹中的邮件，以确保发送的是纯文本文件同时没有一些奇怪的字符。\n精确地描述问题并言之有物 仔细、清楚地描述你的问题或 Bug 的症状。 描述问题发生的环境（机器配置、操作系统、应用程序、以及相关的信息），提供经销商的发行版和版本号（如：Fedora Core 4、Slackware 9.1 等）。 描述在提问前你是怎样去研究和理解这个问题的。 描述在提问前为确定问题而采取的诊断步骤。 描述最近做过什么可能相关的硬件或软件变更。 尽可能的提供一个可以 重现这个问题的可控环境 的方法。 尽量去揣测一个黑客会怎样反问你，在你提问之前预先将黑客们可能遇到的问题回答一遍。\n以上几点中，当你报告的是你认为可能在代码中的问题时，给黑客一个可以重现你的问题的环境尤其重要。当你这么做时，你得到有效的回答的机会和速度都会大大的提升。\nSimon Tatham 写过一篇名为《如何有效的报告 Bug 》的出色文章。强力推荐你也读一读。\n话不在多而在精 你需要提供精确有内容的信息。这并不是要求你简单的把成堆的出错代码或者资料完全转录到你的提问中。如果你有庞大而复杂的测试样例能重现程序挂掉的情境，尽量将它剪裁得越小越好。\n这样做的用处至少有三点。 第一，表现出你为简化问题付出了努力，这可以使你得到回答的机会增加； 第二，简化问题使你更有可能得到 有用 的答案； 第三，在精炼你的 bug 报告的过程中，你很可能就自己找到了解决方法或权宜之计。\n别动辄声称找到 Bug 当你在使用软件中遇到问题，除非你非常、非常 的有根据，不要动辄声称找到了 Bug。提示：除非你能提供解决问题的源代码补丁，或者提供回归测试来表明前一版本中行为不正确，否则你都多半不够完全确信。这同样适用在网页和文件，如果你（声称）发现了文件的 Bug，你应该能提供相应位置的修正或替代文件。\n请记得，还有许多其它使用者没遇到你发现的问题，否则你在阅读文件或搜索网页时就应该发现了（你在抱怨前 [已经做了这些，是吧](# 在提问之前)？）。这也意味着很有可能是你弄错了而不是软件本身有问题。\n编写软件的人总是非常辛苦地使它尽可能完美。如果你声称找到了 Bug，也就是在质疑他们的能力，即使你是对的，也有可能会冒犯到其中某部分人。当你在标题中嚷嚷着有 Bug 时，这尤其严重。\n提问时，即使你私下非常确信已经发现一个真正的 Bug，最好写得像是 你 做错了什么。如果真的有 Bug，你会在回复中看到这点。这样做的话，如果真有 Bug，维护者就会向你道歉，这总比你惹恼别人然后欠别人一个道歉要好一点。\n低声下气不能代替你的功课 有些人明白他们不该粗鲁或傲慢的提问并要求得到答复，但他们选择另一个极端 —— 低声下气：我知道我只是个可悲的新手，一个撸瑟，但...。这既使人困扰，也没有用，尤其是伴随着与实际问题含糊不清的描述时更令人反感。\n别用原始灵长类动物的把戏来浪费你我的时间。取而代之的是，尽可能清楚地描述背景条件和你的问题情况。这比低声下气更好地定位了你的位置。\n有时网页论坛会设有专为新手提问的版面，如果你真的认为遇到了初学者的问题，到那去就是了，但一样别那么低声下气。\n描述问题症状而非你的猜测 告诉黑客们你认为问题是怎样造成的并没什么帮助。（如果你的推断如此有效，还用向别人求助吗？），因此要确信你原原本本告诉了他们问题的症状，而不是你的解释和理论；让黑客们来推测和诊断。如果你认为陈述自己的猜测很重要，清楚地说明这只是你的猜测，并描述为什么它们不起作用。\n蠢问题\n我在编译内核时接连遇到 SIG11 错误， 我怀疑某条飞线搭在主板的走线上了，这种情况应该怎样检查最好？\n聪明问题\n我的组装电脑是 FIC-PA2007 主机板搭载 AMD K6/233 CPU（威盛 Apollo VP2 芯片组）， 256MB Corsair PC133 SDRAM 内存，在编译内核时，从开机 20 分钟以后就频频产生 SIG11 错误， 但是在头 20 分钟内从没发生过相同的问题。重新启动也没有用，但是关机一晚上就又能工作 20 分钟。 所有内存都换过了，没有效果。相关部分的标准编译记录如下…。\n由于以上这点似乎让许多人觉得难以配合，这里有句话可以提醒你：所有的诊断专家都来自密苏里州。 美国国务院的官方座右铭则是：让我看看（出自国会议员 Willard D. Vandiver 在 1899 年时的讲话：我来自一个出产玉米，棉花，牛蒡和民主党人的国家，滔滔雄辩既不能说服我，也不会让我满意。我来自密苏里州，你必须让我看看。） 针对诊断者而言，这并不是一种怀疑，而只是一种真实而有用的需求，以便让他们看到的是与你看到的原始证据尽可能一致的东西，而不是你的猜测与归纳的结论。所以，大方的展示给我们看吧！\n按发生时间先后列出问题症状 问题发生前的一系列操作，往往就是对找出问题最有帮助的线索。因此，你的说明里应该包含你的操作步骤，以及机器和软件的反应，直到问题发生。在命令行处理的情况下，提供一段操作记录（例如运行脚本工具所生成的），并引用相关的若干行（如 20 行）记录会非常有帮助。\n如果挂掉的程序有诊断选项（如 -v 的详述开关），试着选择这些能在记录中增加调试信息的选项。记住，多 不等于 好。试着选取适当的调试级别以便提供有用的信息而不是让读者淹没在垃圾中。\n如果你的说明很长（如超过四个段落），在开头简述问题，接下来再按时间顺序详述会有所帮助。这样黑客们在读你的记录时就知道该注意哪些内容了。\n描述目标而不是过程 如果你想弄清楚如何做某事（而不是报告一个 Bug），在开头就描述你的目标，然后才陈述重现你所卡住的特定步骤。\n经常寻求技术帮助的人在心中有个更高层次的目标，而他们在自以为能达到目标的特定道路上被卡住了，然后跑来问该怎么走，但没有意识到这条路本身就有问题。结果要费很大的劲才能搞定。\n蠢问题\n我怎样才能从某绘图程序的颜色选择器中取得十六进制的的 RGB 值？\n聪明问题\n我正试着用替换一幅图片的色码（color table）成自己选定的色码，我现在知道的唯一方法是编辑每个色码区块（table slot）， 但却无法从某绘图程序的颜色选择器取得十六进制的的 RGB 值。\n第二种提问法比较聪明，你可能得到像是 建议采用另一个更合适的工具 的回复。\n别要求使用私人电邮回复 黑客们认为问题的解决过程应该公开、透明，此过程中如果更有经验的人注意到不完整或者不当之处，最初的回复才能够、也应该被纠正。同时，作为提供帮助者可以得到一些奖励，奖励就是他的能力和学识被其他同行看到。\n当你要求私下回复时，这个过程和奖励都被中止。别这样做，让 回复者 来决定是否私下回答 —— 如果他真这么做了，通常是因为他认为问题编写太差或者太肤浅，以至于对其它人没有兴趣。\n这条规则存在一条有限的例外，如果你确信提问可能会引来大量雷同的回复时，那么这个神奇的提问句会是 向我发电邮，我将为论坛归纳这些回复。试着将邮件列表或新闻群组从洪水般的雷同回复中解救出来是非常有礼貌的 —— 但你必须信守诺言。\n清楚明确的表达你的问题以及需求 漫无边际的提问是近乎无休无止的时间黑洞。最有可能给你有用答案的人通常也正是最忙的人（他们忙是因为要亲自完成大部分工作）。这样的人对无节制的时间黑洞相当厌恶，所以他们也倾向于厌恶那些漫无边际的提问。\n如果你明确表述需要回答者做什么（如提供指点、发送一段代码、检查你的补丁、或是其他等等），就最有可能得到有用的答案。因为这会定出一个时间和精力的上限，便于回答者能集中精力来帮你。这么做很棒。\n要理解专家们所处的世界，请把专业技能想像为充裕的资源，而回复的时间则是稀缺的资源。你要求他们奉献的时间越少，你越有可能从真正专业而且很忙的专家那里得到解答。\n所以，界定一下你的问题，使专家花在辨识你的问题和回答所需要付出的时间减到最少，这技巧对你有用答案相当有帮助 —— 但这技巧通常和简化问题有所区别。因此，问 我想更好的理解 X，可否指点一下哪有好一点说明？ 通常比问 你能解释一下 X 吗？ 更好。如果你的代码不能运作，通常请别人看看哪里有问题，比要求别人替你改正要明智得多。\n询问有关代码的问题时 别要求他人帮你调试有问题的代码，不提示一下应该从何入手。张贴几百行的代码，然后说一声：它不能工作 会让你完全被忽略。只贴几十行代码，然后说一句：在第七行以后，我期待它显示 \u0026lt;x\u0026gt;，但实际出现的是 \u0026lt;y\u0026gt; 比较有可能让你得到回应。\n最有效描述程序问题的方法是提供最精简的 Bug 展示测试用例（bug-demonstrating test case）。什么是最精简的测试用例？那是问题的缩影；一小个程序片段能 刚好 展示出程序的异常行为，而不包含其他令人分散注意力的内容。怎么制作最精简的测试用例？如果你知道哪一行或哪一段代码会造成异常的行为，复制下来并加入足够重现这个状况的代码（例如，足以让这段代码能被编译 / 直译 / 被应用程序处理）。如果你无法将问题缩减到一个特定区块，就复制一份代码并移除不影响产生问题行为的部分。总之，测试用例越小越好（查看 [话不在多而在精](# 话不在多而在精) 一节）。\n一般而言，要得到一段相当精简的测试用例并不太容易，但永远先尝试这样做的是种好习惯。这种方式可以帮助你了解如何自行解决这个问题 —— 而且即使你的尝试不成功，黑客们也会看到你在尝试取得答案的过程中付出了努力，这可以让他们更愿意与你合作。\n如果你只是想让别人帮忙审查（Review）一下代码，在信的开头就要说出来，并且一定要提到你认为哪一部分特别需要关注以及为什么。\n别把自己家庭作业的问题贴上来 黑客们很擅长分辨哪些问题是家庭作业式的问题；因为我们中的大多数都曾自己解决这类问题。同样，这些问题得由 你 来搞定，你会从中学到东西。你可以要求给点提示，但别要求得到完整的解决方案。\n如果你怀疑自己碰到了一个家庭作业式的问题，但仍然无法解决，试试在使用者群组，论坛或（最后一招）在项目的 使用者 邮件列表或论坛中提问。尽管黑客们 会 看出来，但一些有经验的使用者也许仍会给你一些提示。\n去掉无意义的提问句 避免用无意义的话结束提问，例如 有人能帮我吗？ 或者 这有答案吗？。\n首先：如果你对问题的描述不是很好，这样问更是画蛇添足。\n其次：由于这样问是画蛇添足，黑客们会很厌烦你 —— 而且通常会用逻辑上正确，但毫无意义的回答来表示他们的蔑视， 例如：没错，有人能帮你 或者 不，没答案。\n一般来说，避免用 是或否、对或错、有或没有 类型的问句，除非你想得到 是或否类型的回答 。\n即使你很急也不要在标题写 紧急 这是你的问题，不是我们的。宣称 紧急 极有可能事与愿违：大多数黑客会直接删除无礼和自私地企图即时引起关注的问题。更严重的是，紧急 这个字（或是其他企图引起关注的标题）通常会被垃圾信过滤器过滤掉 —— 你希望能看到你问题的人可能永远也看不到。\n有半个例外的情况是，如果你是在一些很高调，会使黑客们兴奋的地方，也许值得这样去做。在这种情况下，如果你有时间压力，也很有礼貌地提到这点，人们也许会有兴趣回答快一点。\n当然，这风险很大，因为黑客们兴奋的点多半与你的不同。譬如从 NASA 国际空间站（International Space Station）发这样的标题没有问题，但用自我感觉良好的慈善行为或政治原因发肯定不行。事实上，张贴诸如 紧急：帮我救救这个毛绒绒的小海豹！ 肯定让你被黑客忽略或惹恼他们，即使他们认为毛绒绒的小海豹很重要。\n如果你觉得这点很不可思议，最好再把这份指南剩下的内容多读几遍，直到你弄懂了再发文。\n礼多人不怪，而且有时还很有帮助 彬彬有礼，多用 请 和 谢谢您的关注，或 谢谢你的关照。让大家都知道你对他们花时间免费提供帮助心存感激。\n坦白说，这一点并没有比清晰、正确、精准并合法语法和避免使用专用格式重要（也不能取而代之）。黑客们一般宁可读有点唐突但技术上鲜明的 Bug 报告，而不是那种有礼但含糊的报告。（如果这点让你不解，记住我们是按问题能教给我们什么来评价问题的价值的）\n然而，如果你有一串的问题待解决，客气一点肯定会增加你得到有用回应的机会。\n（我们注意到，自从本指南发布后，从资深黑客那里得到的唯一严重缺陷反馈，就是对预先道谢这一条。一些黑客觉得 先谢了 意味着事后就不用再感谢任何人的暗示。我们的建议是要么先说 先谢了，然后 事后再对回复者表示感谢，或者换种方式表达感激，譬如用 谢谢你的关注 或 谢谢你的关照。）\n问题解决后，加个简短的补充说明 问题解决后，向所有帮助过你的人发个说明，让他们知道问题是怎样解决的，并再一次向他们表示感谢。如果问题在新闻组或者邮件列表中引起了广泛关注，应该在那里贴一个说明比较恰当。\n最理想的方式是向最初提问的话题回复此消息，并在标题中包含 已修正，已解决 或其它同等含义的明显标记。在人来人往的邮件列表里，一个看见讨论串 问题 X 和 问题 X - 已解决 的潜在回复者就明白不用再浪费时间了（除非他个人觉得 问题 X 的有趣），因此可以利用此时间去解决其它问题。\n补充说明不必很长或是很深入；简单的一句 你好，原来是网线出了问题！谢谢大家 – Bill 比什么也不说要来的好。事实上，除非结论真的很有技术含量，否则简短可爱的小结比长篇大论更好。说明问题是怎样解决的，但大可不必将解决问题的过程复述一遍。\n对于有深度的问题，张贴调试记录的摘要是有帮助的。描述问题的最终状态，说明是什么解决了问题，在此 之后 才指明可以避免的盲点。避免盲点的部分应放在正确的解决方案和其它总结材料之后，而不要将此信息搞成侦探推理小说。列出那些帮助过你的名字，会让你交到更多朋友。\n除了有礼貌和有内涵以外，这种类型的补充也有助于他人在邮件列表 / 新闻群组 / 论坛中搜索到真正解决你问题的方案，让他们也从中受益。\n至少，这种补充有助于让每位参与协助的人因问题的解决而从中得到满足感。如果你自己不是技术专家或者黑客，那就相信我们，这种感觉对于那些你向他们求助的大师或者专家而言，是非常重要的。问题悬而未决会让人灰心；黑客们渴望看到问题被解决。好人有好报，满足他们的渴望，你会在下次提问时尝到甜头。\n思考一下怎样才能避免他人将来也遇到类似的问题，自问写一份文件或加个常见问题（FAQ）会不会有帮助。如果是的话就将它们发给维护者。\n在黑客中，这种良好的后继行动实际上比传统的礼节更为重要，也是你如何透过善待他人而赢得声誉的方式，这是非常有价值的资产。\n如何解读答案 RTFM 和 STFW：如何知道你已完全搞砸了 有一个古老而神圣的传统：如果你收到 RTFM （Read The Fucking Manual） 的回应，回答者认为你 应该去读他妈的手册。当然，基本上他是对的，你应该去读一读。\nRTFM 有一个年轻的亲戚。如果你收到 STFW（Search The Fucking Web） 的回应，回答者认为你 应该到他妈的网上搜索。那人多半也是对的，去搜索一下吧。（更温和一点的说法是 Google 是你的朋友 ！）\n在论坛，你也可能被要求去爬爬论坛的旧文。事实上，有人甚至可能热心地为你提供以前解决此问题的讨论串。但不要依赖这种关照，提问前应该先搜索一下旧文。\n通常，用这两句之一回答你的人会给你一份包含你需要内容的手册或者一个网址，而且他们打这些字的时候也正在读着。这些答复意味着回答者认为\n你需要的信息非常容易获得； 你自己去搜索这些信息比灌给你，能让你学到更多。 你不应该因此不爽；依照黑客的标准，他已经表示了对你一定程度的关注，而没有对你的要求视而不见。你应该对他祖母般的慈祥表示感谢。\n如果还是搞不懂 如果你看不懂回应，别立刻要求对方解释。像你以前试着自己解决问题时那样（利用手册，FAQ，网络，身边的高手），先试着去搞懂他的回应。如果你真的需要对方解释，记得表现出你已经从中学到了点什么。\n比方说，如果我回答你：看来似乎是 zentry 卡住了；你应该先清除它。，然后，这是一个 很糟的 后续问题回应：zentry 是什么？ 好 的问法应该是这样：哦～～～我看过说明了但是只有 -z 和 -p 两个参数中提到了 zentries，而且还都没有清楚的解释如何清除它。你是指这两个中的哪一个吗？还是我看漏了什么？\n处理无礼的回应 很多黑客圈子中看似无礼的行为并不是存心冒犯。相反，它是直接了当，一针见血式的交流风格，这种风格更注重解决问题，而不是使人感觉舒服而却模模糊糊。\n如果你觉得被冒犯了，试着平静地反应。如果有人真的做了出格的事，邮件列表、新闻群组或论坛中的前辈多半会招呼他。如果这 没有 发生而你却发火了，那么你发火对象的言语可能在黑客社区中看起来是正常的，而 你 将被视为有错的一方，这将伤害到你获取信息或帮助的机会。\n另一方面，你偶尔真的会碰到无礼和无聊的言行。与上述相反，对真正的冒犯者狠狠地打击，用犀利的语言将其驳得体无完肤都是可以接受的。然而，在行事之前一定要非常非常的有根据。纠正无礼的言论与开始一场毫无意义的口水战仅一线之隔，黑客们自己莽撞地越线的情况并不鲜见。如果你是新手或外人，避开这种莽撞的机会并不高。如果你想得到的是信息而不是消磨时光，这时最好不要把手放在键盘上以免冒险。\n（有些人断言很多黑客都有轻度的自闭症或亚斯伯格综合症，缺少用于润滑人类社会 正常 交往所需的神经。这既可能是真也可能是假的。如果你自己不是黑客，兴许你认为我们脑袋有问题还能帮助你应付我们的古怪行为。只管这么干好了，我们不在乎。我们 喜欢 我们现在这个样子，并且通常对病患标记都有站得住脚的怀疑）。\nJeff Bigler 的观察总结和这个相关也值得一读 (tact filters )。\n在下一节，我们会谈到另一个问题，当 你 行为不当时所会受到的 冒犯。\n如何避免扮演失败者 在黑客社区的论坛中有那么几次你可能会搞砸 —— 以本指南所描述到的或类似的方式。而你会在公开场合中被告知你是如何搞砸的，也许攻击的言语中还会带点夹七夹八的颜色。\n这种事发生以后，你能做的最糟糕的事莫过于哀嚎你的遭遇、宣称被口头攻击、要求道歉、高声尖叫、憋闷气、威胁诉诸法律、向其雇主报怨、忘了关马桶盖等等。相反地，你该这么做：\n熬过去，这很正常。事实上，它是有益健康且合理的。\n社区的标准不会自行维持，它们是通过参与者积极而 公开地 执行来维持的。不要哭嚎所有的批评都应该通过私下的邮件传送，它不是这样运作的。当有人评论你的一个说法有误或者提出不同看法时，坚持声称受到个人攻击也毫无益处，这些都是失败者的态度。\n也有其它的黑客论坛，受过高礼节要求的误导，禁止参与者张贴任何对别人帖子挑毛病的消息，并声称 如果你不想帮助用户就闭嘴。 结果造成有想法的参与者纷纷离开，这么做只会使它们沦为毫无意义的唠叨与无用的技术论坛。\n夸张的讲法是：你要的是 “友善”（以上述方式）还是有用？两个里面挑一个。\n记着：当黑客说你搞砸了，并且（无论多么刺耳）告诉你别再这样做时，他正在为关心 你 和 他的社区 而行动。对他而言，不理你并将你从他的生活中滤掉更简单。如果你无法做到感谢，至少要表现得有点尊严，别大声哀嚎，也别因为自己是个有戏剧性超级敏感的灵魂和自以为有资格的新来者，就指望别人像对待脆弱的洋娃娃那样对你。\n有时候，即使你没有搞砸（或者只是在他的想像中你搞砸了），有些人也会无缘无故地攻击你本人。在这种情况下，抱怨倒是 真的 会把问题搞砸。\n这些来找麻烦的人要么是毫无办法但自以为是专家的不中用家伙，要么就是测试你是否真会搞砸的心理专家。其它读者要么不理睬，要么用自己的方式对付他们。这些来找麻烦的人在给他们自己找麻烦，这点你不用操心。\n也别让自己卷入口水战，最好不要理睬大多数的口水战 \u0026ndash; 当然，这是在你检验它们只是口水战，并且未指出你有搞砸的地方，同时也没有巧妙地将问题真正的答案藏于其后（这也是有可能的）。\n不该问的问题 以下是几个经典蠢问题，以及黑客没回答时心中所想的：\n问题：我能在哪找到 X 程序或 X 资源？ 问题：我怎样用 X 做 Y？ 问题：如何设定我的 shell 提示？ 问题：我可以用 Bass-o-matic 文件转换工具将 AcmeCorp 档案转换为 TeX 格式吗？ 问题：我的程序 / 设定 / SQL 语句没有用 问题：我的 Windows 电脑有问题，你能帮我吗？ 问题：我的程序不会动了，我认为系统工具 X 有问题 问题：我在安装 Linux（或者 X ）时有问题，你能帮我吗？ 问题：我怎么才能破解 root 帐号 / 窃取 OP 特权 / 读别人的邮件呢？ 问题：我能在哪找到 X 程序或 X 资源？\n回答：就在我找到它的地方啊，白痴 —— 搜索引擎的那一头。天哪！难道还有人不会用 Google 吗？\n问题：我怎样用 X 做 Y？\n回答：如果你想解决的是 Y ，提问时别给出可能并不恰当的方法。这种问题说明提问者不但对 X 完全无知，也对 Y 要解决的问题糊涂，还被特定形势禁锢了思维。最好忽略这种人，等他们把问题搞清楚了再说。\n问题：如何设定我的 shell 提示？？\n回答：如果你有足够的智慧提这个问题，你也该有足够的智慧去 RTFM ，然后自己去找出来。\n问题：我可以用 Bass-o-matic 文件转换工具将 AcmeCorp 档案转换为 TeX 格式吗？\n回答：试试看就知道了。如果你试过，你既知道了答案，就不用浪费我的时间了。\n问题：我的 {程序 / 设定 / SQL 语句} 不工作\n回答：这不算是问题吧，我对要我问你二十个问题才找得出你真正问题的问题没兴趣 —— 我有更有意思的事要做呢。在看到这类问题的时候，我的反应通常不外如下三种\n你还有什么要补充的吗？ 真糟糕，希望你能搞定。 这关我有什么屁事？ 问题：我的 Windows 电脑有问题，你能帮我吗？\n回答：能啊，扔掉微软的垃圾，换个像 Linux 或 BSD 的开源操作系统吧。\n注意：如果程序有官方版 Windows 或者与 Windows 有互动（如 Samba），你 可以 问与 Windows 相关的问题， 只是别对问题是由 Windows 操作系统而不是程序本身造成的回复感到惊讶， 因为 Windows 一般来说实在太烂，这种说法通常都是对的。\n问题：我的程序不会动了，我认为系统工具 X 有问题\n回答：你完全有可能是第一个注意到被成千上万用户反复使用的系统调用与函数库档案有明显缺陷的人，更有可能的是你完全没有根据。不同凡响的说法需要不同凡响的证据，当你这样声称时，你必须有清楚而详尽的缺陷说明文件作后盾。\n问题：我在安装 Linux（或者 X ）时有问题，你能帮我吗？\n回答：不能，我只有亲自在你的电脑上动手才能找到毛病。还是去找你当地的 Linux 使用群组者寻求实际的指导吧（你能在 这儿 找到使用者群组的清单）。\n注意：如果安装问题与某 Linux 的发行版有关，在它的邮件列表、论坛或本地使用者群组中提问也许是恰当的。此时，应描述问题的准确细节。在此之前，先用 Linux 和 所有 被怀疑的硬件作关键词仔细搜索。\n问题：我怎么才能破解 root 帐号 / 窃取 OP 特权 / 读别人的邮件呢？\n回答：想要这样做，说明了你是个卑鄙小人；想找个黑客帮你，说明你是个白痴！\n好问题与蠢问题 最后，我将透过举一些例子，来说明怎样聪明的提问；同一个问题的两种问法被放在一起，一种是愚蠢的，另一种才是明智的。\n蠢问题：\n我可以在哪儿找到关于 Foonly Flurbamatic 的资料？\n这种问法无非想得到 STFW 这样的回答。\n聪明问题：\n我用 Google 搜索过 \u0026ldquo;Foonly Flurbamatic 2600\u0026rdquo;，但是没找到有用的结果。谁知道上哪儿去找对这种设备编程的资料？\n这个问题已经 STFW 过了，看起来他真的遇到了麻烦。\n蠢问题：\n我从 foo 项目找来的源码没法编译。它怎么这么烂？\n他觉得都是别人的错，这个傲慢自大的提问者。\n聪明问题：\nfoo 项目代码在 Nulix 6.2 版下无法编译通过。我读过了 FAQ，但里面没有提到跟 Nulix 有关的问题。这是我编译过程的记录，我有什么做的不对的地方吗？\n提问者已经指明了环境，也读过了 FAQ，还列出了错误，并且他没有把问题的责任推到别人头上，他的问题值得被关注。\n蠢问题：\n我的主机板有问题了，谁来帮我？\n某黑客对这类问题的回答通常是：好的，还要帮你拍拍背和换尿布吗？，然后按下删除键。\n聪明问题：\n我在 S2464 主机板上试过了 X 、 Y 和 Z ，但没什么作用，我又试了 A 、 B 和 C 。请注意当我尝试 C 时的奇怪现象。显然 florbish 正在 grommicking，但结果出人意料。通常在 Athlon MP 主机板上引起 grommicking 的原因是什么？有谁知道接下来我该做些什么测试才能找出问题？\n这个家伙，从另一个角度来看，值得去回答他。他表现出了解决问题的能力，而不是坐等天上掉答案。\n在最后一个问题中，注意 告诉我答案 和 给我启示，指出我还应该做什么诊断工作 之间微妙而又重要的区别。\n事实上，后一个问题源自于 2001 年 8 月在 Linux 内核邮件列表（lkml）上的一个真实的提问。我（Eric）就是那个提出问题的人。我在 Tyan S2464 主板上观察到了这种无法解释的锁定现象，列表成员们提供了解决这一问题的重要信息。\n通过我的提问方法，我给了别人可以咀嚼玩味的东西；我设法让人们很容易参与并且被吸引进来。我显示了自己具备和他们同等的能力，并邀请他们与我共同探讨。通过告诉他们我所走过的弯路，以避免他们再浪费时间，我也表明了对他们宝贵时间的尊重。\n事后，当我向每个人表示感谢，并且赞赏这次良好的讨论经历的时候， 一个 Linux 内核邮件列表的成员表示，他觉得我的问题得到解决并非由于我是这个列表中的 名 人，而是因为我用了正确的方式来提问。\n黑客从某种角度来说是拥有丰富知识但缺乏人情味的家伙；我相信他是对的，如果我 像 个乞讨者那样提问，不论我是谁，一定会惹恼某些人或者被他们忽视。他建议我记下这件事，这直接导致了本指南的出现。\n如果得不到回答 如果仍得不到回答，请不要以为我们觉得无法帮助你。有时只是看到你问题的人不知道答案罢了。没有回应不代表你被忽视，虽然不可否认这种差别很难区分。\n总的来说，简单的重复张贴问题是个很糟的点子。这将被视为无意义的喧闹。有点耐心，知道你问题答案的人可能生活在不同的时区，可能正在睡觉，也有可能你的问题一开始就没有组织好。\n你可以通过其他渠道获得帮助，这些渠道通常更适合初学者的需要。\n有许多网上的以及本地的使用者群组，由热情的软件爱好者（即使他们可能从没亲自写过任何软件）组成。通常人们组建这样的团体来互相帮助并帮助新手。\n另外，你可以向很多商业公司寻求帮助，不论公司大还是小。别为要付费才能获得帮助而感到沮丧！毕竟，假使你的汽车发动机汽缸密封圈爆掉了 —— 完全可能如此 —— 你还得把它送到修车铺，并且为维修付费。就算软件没花费你一分钱，你也不能强求技术支持总是免费的。\n对像是 Linux 这种大众化的软件，每个开发者至少会对应到上万名使用者。根本不可能由一个人来处理来自上万名使用者的求助电话。要知道，即使你要为这些协助付费，和你所购买的同类软件相比，你所付出的也是微不足道的（通常封闭源代码软件的技术支持费用比开源软件的要高得多，且内容也没那么丰富）。\n如何更好地回答问题 态度和善一点。问题带来的压力常使人显得无礼或愚蠢，其实并不是这样。\n对初犯者私下回复。对那些坦诚犯错之人没有必要当众羞辱，一个真正的新手也许连怎么搜索或在哪找常见问题都不知道。\n如果你不确定，一定要说出来！一个听起来权威的错误回复比没有还要糟，别因为听起来像个专家很好玩，就给别人乱指路。要谦虚和诚实，给提问者与同行都树个好榜样。\n如果帮不了忙，也别妨碍他。不要在实际步骤上开玩笑，那样也许会毁了使用者的设置 —— 有些可怜的呆瓜会把它当成真的指令。\n试探性的反问以引出更多的细节。如果你做得好，提问者可以学到点东西 —— 你也可以。试试将蠢问题转变成好问题，别忘了我们都曾是新手。\n尽管对那些懒虫抱怨一声 RTFM 是正当的，能指出文件的位置（即使只是建议个 Google 搜索关键词）会更好。\n如果你决定回答，就请给出好的答案。当别人正在用错误的工具或方法时别建议笨拙的权宜之计（wordaround），应推荐更好的工具，重新界定问题。\n正面的回答问题！如果这个提问者已经很深入的研究而且也表明已经试过 X 、 Y 、 Z 、 A 、 B 、 C 但没得到结果，回答 试试看 A 或是 B 或者 试试 X 、 Y 、 Z 、 A 、 B 、 C 并附上一个链接一点用都没有。\n帮助你的社区从问题中学习。当回复一个好问题时，问问自己 如何修改相关文件或常见问题文件以免再次解答同样的问题？，接着再向文件维护者发一份补丁。\n如果你是在研究一番后才做出的回答，展现你的技巧而不是直接端出结果。毕竟 授人以鱼不如授人以渔。\n相关资源 如果你需要个人电脑、Unix 系统和网络如何运作的基础知识，参阅 Unix 系统和网络基本原理 。\n当你发布软件或补丁时，试着按 软件发布实践 操作。\n鸣谢 Evelyn Mitchel 贡献了一些愚蠢问题例子并启发了编写 如何更好地回答问题 这一节， Mikhail Ramendik 贡献了一些特别有价值的建议和改进。\n","date":"2020-03-17T09:05:34+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/timg.webp","permalink":"https://opoa.top/post/how-to-ask-question-the-smart-way/","title":"提问的智慧"},{"content":" i++ 表示的是先赋值，再自增 ++i 表示的是先自增，再赋值 当然还不止这些，先上一道自己踩过坑的题\npublic class Test { public static void main(String[] args){ int i = 0; i = i++; System.out.println(\u0026#34;i = \u0026#34; + i); } } 这道题程序员都不会陌生，答案是 i = 0;\n可为什么会是 0 呢，这就需要了解 i++ 的底层到底是如何运算的。\n模拟 i++ 底层实现 i = i++; ------------- int i = 0; int temp = i; i = i + 1; i = temp; 可以看到 i 的值先是被赋给了一个临时变量，此时 temp = 0,i 再进行自增 1 运算，i = 1, 最后把 temp 的值赋给了 i; 结果就是 i = 0。\n++i public class Test { public static void main(String[] args){ int i = 0; i = ++i; System.out.println(\u0026#34;i = \u0026#34; + i); } } ++i 就好理解一些了，先进行自增运算，再赋值。结果:\ni = 1; 小技巧 最后贴一个关于如何区分 i++ 与 ++i 的小技巧，算是一个心得吧，希望对初学编程的人能够有所帮助。\n​\ti++ 是先赋值，再自增\n​\t++i 是先自增，再赋值\n如何才能不混淆呢，你可以想象算式的左边有一个赋值号，i++ 想象成 = i++，然后利用 “就近原则”，此时 i 与赋值号更近，所以先进行赋值的操作，再自增，同理，如果是 ++i 的话，+ 离赋值号更近，就先进行自增，再赋值。\n参考资料 i++的底层解释（数据原子性） ","date":"2020-03-05T17:54:56+08:00","image":"https://cdn.jsdelivr.net/gh/oopooa/cdn/cover/girl-scarf-winter.webp","permalink":"https://opoa.top/post/the-difference-between-post-increment-and-pre-increment/","title":"i++ 与 ++i 的区别"}]