普通视图

发现新文章,点击刷新页面。
昨天以前Lawrence's Blog

写在 2025 年末

作者 Lawrence
2025年12月31日 08:00

Tech

互联网行业从业六七年了,最关注的必然是养活自己的 Java 生态。很多人说 Java 很臃肿,写个 Hello World 都费劲。然而实际上只需稍微关注一下最新今年 9 月 Java 25 LTS 最新版本的语法:

void main() {
  IO.println("Hello Java 25!");
}

虽然 JEP 445 这种类似脚本的语法糖代码已经脱离了一切皆对象的原则,但这恰恰是 Java 与时俱进的标志。另外几个不得不提的:结构化并发,虽然仍然在预览阶段,等它 GA 很有必要好好聊聊,配合虚拟线程,Java 的多线程模型简直起飞;模块导入声明简化了大量 import 语句,自 Java 9 以来的模块化的好处初见成效了。

其他领域的观察/观点:

  • C/C++ 有被 Rust 替代的趋势
  • Next.js / React 服务端组件的重大漏洞让 React 开发者感受到 Log4J 类似境遇
  • uv 统治了 Python 包管理器等工具链

我自己关于 AI 的暴论:

  • 搜索引擎这个互联网入口正在被 LLM 取代
  • TailwindCSS 在 LLM 时代存在 Token 红利
  • 前后端混合的同构技术栈(比如 Next.js 服务端渲染)会成为 Vibe Coding 的首选技术栈
  • MCP 协议正在向操作系统端迁移成为终端设备上的基础设施
  • 人类获取知识信息的时间复杂度几乎达到了 O(1)
  • 程序员写的代码是属于长期负债,LLM 产生的代码则是程序员的流动负债,绝非净资产

Work

