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

加群联系作者vx:xiaoda0423

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

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

这样就能做到:

ini 复制代码
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 类:

kotlin 复制代码
@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没有指定泛型,导致是原生的裸类型

ini 复制代码
static BlockingQueue blockingQueue = new LinkedBlockingQueue();

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

ini 复制代码
static BlockingQueue<Runnable> blockingQueue = new LinkedBlockingQueue<>();

2. WebSocket @Autowired 注入问题

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

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

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

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

typescript 复制代码
private static HttpInvokeService httpInvokeServiceStatic;
@Autowired
public void setHttpInvokeService(HttpInvokeService httpInvokeService){
    RTDataWebSocket.httpInvokeServiceStatic = httpInvokeService;
}

3. fixedThreadPool线程池无保护

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

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

vbnet 复制代码
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() 了。

建议做保护判断

ini 复制代码
Set<Session> sessions = topicSessionMap.get(topic);
if (sessions != null) {
    sessions.add(session);
}

否则可能偶现空指针。

5. 异常处理太轻了

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

  • 客户端非法数据 ➔ 可直接 close
  • JSON 解析失败 ➔ 返回错误提示
  • 内部服务异常 ➔ 正常返回 error code,不要让客户端无限重连

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

lua 复制代码
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+数据库持久化 需要完整心跳记录,审计需求 数据不丢,功能完整 开发复杂,读写压力大
相关推荐
mzlogin10 分钟前
Java|小数据量场景的模糊搜索体验优化
java·后端
lozhyf32 分钟前
基于springboot的商城
java·spring boot·后端
无心水34 分钟前
【Java面试笔记:基础】6.动态代理是基于什么原理?
java·笔记·面试·动态代理·cglib·jdk动态代理
小爷毛毛_卓寿杰1 小时前
【Dify(v1.2) 核心源码深入解析】App 模块:Entities、Features 和 Task Pipeline
人工智能·后端·python
java奋斗者1 小时前
健身房管理系统(springboot+ssm+vue+mysql)含运行文档
spring boot·后端·mysql
周星星日记1 小时前
14.vue3中keepAlive实现原理
前端·vue.js·面试
用户638982245891 小时前
查漏补缺:Seata分布式事务的使用
后端
海底火旺1 小时前
JavaScript 中 '+' 的隐式转换:你需要知道的 9 种魔法行为
前端·javascript·面试
用户638982245891 小时前
查漏补缺:Sentinel的使用简介
后端
l软件定制开发工作室2 小时前
Spring开发系列教程(26)——异步处理
java·后端·spring