Guava EventBus源码深度解析:线程模型、分发机制与异常处理

简介:本资源是一份面向Java中高级开发者与源码学习者的Guava EventBus框架深度解析材料,聚焦Google Guava 18.0版本中EventBus核心机制的原理剖析与实践梳理。资源包含26个文件,涵盖15个关键Java源码与测试类(如EventBus、SubscriberRegistry等)、6张流程图与状态图PNG(含register/post/unregister全流程可视化)、3份UML类图(eventbus.uml等)清晰呈现组件关系,以及README.md说明文档和pom.xml构建配置,整体压缩包仅775KB,轻量易读。已有1898人学习下载,适合希望深入理解事件总线设计思想、解耦通信模型及线程调度机制(如AsyncEventBus与@AllowConcurrentEvent注解应用)的开发者。学习者可获得完整源码级笔记、事件分发生命周期图解、订阅注册注销全流程分析及典型使用陷阱总结,助力从API调用跃升至框架内核认知。

1. Guava EventBus 源码学习:不是抄个@Subscribe就算懂事件总线,它底层的线程模型、事件分发顺序、异常传播机制才是真考点

你写过EventBus,注册监听器、发个事件、控制台打印一行日志——看起来跑通了。但某天线上服务偶发卡顿,监控显示EventBus的post()调用耗时突增到 200ms;又或者某个监听器抛了NullPointerException,结果整个事件链直接中断,后续监听器一个没执行;再或者你在多线程环境下反复register/unregister,发现内存里残留了大量Subscriber对象,GC 都收不干净……这些都不是玄学,全是Guava EventBus源码里白纸黑字写死的行为。这份源码学习资源,不是带你逐行读EventBus.java,而是聚焦三个硬核切口:事件分发的同步/异步边界在哪、订阅者方法如何被反射定位与缓存、异常未捕获时的默认熔断策略。适合正在用EventBus做模块解耦、但还没看过SubscriberRegistry类的 Java 工程师,也适合想搞懂“轻量级事件总线”设计范式的中级开发者。它不教你怎么用 API,而是告诉你——当你调用post()的那一刻,JVM 里到底发生了什么。


2. EventBus 核心机制拆解:从post()入口到Subscriber执行的完整调用链

2.1post()触发的三层分发结构:Dispatcher是调度中枢,不是简单 for 循环

EventBus.post(Object event)看似简单,实则触发三重委托:

  1. 事件类型解析层:EventBus.resolveAndDispatch(event)先通过TypeToken获取事件运行时类型(支持泛型擦除后的精确匹配),再查SubscriberRegistry获取所有匹配该类型的Subscriber列表;
  2. 分发策略层:交由Dispatcher实例处理,Guava 默认使用LegacyAsyncDispatcher(异步)或PerThreadQueuedDispatcher(线程局部队列),而非直接遍历执行;
  3. 执行层:每个Subscriber封装了目标对象、方法引用、参数绑定逻辑,最终通过invokeSubscriberMethod()反射调用。

关键点在于:Dispatcher不是立即执行,而是决定“何时、在哪个线程、以什么顺序”执行。比如PerThreadQueuedDispatcher会把事件暂存在ThreadLocal<Queue>中,等到当前线程下一次post()或显式调用flush()时批量消费——这解释了为什么你在单线程里连续post()多个事件,监听器却不是按发送顺序执行。

// 源码关键路径示意(基于 Guava 31.1-jre) public void post(Object event) { // 1. 解析事件类型并获取匹配的 Subscriber 列表 Iterator<Subscriber> eventSubscribers = subscribers.getSubscribers(event); // 2. 交由 Dispatcher 分发(注意:此处不执行!) dispatcher.dispatch(event, eventSubscribers); }

提示:dispatcher.dispatch()是整个事件生命周期的分水岭。同步模式下它会立刻遍历执行;异步模式下它只入队,真正的执行延迟到ExecutorService的线程池取任务。别误以为post()返回就代表监听器已执行完毕。

2.2Subscriber的生成与缓存:SubscriberRegistry如何用ConcurrentMap实现高性能注册

register(Object listener)的核心是SubscriberRegistry.registerSubscriber(listener)。它不做简单List.add(),而是构建一个Map<Class<?>, Set<Subscriber>>结构:

  • Key 是监听器方法声明的事件类型(如OrderCreatedEvent.class);
  • Value 是所有能接收该类型事件的Subscriber集合(一个监听器类可能有多个@Subscribe方法);
  • 底层用ConcurrentHashMap+CopyOnWriteArraySet保证并发安全,避免register/unregister时锁整张表。