上一份工作在 SaaS 行业,属于人力资源行业内领头公司的老员工出来创业组建的一只小团队。总结下来的感受,SaaS 在国内很难做。由于是新项目,客户本就不多,最后几个月需求明显骤减,年中果然吃了散伙饭。虽然很快就换了工作,但每次想起那些代码写下后没有回响,仍然很难释怀。当然创业公司的好处就是很自由,公司架构精简,老板兼职商务销售经常出差,产品兼职人事/测试,不打卡,不用和外部客户沟通,节假日也几乎不会出现紧急线上问题,或许是因为没业务:(。我来到上海第一年做在线教育也是类似的团队。可能我自己一直喜欢从零到一构建点东西,所以更偏向这类创业公司的氛围。

换了工作后,在新公司从事互金相关业务,工作性质和公司规模导致部分福利或多或少不如先前,开发需求也多出了不少。但新公司有一定规模的业务,节奏稳定、方向明确,每个人只需要把自己负责的那一条脉络梳理好即可。

从个人成长的角度看,这样的环境也并非没有价值。系统规模更大,历史包袱更重,也意味着需要更谨慎地做决策、更耐心地理解上下游逻辑。现在看来,开发在多数时候并不是用来「创造」,而是用来「维持」:保证稳定、避免风险、在有限的空间内做最优解,这本身也是一种能力。只是偶尔在需求间隙,还是会不自觉地回想起以前那些可以随意推翻重来的日子。那时写代码,更多是在回答「能不能这样设计」;而现在,更多是在权衡「应不应该这么改」「如何避免改动的破坏性影响」。前者更像是在白纸上画草图,后者则是在维护一栋已经上路的汽车,各有意义,也各有约束,无可厚非。

或许职业生涯本就会在不同阶段切换角色:有时是开荒的人,有时是守成的人。眼下这段经历,大概更接近后者。至于未来是否还会再回到从零开始的状态,目前还很难下结论。但至少在当下,把手头的事情做好,理解这套体系为何如此运转,也未尝不是一种积累。毕竟,很多判断只有在真正站到另一种位置上,才能看得更清楚。

Life

Music

Movie / Shows

年初用国补还买了小米电视 S55 MiniLED,但今年都基本没怎么剧和电影,可以说是最近十年看的最少的一年了。每周必看 B 站主要的几个 Up:小 Lin 说,Koala 聊开源,马督工,jyhachi。

2026 年期待的剧集:豺狼的日子 第二季投行风云 第四季

Things

  • 年度最佳 IDE:Zed
  • 年度最烂开发工具:Postman
  • 年度最烂日本品牌:松下
  • 年度最佳日本品牌:三菱电机
  • 年度最吃灰设备:PlayStation 5

用 Zed Vibe Coding 了一个页面,罗列了最近几年买的主要电子设备。AI 就是用来写这种东西快准狠。

我的 IDE 除了 JetBrains 之外现在用的就只有 Zed 了。Atom 项目原班人马的新作品,非 Java 项目可以全用 Zed,原生支持 Vim 操作。就图一个简单清爽内置 AI。 Qoder JetBrains Plugin 插件做的蛮好的,我也在用,但 JetBrains 自家的 AI Assistant 是真的拉垮,BYOK 完全不可用。

Postman 最烂的地方在于它作为一个 HTTP 客户端搞出一堆依赖自家网络的用户功能,第一天用它的时候就很讨厌,工作原因还不得不去用它。

老婆给我买了一款松下剃须刀,能用,但声音巨大,操作交互非常不友好,对不起它大几百的价格,找官方解决结果还要返厂检修。我可怕麻烦了,讨厌一个品牌就是这么简单粗暴。

今年年初我老家的卧室国产空调用十多年后罢工了,老爸趁着有国补换空调,我推荐了三菱电机。我和老婆回家开了几天感觉很非常好,安静且高效。国庆回家后发现我爸给他自己卧室也换上了同款空调。

PS 5 又吃灰了一整年,我还不考虑出售。总想着放假开起来玩通宵,但真到假期了,还是懒得开。

Investment

花了一个月时间通过了基金从业资格考试,除了琐碎的法规之外,也掌握了不少金融衍生品和金融工具(期权/期货/互换/逆回购/REITs/可转债/ABS...),尽管先前在投行的工作或多或少接触过,但要达到应付行业考试的程度还是得系统学习。这次学习机会重塑了我对投资的认知,几个重要的心得也在这里分享:

  • 投机交易者为市场提供流动性
  • 股票的价格本质是未来现金流的折现
  • 现代投资组合理论本质讨论的不是收益最大化,而是如何在不确定性中构建一个更抗波动的投资组合
  • 市场只对承担系统性风险的投资者给予补偿

在学习之前,我总是在想市场的数学模型是什么,原来这正是马科维茨的现代投资组合理论。它用数学方法证明了不要把鸡蛋放在一个篮子里。它提醒我,真正成熟的决策并不是减少犯错,而是让系统在犯错时依旧可行。这种思路,在投资之外同样成立。

看了两场段永平访谈,总结起来还是那句话:买股票就是买公司。也经常睡前看猫笔刀(之前公司同事推荐的公众号),前几天还参与了一波场内白银 LOF 套利,虽然入场晚了点,但投机者慈善家是真金白银提供流动性。

摘录猫笔刀的一段话:

有人问我期指交割日是不是都会砸盘,我说不会,你只要去学习一下期指是什么样的金融产品,期指的运行方式,期指的交割规则,就知道它对 A 股没影响。但问题是这市场里 80% 以上的人不会学也不想学,他们更愿意接受不用动脑子的暴论,这样的人多到一定程度,市场就会自我实现。共识是有价值的,哪怕是错误的共识。

End

今年一直骑行上下班,每天骑五六十分钟,年底还被交警罚了一单。最近还感冒了一回,稍微吃点热食浑身就出汗了,没吃一粒药完全靠自愈。住的地方商圈发现了一家淮扬菜店,点了很多次他家外卖,还到店吃了几次。

多抓鱼上买了一些书,还淘到了一本绝版刊物《独唱团》。《南方周末》今年的新年献词写的像学生作文,也不知道现在报刊亭还有的卖么?或者,现在还有报刊亭吗?

我今年每一天都会刷 RSS,未来或者余生,我仍然会以它作为信息获取的主要途径;看到一些网站没有提供 RSS 源,我会给网站作者发邮件请求提供,再不济,我会基于 RSSHub 之类的工具自己生成对应的 Feed。

AI 应用爆发的年代,前文我提到人类获取信息的时间复杂度几乎达到了 O(1),我相信很多博主在博客上分享的也都不会再是纯知识类文章了,因为除了自我加深印象之外,没有意义。我也不会再写这样的内容,因为我的博客也不是维基。未来可能会分享自己对一些事物的理解,或者,单纯的流水账式吐槽。

大概就这些吧。

在 Flyway 迁移类中实现依赖注入

作者 Lawrence
2024年4月4日 08:00

Flyway 是什么

Flyway 是一款开源的,基于 Java 实现的数据库内容变更控制工具。它提供了 CLI、Java API、Maven/Gradle Plugin 等多种方式方便开发人员将数据库的表结构改动、内容变动以可追溯的代码形式进行管理和部署。Flyway 支持包括大多数主流的关系型数据库:MySQL、SQL Server、Oracle Database、PostgreSQL、SQLite、TiDB、MariaDB 等,对于 MongoDB 的支持尚在预览阶段(更推荐 Mongock),同类竞品有 Liquibase,但 Liquibase 的使用相比 Flyway 更复杂,有额外的概念作为学习成本。

配合 Spring Boot 使用

遵循 SpringBoot 广为人知的约定大于配置,Spring 官方提供了 Flyway 的自动配置实现

@AutoConfiguration(after = { DataSourceAutoConfiguration.class,JdbcTemplateAutoConfiguration.class,HibernateJpaAutoConfiguration.class })
@ConditionalOnClass(Flyway.class)
@Conditional(FlywayDataSourceCondition.class)
@ConditionalOnProperty(prefix = "spring.flyway", name = "enabled", matchIfMissing = true)
@Import(DatabaseInitializationDependencyConfigurer.class)
@ImportRuntimeHints(FlywayAutoConfigurationRuntimeHints.class)
public class FlywayAutoConfiguration { /*...*/ }

全限定类名 org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration 会在检测到以下几种状态下实现自动配置:

  1. 存在 Flyway 类 (即 org.flywaydb.core.Flyway)
  2. 满足 FlywayDataSourceCondition 类中的 Bean 或 Properties 条件,其实就是 DataSource 和数据库 URL 等连接配置存在
  3. 存在 spring.flyway.enabled=true 属性配置

对于 MySQL 而言,除了必须的 JDBC 依赖之外,需要引入 Flyway 自身依赖,SpringBoot 已经在 pom 中声明过版本号,因此此处无需额外定义版本字段:

<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-mysql</artifactId>
</dependency>

具体使用有两种方式:

  1. 原生 SQL
  2. Java API

Flyway 会基于默认配置的文件夹路径 classpath:db/migration 发现版本变更文件或实现类。

V{VERSION}__{DESCRIPTION}.sql
V{VERSION}__{DESCRIPTION}.java

VERSION 可使用包含小数点、下划线的字符串版本,DESCRIPTION 则是简单的描述文本。中间的分割符是两个下划线。可通过 spring.flyway 配置自定义前后缀。

V{VERSION}__{DESCRIPTION}.sql 文件需要存放在 src/main/resources/db/migration 文件夹下; Java 类则需要定义包名 db.migration 并且继承父类 BaseJavaMigration,重写如下方法:

void migrate(Context context) throws Exception;

按照官方的写法,我们就可以在 migrate() 方法中去用 Java 代码实现数据库内容版本变动了,但是这里有一个缺点,那就是我们在 Spring 环境下无法针对 V{VERSION}__{DESCRIPTION}.java 进行依赖注入,只能尝试通过原生 JDBC 的方式、静态方法等去写比较朴素的 SQL 实现,而不能充分成分利用现有的 DAO 接口来做业务数据的更新。因此需要自定义配置 Flyway。

向 Flyway Migration 实现类进行依赖注入

package me.lawrenceli.migration.config;

import jakarta.annotation.PostConstruct;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.migration.JavaMigration;
import org.flywaydb.core.api.output.MigrateResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class FlywayConfig {
    // 自定义迁移历史表名
    private static final String SCHEMA_HISTORY_TABLE = "schema_changes";

    @Autowired
    private DataSource dataSource;

    @Autowired
    private ApplicationContext applicationContext;

    @PostConstruct
    public void migrate() {
        log.info("Flyway, 启动!");
        // 通过 Spring 容器获取所有迁移实现类
        // 这样一来,所有实现类就不再需要定义在 package `db.migration` 下,可以放在任何支持 Bean 扫描的位置。
        JavaMigration[] migrationBeans = applicationContext
                .getBeansOfType(JavaMigration.class)
                .values()
                .toArray(new JavaMigration[0]);
        Flyway flyway = Flyway.configure()
                .dataSource(dataSource) // 通过原本的 DataSource Bean 实现无需配置 flyway 自身的 JDBC URL
                .locations("db/migration") // 默认迁移脚本路径
                .table(SCHEMA_HISTORY_TABLE) // 默认迁移历史表为 `flyway_schema_history`
                .baselineOnMigrate(true) // 默认 false, 对以存在的数据库做首次迁移必须设置开启
                .baselineVersion("0") // 默认 "1"
                .executeInTransaction(true) // 将迁移作为事务,你懂的
                .installedBy(applicationContext.getId()) // 将微服务名作为迁移执行者
                .javaMigrations(migrationBeans) // 注册迁移类
                .load();
        MigrateResult migrate = flyway.migrate(); // 执行迁移,依次调用子类实现
        log.info("Flyway 迁移了 {} 版. {}", migrate.migrationsExecuted, migrate.success);
    }
}

由于 Flyway 的配置基于这种手动配置,因此需要在 SpringBoot 启动类上排除原有的自动配置类,以防止自动配置存在加载冲突。

@SpringBootApplication(exclude = FlywayAutoConfiguration.class)
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

}

