文章目录
- 前言
- 背景
- 初步调研实现思路方案
- 核心需求分析
- 技术方案对比
- 实现思路
- 初步功能设计
- 关键设计决策
- 实现步骤与代码
- 第一步:核心关闭管理器实现
- 第二步:集成到钉钉客户端管理器(注册关闭)
- 总结说明
- 资料获取
前言
博主介绍:✌目前全网粉丝4W+,csdn博客专家、Java领域优质创作者,博客之星、阿里云平台优质作者、专注于Java后端技术领域。
涵盖技术内容:Java后端、大数据、算法、分布式微服务、中间件、前端、运维等。
博主所有博客文件目录索引:博客目录索引(持续更新)
CSDN搜索:长路
视频平台:b站-Coder长路
背景
在企业级应用开发中,服务的稳定性和可靠性是至关重要的考量因素。在实际生产环境中,应用经常会遇到各种异常情况:内存溢出导致JVM崩溃、系统资源耗尽触发强制重启、部署平台主动销毁容器实例等。这些异常退出场景往往使得应用无法正常执行资源清理操作,进而引发一系列严重问题:
- 数据库连接池连接未释放,导致连接泄漏
- 网络长连接(如WebSocket、钉钉Stream连接)未正常关闭
- 文件句柄未释放,造成资源浪费
- 消息队列消费者未正确取消订阅
- 分布式锁未释放,引发死锁问题
近期在开发钉钉机器人集成服务时,我们就遇到了这样一个具体问题:当应用异常重启时,如何确保所有已建立的钉钉Stream模式客户端连接能够被正确关闭?这不仅关系到系统资源的有效管理,更直接影响整个服务的稳定性和可维护性。
初步调研实现思路方案
核心需求分析
通过对问题的深入剖析,识别出以下几个本质需求:
1. 异常退出的可靠捕获
// 伪代码:我们需要捕获的信号包括// - 正常关闭: kill -15// - 强制杀死: kill -9 (无法捕获)// - Ctrl+C中断// - 系统资源耗尽// - 代码异常导致JVM退出2. 统一资源管理接口
// 期望的使用方式应该是简单一致的resourceManager.register(resourceId,()->{// 关闭逻辑connection.close();fileHandle.release();lock.unlock();});3. 执行顺序和异常隔离
- 关闭操作应该顺序执行,避免并发问题
- 单个资源的关闭异常不应影响其他资源
- 需要有完善的日志记录和错误处理
4. 与业务代码解耦
// 不好的做法:关闭逻辑散落在各个业务类中publicclassDingTalkClient{publicvoidclose(){// 关闭逻辑}}// 好的做法:统一注册,集中管理技术方案对比
基于核心需求,评估了多种技术方案:
方案一:手动关闭管理
// 优点:控制精确// 缺点:容易遗漏,无法处理异常退出publicclassManualShutdown{publicvoidshutdown(){client1.close();client2.close();// 可能遗漏client3}}方案二:Spring框架管理
@ComponentpublicclassSpringManagedBean{@PreDestroypublicvoiddestroy(){// 关闭逻辑}}// 缺点:依赖Spring容器,无法处理容器外异常方案三:JVM Shutdown Hook
Runtime.getRuntime().addShutdownHook(newThread(()->{// 关闭逻辑}));// 优点:能捕获大多数异常退出场景// 缺点:需要自行管理注册和执行方案四:第三方库
- Apache Commons Daemon:功能强大但配置复杂
- Airline:轻量级但功能有限
经过综合评估,决定基于JVM Shutdown Hook构建自定义解决方案,它能够在保证功能完整性的同时,提供最大的灵活性和可控性。
实现思路
初步功能设计
设计一个分层架构的关闭管理系统:
- 注册层:提供简洁的API用于注册关闭操作
- 管理层:维护关闭操作的有序集合,处理并发安全
- 执行层:在适当时机顺序执行所有关闭操作
- 容错层:确保单个操作的失败不影响整体关闭流程
关键设计决策
1. 单例模式确保全局唯一
// 确保整个JVM中只有一个关闭管理器实例publicclassShutdownManager{privatestaticfinalShutdownManagerINSTANCE=newShutdownManager();}2. 线程安全的数据结构
// 使用CopyOnWriteArrayList保证读写线程安全privatefinalList<Runnable>shutdownHooks=newCopyOnWriteArrayList<>();3. 幂等性设计
// 防止重复执行关闭操作privatevolatilebooleanisShuttingDown=false;4. 异常隔离机制
// 单个关闭操作的异常不应影响其他操作try{hook.run();}catch(Exceptione){log.error("Error executing shutdown hook",e);// 继续执行下一个hook}实现步骤与代码
第一步:核心关闭管理器实现
packagecom.dtstack.knowledge.ai.server.manager;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.CopyOnWriteArrayList;importjava.util.List;/** * 统一关闭管理器 * * <p>该管理器负责在JVM关闭时执行所有注册的关闭操作,确保资源被正确释放。 * 支持按key管理关闭操作,提供注册和取消注册功能。</p> * @author changlu * @since 2025-10-24 * * 示范用例: // 注册 ShutdownManager.getInstance().registerShutdownHook(key, () -> { try { ... } catch (Exception e) { log.error("Error executing shutdown hook for DingTalk client, aid: {}", aid, e); } }); // 解绑 ShutdownManager.getInstance().unregisterShutdownHook(key); * */publicclassShutdownManager{privatestaticfinalLoggerlog=LoggerFactory.getLogger(ShutdownManager.class);/** * 单例实例,采用饿汉式实现确保线程安全 */privatestaticfinalShutdownManagerINSTANCE=newShutdownManager();/** * 关闭操作映射表,使用ConcurrentHashMap保证线程安全 * key: 资源标识, value: 关闭操作 */privatefinalMap<String,Runnable>shutdownHookMap=newConcurrentHashMap<>();/** * 关闭状态标志,volatile确保多线程环境下的可见性 */privatevolatilebooleanisShuttingDown=false;/** * 私有构造函数,注册JVM关闭钩子 */privateShutdownManager(){registerJvmShutdownHook();}/** * 注册JVM关闭钩子 */privatevoidregisterJvmShutdownHook(){Runtime.getRuntime().addShutdownHook(newThread(()->{log.info("JVM shutdown hook triggered, preparing to execute {} shutdown operations",shutdownHookMap.size());executeShutdown();}));log.debug("JVM shutdown hook registered successfully");}/** * 获取单例实例 * * @return ShutdownManager单例实例 */publicstaticShutdownManagergetInstance(){returnINSTANCE;}/** * 注册关闭操作 * * <p>使用指定的key注册关闭操作,相同的key会覆盖之前的操作</p> * * @param key 资源唯一标识 * @param closeAction 关闭操作 */publicvoidregisterShutdownHook(Stringkey,RunnablecloseAction){if(isShuttingDown){log.warn("Shutdown process has started, new registration will be ignored, key: {}",key);return;}if(key==null||key.trim().isEmpty()){log.warn("Attempt to register shutdown hook with null or empty key, operation ignored");return;}if(closeAction==null){log.warn("Attempt to register null shutdown hook for key: {}, operation ignored",key);return;}shutdownHookMap.put(key,closeAction);log.debug("Shutdown hook registered successfully, key: {}, current total: {}",key,shutdownHookMap.size());}/** * 取消注册关闭操作 * * <p>移除指定key对应的关闭操作,适用于资源主动释放的场景</p> * * @param key 要移除的资源标识 * @return 如果成功移除返回true,如果key不存在返回false */publicbooleanunregisterShutdownHook(Stringkey){if(key==null){log.warn("Attempt to unregister shutdown hook with null key");returnfalse;}Runnableremoved=shutdownHookMap.remove(key);if(removed!=null){log.debug("Shutdown hook unregistered successfully, key: {}, current total: {}",key,shutdownHookMap.size());returntrue;}else{log.debug("Shutdown hook not found for unregister, key: {}",key);returnfalse;}}/** * 检查指定key的关闭操作是否已注册 * * @param key 资源标识 * @return 如果已注册返回true,否则返回false */publicbooleanisRegistered(Stringkey){returnshutdownHookMap.containsKey(key);}/** * 执行所有关闭操作 */publicvoidexecuteShutdown(){// 双重检查锁定,防止重复执行if(isShuttingDown){return;}synchronized(this){if(isShuttingDown){return;}isShuttingDown=true;}log.info("Starting shutdown process, total operations to execute: {}",shutdownHookMap.size());longstartTime=System.currentTimeMillis();intsuccessCount=0;intfailureCount=0;// 顺序执行所有关闭操作for(Map.Entry<String,Runnable>entry:shutdownHookMap.entrySet()){Stringkey=entry.getKey();Runnablehook=entry.getValue();try{log.debug("Executing shutdown hook for key: {}",key);hook.run();successCount++;log.debug("Shutdown hook executed successfully, key: {}",key);}catch(Exceptione){failureCount++;log.error("Error executing shutdown hook, key: {}",key,e);}}// 清空已执行的关闭操作shutdownHookMap.clear();longduration=System.currentTimeMillis()-startTime;log.info("Shutdown process completed in {}ms, success: {}, failure: {}",duration,successCount,failureCount);}/** * 手动触发关闭流程 */publicvoidshutdown(){log.info("Manual shutdown triggered");executeShutdown();}/** * 获取当前注册的关闭操作数量 */publicintgetHookCount(){returnshutdownHookMap.size();}/** * 检查是否正在关闭过程中 */publicbooleanisShuttingDown(){returnisShuttingDown;}/** * 获取所有已注册的key * * @return 已注册的key集合 */publicjava.util.Set<String>getRegisteredKeys(){returnjava.util.Collections.unmodifiableSet(shutdownHookMap.keySet());}}第二步:集成到钉钉客户端管理器(注册关闭)
publicstaticvoidbindDingdingBot(Stringaid,DingdingBotConfigdingdingBotConfig,AiMessageProcessoraiMessageProcessor){...// 创建钉钉流式客户端OpenDingTalkClientopenDingTalkClient=OpenDingTalkStreamClientBuilder.custom().credential(newAuthClientCredential(appKey,appSecret)).registerCallbackListener(DingTalkStreamTopics.BOT_MESSAGE_TOPIC,newChatBotCallbackListener(newRobotPrivateMessageService(accessTokenService,robotCode,appKey,appSecret),aiMessageProcessor)).build();try{// 启动客户端连接openDingTalkClient.start();openDingdingClientMap.put(aid,openDingTalkClient);// 注册关闭钩子registerShutdownHook(aid);log.info("DingTalk bot bound successfully, aid: {}, current client count: {}",aid,openDingdingClientMap.size());}catch(Exceptione){log.error("Failed to bind DingTalk bot, aid: {}, appKey: {}",aid,appKey,e);thrownewRuntimeException("Bind DingTalk bot failed",e);}}/** * 注册关闭钩子 * @param aid 应用标识 */privatevoidregisterShutdownHook(Stringaid){ShutdownManager.getInstance().registerShutdownHook(aid,()->{try{log.info("Executing shutdown hook for DingTalk client, aid: {}",aid);booleansuccess=OpenDingTalkClientManager.closeClient(aid);if(success){log.info("DingTalk client closed successfully in shutdown hook, aid: {}",aid);}else{log.warn("DingTalk client not found or already closed, aid: {}",aid);}}catch(Exceptione){log.error("Error executing shutdown hook for DingTalk client, aid: {}",aid,e);}});log.debug("Shutdown hook registered for DingTalk client, aid: {}",aid);}/** * 取消注册关闭钩子 * @param aid 应用标识 */privatevoidunregisterShutdownHook(Stringaid){booleanunregistered=ShutdownManager.getInstance().unregisterShutdownHook(aid);if(unregistered){log.debug("Shutdown hook unregistered successfully, aid: {}",aid);}else{log.debug("Shutdown hook not found for unregister, aid: {}",aid);}}总结说明
该方案的核心在于将资源管理的责任从分散的业务代码中集中到统一的管理器中,通过JVM Shutdown Hook机制为各种异常退出场景提供了安全网。在实际生产环境中,有效避免了连接泄漏和资源浪费,提升了系统的整体稳定性和可维护性。
资料获取
大家点赞、收藏、关注、评论啦~
精彩专栏推荐订阅:在下方专栏👇🏻
- 长路-文章目录汇总(算法、后端Java、前端、运维技术导航):博主所有博客导航索引汇总
- 开源项目Studio-Vue—校园工作室管理系统(含前后台,SpringBoot+Vue):博主个人独立项目,包含详细部署上线视频,已开源
- 学习与生活-专栏:可以了解博主的学习历程
- 算法专栏:算法收录
更多博客与资料可查看👇🏻获取联系方式👇🏻,🍅文末获取开发资源及更多资源博客获取🍅