分布式微服务系统架构第121集:AppCtxHolder

加群联系作者vx:xiaoda0423

仓库地址:https://webvueblog.github.io/JavaPlusDoc/

https://1024bat.cn/

 AppCtxHolder 的作用

这是一个Spring上下文工具类,主要是为了在非 Spring 管理的地方(比如普通的工具类、线程、WebSocket类等)
也能拿到 Spring 容器里的 Bean 对象,实现全局访问 Spring 容器

主要功能总结

方法

作用

setApplicationContext(ApplicationContext context)

Spring 启动时,自动注入 ApplicationContext

getSpringBean(String beanName)

根据 bean 名字,获取 Spring 容器里的对象

getBeanDefinitionNames()

获取所有已注册的 Spring Bean 名字列表

为什么要有 AppCtxHolder?

因为有些时候我们想拿到 Spring 中的 Bean,但是:

  • 当前类不是被 Spring 托管的(比如普通 Java 类、第三方库、手动 new 出来的对象)

  • 不是通过 @Autowired 注入的

  • 也不是通过 @Component/@Service 扫描到的

这时就需要有一个能全局存 ApplicationContext 的工具类 ➔ 也就是 AppCtxHolder

这样就能做到:

MyService myService = (MyService) AppCtxHolder.getSpringBean("myService");

直接拿到 Bean 用!

应用场景举例

场景

是否适合用 AppCtxHolder

WebSocket 的 @ServerEndpoint 类中注入 Service

✅ 很常用

普通工具类中调用 Spring Bean

自定义线程(比如 Runnable、TimerTask)中想用 Spring Bean

外部 SDK / 框架接入后要访问 Spring 服务

Spring Boot正常托管的 Controller、Service中

❌ 不需要,直接 @Autowired

️ 一个典型使用案例

比如我们有一个 @ServerEndpoint 的 WebSocket 类:

@ServerEndpoint("/ws")
public class MyWebSocket {

    private MyService myService;

    @OnOpen
    public void onOpen(Session session) {
        this.myService = (MyService) AppCtxHolder.getSpringBean("myService");
        myService.doSomething();
    }
}

✅ 这样,WebSocket也能用上 Spring 的依赖了!
而不需要传统的 @Autowired,因为 @ServerEndpoint 本身是由 Web容器(Tomcat/Undertow/Jetty)托管的,不是 Spring 托管的!

⚡ 注意小点

  • AppCtxHolder.context 是静态的,所以是全局唯一的。

  • 在项目启动阶段(Spring容器加载完成后),context 就被赋值好了。

  • 如果发现 context==null,说明可能是 没有被 Spring 扫描到(比如没加 @Component),或者项目启动顺序有问题。

总结一句话

AppCtxHolder 是让你在任何地方都能访问 Spring 容器中 Bean 的万能工具。

主要问题 & 风险点

1. BlockingQueue没有指定泛型,导致是原生的裸类型

static BlockingQueue blockingQueue = new LinkedBlockingQueue();

 风险:类型不安全,可能会导致 ClassCastException。 ✅ 建议改成带泛型

static BlockingQueue blockingQueue = new LinkedBlockingQueue<>();

2. WebSocket @Autowired 注入问题

WebSocket 实例是多实例(每连接一个客户端就 new 一个),而Spring容器默认是单例

你直接用 @Autowired,在生产环境高并发下,可能注入失败或者导致奇怪的问题

 风险:依赖注入失效,空指针异常。

✅ 标准做法是用静态注入,比如写个静态工具类或 BaseEndpointConfigure 中注册注入(Spring Boot 2.3+ 支持这种方式),或者简单一点手动持有:

private static HttpInvokeService httpInvokeServiceStatic;
@Autowired
public void setHttpInvokeService(HttpInvokeService httpInvokeService){
    RTDataWebSocket.httpInvokeServiceStatic = httpInvokeService;
}

3. fixedThreadPool线程池无保护

你 publishTopic 里的线程池任务,虽然有队列大小判断,但是线程池本身是没有拒绝策略的(默认 AbortPolicy),如果高并发或者阻塞严重,可能导致抛异常。

 风险RejectedExecutionException 把线程打爆。 ✅ 建议加保护,比如 try-catch 包裹:

