加群联系作者vx:xiaoda0423
仓库地址:webvueblog.github.io/JavaPlusDoc...
📦 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. 部分地方存在并发安全风险
虽然大部分用了 ConcurrentHashMap
、CopyOnWriteArraySet
,但是:
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. 异常处理太轻了
在 @OnMessage
、publishTopic
中,异常都是简单 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+数据库持久化 | 需要完整心跳记录,审计需求 | 数据不丢,功能完整 | 开发复杂,读写压力大 |