最后,定义一个 Component Bean 去实现 BaseJavaMigration:

package me.lawrenceli.balabala.migration;

import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class V2__QueryExample extends BaseJavaMigration {

    @Autowired
    private MyMapper myMapper; // Bean of DAO

    @Override
    public void migrate(Context context) throws Exception {
        Data data = myMapper.selectById(2024L);
        // ... other CRUD codes with Java
    }
}

这样,所有的迁移类都可以方便地使用依赖注入来愉快地做 CRUD 了。经过实践,Flyway 会在数据库连接配置后、HTTP 服务暴露(也就是 Servlet 容器监听端口)前同步地执行完所有迁移,因此无需担心执行时机影响线上服务。

参考

Apple TV

作者 Lawrence
2023年1月21日 08:00

过年回老家看电视,运营商送的网络电视盒子主屏幕花花绿绿,我一个程序员都费了好久才找到地方卫视的直播频道。

索性去电商平台搜搜看有没有更好的硬件、翻看各种测评文章和视频。清晰了需求定位后,我果断找了一家有现货的店铺下单了一款美版 Apple TV 2022 (4K)。

Apple TV
Apple TV

一直担心年前收不到货,没想到快递很敬业地发过来了。

由于购买之前就已经熟悉了大部分使用细节,所以安装、使用的时候毫不费力;像是把玩过很久的玩具一样自然流畅。

用美区 Apple ID 购买了很多付费应用,主要都是一些国内独立开发者的作品。

  • Alplayer
  • APTV
  • IIVA
  • Miao Projects
  • VidHub

很难想象不少开发者会为国内极其小众的平台开发上架了如此小而美的 tvOS App。

搜集一些电视直播源,我就反常地看起了 CCTV。比期待的画质高出不少。遥控器的金属质感像第一次摸到棱角分明的 iPhone 5S 一样爱不释手!还用它和我爸玩了一局桌球游戏。

种种体验让我想起知乎上一个回答

长这么大,听过最清晰的《义勇军进行曲》是在 Apple Music。

我的默认应用

作者 Lawrence
2023年12月3日 08:00

最近看到很多博主在 App Defaults 中分享了他们的默认应用程序。以下是我自己的:

  • 📨 邮件客户端
    • Apple 邮件
  • 📮 邮件服务器
    • Outlook(中国大陆可用)
    • Lark 邮件 hi@lawrenceli.me
  • 📝 注释
    • Apple Notes(使用 montaigne.io,可以将 Apple Notes 成为一个静态网站)
  • ✅ 待办事项
    • Apple 提醒
  • 📷 iPhone 照片拍摄
    • Apple 相机
  • 🟦 照片管理
    • Apple 照片与 iCloud 同步
  • 📆 日历
    • 服务:iCloud 日历
    • 客户端:Apple 日历
  • 📁 云文件存储
    • iCloud 云盘 (iCloud+)
    • 阿里云盘(适用于 Apple TV)
  • 📖 RSS
  • 🙍🏻‍♂️ 通讯录
    • N/A,没有联系人。
  • 🌐 浏览器
    • Mac 上的 Firefox
    • iOS 上的 Safari
  • 💬 聊天
  • 🔖 书签
  • 📑 稍后阅读
    • Reeder + Instapaper
  • 📜 文字处理
    • ONLYOFFICE(开源 Office,与微软完全兼容)
    • Obsidian(流行的 Markdown 编辑器,可通过 iCloud 同步)
  • 📈 电子表格
    • ONLYOFFICE(开源 Office)
  • 📊 演示
    • Slidev(通过 Markdown 生成幻灯片)
  • 🛒 购物清单
    • 在 Apple 提醒共享提醒列表
  • 🍴 膳食计划
  • 💰 预算和个人理财
  • 📰 新闻
  • 🎵 音乐
  • 🎤 播客
  • 🔐 密码管理
    • iCloud KeyChain
    • 2FAS 多因素身份验证

额外默认设置

ActivityPub 协议的简单实现

作者 Lawrence
2023年1月11日 08:00

Warning

这不是一篇严肃的 ActivityPub 教程,仅仅是一些基于个人实现时的简单概括。该网站并不支持所有 ActivityPub 协议要求。由于联邦宇宙实例众多而本人服务器资源有限,笔者可能会关闭本站 ActivityPub 服务。

Aaron Swartz 于十年前的这个时候自杀了。他起草的 RSS (1.0) 协议和 John Gruber 一起设计、创造的 Markdown 至今一直拥有大量互联网用户。这十年间互联网并没有因他的离世而产生 Open Web 原教旨主义者所期待的愿景。类似「剑桥分析公司」的事情你我都有耳闻。万维网的发明人 Tim Berners-Lee 博士后来提出了 SoLiD 项目 —— 通过将用户数据和应用彻底分离,来实现用户对自身数据的完全掌控。ActivityPub 协议与之类似,但仅面向社交网站。如今,ActivityPub 已经成为了 W3C 的推荐标准;Elon Musk 收购 Twitter 公司之后,由于 "Hardcore Software Engineering" 所展露出的负外部性,Mastodon (长毛象)成为了最火热的分布式/去中心化社交网络平台,而 Mastodon 正是 ActivityPub 的实现之一。这个 Implementation Report 页面展示了一些实现了 ActivityPub 协议的网站列表。

折腾了几天,终于在百忙之中将这个小小的网站基本实现了 ActivityPub 最主要的接口。下面简单梳理一下大致实现的 Server to Server 接口,这些接口对于一个静态博客足矣。

本站点实现 ActivityPub 的所有 REST API 均系由 ▲ Vercel Serverless Function (JavaScript) 驱动。

WebFinger

此 API 的定义参考 RFC 7033。这个 WebFinger 协议目的是提供一种针对单个域名的用户发现方式。考虑到此 API 必须使用 Content-Type: application/jrd+json 作为 HTTP 的报文响应类型,因此不推荐直接使用静态文件托管 JSON,请使用 REST API 来构建此实现。