try {
    fixedThreadPool.submit(() -> {
        ...
    });
} catch (RejectedExecutionException e) {
    log.error("任务提交失败: {}", e.getMessage(), e);
}

或者给线程池配置自定义 RejectedExecutionHandler(比如丢弃、日志打印)。

4. 部分地方存在并发安全风险

虽然大部分用了 ConcurrentHashMapCopyOnWriteArraySet,但是:

  • topicSessionMap.get(topic).add(session) 和 topicSessionMap.get(topic) 这种先 get 再 add的模式,在极端并发下可能出现 NPE

  • 因为 get() 后对象可能被其他线程 remove() 了。

✅ 建议做保护判断

Set sessions = topicSessionMap.get(topic);
if (sessions != null) {
    sessions.add(session);
}

否则可能偶现空指针。

5. 异常处理太轻了

在 @OnMessagepublishTopic 中,异常都是简单 log,实际上在生产环境要更细分:

  • 客户端非法数据 ➔ 可直接 close

  • JSON 解析失败 ➔ 返回错误提示

  • 内部服务异常 ➔ 正常返回 error code,不要让客户端无限重连

✅ 可以增加一层异常分类,比如:

catch (JsonSyntaxException e) {
    log.error("非法JSON:{}", e.getMessage(), e);
    session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "Invalid JSON"));
} catch (Exception e) {
    log.error("系统异常:{}", e.getMessage(), e);
}

@OnOpen

可以记录连接数,或者加白名单校验(防止乱连)

内存泄漏

如果客户端断开了但 topicSessionMap 里的 session 没移除干净,会慢慢堆积,最好在 onClose 保证彻底清理

WebSocket标准一般需要定时心跳机制(比如客户端每隔30秒 ping),防止假死连接。如果没有,需要后续考虑

监听 Kafka 消息 ➔ 解析 JSON 成对象 ➔ 根据事件类型做不同处理逻辑 ➔ 最后手动提交 offset

系统版本迭代升级时,像这种存在于 内存(ConcurrentHashMap 中的心跳数据(状态)会全部丢失
这是因为:内存 = 进程级存储,一旦程序重启、崩溃或者升级部署,JVM内存清空,所以这些心跳数据也会全部消失

1. 轻量型方案 —— 临时丢失可以接受

如果心跳数据只是为了监控“当前状态”,短时间丢失是可以接受的(比如上线后几秒内就能补回新的心跳),
内存存储+客户端自动补报心跳是足够的,
部署时加个提示或者在界面上打个“升级中”的小标识就行。

适合:

  • 客户端每隔几秒发送心跳。

  • 丢几秒的数据对业务无影响。

✅ 优点:简单,资源占用小。
❗ 缺点:升级瞬间状态断档,可能影响监控系统准确率。

2. 中等方案 —— 心跳数据持久化到 Redis

如果要求稍微高一点,希望系统升级重启后依然能拿到上一次心跳数据

系统重启时,可以从 Redis 恢复一部分最近的心跳数据

适合:

  • 需要比较稳定的心跳状态展示。

  • 升级过程中不能影响监控准确率太多。

✅ 优点:高可用,快速恢复。
❗ 缺点:稍微增加一点 Redis 使用量和开发复杂度。

3. 重型方案 —— 心跳+状态全量落盘(数据库)

如果你需要心跳记录做审计、报警,或者心跳信息必须长时间保留,
那就不仅要存 Redis,还要异步入库(MySQL / PostgreSQL):

  • 心跳数据写一张 heartbeat_status 表。

  • 每次心跳更新一个字段 last_online_time

适合:

  • 金融、政府、电力、医疗等必须记录设备状态变化的领域。

  • 需要对心跳数据做离线分析、报表统计。

✅ 优点:超稳定,满足数据留存需求。
❗ 缺点:开发复杂,性能开销大,要设计好表和更新策略。

小结一下

方案

适用场景

优点

缺点

只内存

能接受短时断档

简单快速

升级必丢数据

Redis缓存

要求重启后快速恢复

可容灾,性能高

增加Redis压力

Redis+数据库持久化

需要完整心跳记录,审计需求

数据不丢,功能完整

开发复杂,读写压力大

你可能感兴趣的:(分布式,微服务,系统架构,架构,云原生)