1. 解决 Groovy 引起的一次 OOM 告警

    线上收到告警,有个服务的一个 pod was OOM killed. 问题分析 从监控系统来看,被 kill 的节点 A 在重启前,堆内存使用随着 YoungGC 规律波动,元空间占用较高,且一直缓慢增长到了400MB以上——该应用代码量不大,按理不应该占用这么多。 而与它同容器组的另一个节点 B 看起来更不正常,平均响应时间明显长于另外的节点,且在堆内存已经降下来的情况下还多次 FullGC,并且有很多 java.lang.OutOfMemoryError。晚些时候该节点触发了两次 FullGC 次数过多的告警。 OutOfMemoryError 异常堆栈: java.lang.OutOfMemoryError : Metaspace at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:763) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) at groovy.lang.GroovyClassLoader.access$400(GroovyClassLoader.java:62) at groovy.lang.GroovyClassLoader$ClassCollector.createClass(GroovyClassLoader.java:500) at groovy.lang.GroovyClassLoader$ClassCollector.onClassNode(GroovyClassLoader.java:517) at groovy.lang.GroovyClassLoader$ClassCollector.call(GroovyClassLoader.java:521) at org.codehaus.groovy.control.CompilationUnit$16.call(CompilationUnit.java:822) at org.codehaus.groovy.control.CompilationUnit.applyToPrimaryClassNodes(CompilationUnit.java:1053) at org.codehaus.groovy.control.CompilationUnit.doPhaseOperation(CompilationUnit.java:591) at org.codehaus.groovy.control.CompilationUnit.processPhaseOperations(CompilationUnit.java:569) at org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:546) at groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:298) at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:268) at groovy.lang.GroovyShell.parseClass(GroovyShell.java:688) at groovy.lang.GroovyShell.parse(GroovyShell.java:700) at groovy.lang.GroovyShell.evaluate(GroovyShell.java:584) at groovy.lang.GroovyShell.evaluate(GroovyShell.java:623) at groovy.lang.GroovyShell.evaluate(GroovyShell.java:594) at org.springframework.scripting.groovy.GroovyScriptEvaluator.evaluate(GroovyScriptEvaluator.java:118) 结合以上异常堆栈与节点 B 的现象推测: 执行 GroovyScriptEvaluator.evaluate 时,会动态生成一些 Class,导致元空间占用持续上升; FullGC 主要不是为了回收堆内存,很可能是为了回收元空间; FullGC 也无法成功回收 1 中动态生成的 Class 占用的元空间。 根据推测,用 Groovy 和 Metaspace 作为关键字进行了一些搜索,找到如下一篇相关性比较高的文章:记一次线上Groovy导致的OOM的问题解决过程 以及它里面引用的文章: Groovy 动态加载类踩中的那些坑 里面提到了 Groovy 的一个 Bug: ClassInfo.globalClassValue lead to GroovyClassLoader can’t unload classes,大意是 Groovy 动态生成的类因为被缓存和引用,导致无法 unload,从而引发元空间随着时间推移一直增长且无法释放。在 Groovy 2.4.6 引入,2.4.8 修复。 检查我们项目里的 groovy-all 包版本,是 2.4.7,那很有可能命中这个 bug。 本地验证 构建一个测试应用,启动后循环调用 GroovyScriptEvaluator.evaluate,如: @SpringBootApplication public class GroovyOomDemoApplication implements CommandLineRunner { public static void main(String[] args) { SpringApplication.run(GroovyOomDemoApplication.class, args); } @Override public void run(String... args) throws Exception { GroovyScriptEvaluator evaluator = new GroovyScriptEvaluator(); ScriptSource scriptSource = new StaticScriptSource("a == 3"); Map<String, Object> params = new HashMap<>(0); Random rand = new Random(); while (true) { Integer a = rand.nextInt(10); params.put("a", a); Object result = evaluator.evaluate(scriptSource, params); System.out.printf("a = %d, result is %s%n", a, result); } } } 然后在运行的 JVM 参数里添加一些参数: -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=64m -verbose:class -verbose:gc 设置最大元空间大小、打印类的 load/unload、以及 GC 的信息。 测试代码发布在 https://github.com/mzlogin/groovy-oom-demo 使用 groovy-all 2.4.7 版本运行的情况 控制台打印: …… [Loaded Script1 from file:/groovy/shell] a = 1, result is false [Loaded Script1 from file:/groovy/shell] a = 2, result is false [Loaded Script1 from file:/groovy/shell] a = 0, result is false [Loaded Script1 from file:/groovy/shell] a = 8, result is false [GC (Metadata GC Threshold) 838057K->253201K(1080832K), 0.1350074 secs] [Full GC (Metadata GC Threshold) 253201K->244956K(1232896K), 0.4860932 secs] [GC (Last ditch collection) 244956K->245557K(1421824K), 0.0403506 secs] …… Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main" …… Profiler: 从控制台打印以及 Profiler 来看,程序先是打印了很多 [Loaded Script1 from file:/groovy/shell],然后最后当 Non-Heap Memory 占用很高之后,开始因为达到 Metadata GC Threshold,疯狂 YongGC + FullGC,但 Non-Heap Memory 也降不下来,最终程序很快直接挂掉了。 升级为 groovy-all 2.4.8 版本运行的情况 控制台打印: …… [Loaded Script1 from file:/groovy/shell] a = 9, result is false [Loaded Script1 from file:/groovy/shell] a = 3, result is true [Loaded Script1 from file:/groovy/shell] a = 7, result is false [Loaded Script1 from file:/groovy/shell] a = 7, result is false [GC (Metadata GC Threshold) 722452K->251702K(1090560K), 0.0483118 secs] [Full GC (Metadata GC Threshold) 251702K->240778K(1254912K), 0.4303570 secs] [GC (Last ditch collection) 240778K->241270K(1373696K), 0.0274501 secs] [Full GC (Last ditch collection) [Unloading class Script1 0x00000007c103c428] [Unloading class Script1 0x00000007c103bc28] [Unloading class Script1 0x00000007c103b428] [Unloading class Script1 0x00000007c103ac28] [Unloading class Script1 0x00000007c103a428] …… Profiler: 同上面一样,程序开始也是打印了很多 [Loaded Script1 from file:/groovy/shell],但不同的是达到 Metadata GC Threshold 进行 GC 之后,可以将 Non-Heap Memory 占用降下来,并且从控制台可以看到在 GC 时打印了很多 [Unloading class Script1 xxx],程序持续运行很长时间也没问题。 另外有个疑问:这个应用上线很长时间了,与 Groovy 相关的逻辑很久没有动过了,为什么以前没有出现这种现象? 答案:以前这个应用时不时会发一次版,重置 Metaspace,而这次有两个多月没有发版了,Metaspace 一直增长,最终达到了阈值。 解决方法 升级 groovy-all 至 2.4.8(含)版本以上。 参考 记一次线上Groovy导致的OOM的问题解决过程 Groovy 动态加载类踩中的那些坑 ClassInfo.globalClassValue lead to GroovyClassLoader can’t unload classes

    2023/03/22 Java

  2. 读书:哲学家们都干了些什么

    哲学本质上是人理解人、人认识人的理性活动,是对世界基本和普遍之问题研究的学科,是关于世界观的理论体系。——百度百科 这本书应该是想用不那么严肃的方式串起整个哲学史,可能有一些地方不那么详实和严谨,但对于我这样的小白来讲算是不错的入门读物。 开篇 两个问题: 人为什么活着? 人生的意义是什么? 重要人物 苏格拉底 雅典 西方人的「至圣先师」,类比中国的孔子 一辈子做得最多的事就是问问题,不打击会死星人 柏拉图 雅典 苏格拉底的学生 重视心灵理性 亚里士多德 马其顿王国、亚历山大帝国 柏拉图的学生亚历山大的老师 重视现实经验 「吾爱吾师,吾更爱真理。」 保罗 罗马帝国 和耶稣处于同一时代 从犹太教皈依基督教 利用希腊哲学作为传教的武器,完善了基督教的理论基础《保罗书信》成为《新约》的重要组成部分 奥古斯丁 教父哲学家基督教历史上重要的圣贤 贡献之一,解决了一个长久困扰基督教的逻辑漏洞: Q:《圣经》说上帝是全知、全能和全善的,那为什么会允许人间存在那么多丑恶和痛苦?我们知道,《圣经》里说亚当和夏娃偷吃了禁果,所以被逐出伊甸园,人类才会开始无尽地受苦。上帝既然知道亚当和夏娃会偷吃禁果,为什么一开始不去阻止他们? A: 关键在于自由。上帝给了亚当夏娃和人类自由意志,所以也必须让人类有作恶的可能。 阿奎纳 经院哲学的高峰 提出了五个方法来证明上帝的存在。比如一个最简单的概括:世上万事万物都要有另一个事物作为它的原因,那么必然存在一个最初的原因,这个原因就是上帝。 马丁.路德 神圣罗马帝国(今德国) 得益于造纸术、印刷术,以哲学为武器反抗罗马教廷,最终基督教分裂成天主教(罗马)、新教(路德)、东正教(东罗马帝国) 笛卡尔 荷兰 数学派哲学家(理性主义,推理演绎) 亲自思考世界,读「世界这本大书」。我的知识不在先贤的书里,而是在实践检验里。 我眼前的这个世界是不是都是假的?我见到的一切会不会都是幻觉、都是梦境?「我思故我在」,二元论(心灵与外界,相互独立、平等,虽可以相互影响,但谁也不能完全决定另一个)意识到可以将欧氏几何的演绎推理方法应用到哲学上来,基于一些不言自明的公设,推导出整个哲学世界。 「不管多么荒谬、多么不可置信的事,无一不是这个或那个哲学家主张过的。」 斯宾诺莎 荷兰 笛卡尔的继承者 生活困苦的哲学家,关注个人幸福。自己的幸福应该通过理性思考来追求,在未得出最终答案之前如何生活?一、说话尽量让别人明白,只要别人的要求不会影响我们实现自己的目标,尽量满足;二、只享受为保持健康所必须的生活乐趣;三、只求取为生活和健康所必需的金钱; 实现了笛卡尔设想的按照欧氏几何学的模式来建立哲学体系。最有影响力的著作:《按几何顺序证明的伦理学》 洛克 英国 科学派哲学家(经验主义,归纳法) 无根之木——科学派对数学派的攻击非常准确。科学派的弱点:没法保证结论的可靠性。白板说:人的经验是从后天的客观世界而来。 莱布尼茨 德国 数学派哲学家(理性主义,推理演绎) 与洛克辩论的书信:《人类理智新论》。单子论:物质是占据空间的,只要是能占据空间的东西就可以被分成更小的东西,无限分之后,剩下的是不占据空间的「东西」(精神,非物质,起名单子)。和牛顿各自独立发明微积分。 「世上没有两片树叶是相同的。」 牛顿 英国 物理学家数学家天文学家哲学家(偏经验主义,机械论)神学家炼金术士 为机械论打下基础。机械论/机械唯物主义:用物理学、数学公式来解释包括人类意识在内的整个世界。 霍布斯 英国 机械论的急先锋。 机械论很容易推导出虚无主义、享乐主义,还有决定论:既然世间万物都可以用物理规律来解释,那么每一事件之间必然要遵循严格的因果关系。如果人的意识是完全由物质决定的,那肯定也得服从严格的物理定律。那么,整个世界该如何发展,该走向何处,都是由自然定律决定好了的。(一旦接受这个设定,就意味着人类没有了自由的意志。) 休谟 英国 业余哲学家(偏经验主义) 挑战机械论和整个科学界。动摇科学和哲学的根基。理性主义属于独断论,经验主义又不能证明事物之间存在因果关系。只有两类知识是可靠的:一类是像逻辑和几何那样,既逻辑严谨又不依赖于外物存在的知识;一类是我们感官体验到的知识。再多的偶然观测也不能得出必然的结论。 「你怎么知道明天的太阳会照样升起?」 康德 德国 《纯粹理性批判》 人类感觉到的世界,也就是「物自体」(世界的真面目)经过「先天认识形式」(人类心灵中的某个特殊的机制)加工后得到的东西,我们叫做「表象」。把世界分成了两个部分。一个部分完全不可知,另一个部分则可以用更改把握。不可知的那部分因为永远不可知,所以对我们的生活没有什么影响。只要我们在可把握的世界里生活,理性就又恢复了威力。 黑格尔 德国 形而上学的巅峰 辩证法。理性经过不断的辩证,就可以完全符合客观世界的真实面貌。绝对精神不是静止不动的一个东西,而是整个历史在不断运动的这个过程本身,这个「运动过程」才是终极真理。 叔本华 悲观主义者 康德思想的继承者讨厌黑格尔 《作为意志和表象的世界》宇宙中万事万物背后都有生命意志在驱动。生命意志是邪恶的,是痛苦的源泉。满足欲望会带来快乐,但欲望本质上是痛苦之源。满足不了欲望,人会痛苦。满足了欲望,人又会产生新的、更高的欲望,还是会痛苦。 尼采 德国 叔本华的崇拜者讨厌黑格尔 世界观带有强烈的激情,觉得应该承认痛苦,迎战痛苦。精英主义:把人分为强者和弱者,道德分为贵族道德和奴隶道德。推崇强者,大部分强都都被奴隶道德压抑着。 将叔本华理论里的「生命意志」改造成「权力意志」,权力意志是征服的意志,在权力意志的驱使下,人类去研究世界不是为了简单地求知,而是为了能更好地控制世界。 「上帝死了。」 克尔凯郭尔 讨厌黑格尔 强调个人的选择。反对用理性研究宗教。 揭示了形而上学和自由意志的矛盾。 罗素 英国 逻辑实证主义 《数学原理》逻辑实证主义:用严格符合逻辑的语言来进行哲学研究与表达(如逻辑符号)。 「三种单纯又极其强烈的激情支配着我的一生:对爱情的渴望,对知识的追求,以及对于人类苦难的不可遏制的同情。」「须知参差多态,乃是幸福本源。」 维特根斯坦 奥地利 罗素学生 《逻辑哲学论》绝对的更改得不到任何有意义的结论,理论思维和现实之间的矛盾,更改无法担负从总体上解释世界、指导生活的任务。 「凡是可说的事情,都可以说清楚,凡是不可说的事情,我们必须保持沉默。」「人生问题的解答在于对这个问题的消除。」 波普尔 奥地利 证伪主义提出 一个检验科学理论的重要标准:证伪。科学理论必须能提出一个可供证伪的事实,假如这个事实一经验证,便承认该理论是错的。如果暂时没有人能证明它是错的,那它暂时就是对的。终结形而上学:凡是终极真理,都是毫无意义的命题,不值一提。 其它 实用主义 哲学也得像科学这样,不再说空话,不再讨论空泛的大部分,而是重视哲学的实用性。 「黑猫白猫,能抓住老鼠就是好猫。」 摘录 对人伤害最大的其实不是一时的痛苦,而是对未来痛苦的恐惧。这就像打针对于孩子来说,可怕的地方在于排队,在于来苏水味、叮叮当当的针管及胳膊上的凉意。真正的肉体疼痛与此相比微不足道。我们怕穷,并不是因为我们不能忍受粗糙的吃穿,而是因为不愿意整日生活在对贫穷的恐惧和屈辱中。我们不愿意忍受的是那种担惊受怕的状态。 所以,在面对痛苦的时候,我们应该把自己的感受局限在此时一瞬,而不要顾及那些未到的痛苦。 「先验」和「先天」差不多。意思是,先于经验,说某些东西是在人获取经验之前就存在的。 我们追求个人幸福的最高境界,不是纵欲,而是内心的平静。 休谟说,无论我们过去看到多少重复发生的事件,我们也不能断言这事件在未来一定会再次发生。 实际上,马克思当年为了维护工人阶级利益提出的很多要求,大部分都被资本主义国家接受并且实现了。如今这种改良式的资本主义在西方颇受欢迎,这可以让我们看到实用主义在西方的用处。 当人意识到人生没有目的的时候,对目的的本能渴望和没有目的的现实就会发生强烈的冲突,让人产生荒谬感。 用西西弗的比喻来说,我们只能在推石头的时候哄自己说这么做是有意义的,并且乐在其中。这个哄骗自己的借口,就是人生意义。 对开篇两个问题的回应 这两个问题,其实可以认为是一个问题。 很多人终其一生都在苦苦追寻这个问题的答案,如果有标准答案,那不知道是人类的幸还是不幸。 上面摘录中有一句话: 用西西弗的比喻来说,我们只能在推石头的时候哄自己说这么做是有意义的,并且乐在其中。这个哄骗自己的借口,就是人生意义。 书的最后一章里还给了一个方法,就是问自己一个问题:你为什么不自杀?你的回答就是你现在的人生意义。 一点小感想 也许一些比较古早的哲学家的思想与观点,在今天看来甚至有些可笑,但一切都要结合历史的进程来看,宗教、商业、科学、哲学经过漫长的发展才成了现在的样子,这些哲人在他们所处的年代就已经是最顶尖的思考者,我们只不过是站在巨人的肩膀上罢了。在历史的长河中未来的某个时间点,回过头来审视我们这个时代的思想,也很可能会感觉到巨大的局限性。 书籍推荐 《西方哲学史》——罗素,上下卷 参考 关键人物梳理(哲学家们都干了些什么?)书评

    2023/03/08 Blog

  3. 修复 MacVim 9.0 的 Python3 支持

    前两天刚刚升级到了 MacVim 9.0 的最新版本,日常编辑编辑文字没遇到过什么问题,直到今天动了一下插件。 发现问题 今早看到一个有意思的 Vim 插件,安装上试用了下,感觉对我来说不太实用,就删掉配置,打算运行 :PlugClean 清理掉它,结果 MacVim 提示我即将删掉的插件有两个——除了试用的这个以外,还有 LeaderF。 LeaderF 是我用得比较多的插件之一了,我并没有表明意图我要删掉它,是发生了什么让 vim-plug 这样以为呢?肯定是有什么误会。 我的 _vimrc 文件里,添加 LeaderF 插件是这样写的: if has('python') || has('python3') Plug 'Yggdroot/LeaderF', { 'do': ':LeaderfInstallCExtension' } endif 于是打开一个 MacVim 窗口,试了下 :echo has('python') 和 :echo has('python3'),输出竟然都是 0,那就难怪了…… 分析问题 一开始主要想弄清楚两点: 我使用的 MacVim 版本编译时究竟有没有启用 Python 支持? 在 MacVim 窗口里运行 :version,可以看到 +python/dyn 和 +python3/dyn,那说明同时启用了 Python 和 Python3 支持。 我本地有没有安装 Python? $ python zsh: command not found: python $ brew list | grep python python@3.10 python@3.8 python@3.9 $ python3 Python 3.9.12 (main, Mar 26 2022, 15:51:15) [Clang 13.1.6 (clang-1316.0.21.2)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> 可以看到我本地安装了 Python3 的 3.8、3.9、3.10 三个版本,默认 3.9,没有安装 Python2。 这没什么问题,那继续找,尝试下在 MacVim 里执行 Python3 语句: :py3 import sys; 结果输出了一堆报错: E370: 无法加载库 /usr/local/Frameworks/Python.framework/Versions/3.10/Python:dlopen(/usr/local/Frameworks/Python.fram ework/Versions/3.10/Python, 0x0009): tried: '/usr/local/Frameworks/Python.framework/Versions/3.10/Python' (no such fil e), '/Library/Frameworks/Python.framework/Versions/3.10/Python' (no such file), '/System/Library/Frameworks/Python.fra mework/Versions/3.10/Python' (no such file) E263: 抱歉,此命令不可用,无法加载 Python 库。 它要找的这个文件路径确实不存在……毕竟我默认的是 3.9 版本,所以 /usr/local/Frameworks/Python.framework/Versions/ 下只有 3.9 和 current 目录,没有 3.10。 它为啥放着配置好的 3.9 版本不用,非得这么头铁去找 3.10 版本呢?这个问题先不回答,留待后面的刨根问底环节。现在先解决问题。 解决问题 在网上将以上错误信息搜索一番后,了解到了可以通过设置 pythonthreedll 来指定动态加载的 Python3 支持库。 另外,也了解了一下,通过 brew 安装的多个 Python 版本如何切换默认版本。 所以这个小问题找到了两种解决方法: 一、在 _vimrc 里添加配置,指定动态加载的 Python3 支持库路径,比如: let &pythonthreedll='/usr/local/Frameworks/Python.framework/Versions/3.9/python' 二、切换系统默认 Python3 版本,比如这里 MacVim 寻找 3.10 版本,我就把默认的切换到 3.10 版本好了: brew unlink python@3.9 brew link python@3.10 经验证以上两个方法都可以解决问题,我最终用了第二种。 刨根问底 上面我们遗留了一个问题,为什么 MacVim 那么头铁非要加载 3.10 版本的 Python 支持库呢? 首先看一下 pythonthreedll 的帮助文档说明: :h pythonthreedll 可以看到: 'pythonthreedll' string (default depends on the build) global {only available when compiled with the |+python3/dyn| feature} Specifies the name of the Python 3 shared library. The default is DYNAMIC_PYTHON3_DLL, which was specified at compile time. Environment variables are expanded |:set_env|. This option cannot be set from a |modeline| or in the |sandbox|, for security reasons. 也就是说默认值是在编译时指定的 DYNAMIC_PYTHON3_DLL 值,按我理解那就是说如果没有在配置文件里人为指定,那它就是会按编译时指定的去加载。 那编译时的 DYNAMIC_PYTHON3_DLL,我们可以在 MacVim 的官方仓库 .github/worflows/ci-macvim.yaml 里找到,关键内容: ... vi_cv_dll_name_python3: /usr/local/Frameworks/Python.framework/Versions/3.10/Python # Make sure to keep src/MacVim/vimrc synced with the Python version here for the Python DLL detection logic. ... grep -q -- "-DDYNAMIC_PYTHON3_DLL=\\\\\"${vi_cv_dll_name_python3}\\\\\"" src/auto/config.mk ... 至此破案了。 参考 https://www.jianshu.com/p/18f06d12348c

    2022/09/22 Vim

  4. 使用 iPhone 感觉不好的几点体验

    上周有天晚上加完班下雨了,媳妇儿开车顺道带我回家,到家后过了一会儿她突然说,「对了,给你个东西」,然后从包里拿出一个黑盒子递给我,我正坐那刷沙雕视频,也没细看,见挺小一个盒子,随口问了一句「啥?充电宝啊?」,得到的是充满鄙夷的回复,「我们家需要那玩意儿吗,我买那干啥?」,接过来翻面一看,哦豁原来是 iPhone 13 Pro。 使用这几天后,对比原来用小米手机,感觉有几点不那么方便的地方,记录一下,看后面用习惯之后是否会有改观。 屏幕解锁 Face ID 挺好用的,但是,戴上口罩它不好使了,又不支持指纹解锁,在外面每次使用手机就要手动输入 6 位数字密码,就很尴尬。 不过听说开发版已经支持口罩解锁了,这一点后续应该会得到改善。 Update 2022/03/14: iOS 更新到 15.4 之后,已经支持戴口罩解锁,虽然有少量时候不是那么灵敏,但总体体验已经不错了。 NFC 相关功能 以刷公交卡为例(这个可能跟地域性体验相关),苹果钱包里目前不支持添加武汉通公交卡,这样用 NFC 刷地铁只能直接刷 Apple Pay 从银行卡里扣,这就需要先解锁手机,手动输入一波密码(因为在外面都戴着口罩无法面容解锁),然后还要按键确认,体验不好,这个得等啥时候支持武汉通了才会有改善。 另外,貌似也不支持写入加密门禁卡。 Update 2022/03/14: iOS 更新到 15.4 之后,支持戴口罩面容解锁了,NFC 刷武汉地铁的操作变成了唤起 Apple Pay - 面容解锁 - 刷卡,体验好了一些,当然比直接盲刷还是有差距。 充电 现在支持快充的安卓手机充电比苹果要快得多,这个对特定硬件来讲,暂时应该没有啥改善的可能性。 管理照片/文件 安卓手机连接上电脑后,就能像管理一个普通文件夹一样操作照片、文件等;苹果手机与此不同,我用「迁移到 iOS」应用同步旧数据时,最后两分钟报错了,我先没放在心上,照常使用,后来发现照片少了挺多,就想办法重新拷贝。尝试了一些办法,折腾了大半天才解决。 尝试过的方法: 从 MacBook 同步照片/文件夹,用这个办法,拷贝照片没问题,但有个坑,拷贝过去的文件手机上无法删除,只能在电脑上将对应的文件删除后,手机连上电脑,再次同步。这就很令人心塞……我在手机上看着那些文件,却无法管理。最终放弃该方案。 在 MacBook 和 iPhone 上同时开启照片的 iCloud 同步,最终也是使用了这种方法。遇到的坑:iCloud 上传照片是不去重的,只好上 https://icloud.com 将所有照片清空(包括最近删除的),然后重新把所有照片导到电脑的「照片」里。这里有个教训是打开 iCloud 网站请使用 Safari 浏览器,用其它浏览器(如 Edge)各种报错打不开。 APP 里返回 安卓全面屏手机一般可以设置为左、右边缘滑动均触发返回,但苹果手机只能由左边缘触发,右边缘无反应,右手持机时体验不佳。 呼吸灯 苹果手机没有前置呼吸灯。虽然可以设置为有通知时相机闪光灯闪烁,但在背面啊,而且,动静太大。 微信语音/视频通话 有天偶然看到一个新闻说微信 iOS 版在内测 Callkit,然后找人微信 call 了我一下,才发现在锁屏界面是无法直接接听微信语音和视频通话的,一脸问号,今夕何夕? 后续有想到的其它的点,或者这几个点体验有变化了,我再更新。

    2022/03/06 Blog

  5. 在 Mac 下编译 chaosblade

    首先声明,这不是一份指南,这是一份失败的操作流水记录。我得到的最终结果:编译成功,但是无法运行,放弃在 Mac 平台直接使用此工具,乖乖用 Linux 或者容器环境。 以下问题的遇到和解决记录,基本是按时间序。 chaosblade 项目的 README 上自行编译部分 说明很简单,就是 make build_darwin 这么一条命令而已。 环境准备 安装 go,https://go.dev/ 下载最新版安装,我安装的是 1.17.7 版本,然后将 /usr/local/go/bin 添加到 PATH。 安装和配置 JAVA 环境。(我以前装过,本次先没动它,但实际后面也遇到问题与此相关。) 安装 Docker Desktop。 下载源码,开始编译 git clone git@github.com:chaosblade-io/chaosblade.git cd chaosblade make build_darwin 然后,兵来将挡,水来土掩的问题解决之旅开始了。 遇到问题,解决问题 0x01 网络问题 编译过程中需要下载一些源码和二进制文件,首先遇到了网络问题。 如果遇到 timeout 或者 fatal: 无法访问 'xxxx':LibreSSL SSL_connect: Operation timed out in connection to xxx.com:443 之类的提示,一般是因为有一些资源偶尔需要科学上网才能访问。 解决方法: 终端挂代理: export http_proxy=http://127.0.0.1:54107 export https_proxy=http://127.0.0.1:54107 export no_proxy="chaosblade.oss-cn-hangzhou.aliyuncs.com" 其中代理自备,no_proxy 那一行是指定从阿里域名下载不走代理。 如果遇到下载 https://chaosblade.oss-cn-hangzhou.aliyuncs.com/agent/github/sandbox/sandbox-1.3.1-bin.zip 特别慢,几 kb/s 的速度,但是用浏览器下载很快,那可以先停掉编译,直接用浏览器下载好该文件,放到 target/cache/chaosblade-exec-jvm/build-target/cache 下面,再重新开始编译。 同理: 如果遇到下载 https://chaosblade.oss-cn-hangzhou.aliyuncs.com/agent/release/tools.jar 特别慢,可以先停掉编译,直接用浏览器下载好该文件,放到 target/cache/chaosblade-exec-jvm/build-target/cache 下面,再重新开始编译。 例外是如果遇到下载 https://chaosblade.oss-cn-hangzhou.aliyuncs.com/agent/github/1.5.0/chaosblade-1.5.0-linux-amd64.tar.gz 特别慢,手动下载,放到 target/cache/chaosblade-operator/build/cache 也不行,编译过程只认自己下载的。 由以上得出的小技巧: 如果从 GitHub release 界面下载 chaosblade 的 release 文件特别慢,比如 https://github.com/chaosblade-io/chaosblade/releases/download/v1.3.0/chaosblade-1.3.0-darwin-amd64.tar.gz,可以将链接替换为 https://chaosblade.oss-cn-hangzhou.aliyuncs.com/agent/github/1.3.0/chaosblade-1.3.0-darwin-amd64.tar.gz 加速下载。 0x02 go 依赖模块问题 如果遇到 exec/model.go:22:2: missing go.sum entry for module providing package github.com/chaosblade-io/chaosblade-exec-os/exec (imported by github.com/chaosblade-io/chaosblade-exec-docker/exec); to add: go get github.com/chaosblade-io/chaosblade-exec-docker/exec exec/executor_execin.go:29:2: missing go.sum entry for module providing package github.com/chaosblade-io/chaosblade-spec-go/channel (imported by github.com/chaosblade-io/chaosblade-exec-docker/exec); to add: go get github.com/chaosblade-io/chaosblade-exec-docker/exec exec/client.go:29:2: missing go.sum entry for module providing package github.com/chaosblade-io/chaosblade-spec-go/spec; to add: go mod download github.com/chaosblade-io/chaosblade-spec-go exec/container.go:24:2: missing go.sum entry for module providing package github.com/chaosblade-io/chaosblade-spec-go/util; to add: go mod download github.com/chaosblade-io/chaosblade-spec-go make[1]: *** [build_yaml] Error 1 make: *** [docker] Error 2 到 target/cache/chaosblade-exec-docker 和 target/cache/chaosblade-exec-os 目录下执行 go mod tidy 整理依赖,参考 https://stackoverflow.com/questions/67203641/missing-go-sum-entry-for-module-providing-package-package-name 0x03 Java 编译问题 如果遇到报错: [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-compile) on project chaosblade-exec-spi: Compilation failure: Compilation failure: [ERROR] 不再支持源选项 6。请使用 7 或更高版本。 [ERROR] 不再支持目标选项 6。请使用 7 或更高版本。 [ERROR] -> [Help 1] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException [ERROR] [ERROR] After correcting the problems, you can resume the build with the command [ERROR] mvn <args> -rf :chaosblade-exec-spi make[1]: *** [build_java] Error 1 make: *** [java] Error 2 修改 target/cache/chaosblade-exec-jvm/chaosblade-exec-spi/pom.xml 里的 <configuration> <source>6</source> <target>6</target> </configuration> 为 <configuration> <source>8</source> <target>8</target> </configuration> 同理,后面还会遇到类似的报错,对应还要修改: target/cache/chaosblade-exec-jvm/chaosblade-exec-common/pom.xml target/cache/chaosblade-exec-jvm/chaosblade-exec-service/pom.xml target/cache/chaosblade-exec-jvm/chaosblade-exec-bootstrap/chaosblade-exec-bootstrap-jvmsandbox/pom.xml target/cache/chaosblade-exec-jvm/chaosblade-exec-plugin/chaosblade-exec-plugin-dubbo/pom.xml target/cache/chaosblade-exec-jvm/chaosblade-exec-plugin/chaosblade-exec-plugin-jvm/pom.xml target/cache/chaosblade-exec-jvm/chaosblade-exec-plugin/chaosblade-exec-plugin-mysql/pom.xml target/cache/chaosblade-exec-jvm/chaosblade-exec-plugin/chaosblade-exec-plugin-postgrelsql/pom.xml target/cache/chaosblade-exec-jvm/chaosblade-exec-plugin/chaosblade-exec-plugin-servlet/pom.xml target/cache/chaosblade-exec-jvm/chaosblade-exec-plugin/chaosblade-exec-plugin-jedis/pom.xml target/cache/chaosblade-exec-jvm/chaosblade-exec-plugin/chaosblade-exec-plugin-elasticsearch/pom.xml target/cache/chaosblade-exec-jvm/chaosblade-exec-plugin/chaosblade-exec-plugin-hbase/pom.xml 另外: target/cache/chaosblade-exec-jvm/chaosblade-exec-plugin/pom.xml target/cache/chaosblade-exec-jvm/chaosblade-exec-plugin/chaosblade-exec-plugin-dubbo/pom.xml(存疑) target/cache/chaosblade-exec-jvm/chaosblade-exec-plugin/chaosblade-exec-plugin-http/pom.xml(存疑) 里是需要添加 <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins> </build> 如果遇到 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-compile) on project chaosblade-exec-plugin-redisson: Compilation failure [ERROR] /Users/mazhuang/github/chaosblade/target/cache/chaosblade-exec-jvm/chaosblade-exec-plugin/chaosblade-exec-plugin-redisson/src/main/java/com/alibaba/chaosblade/exec/plugin/redisson/RedissonEnhancer.java:[52,23] 对info的引用不明确 [ERROR] org.slf4j.Logger 中的方法 info(java.lang.String,java.lang.Object...) 和 org.slf4j.Logger 中的方法 info(java.lang.String,java.lang.Throwable) 都匹配 [ERROR] [ERROR] -> [Help 1] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException [ERROR] [ERROR] After correcting the problems, you can resume the build with the command [ERROR] mvn <args> -rf :chaosblade-exec-plugin-redisson make[1]: *** [build_java] Error 1 make: *** [java] Error 2 修改 target/cache/chaosblade-exec-jvm/chaosblade-exec-plugin/chaosblade-exec-plugin-redisson/src/main/java/com/alibaba/chaosblade/exec/plugin/redisson/RedissonEnhancer.java LOGGER.info("method command {}", ReflectUtil.invokeMethod(command, "getName", new Object[0], false)); 改成 Object tmpResult = ReflectUtil.invokeMethod(command, "getName", new Object[0], false); LOGGER.info("method command {}", tmpResult); 如果遇到 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-compile) on project chaosblade-exec-common: Fatal error compiling: java.lang.IllegalAccessError: class lombok.javac.apt.LombokProcessor (in unnamed module @0x45da40ad) cannot access class com.sun.tools.javac.processing.JavacProcessingEnvironment (in module jdk.compiler) because module jdk.compiler does not export com.sun.tools.javac.processing to unnamed module @0x45da40ad -> [Help 1] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException [ERROR] [ERROR] After correcting the problems, you can resume the build with the command [ERROR] mvn <args> -rf :chaosblade-exec-common make[1]: *** [build_java] Error 1 make: *** [java] Error 2 将 target/cache/chaosblade-exec-jvm/chaosblade-exec-common/pom.xml lombok 版本由 1.18.10 改为 1.18.20,参考 https://www.cnblogs.com/ZZG-GANGAN/p/14789050.html 如果遇到 The JAVA_HOME environment variable is not defined correctly, this environment variable is needed to run this program. make[1]: *** [build_java] Error 1 make: *** [java] Error 2 那配置 JAVA_HOME: /usr/libexec/java_home -V vim ~/.zshrc export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home export PATH=$JAVA_HOME/bin:$PATH :wq source ~/.zshrc 0x04 Docker 问题 如果遇到 docker: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?. See 'docker run --help'. make: *** [upx] Error 125 启动 Docker Desktop,然后再开始编译。 0x0x 未知 编码过程中有个警告,不知道有无影响: WARN[0000] parse java spec failed, so skip it, open build/cache/chaosblade/yaml/chaosblade-jvm-spec-1.5.0.yaml: no such file or directory 结果 终于编译成功了,生成了 target/chaosblade-1.5.0.tar.gz 文件。 但解压后执行 ./blade,输出: zsh: killed ./blade 下载官方 Release 的 1.3.0 的 darwin 版本文件,也是报同样的问题。 到 Issues 里翻到了几个类似的问题,官方给出的 建议,比如修改系统【安全与隐私】-【通用】配置等,经验证都无效。 至此,如文首所述,放弃了在 Mac 平台下折腾,直接用 Linux 或者容器环境来体验。

    2022/02/19 混沌工程

  6. 给 zsh 自定义命令添加参数自动补全

    有时我会自定义一些 zsh 命令,以便提升某些高频操作的效率。本文记录我给一个自定义命令添加参数自动补全的方法。 场景 我自定义了一个 zsh 命令 gmt,执行 gmt <b2>,可以将当前所在的 git 分支 merge 到 <b2> 这个分支。 它具体完成以下工作: 切换到 git 分支 <b2>; 将 <b2> 分支更新到最新; 询问是否合并,输入 y 则进行分支合并。 也就是用一条命令完成一个 git checkout b2、git pull origin b2、git merge b1 这样的组合操作。 用了一段时间,可以省一些事,美中不足的就是有时候分支名称比较长,只能手动输入,没有自动补全。 期望效果 输入 gmt ,然后按 tab,自动提示本地的所有 git 分支名称; 输入 gmt fe,然后按 tab,自动补全以 fe 开头的 git 分支名称; 实现方法 在 zsh 配置文件中添加如下代码: compdef _git_merge_to_comp git_merge_to _git_merge_to_comp() { local -a git_branches git_branches=("${(@f)$(git branch --format='%(refname:short)')}") _describe 'command' git_branches } 注:git_merge_to 是一个自定义的函数,gmt 是这个函数的 alias。 这段代码的意思就是使用 _git_merge_to_comp 这个函数来给 git_merge_to 命令做自动补全,自动补全的候选列表是当前项目的所有本地 git 分支名称。 其中: compdef、_describe 等的用法,可以参考 zsh 的官方文档 Completion System。 git_branches=("${(@f)$(git branch --format='%(refname:short)')}") 的意思是,将 git branch --format='%(refname:short)' 命令的输出按行分割后形成一个字符串数组,赋值给 git_branches 变量,这部分可以参考 How to properly collect an array of lines in zsh。 我的 zsh 配置都上传到了 https://github.com/mzlogin/config-files,有需要可以参考下。 效果演示 参考 Completion System How to properly collect an array of lines in zsh

    2022/02/12 Shell

  7. 我的 2021 盘点

    对于 2021 年,如果仅从岁月流逝的角度,当然是期望时光时光慢些吧;如果从这一年外界的疯狂变幻来讲,又想对它说慢走不送。但终会无力地看清,时间的洪流上并无刻度,你喜欢或者不喜欢,它并不在意,兀自「逝者如斯夫,不舍昼夜」。 怀着这样微妙的情绪,伴着这几天微信群和朋友圈里大家抒发的美好祝愿来到了新的一年。 是时候略作盘点,挥别过往,纵情向前。 大事记 总体来讲,今年没什么大的变化,如果非要记一笔,那我会选因为疫情被隔离的经历。 虽然常居武汉,但之前疫情最严重的时候我在老家,并没有最切身的感受,这一次却是从头经历了从成为密接,到为了和小别能做上核酸检测打各种电话求助,到隔离期间继续熬夜做项目与「隔友们」一起苦中作乐,到为了各自复工的事愁肠百结,到生活终于回归平静,个人的情绪在忐忑绝望苦闷庆幸间横跳,家人们想必也饱受煎熬,还好大家最后都无恙通关。 疫情已经深刻地改变了我们的生活,时至今日,我还有亲友在西安与之斗争,愿大家都平安,愿能摘下口罩自由呼吸的那天早点到来。 分类记录 按例简单分类作回顾。 运动与健康 这次调整了一下顺序,将这一项提到了分类的最前面,也是因为随着年龄的增长,越来越体会到健康的重要性。 关键词:伤病,牙齿,保险,癌症。 今年的运动量仍然以羽毛球为主,跑步基本没有,游泳完全没有。还是作为公司武汉办公室羽毛球活动的主要发起者,每周组织活动,另外周末参加了少量民间活动,技术上没有什么进步,出勤率还算可以。 去年体检时报出的脂肪肝趋势,今年消失了,血脂和尿酸偏高的情况也有所改善,但「伤病」有点多,运动时两次崴脚一次腰伤,自己推测原因可能是除了羽毛球没有什么其它运动量,肌肉退化了,后面得搭配一些有氧和力量训练。年末买了个椭圆机放家,小别同学用得更多一点,不知道啥时候会变成晾衣架。 另外值得一说的是终于去医院持续接受牙周治疗了,牙齿的健康状态应该得到一定的修复,躺在治疗椅上脑补医生作业的时候真的很恐怖。每个人都要认真保养牙齿。 最近一年多的时间里,给我和小别同学陆续把保险配置上了,有点迟,有一些除外承保的情况,好歹也算是能稍安心一点了。 肺癌,这可能是我周围听说的最普遍的癌症,就身边的亲人来讲就有几例,我们这一辈人也有因此而凋零的了,彼时内心百感交集,此时也憋不出什么话语,只想说世事无常,按期体检,珍惜亲友。 书影音 数据统计自豆瓣记录。 统计数字与直观感受略有不同。 疫情的这两年感觉窝在家看电影电视剧的时候比之前要多了,但实际上数量并无明显增长。当然也可能是有一些未标记。 今年看过的影视推荐: 《旺达幻视》:剧情喜欢,奥妹很美。 《肖申克的救赎》:希望至美。 《让子弹飞》:可以细品。 读书的形式还是以 Kindle 阅读为主,略有不同的是现在读过觉得好的书会再买一本实体书收藏。 今年看过的图书推荐,两本都是能带来一些不同的阅读体验的: 《沉默的病人》:双故事线,有时间差,挺特别。 《禅与摩托车维修艺术》:很独特的一本书,读起来有小学语文老师常说的「夹叙夹议」的感觉。 GitHub 与博客 GitHub 上的记录: 226 contributions,比 2020 年的 303 次进一步下降; 最长连击记录 10 天,2021-11-08 到 2021-11-17; 有 10 个自然周没有任何记录。 这些指标能一定程度反映对技术的关注和参与度,需要继续提醒自己警惕,技术仍是立身之本。 博客上今年只发了廖廖 5 篇文章,平均每个季度一篇的节奏。 我可能陷入了一个误区,每次有值得一写的话题,就会随手记到一个清单,想着后面花点时间回顾下,做到干货足够多,相关知识点都讲到,然后再发出去,殊不知后面可能再不会花时间去看了。日积月累,这个清单里已经攒了不下上百条,它们之中的大多数,大概率是不会转化成输出了。 往后可以尝试转变思路,「及时输出」,热乎的现场,热乎的记忆,转化成热乎的文章,有不完善的地方「迭代」即可。 偶尔「草率」一点,没什么大不了的。 工作 不好不坏,工作内容上以技术类为主,相对能比较专心纯粹地做事,算是我比较喜欢的节奏,要是更不卷就更好了。dddd,不多言。 投资 以指数基金投资为主,受中丐互怜的拖累,今年整体收益负几个点,不过心态还好,相信时间维度拉长结果不会差。 其它 看了下三年前的年度盘点,发现吉他那时候就买了,而今技术也没有什么进步,还没有放弃治疗,继续挣扎; 今年的旅行记录为 0,没有到过武汉和家乡以外的地方,希望新的一年疫情消散,想去哪的时候就能出发。 结语 流水账记完了,犹豫该不该来点正能量 FLAG,就如那些「人间清醒」的反鸡汤所言,今天的我和昨天的我并没有什么两样,并不会因为年份加一就有什么焕然一新。 那就祝自己和大家新年快乐,健康平安吧。 慢慢的,把眼下的日子过下去,就好。

    2022/01/01 Blog

  8. 如何让 Spring Security 「少管闲事」

    记两种让 Spring Security「少管闲事」的方法。 遇到问题 一个应用对外提供 Rest 接口,接口的访问认证通过 Spring Security OAuth2 控制,token 形式为 JWT。因为一些原因,某一特定路径前缀(假设为 /custom/)的接口需要使用另外一种自定义的认证方式,token 是一串无规则的随机字符串。两种认证方式的 token 都是在 Headers 里传递,形式都是 Authorization: bearer xxx。 所以当外部请求这个应用的接口时,情况示意如下: 这时,问题出现了。 我通过 WebSecurityConfigurerAdapter 配置 Spring Security 将 /custom/ 前缀的请求直接放行: httpSecurity.authorizeRequests().regexMatchers("^(?!/custom/).*$").permitAll(); 但请求 /custom/ 前缀的接口仍然被拦截,报了如下错误: { "error": "invalid_token", "error_description": "Cannot convert access token to JSON" } 分析问题 从错误提示首先可以通过检查排除掉 CustomWebFilter 的嫌疑,自定义认证方式的 token 不是 JSON 格式,它里面自然也不然尝试去将其转换成 JSON。 那推测问题出在 Spring Security 「多管闲事」,拦截了不该拦截的请求上。 经过一番面向搜索编程和源码调试,找到抛出以上错误信息的位置是在 JwtAccessTokenConverter.decode 方法里: protected Map<String, Object> decode(String token) { try { // 下面这行会抛出异常 Jwt jwt = JwtHelper.decodeAndVerify(token, verifier); // ... some code here } catch (Exception e) { throw new InvalidTokenException("Cannot convert access token to JSON", e); } } 调用堆栈如下: 从调用的上下文可以看出(高亮那一行),执行逻辑在一个名为 OAuth2AuthenticationProcessingFilter 的 Filter 里,会尝试从请求中提取 Bearer Token,然后做一些处理(此处是 JWT 转换和校验等)。这个 Filter 是 ResourceServerSecurityConfigurer.configure 中初始化的,我们的应用同时也是作为一个 Spring Security OAuth2 Resource Server,从类名可以看出是对此的配置。 解决问题 找到了问题所在之后,经过自己的思考和同事间的讨论,得出了两种可行的解决方案。 方案一:让特定的请求跳过 OAuth2AuthenticationProcessingFilter 这个方案的思路是通过 AOP,在 OAuth2AuthenticationProcessingFilter.doFilter 方法执行前做个判断 如果请求路径是以 /custom/ 开头,就跳过该 Filter 继续往后执行; 如果请求路径非 /custom/ 开头,正常执行。 关键代码示意: @Aspect @Component public class AuthorizationHeaderAspect { @Pointcut("execution(* org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter.doFilter(..))") public void securityOauth2DoFilter() {} @Around("securityOauth2DoFilter()") public void skipNotCustom(ProceedingJoinPoint joinPoint) throws Throwable { Object[] args = joinPoint.getArgs(); if (args == null || args.length != 3 || !(args[0] instanceof HttpServletRequest && args[1] instanceof javax.servlet.ServletResponse && args[2] instanceof FilterChain)) { joinPoint.proceed(); return; } HttpServletRequest request = (HttpServletRequest) args[0]; if (request.getRequestURI().startsWith("/custom/")) { joinPoint.proceed(); } else { ((FilterChain) args[2]).doFilter((ServletRequest) args[0], (ServletResponse) args[1]); } } } 方案二:调整 Filter 顺序 如果能让请求先到达我们自定义的 Filter,请求路径以 /custom/ 开头的,处理完自定义 token 校验等逻辑,然后将 Authorization Header 去掉(在 OAuth2AuthenticationProcessingFilter.doFilter 中,如果取不到 Bearer Token,不会抛异常),其它请求直接放行,也是一个可以达成目标的思路。 但现状是自定义的 Filter 默认是在 OAuth2AuthenticationProcessingFilter 后执行的,如何实现它们的执行顺序调整呢? 在我们前面找到的 OAuth2AuthenticationProcessingFilter 注册的地方,也就是 ResourceServerSecurityConfigurer.configure 方法里,我们可以看到 Filter 是通过以下这种写法添加的: @Override public void configure(HttpSecurity http) throws Exception { // ... some code here http .authorizeRequests().expressionHandler(expressionHandler) .and() .addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class) .exceptionHandling() .accessDeniedHandler(accessDeniedHandler) .authenticationEntryPoint(authenticationEntryPoint); } 核心方法是 HttpSecurity.addFilterBefore,说起 HttpSecurity,我们有印象啊……前面通过 WebSecurityConfigurerAdapter 来配置请求放行时入参是它,能否在那个时机将自定义 Filter 注册到 OAuth2AuthenticationProcessingFilter 之前呢? 我们将前面配置放行规则处的代码修改如下: // ... httpSecurity.authorizeRequests().registry.regexMatchers("^(?!/custom/).*$").permitAll() .and() .addFilterAfter(new CustomWebFilter(), X509AuthenticationFilter.class); // ... 注: CustomWebFilter 改为直接 new 出来的,手动添加到 Security Filter Chain,不再自动注入到其它 Filter Chain。 为什么是将自定义 Filter 添加到 X509AuthenticationFilter.class 之后呢?可以参考 spring-security-config 包的 FilterComparator 里预置的 Filter 顺序来做决定,从前面的代码可知 OAuth2AuthenticationProcessingFilter 是添加到 AbstractPreAuthenticatedProcessingFilter.class 之前的,而在 FilterComparator 预置的顺序里,X509AuthenticationFilter.class 是在 AbstractPreAuthenticatedProcessingFilter.class 之前的,我们这样添加就足以确保自定义 Filter 在 OAuth2AuthenticationProcessingFilter 之前。 做了以上修改,自定义 Filter 已经在我们预期的位置了,那么我们在这个 Filter 里面,对请求路径以 /custom/ 开头的做必要处理,然后清空 Authorization Header 即可,关键代码示意如下: @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; if (request.getServletPath().startsWith("/custom/")) { // do something here // ... final String authorizationHeader = "Authorization"; HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper((HttpServletRequest) servletRequest) { @Override public String getHeader(String name) { if (authorizationHeader.equalsIgnoreCase(name)) { return null; } return super.getHeader(name); } @Override public Enumeration<String> getHeaders(String name) { if (authorizationHeader.equalsIgnoreCase(name)) { return new Vector<String>().elements(); } return super.getHeaders(name); } }; filterChain.doFilter(requestWrapper, servletResponse); } else { filterChain.doFilter(servletRequest, servletResponse); } } 小结 经过尝试,两种方案都能满足需求,项目里最终使用了方案一,相信也还有其它的思路可以解决问题。 经过这一过程,也暴露出了对 Spring Security 的理解不够的问题,后续需要抽空做一些更深入的学习。 参考 https://www.cnblogs.com/alalazy/p/13179608.html

    2021/12/26 Java

  9. 读叔本华《人生的智慧》

    最近读完了这本小书,想写点什么,但又似乎暂时表达不出,先挖个读书笔记的坑在这里,后面有所感了也许会填上。 读此书的过程中,感觉前半部分比较有意思,后半部分都是在大讲所谓骑士荣誉,看得很烦。 摘录 美貌是一封公开的推荐信。 只要对生活稍作考察,我们就会发现,人类幸福的两大宿敌是痛苦和无聊。我再补充一下,当我们足够幸运逃离了其中一端时,我们就接近了另外一端,也就是说,免得了痛苦却免不了无聊,反之亦然。实际上,生活就像是钟摆一般,在这两端之间或激烈或温和地来回摇摆——要么痛苦,要么无聊,反正总有一项逃不掉。 麻木迟钝是一种摆在脸上、印在心底的空虚状态——人们对外部发生的一些琐碎的事情表现出不停的、强烈的关注,同样也暴露了他们内在的空虚。这就是无聊的真正根源——内心空虚的人为了寻求刺激,不断用各种无谓的东西充塞大脑和心灵,单调又乏味。 根据亚里士多德的学说,幸福在于能够施展才能;斯托拜乌在他对逍遥派(即亚里士多德学派)哲学的阐述中也说了,「幸福意味着充满活力地做你擅长的事并获得预期的结果」。 我们乐于受人尊敬,并不是热爱「尊敬」本身,我们爱是只是受人尊敬带来的好处。

    2021/10/25 Blog

  10. 利用 XXL-JOB 实现灵活控制的分片处理

    本文讲述了一种利用 XXL-JOB 来进行分片任务处理的方法,另外加入对执行节点数的灵活控制。 场景 现在一张数据表里有大量数据需要某个服务端应用来处理,要求: 能够并行处理; 能够较灵活地控制并行任务数量。 压力较均衡地分散到不同的服务器节点; 思路 因为需要并行处理同一张数据表里的数据,所以比较自然地想到了分片查询数据,可以利用对 id 取模的方法进行分片,避免同一条数据被重复处理。 根据第 1、2 点要求,本来想通过对线程池的动态配置来实现,但结合第 3 点来考虑,服务器节点数量有可能会变化,节点之间相互无感知无通信,自己在应用内实现一套调度机制可能会很复杂。 如果有现成的独立于这些服务器节点之外的调度器就好了——顺着这个思路,就想到了已经接入的分布式任务调度平台 XXL-JOB,而在阅读其 官方文档 后发现「分片广播 & 动态分片」很贴合这种场景。 方案 利用 XXL-JOB 的路由策略「分片广播」来调度定时任务; 通过任务参数传入执行任务节点数量; 定时任务逻辑里,根据获取到的分片参数、执行任务节点数量,决策当前节点是否需要执行,分片查询数据并处理: 如果 分片序号 > (执行任务节点数量 - 1),则当前节点不执行任务,直接返回; 否则,取 分片序号 和 执行任务节点数量 作为分片参数,查询数据并处理。 这样,我们可以实现灵活调度 [1, N] 个节点并行执行任务处理数据。 主要代码示例 JobHandler 示例: @XxlJob("demoJobHandler") public void execute() { String param = XxlJobHelper.getJobParam(); if (StringUtils.isBlank(param)) { XxlJobHelper.log("任务参数为空"); XxlJobHelper.handleFail(); return; } // 执行任务节点数量 int executeNodeNum = Integer.valueOf(param); // 分片序号 int shardIndex = XxlJobHelper.getShardIndex(); // 分片总数 int shardTotal = XxlJobHelper.getShardTotal(); if (executeNodeNum <= 0 || executeNodeNum > shardTotal) { XxlJobHelper.log("执行任务节点数量取值范围[1,节点总数]"); XxlJobHelper.handleFail(); return; } if (shardIndex > (executeNodeNum - 1)) { XxlJobHelper.log("当前分片 {} 无需执行", shardIndex); XxlJobHelper.handleSuccess(); return; } shardTotal = executeNodeNum; // 分片查询数据并处理 process(shardIndex, shardTotal); XxlJobHelper.handleSuccess(); } 分片查询数据示例: select field1, field2 from table_name where ... and mod(id, #{shardTotal}) = #{shardIndex} order by id limit #{rows}; 进一步思考 如果需要更大的并发量,需要有大于应用节点数量的任务并行,如何处理? 两种思路: 通过任务参数传入一个并发数,单个节点在处理任务时,将查询到的数据按这个数字进行再分片,交由线程池并行处理; 配置 M 个定时任务,指定相同的 JobHandler,给它们编号 0、1、2…M,并将定时任务编号和 M 这两个数,由任务参数传入,定时任务逻辑里,先根据分片参数、定时任务编号、M,重新计算出新的分片参数,如 分片序号 = (分片序号 * M) + 定时任务编号,分片总数 = 分片总数 * M,再查询数据并处理。 如果有可能频繁调整任务执行逻辑,包括可能要新增任务参数等,而不想重启服务器,如何解决? 可以考虑使用 XXL-JOB 的「GLUE模式」任务,能够在线编辑和更新定时任务执行逻辑。 参考 分布式任务调度平台XXL-JOB

    2021/06/19 Java