https://example.com/.well-known/webfinger?resource=acct:lawrence@example.com

subject 中的 URI 内容后半段和电子邮件非常像 —— ActivityPub 最终的实现效果也和电子邮件类似!

{
  "subject": "acct:lawrence@lawrenceli.me",
  "aliases": [],
  "links": [
    {
      "rel": "http://webfinger.net/rel/profile-page",
      "type": "text/html",
      "href": "https://lawrenceli.me/about"
    },
    {
      "rel": "self",
      "type": "application/activity+json",
      "href": "https://lawrenceli.me/api/activitypub/actor"
    }
  ]
}

links 中会添加上我们即将要实现的 Actor API。

除此 WebFinger 之外,以下所有 API 都必须设置 Content-Type: application/activity+json 作为响应头。ActivityPub 服务端(比如一个 Mastodon 实例)都会在请求头使用 Accept: application/activity+json 类似的形式来要求我们的实例返回对应的报文格式。

Actor

Actor 就是 Activity 的参与者。WebFinger 会暴露此用户信息 (Profile) 接口。通过此 API,可以告知 ActivityPub 所有关于此用户的其他 API Endpoint,比如用户的 Outbox、Inbox、Followers 等等。所以这些 API 的具体 URL 都可以由自己去定义,而非一成不变。

除此之外,需要提供用户的 PublicKey 来验明身份。我们只需要在自己本地生成一对密钥就可以了。服务端通信中,发往不同 ActivityPub 的实例 HTTPS 请求都需要经过密钥加密。

openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
{
  "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
  "id": "https://lawrenceli.me/api/activitypub/actor",
  "type": "Person",
  "name": "Lawrence Li",
  "preferredUsername": "lawrence",
  "summary": "Blog",
  "inbox": "https://lawrenceli.me/api/activitypub/inbox",
  "outbox": "https://lawrenceli.me/api/activitypub/outbox",
  "followers": "https://lawrenceli.me/api/activitypub/followers",
  "icon": ["https://lawrenceli.me/images/author/Lawrence.png"],
  "publicKey": {
    "id": "https://lawrenceli.me/api/activitypub/actor#main-key",
    "owner": "https://lawrenceli.me/api/activitypub/actor",
    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0RHqCKo3Zl+ZmwsyJUFe\niUBYdiWQe6C3W+d89DEzAEtigH8bI5lDWW0Q7rT60eppaSnoN3ykaWFFOrtUiVJT\nNqyMBz3aPbs6BpAE5lId9aPu6s9MFyZrK5QtuWfAGwv9VZPwUHrEJCFiY1G5IgK/\n+ZErSKYUTUYw2xSAZnLkalMFTRmLbmj8SlWp/5fryQd4jyRX/tBlsyFs/qvuwBtw\nuGSkWgTIMAYV71Wny9ns+Nwr4HYfF5eo2zInpwIYTCEbil79HcikUUTTO/vMMoqx\n46IiHcMj0SPlzDXxelZgqm0ojK2Z7BGudjvwSbWq/GtLoaXHeMUVpcOCtpyvtLr2\nYwIDAQAB\n-----END PUBLIC KEY-----"
  }
}

Outbox

类似 RSS/JSON Feed, 类型为 OrderedCollection,必须按照时间顺序将最新内容放在 orderedItems 的最前。

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://lawrenceli.me/api/activitypub/outbox",
  "summary": "Blog",
  "type": "OrderedCollection",
  "totalItems": 1,
  "orderedItems": []
}

OrderedItems 数组中的单个 Item (一般为 Note) 可以是如下形式:

{
  "@context": ["https://www.w3.org/ns/activitystreams"],
  "id": "https://lawrenceli.me/blog/ssg-ssr",
  "type": "Note",
  "published": "Thu, 20 Feb 2020 00:00:00 GMT",
  "attributedTo": "https://lawrenceli.me/api/activitypub/actor",
  "content": "<a href=\"https://lawrenceli.me/blog/ssg-ssr\">When to Use Static Generation v.s. Server-side Rendering</a><br>SSG & SSR",
  "url": "https://lawrenceli.me/blog/ssg-ssr",
  "to": ["https://www.w3.org/ns/activitystreams#Public"],
  "cc": ["https://lawrenceli.me/api/activitypub/followers"]
}

在 ActivityPub 中,所有的对象都必须要提供一个 id 来作为唯一的全局标识符。而且,这个 id 必须是公开可访问的 URI,即可以通过此 id 来访问到此资源对象本身。 例上述如 Outbox 中的一项 Note 可以通过如下 curl 请求得到:

curl https://lawrenceli.me/blog/ssg-ssr -H "Accept: application/activity+json"

而如果你用浏览器直接打开这个 URL,你将会看到的是一个网页。原因就在于 Accept 这个请求头。

Inbox

本质是一个必须支持 POST 请求的 WebHook。当联邦宇宙中其他用户对你的内容作出了一些交互(比如关注、回复、收藏、转发、删除等操作),会触发此 WebHook。你需要根据 Activity 的类型去处理这些 Payload。一般来说,我们会使用自己的数据库来配合 Inbox Message 做 CRUD。

数据存在自己的数据库之后,你就可以直接在自己的站点上去展示它们。要保持数据于联邦宇宙中的一致性,你需要处理好所有消息类型,并做到接口的幂等 —— 因为 Mastodon 实例会有重试机制。

Followers

关注者列表 API。当 Inbox 接收到来自其他用户的关注请求时,可以获取用户账户后保存到数据库然后通过此 API 展示出来。类型为 OrderedCollection。也是最简单的一个接口。

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://lawrenceli.me/api/activitypub/followers",
  "type": "OrderedCollection",
  "totalItems": 1,
  "orderedItems": ["https://mstdn.social/users/lawrence"]
}

Note / Article

需要针对实现 Outbox 中的每一个 orderedItemid 中的 URI 实现一个 JSON 输出。形式可以和 Outbox 中单个 Item 保持一致。 除了 Note 之外,ActivityPub 可以有其他类型的资源,比如长文章的 Article、视频资源 Video。不同 ActivityPub 的实现平台对不同资源的展示方式不尽相同。

我的博客页面地址和对应 Activity ID 的 URI 在 URL 形式上保持了一致。因此在实现此 API 后,用户可以在任何 Mastodon 实例的搜索栏中通过搜索我的博客文章页地址来发现它对应的 Mastodon 贴文(由 Outbox 生成);在完全实现 Inbox 后,对贴文的交互数据就能够展示在我的网站上。比如文章页面最下方的 Replies

To Do

我的站点没有完全实现所有 ActivityPub 协议,比如 Inbox 消息目前仅处理了 Create Note 和 Accept Follower,还有许多消息类型亟待实现;大部分接受 GET 请求的接口也应当适当配置缓存;Inbox 要严格验证发送者的密钥。

社区实现

很巧合地发现 Cloudflare 也在同一时间段开发了兼容 Mastodon 的 ActivityPub 实现:WildeBeest,有兴趣可以直接用他们的商业化技术栈来部署一个小型实例,或者直接参考他们的代码,用自己擅长的服务端语言实现自己的 ActivityPub。

Ref

草台班子

作者 Lawrence
2022年9月1日 08:00

鲁迅童年时代看的那些社戏,也叫草台戏,草台班子得名于此。通常有三五人在乡村空旷处搭上一处简陋布篷,水平不一的戏曲演员唱些群众喜闻乐见的戏曲,天黑了收点门票钱就拆台收东西继续赶往下一处演出。我很小的时候在外婆家有幸听过一回,都是老人带着小孩去听戏,内容全无印象。

后来,见到有位网友有如下生动总结:

我工作以后才发现,大家都是草台班子。XX 草台,企业草台,我也草台,大家都草台,凑合赚钱过日子。一个企业,看着像一台奔驰在高速公路上的豪华轿车,里面其实是几个人蹬着自行车顶个壳。路上的车都是这样,大家谁都不戳破。

我们童年时代看大人们做事、参与各种社会活动,可能总会觉得他们应该是清晰、明确地知道自己在做什么,并且知道自己为什么要这样做,甚至基于此会构建出科学理性的决策。毕业多年后,结合我所经历的一些工作(长三角地区,囊括上市公司、外企、创业团队),以及所认识的在诸多行业、诸多企事业单位的打工人经历,愈加肯定上述观点。

马某去年在某峰会上的一席话,正是启发我找到类似观点的导火线:「中国金融没有系统性风险,因为中国的金融基本没有系统」。

他所谓的「系统」是什么?会不会是一系列共识协议?或者法律条文?亦或是由上到下的多级政府部门?还是说某类高度复杂的中心化组织?... 我不知道,听众也不知道,或许马某自己也不知道。这个系统是什么我们无从得知,但唯一确定的是,它一定不是草台。

我不相信「车到山前必有路」。我一直坚信,人一定会犯错;而且往往出于本位主义的影响,会形成对现状的误判。系统性的思维有助于减少这类情况的发生,无论现状是如何的草台。拥有系统性思维的人却总是那么少。你不可能对于一项没有经历过的事物、事件产生全面的、全局的、深刻的洞见,这种看待事物的方式往往被人形容成「上帝视角」。问题来了,没有上帝,如何拥有系统性认知?现代工业提供了一种可能,机器。机器永远不会欺骗我们。我们可以利用计算机软件,按照既定的领域 (Domain) 规则,使用恰当的计算机语言,构建出运行在物理硬件设备上的、具备现实意义的真正的系统 —— 只要它有确定的规则和输入。

除了计算机领域,我目前没有见到所谓类似的系统了。软件工程已经教我们用科学的方法论去驾驭计算机科学,但人类社会总会有一些草台班子的案例:

  • 2020.1 「能」「明白」
  • 2021.1 大连车务段,全力攻关一昼夜
  • 2022.2 某外交部发言人对俄乌战争开战前的评论:M 国情报就是个笑话
  • 2022.6 某岛国公务员:因醉酒丢失包含 46 万公民信息的 U 盘
  • 2022.7 某地某公权力部门:十亿公民数据泄露
  • 2022.8 某地大数据中心:4850 万数据泄露
  • 2022.9 某地隔离转运大巴车侧翻致 27 人遇难
  • ...

人们愿意相信使用计算机技术所展现出来的数据给人带来安全感,一种「啊,都在系统中」的幻象,一种不那么草台的假象。当然,上述草台案例虽然看似和技术息息相关,本质上和它关系不大,更多时候是工程问题和流程问题,本质还是人的问题。对于数据泄露事件,他们决口不会承认,甚至会采取审查措施防止事件放大,至少邻国事件的主角还会假装鞠一躬以应付公众。

除上述领域外,方方面面都有类似的草台班子。「微博办案」、苏州和服事件、「专家建议」,无一不彰显司法或舆论的草台。法治的道路还有很远要走,十年前的宪政梦也至今仍然不让做。

现代社会发展的历史,也是革草台命的历史。或许有人会因为工业化的成就而沾沾自喜,但在脱节的国度里我反而希望有更多冒名顶替综合征患者。加入外企一年多,我感受到 M 国公司为 Business Continuity 做出的基础设施投入相比国内的草台来说高了不少,也让员工产生出「你处于我们的系统之中」这样的幻想,但是我们不能保证这些基础设施都不是草台,因为供应链攻击是常有的事。一个恰当的 meme:

Mordern Infrastructor
Mordern Infrastructor

大多数人原本或许并不草台,当他们加入某个草台团体时,会因团体的草台而被迫草台,或者离开。死海效应则是最佳例证。西语也有类似观点:

Culture eats strategy for breakfast.

不可否认,所有有人的地方都会有草台班子的影子,从真正的草台戏到硅谷华尔街,从派出所到外交部。毛主席就讲过:

......只是一个空架子,其内面全没有什么东西...... 生息了四千多年,不知干什么去了?一点没有组织,一个有组织的社会看不见,一块有组织的地方看不见...... 没有科学脑筋,不知分析与概括的关系,有小的细胞才有大的有机体,有分子的各个才有团体。 《毛泽东早期文稿》

或许有人说,这个社会需要草台,因为草台少了唱戏的可能会饿死,社会达尔文可不是闹着玩的。但更多时候这会让自己意识到:时刻保持谦卑和同理心,降低对群体的期望,厘清群己权界,才能和这个草台世界和解 —— 那是机器不曾具备的素质。

倘若消极看待整个人类社会,荒诞的草台班子或许是唯一最终能看到的答案。之所以荒诞,是因为对客观世界的认知能力没能达到一定层次而造成的心理落差 —— 这个世界的本质就是荒诞,只是草台现象的必然发生超出人类的合理认知;悲观的人一次次看到这种落差的存在会陷入本不该有的精神内耗。对此也许我们可以改变自身的眼光,始终抱以善意、同情的态度去解释所谓草台班子的行为,并积极参与、鼓励、帮助改变不合理的、荒诞的规则,构建符合当代人类认知的合理的但不绝对合理甚至未来还显得荒诞的社会意识。

生命的力量在于不顺从,或许我所能发现人类社会中的唯一不草台的地方在于人类不接受草台的设定本身,

本文受下列文章启发有所感:

这个网站是如何构建的