更关键的是方法查找缓存:首次扫描监听器类时,Subscriber会通过AnnotatedMethodsFinder反射遍历所有 public 方法,检查@Subscribe注解,并缓存Method对象及参数类型。后续post()时直接复用,省去重复反射开销。

// Subscriber 构造关键逻辑(简化) public Subscriber(Object target, Method method) { this.target = Preconditions.checkNotNull(target); this.method = Preconditions.checkNotNull(method); // 缓存方法参数类型(通常只有一个参数:事件对象) this.eventType = method.getParameterTypes()[0]; // 设置访问权限(避免 private 方法调用失败) this.method.setAccessible(true); }

参数说明:

  • target:监听器实例(如new OrderService());
  • method:被@Subscribe标记的方法(如onOrderCreated(OrderCreatedEvent e));
  • eventType:方法参数类型,用于SubscriberRegistry匹配事件;
  • setAccessible(true)是必须的——否则private方法无法被反射调用,会抛IllegalAccessException。

2.3 异常传播的默认策略:LoggingHandler是兜底,不是静默吞掉

当某个Subscriber执行中抛出未捕获异常(如RuntimeException),EventBus默认行为是:

  • 中断当前事件的剩余分发(即后续Subscriber不再执行);
  • 将异常交给SubscriberExceptionHandler处理;
  • Guava 内置的LoggingHandler仅记录 ERROR 日志,不会 re-throw,也不会影响其他事件。

这意味着:一个监听器崩了,只影响本次事件,不影响下一次post()。但如果你需要更激进的策略(如事件失败时回滚数据库事务),必须自定义SubscriberExceptionHandler并传入EventBus构造器。

// 自定义异常处理器示例 EventBus eventBus = new EventBus(new LoggingHandler() { @Override public void handleException(Throwable thrown, SubscriberExceptionContext context) { // 记录日志后,可主动触发告警或补偿逻辑 logger.error("Event dispatch failed for subscriber: {}", context.getSubscriber(), thrown); // 注意:此处不能 throw 新异常,否则 EventBus 会 crash } });

逻辑说明:SubscriberExceptionContext提供了上下文信息——getSubscriber()返回出错的Subscriber实例,getEvent()返回当前事件对象,getSubscriberMethod()返回被调用的方法。这是做精准诊断的唯一入口。


3. 注册与注销的避坑指南:unregister()不等于内存释放,WeakReference是双刃剑

3.1 现象:unregister()后Subscriber仍被SubscriberRegistry持有,导致内存泄漏

现象:频繁创建/销毁监听器对象(如 Spring Bean 生命周期内动态注册),unregister()调用后,堆内存中Subscriber实例数持续增长,jmap -histo显示com.google.common.eventbus.Subscriber占用 Top 3。
原因:SubscriberRegistry内部的subscribersByType是强引用Map,unregister()仅从Map中移除Subscriber,但若监听器对象本身被其他地方强引用(如静态变量、缓存、线程局部变量),Subscriber的target字段仍持有该对象,GC 无法回收。
解决:确保监听器对象无外部强引用;或改用WeakEventBus(需自行实现,Guava 不提供);更推荐方案——监听器设计为无状态,复用单例实例,避免高频创建。

3.2 现象:异步EventBus下unregister()时机不当,导致事件仍在执行中就被移除

现象:使用AsyncEventBus时,在监听器方法内调用eventBus.unregister(this),随后该监听器仍收到后续事件,甚至出现NullPointerException(因target已被置空)。
原因:AsyncEventBus的Dispatcher将事件放入队列后,unregister()只清理注册表,但队列中已排队的事件仍会执行。Subscriber对象本身未被销毁,只是从注册表移除,其target仍有效。
解决:禁止在监听器方法内调用unregister();应在业务逻辑明确结束监听时(如 Spring@PreDestroy)统一注销;若必须动态注销,需配合AtomicBoolean标志位在方法内判断是否跳过执行。

3.3 现象:@Subscribe方法参数类型不匹配,事件无声丢失