作者 Lawrence
2022年8月27日 08:00

使用方式

本站源码完全公开:GitHub - la3rence/site.

直接将 Markdown 文件 (.md) 存放在 posts 文件夹下即可渲染当前文件,使之成为静态页面(SSG)。 博客的首页索引目录也为同步生成,而无需手动维护。

About 页面也同理基于 readme.md 文件生成而来。

博客相关的信息配置,如标题、作者等可在 lib/config.mjs 文件中配置。

推荐使用 pnpm 来作为 node.js 的依赖管理工具(相比官方 npm,pnpm 拥有非常大的优势: 速度更快,且节省空间)。

pnpm install
pnpm dev

# [Optional] install turbo via `pnpm i -g turbo`
# and you can try `turbo` for run any script in package.json like:
turbo dev
turbo build

本地访问 http://localhost:3000 即可。

通过 pnpm build 来打包,pnpm start 则用于生产环境的启动。 通过 pnpm fmt 来将所有代码和文本进行格式化。

这篇文章展示了此博客项目所能展示的一切媒体信息,比如代码引用、表格展示、图片、视频、豆瓣卡片等等。

2024/07 更新:

  • 顶部导航栏启用毛玻璃动态悬浮
  • 全新的 edge to edge 设计风格,充分利用屏幕区域展示代码和图片

技术细节

此站点由 Vercel 公司开源的 Next.js 框架和 TailwindCSS 样式构成。前者是一项基于 React 的 SSG/SSR 开源项目,后者是一个目前流行的原子化 CSS 库,让不太会写 CSS、基础薄弱的我也能快速的写出灵活的样式。

Next.js 会主动调用我们写好的一些函数 (getStaticProps()),让组件得到数据的输入,从而在构建阶段将 React 组件提前渲染完成。remark 库可以将原生的 markdown 语法编译成 html 对应的 dom - 在此项目中,我们让它固定遍历 posts 文件夹下的 markdown 文件,依次编译,让其作为 [id].js 的动态路由页面的 props,从而渲染出博客文章:

export const getStaticProps = async context => {
  const { id } = context.params;
  const mdData = await getMdContentById(id);
  return {
    props: mdData,
  };
};

这样做的好处很多:

  • 适合被 CDN 缓存
  • 优秀的 SEO 表现
  • 节省网络带宽、流量
  • O(1) 时间复杂度的 TTFB

总之一个字,快!如果你尝试关闭当前浏览器的 JavaScript 功能,这个网站也一样能正确渲染并展现。

不光如此,Next.js 也提供了服务端渲染的能力,同样也能带来较好的 SEO 体验,不展开说了。

上述这种技术被称之为 JAMstackJavaScript, API & Markup.

一种有趣的说法是,JAMstack 是 CDN 优先的应用程序。

It's now possible, instead, to push content directly to the network and design frameworks that optimize for this capability. As a result, with optimizations like static asset hoisting, websites are now becoming faster and more reliable than ever before.

Movie

你可以编写特定的 React 组件来让 Markdown 支持更丰富的页面内容,这种实现方式和 MDX 类似。 The source of this component is coded via react component in markdown!

Code:

// filename: how.md
<div>
  <douban id="3042261"></douban>
<div>

Result:

Images

Random Image
Random Image

Video

可以直接通过 <bilibili /> 组件展示来自于 B 站视频,基于 iframe.

Tweet / 𝕏

<tweet /> 组件展示推文:

Open Graph

Open Graph (OG, 开放图谱协议) 用于社交网络分享网页时展示特定的富媒体信息。

Open Graph
Open Graph

Tables

JAMstack.

ROLE PROVIDED BY
J Client-side JS injected via React Hooks (state, event listeners, effects)
A API pages inside the pages/api directory.
M Pages with no data dependencies or pages with static data deps that trigger build-time static site generation.

GitHub Gist

export default function compare<T>(a0: T, b0: T): number {
if (a0 === b0) return 0;
if (a0 > b0) return 1;
return -1;
}
view raw gist-test.ts hosted with ❤ by GitHub

WebSub for RSS

基于 GitHub Actions 来支持 WebSub 标准,即之前的 PubSubHubbub。这使得支持 WebSub 的 RSS 客户端不仅能立刻获取到新文章,也能减少客户端和服务端之间的轮询次数以及流量。细节: site/issue/324.

ActivityPub

网站通过最基本的一些 ActivityPub 和 WebFinger 协议部分实现了与联邦宇宙的交互。实现细节

I18n

网站通过简单的配置以支持国际化。通过 markdown 文件名的中间后缀来和 Next.js 原生 locale 路由来实现区分语言类型展示不同文本。

And more to do

Markdown 目前采用 GitHub Flavored Markdown (GFM) ,并尝试添加新的语法元素支持。

例如:https://github.com/orgs/community/discussions/16925

Note

Critical content demanding immediate user attention due to potential risks.

Ref 参考链接

WebSocket 集群方案

作者 Lawrence
2020年12月22日 08:00

基于 Spring Cloud 使用一致性哈希算法实现分布式 WebSocket. / 基于 RabbitMQ 广播实现分布式 WebSocket.

WebSocket 协议

参考 RFC 标准规范。

思路

核心问题在于具备 WebSocket 服务端的 SpringBoot 或 SpringCloud 等微服务应用是有状态的。服务端的内存中存在维持连接的 Session。客户端连接服务器时,只能和集群中的唯一一个服务实例来连接,而且数据传输过程中也直接和此实例通信。

解决方案有很多。

方案一:将状态放到应用之外的存储中,但不可行

这个方案非常类似于 HTTP 协议的 Token 实现(非 JWT),Java 中具体的例子就是 Spring Session 以及 Spring Security 的 Token Store,比如 RedisTokenStore 实现了将 OAuth2 Token 存放到 Redis 的逻辑。此方案的核心前提是可序列化。WebSocket 协议和 HTTP 协议并不同,一个 WebSocket Session 对应于一次 TCP Socket 连接而非数据,新的连接将会基于新的四元组,不可能做到序列化。

方案二:一致性哈希

像微服务注册中心一样,维护一个全局映射关系 —— 当前客户端的 WebSocket 连接是和哪一个实例/节点保持的。每次通信都经过此注册中心或哈希,查询到具体的服务实例,将连接交给它。同样会遇到一些问题,比如:哈希环的实现、如何解决一致性问题(单节点故障的 Session 迁移重连将代价降到最低)、CAP 取舍、实例的区分依据是什么(IP ? 实例 ID?)、可能与注册中心构成强依赖、以及部署环境的变更带来运维上的挑战等难题。这个方案实施有一定的成本和技术限制,可以参考这篇文章的大体思路,理解了核心的哈希环的概念(如下图),再配合注册中心、服务上下线消息,就能完成基本的实现。我利用 Nacos、Redis、RabbitMQ 基于 Spring Cloud 写了一个通过一致性哈希来实现的全栈项目,前端和后端都已开源 —— WebSocket 集群的一致性哈希实践(Spring Cloud 后端) - GitHub , WebSocket 客户端模拟与服务列表(React 前端) - GitHub, 最近刚刚写好基本的功能,参考 @ufiredong 的方式利用 Java 语言的 Docker SDK 实现模拟 WebSocket 实例的上下线。未来还会稍微做细微的优化。

HashRing: 虚拟节点上线后部分客户端需要迁移节点
HashRing: 虚拟节点上线后部分客户端需要迁移节点

另外,Nacos 作为微服务的注册中心是国内比较流行的选择。但有人早就向 Nacos 社区提出了维护哈希环的议题;姑且不说此议题是否合理,但 Nacos 的维护人员竟然没有能够理解这样的需求,还是有点令人诧异。

最终效果如下, 上线一个新的服务实例后,客户端会自动根据最新哈希环重新路由到最新节点上,收发消息一切正常:

方案三:广播队列

每次连接都需要通知到所有实例,实例各自都判断连接状态在不在自己这里,不在的话直接忽略,在就处理。类似于发布订阅模型,可以通过 MQ(RabbitMQ、Kafka、RocketMQ 等)或 Redis 做到。此方案实现简单,适用于集群规模不大的场景,因为需要所有的节点进行判断或计算。我愿称之为:分布式事件驱动。

以下基于 RabbitMQ 做了简单实现。

简单广播实现 WebSocket 集群

声明式 API 的好处就体现在这里:短短几行就直接利用了 RabbitMQ 的 Java SDK 创建好交换机和队列以及绑定关系。

package me.lawrenceli.websocket.server.configuration;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.AnonymousQueue;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class MqConfig {

    private static final String FANOUT_EXCHANGE = "websocket-exchange";

    @Bean
    public FanoutExchange fanoutExchange() {
        log.info("创建广播交换机 [{}]", FANOUT_EXCHANGE);
        return new FanoutExchange(FANOUT_EXCHANGE);
    }

    @Bean
    public AnonymousQueue queueForWebSocket() {
        log.info("创建用于 WebSocket 的匿名队列");
        return new AnonymousQueue();
    }

    /**
     * @param fanoutExchange    交换机
     * @param queueForWebSocket 队列
     * @return Binding
     */
    @Bean
    public Binding bindingSingle(FanoutExchange fanoutExchange, AnonymousQueue queueForWebSocket) {
        log.info("把匿名队列 [{}] 绑定到广播交换器 [{}]", queueForWebSocket.getName(), fanoutExchange.getName());
        return BindingBuilder.bind(queueForWebSocket).to(fanoutExchange);
    }

}

然后是生产者和消费者。生产者将消息发送到队列中进行广播,集群的消费者监听队列,判断此 WebSocket Session 是否在当前消费节点再做进一步处理。

消息生产方,一般是外层接收到消息通信请求,然后调用进行集群内广播:

package me.lawrenceli.websocket.server.configuration;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class FanoutSender {

    @Autowired
    private FanoutExchange fanoutExchange;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send(Object message) {
        log.info("开始发送广播: [{}]", message.toString());
        rabbitTemplate.convertAndSend(fanoutExchange.getName(), "", message);
    }

}

消息消费方,多节点共同消费同一消息,区分是否需要自己来处理:

package me.lawrenceli.websocket.server.configuration;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class FanoutReceiver {

    @RabbitListener(queues = "#{queueForWebSocket.name}")
    public void singleReceiver(Object message) {
        log.info("队列接收到了消息: [{}]", message.toString());
        // 判断 WebSocket Session 是否在当前节点
        // 一般维护在一个 ConcurrentHashMap 静态变量中,这里叫 sessionMap
        // message 中有类似 SessionId 的字段
        if (sessionMap.contains(sessionId)) {
            log.info("WebSocket Session 在当前节点");
            // 执行相应的流程
        } else {
            log.info("当前节点无此 WebSocket Session");
            // 什么都不用做,直接忽略
        }
    }

}

注意,方案 2、方案 3 都没有解决单节点故障问题,因为从本质上讲,没有实现 WebSocket Session 的全局共享。但是方案二能够依赖哈希环的更新,最大程度上地降低迁移连接的代价,客户端会有重试的机制来针对单节点故障问题做容错。如果规模较大,项目复杂,且客户端连接数多,推荐使用一致性哈希的方案二,当然下面也有其他的社区方案。

其他案例

即刻 App 后端基于 Node.js 使用了 socket.io 实现多端实时通信,Joway 的这篇文章交代的一些细节也涉及了分布式部署的问题。

socket.io 官网也提供了多节点的使用方法,不外乎 IP Hash 之类的方案。

Kubernetes 内的 Websocket 集群

如果部署环境基于 Kubernetes 内的 WebSocket 集群,也可以参考阳明的这篇文章来实践 (利用 Service 的 sessionAffinity 会话亲和确保每次将来自特定客户端的连接传递到同一 Pod:使用 ClientIP 声明)。

Solidot 机器人

作者 solibot
2020年9月5日 08:00

奇客的资讯,重要的东西 🤖️

这是中国社交网络饭否上的一个机器人。它是一个基于 Vercel 、mongoDB 的免费数据库、以及 GitHub Action 提供的声明式定时任务一起运行来实现的 Serverless 实践。作者充分利用互联网免费基础设施 (白嫖),让它每隔一段时间(大概 30 分钟)爬取 solidot.org 网站的 RSS,比较新旧的数据后,将新的内容通过 饭否 Node SDK 发布。

solidot

源代码以 MIT 协议开放,写的很简陋,凑合着能用。

你也可以帮我调用这个 REST API 来帮助触发机器人行动:

curl -XGET -L https://post-solidot-news-to-fanfou.now.sh/api/start

若返回空数组则对应时间段内没有新的内容,反之若抓取到新的内容后,直接返回饭否发布此内容后的 API 响应。

Todo

  • Refactor with ESM

When to Use Static Generation v.s. Server-side Rendering

作者 Lawrence
2020年2月20日 08:00

We recommend using Static Generation (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.

You can use Static Generation for many types of pages, including:

  • Marketing pages
  • Blog posts
  • E-commerce product listings
  • Help and documentation

You should ask yourself: "Can I pre-render this page ahead of a user's request?" If the answer is yes, then you should choose Static Generation.

On the other hand, Static Generation is not a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.

In that case, you can use Server-Side Rendering. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.

The World As I See It

作者 Lawrence
2019年8月18日 08:00

爱因斯坦的美国观。

What first strikes the visitor with amazement is the superiority of this country, in matters of technics and organization. Objects of everyday use are more solid than in Europe, houses infinitely more convenient in arrangement. Everything is designed to save human labour. Labour is expensive, because the country is sparsely inhabited in comparison with its natural resources. The high price of labour was the stimulus which evoked the marvelous development of technical devices and methods of work.

这段话由爱因斯坦在《 The World As I See It 》中提到。

美国高昂的人力成本促使技术设备、工作方式的惊人发展。现在想想一个案例:中国之所以至今仍有很多挨家挨户抄水电表的人员,不是因为技术上实现不了自动化,而是中国人口太多,社会就业压力大,宁可牺牲效率来换稳定。

站着说话不腰疼的我自己一直奉行一个原则:拒绝机械重复劳作,应当充分发挥机器的优势和人类的主观能动,即将有规则的、确定的流程交给机器去执行,而把富有创造性的工作交给人类。

BLM 运动此起彼伏,许多开源项目在其官网首页顶部加上了醒目的 Slogan,对于美国系统性种族主义此文不作讨论。Kubernetes 官网谈了它的 社区的价值观,我认为这也是多数开放源码项目社区的价值观:

We value time spent automating repetitive work more highly than toil.

路径依赖与货物崇拜

作者 Lawrence
2019年3月12日 08:00

路径依赖原本是经济学中的一个名词,我并不深知其具体领域的定义,但用通俗的话来解释起来非常简单:过去的决策将会限制、缩小未来的可选择性。

笔者首次了解「路径依赖」是源于 BYVoid 的博客文章—— 在 Google 的这四年。类比成贪心算法的局限,他提出的解决方案是模拟退火:

模拟退火算法的理念是在初期引入随机性,随着迭代次数的增加而减小随机因子,这样最终收敛到全局最优解的机率更大。跟随这个理念来考虑职业的选择,我决定跳出现有的路径,哪怕是这个路径的前方一片光明,现在只是为了体验不同的选择。

再讲一个宗教故事。

二战期间,美军在太平洋的塔纳岛上建了一个临时基地。岛上原住民看到美军的战斗机,以为是「大铁鸟」。美军还会送一些有用的物资给岛上原住民。原住民没见过世面,都以为美军是神。后来,二战结束,美军撤走,还留了一点物资在岛上。岛上居民认为这些物资很神奇,相信这些美国大兵还会送货物过来,纷纷信仰、崇拜起了美军军服和物资(以吸引「大铁鸟」降临)。基于此,原住民发展出了自己的宗教—— 约翰弗鲁姆教。这就是货物崇拜(Cargo Cult)。

货物崇拜在软件工程领域延伸出了货物崇拜软件工程与货物崇拜编程。

货物崇拜软件工程是货物崇拜科学的一个实例。货物崇拜科学是诺奖得主 Richard Feynman 于 1974 年提出的一门伪科学概念。这种类似科学的方法论从道德上就背叛了科学精神。

以下为货物崇拜软件工程的主观实践:

  • 盲目模仿成功开发团队的表面现象
  • 机械套用软件开发过程却不知其由(即货物崇拜编程)

货物崇拜和路径依赖存在交集。具体表现在开发者的机械式开发。最常见的是:

  • jQuery 一把梭

我发现有前端开发者会在使用 Vue/React 等基于 Virtual DOM 的声明式 UI Library 的情况下依旧使用 jQuery 操作 DOM。这是典型的货物崇拜编程实践,因为这样的开发人员完全不理解 Vue/React 是做什么的,因为已经习惯了 $ 的 API,哪管 DOM Virtual 不 Virtual;姑且撇开 Virtual DOM 的性能不说,这样做的开发人员往往也没能够理解声明式 UI 框架的内涵。

上文提到的「模拟退火」是破解路径依赖困境的法则之一。除此之外,笔者认为最适合大多数人的做法是对事物保持强烈的好奇心。正如 Aaron Swartz 所云:

Be curious. Read widely. Try new things. What people call intelligence just boils down to curiosity.

一些链接:

Invert binary tree

作者 Lawrence
2017年12月22日 08:00

A problem that Homebrew's author complained.


Invert a binary tree. Problem from leetcode.

Here's the input:

     4
   /   \
  2     7
 / \   / \
1   3 6   9

and the out put looks like:

     4
   /   \
  7     2
 / \   / \
9   6 3   1

This problem was inspired by this tweet.

Here's the solution implemented in Java.

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public TreeNode invertTree(TreeNode root) {
        if(null != root){
            TreeNode leftChild = root.left;
            root.left = root.right;
            root.right = leftChild;
            root.left = invertTree(root.left);
            root.right = invertTree(root.right);
        }
        return root;
    }
}

There's much different logic behind software engineering with computer science. Max replied this question about it on quora.com after 2 years.

没有银弹

作者 Lawrence
2017年9月1日 08:00

维基: https://zh.wikipedia.org/wiki/没有银弹

文献:Brooks, F. P.,"No silver bullet—essence and accidents of software engineering, " in Information Processing 86, H. J. Kugler, ed. Amsterdam: Elsevier Science (North Holland), 1986, pp. 1069-1076.

结论:不会有任何单一的软件工程上的突破(银弹),能够让程序设计的生产力得到一个数量级的提升。

人月神话

维基:https://zh.wikipedia.org/wiki/人月神话

豆瓣:https://book.douban.com/subject/1102259/

结论:人力和时间并不呈现线性关系。以大量人员和较短的时间,并不能缩短软件的开发进度。

公式:

沟通管道数量(n 为一个项目团队中的人数,显然,人数越多,沟通成本越大):

p = n(n-1)/2

Brooks 法则:

Adding manpower to a late software project makes it later.

组织:

提倡类似外科手术团队的组织。在接受相同的训练、同样都是两年资历的情况下,优秀专业程序员的生产力要比差劲的程序员好上十倍。

系统设计的民主与专制:

整体性:需要专制,设计必须出自于一个人的想法,或是极少数人的一致决定。

评价标准:功能概念复杂度比。功能多不见得是好事。

避免第二系统过度设计。

Two Forms of Pre-rendering

作者 Lawrence
2023年11月29日 21:09

Next.js has two forms of pre-rendering: Static Generation and Server-side Rendering. The difference is in when it generates the HTML for a page.

  • Static Generation is the pre-rendering method that generates the HTML at build time. The pre-rendered HTML is then reused on each request.
  • Server-side Rendering is the pre-rendering method that generates the HTML on each request.

Importantly, Next.js lets you choose which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.

❌
❌