现象:post(new PaymentCompletedEvent()),但PaymentService.onPaymentCompleted(PaymentCompletedEvent e)未被调用,控制台无任何日志,事件像石沉大海。
原因:SubscriberRegistry匹配时严格比对Method.getParameterTypes()[0]与事件实际类型。若方法签名是onPaymentCompleted(Object e)或onPaymentCompleted(PaymentEvent e)(父类),而事件是PaymentCompletedEvent子类,默认不启用继承匹配(Guava 30+ 默认关闭strict模式)。
解决:确认方法参数类型与事件类型完全一致;或启用继承匹配(不推荐,增加反射开销):

// 构造 EventBus 时开启继承匹配(慎用) EventBus eventBus = new EventBus( new AsyncEventBus(Executors.newCachedThreadPool(), new SubscriberFindingStrategy() { @Override protected Set<Method> findAllMethods(Class<?> listenerClass) { return super.findAllMethods(listenerClass); // 默认已支持 } })); // 实际生效需在 SubscriberRegistry 构造时传入 allowInheritance=true

3.4 现象:多线程并发register/unregister,SubscriberRegistry出现ConcurrentModificationException

现象:高并发场景下,多个线程同时调用register()和unregister(),偶发抛出java.util.ConcurrentModificationException。
原因:SubscriberRegistry内部subscribersByType是ConcurrentHashMap,但遍历操作(如getSubscribers())仍可能触发CME,尤其当ConcurrentHashMap在扩容时。
解决:Guava 31+ 已修复此问题,升级到最新版;若无法升级,避免在post()过程中调用register/unregister;将注册/注销操作收敛到应用启动/关闭阶段,运行时只post()。


4. 源码调试实战:用断点追踪一次post()的完整生命周期

4.1 调试环境搭建:精准定位EventBus版本与断点位置

首先确认项目依赖的 Guava 版本(mvn dependency:tree | grep guava),本例基于guava:31.1-jre。关键断点位置:

  • EventBus.post(Object):入口,观察事件对象;
  • SubscriberRegistry.getSubscribers(Object event):查看匹配的Subscriber数量与类型;
  • PerThreadQueuedDispatcher.dispatch(Object event, Iterator<Subscriber> subscribers):确认是否入队;
  • Subscriber.invokeSubscriberMethod(Object event):最终执行点,检查method.invoke(target, event)。

注意:若使用AsyncEventBus,dispatch()断点可能不触发(因异步队列),此时需在AsyncEventBus的executor线程中设断点,或改用LegacyAsyncDispatcher(已废弃但便于调试)。

4.2 断点验证:post()后Subscriber是否真的执行?

以OrderCreatedEvent为例,设置断点后执行流程:

  1. EventBus.post(new OrderCreatedEvent("ORD-001"))→ 进入post();
  2. getSubscribers()返回Set<Subscriber>,大小为 2(InventoryService和NotificationService);
  3. dispatch()将两个Subscriber加入当前线程的ThreadLocal<Queue>;
  4. 关键观察:post()方法返回,但断点未进入Subscriber.invokeSubscriberMethod();
  5. 此时手动触发AsyncEventBus.flush()或等待executor线程取任务,断点才命中执行逻辑。

这验证了:post()的语义是“发布”,不是“执行完成”。若业务强依赖监听器执行完毕(如发短信成功才更新订单状态),必须同步EventBus或显式awaitTermination()。

4.3 参数传递验证:Subscriber如何绑定事件对象到方法参数

在Subscriber.invokeSubscriberMethod()断点处,检查局部变量:

  • this.method.getParameterTypes()→[class com.example.OrderCreatedEvent];
  • event.getClass()→class com.example.OrderCreatedEvent;
  • this.target→com.example.InventoryService@1a2b3c;
  • this.method→public void com.example.InventoryService.onOrderCreated(com.example.OrderCreatedEvent)。

此时执行method.invoke(target, event),参数event被原样传入方法。无自动类型转换、无 JSON 序列化,纯 Java 对象直传。若事件对象含transient字段或未实现Serializable,在跨 JVM 场景下会失效——但这不属于EventBus职责,它只负责进程内通信。


5. 生产环境加固技巧:从源码反推的 4 个不可妥协的配置项

5.1 必须禁用StrictSubscriberFindingStrategy:反射开销与类加载风险

Guava 提供StrictSubscriberFindingStrategy(默认不启用),它会在register()时强制校验@Subscribe方法是否满足:

  • 方法非 static、非 abstract;
  • 参数有且仅有一个;
  • 方法名不以test开头(防 JUnit 误注册)。

看似严谨,但生产环境应禁用:

  • 反射校验增加register()耗时,高频注册场景(如动态插件)易成瓶颈;
  • 某些字节码增强框架(如 Lombok@SneakyThrows)可能干扰反射判断,导致合法方法被跳过;
  • SubscriberRegistry本身已在invokeSubscriberMethod()中做运行时校验(IllegalArgumentException),失败成本远低于注册期阻塞。

正确做法:保持默认SubscriberFindingStrategy,依赖运行时异常捕获。

5.2 线程池必须显式配置:AsyncEventBus的Executor决定系统稳定性

AsyncEventBus构造器必须传入Executor,绝不可用Executors.newCachedThreadPool():

  • CachedThreadPool创建无限线程,突发流量下 OOM;
  • 无拒绝策略,任务堆积导致LinkedBlockingQueue内存溢出;
  • 线程名无业务标识,故障时无法区分是EventBus还是业务线程。

推荐配置:

ThreadFactory threadFactory = new ThreadFactoryBuilder() .setNameFormat("eventbus-pool-%d") .setDaemon(true) .build(); ExecutorService executor = new ThreadPoolExecutor( 4, // corePoolSize 16, // maxPoolSize 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000), // 有界队列 threadFactory, new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝时由调用线程执行 ); AsyncEventBus eventBus = new AsyncEventBus(executor);

参数说明:

  • corePoolSize=4:保底线程数,应对日常流量;
  • maxPoolSize=16:防突发,避免线程爆炸;
  • queue=1000:有界队列,超限触发拒绝策略;
  • CallerRunsPolicy:拒绝时由post()调用线程执行,自然限流。

5.3 事件对象必须不可变:final字段与防御性拷贝是底线

EventBus不对事件对象做任何保护,若监听器修改了事件字段(如event.setStatus("PROCESSED")),后续监听器看到的是已被污染的对象。所有事件类必须设计为不可变:

public final class OrderCreatedEvent { private final String orderId; private final BigDecimal amount; private final Instant createdAt; public OrderCreatedEvent(String orderId, BigDecimal amount) { this.orderId = Preconditions.checkNotNull(orderId); this.amount = Preconditions.checkNotNull(amount).setScale(2, RoundingMode.HALF_UP); this.createdAt = Instant.now(); } // 仅 getter,无 setter public String getOrderId() { return orderId; } public BigDecimal getAmount() { return amount; } public Instant getCreatedAt() { return createdAt; } }

提示:若事件需携带List或Map,必须用ImmutableList.copyOf()或Collections.unmodifiableList()包装,否则监听器仍可修改内部集合。

5.4 监听器方法必须幂等:EventBus不保证投递次数

EventBus是“最多一次”(At-Most-Once)语义,不提供重试、去重、事务一致性保障。若监听器执行失败(如网络超时),EventBus不会重发。因此:

  • @Subscribe方法必须设计为幂等:如数据库操作用INSERT ... ON CONFLICT DO NOTHING;
  • 避免在监听器内做非幂等操作(如发邮件、扣库存);
  • 关键业务应将事件持久化到 DB,再由独立消费者处理,EventBus仅作进程内通知。

从那以后我每次设计@Subscribe方法,都强制走一遍「幂等性 checklist」:

  1. 是否只读?→ 是,加@Transactional(readOnly=true);
  2. 是否写 DB?→ 是,SQL 必须带唯一约束或UPSERT;
  3. 是否调外部服务?→ 是,必须带业务幂等 key(如orderId+eventType);
  4. 是否有状态变更?→ 是,状态机流转必须支持重复触发。

希望帮到你。

本文还有配套的精品资源,点击获取

相关新闻

UI Recorder 实战:基于录制回放的低门槛Web自动化测试方案

UI Recorder 实战:基于录制回放的低门槛Web自动化测试方案

1. 项目概述&#xff1a;UI Recorder 到底解决了什么问题 做Web端测试的同学应该都有过这种经历&#xff1a;手工测试点来点去&#xff0c;一天下来眼睛花了、手腕酸了&#xff0c;回归用例还有一大堆没跑完。想上自动化&#xff0c;又绕不开那套学习曲线陡峭的框架——写定位器…

2026/10/20 18:15:49 阅读更多 →
从回调地狱到响应式数据流:RxJava核心机制与工程落地

从回调地狱到响应式数据流:RxJava核心机制与工程落地

前段时间我重构了一个老旧的数据采集模块&#xff0c;事情不大但特别典型&#xff1a;十几路传感器、一路串口、一路网络推送&#xff0c;全都往同一个界面汇聚。重构之前代码里全是回调嵌套&#xff0c;想改一个过滤条件得顺着调用链翻半天&#xff1b;重构之后整体逻辑变成一…

2026/10/20 18:15:49 阅读更多 →
refin-bin-0.14.tar 文本后处理工具包:日志清洗与规则部署实战

refin-bin-0.14.tar 文本后处理工具包:日志清洗与规则部署实战

简介&#xff1a;rEFInd 是一款面向 UEFI 固件环境的开源启动管理器&#xff0c;常用于 Linux、macOS 与 Windows 多系统共存的引导切换&#xff0c;可展示图形化启动菜单&#xff0c;支持自定义超时、默认系统与图标&#xff0c;适合具备一定 UEFI 基础的桌面用户、双系统爱好…

2026/10/20 18:15:49 阅读更多 →

最新新闻

TensorFlow鞋类识别实战:Fashion MNIST图像分类全流程

TensorFlow鞋类识别实战:Fashion MNIST图像分类全流程

前段时间重新整理之前的深度学习实验笔记&#xff0c;翻到鞋类识别&#xff08;Fashion MNIST&#xff09;这个项目时&#xff0c;觉得特别值得拿出来单独写一篇。原因很简单&#xff1a;很多人入门深度学习时&#xff0c;都会被建议先跑通手写数字识别&#xff08;MNIST&#…

2026/10/20 20:57:37 阅读更多 →
用AI辅助Kamailio路由排错:一场人机讨论实录与故障切换实践

用AI辅助Kamailio路由排错:一场人机讨论实录与故障切换实践

最近接手一套老的SIP通信平台&#xff0c;核心信令是Kamailio撑着。业务要新增一个较复杂的路由策略&#xff1a;同一个主叫号码&#xff0c;在不同时间段、针对不同被叫前缀&#xff0c;要分别走不同的网关&#xff1b;而其中一路网关做故障切换时&#xff0c;还要保证已经建立…

2026/10/20 20:57:37 阅读更多 →
LibVLC轻量级视频播放模块:嵌入C/C++工程的硬件解码方案

LibVLC轻量级视频播放模块:嵌入C/C++工程的硬件解码方案

简介&#xff1a;本资源是一份基于C/C与VLC开源库开发Windows视频播放器的完整工程实践包&#xff0c;面向具备基础C编程能力、希望掌握多媒体开发技能的中级开发者及高校课程设计学习者。资源聚焦于在Visual Studio 2022环境下集成libvlc实现视频加载、播放控制、窗口渲染等核…

2026/10/20 20:57:37 阅读更多 →
AI Agent创业团队如何配置一支高效的Harness Engineering团队

AI Agent创业团队如何配置一支高效的Harness Engineering团队

1. 为什么需要一支专门的Harness Engineering团队先从一个很现实的场景说起&#xff1a;你所在的创业团队花三个月做出了一个基于大模型的Agent产品Demo&#xff0c;演示给投资人看时效果炸裂&#xff0c;但真正进入客户试用阶段&#xff0c;问题接踵而来——模型给出正确意图却…

2026/10/20 20:57:37 阅读更多 →
视觉语言模型与RGB-D感知融合:三条进化主线与工程实践

视觉语言模型与RGB-D感知融合:三条进化主线与工程实践

1. 从 Transformer 到多模态感知的演进逻辑视觉语言模型和 RGB-D 感知这两个方向&#xff0c;过去几年基本是各走各的路。做 VLM 的人盯着图文对齐、指令跟随、视觉问答这些任务&#xff0c;做 RGB-D 的人则埋头于点云配准、深度补全、SLAM 建图。但最近一年&#xff0c;这两条…

2026/10/20 20:57:37 阅读更多 →
VTI介质地震波数值模拟:MATLAB实现与各向异性正演避坑指南

VTI介质地震波数值模拟:MATLAB实现与各向异性正演避坑指南

简介&#xff1a;这份资源面向地震勘探与地球物理方向的学习者和研究者&#xff0c;提供一套基于Matlab的VTI介质地震波场数值模拟程序&#xff0c;用于分析各向异性介质中地震波的传播规律。VTI介质在垂直与平行层理方向上纵波速度存在差异&#xff0c;是实际地壳中常见的各向…

2026/10/20 20:56:32 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →