基于Java实现优雅关闭的规范化方案设计与实现

文章目录

前言

博主介绍:✌目前全网粉丝4W+,csdn博客专家、Java领域优质创作者,博客之星、阿里云平台优质作者、专注于Java后端技术领域。

涵盖技术内容:Java后端、大数据、算法、分布式微服务、中间件、前端、运维等。

博主所有博客文件目录索引:博客目录索引(持续更新)

CSDN搜索:长路

视频平台:b站-Coder长路

背景

在企业级应用开发中,服务的稳定性和可靠性是至关重要的考量因素。在实际生产环境中,应用经常会遇到各种异常情况:内存溢出导致JVM崩溃、系统资源耗尽触发强制重启、部署平台主动销毁容器实例等。这些异常退出场景往往使得应用无法正常执行资源清理操作,进而引发一系列严重问题:

  • 数据库连接池连接未释放,导致连接泄漏
  • 网络长连接(如WebSocket、钉钉Stream连接)未正常关闭
  • 文件句柄未释放,造成资源浪费
  • 消息队列消费者未正确取消订阅
  • 分布式锁未释放,引发死锁问题

近期在开发钉钉机器人集成服务时,我们就遇到了这样一个具体问题:当应用异常重启时,如何确保所有已建立的钉钉Stream模式客户端连接能够被正确关闭?这不仅关系到系统资源的有效管理,更直接影响整个服务的稳定性和可维护性。

初步调研实现思路方案

核心需求分析

通过对问题的深入剖析,识别出以下几个本质需求:

1. 异常退出的可靠捕获

java 复制代码
// 伪代码:我们需要捕获的信号包括
// - 正常关闭: kill -15
// - 强制杀死: kill -9 (无法捕获)
// - Ctrl+C中断
// - 系统资源耗尽
// - 代码异常导致JVM退出

2. 统一资源管理接口

java 复制代码
// 期望的使用方式应该是简单一致的
resourceManager.register(resourceId, () -> {
    // 关闭逻辑
    connection.close();
    fileHandle.release();
    lock.unlock();
});

3. 执行顺序和异常隔离

  • 关闭操作应该顺序执行,避免并发问题
  • 单个资源的关闭异常不应影响其他资源
  • 需要有完善的日志记录和错误处理

4. 与业务代码解耦

java 复制代码
// 不好的做法:关闭逻辑散落在各个业务类中
public class DingTalkClient {
    public void close() {
        // 关闭逻辑
    }
}

// 好的做法:统一注册,集中管理

技术方案对比

基于核心需求,评估了多种技术方案:

方案一:手动关闭管理

java 复制代码
// 优点:控制精确
// 缺点:容易遗漏,无法处理异常退出
public class ManualShutdown {
    public void shutdown() {
        client1.close();
        client2.close();
        // 可能遗漏client3
    }
}

方案二:Spring框架管理

java 复制代码
@Component
public class SpringManagedBean {
    @PreDestroy
    public void destroy() {
        // 关闭逻辑
    }
}
// 缺点:依赖Spring容器,无法处理容器外异常

方案三:JVM Shutdown Hook

java 复制代码
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    // 关闭逻辑
}));
// 优点:能捕获大多数异常退出场景
// 缺点:需要自行管理注册和执行

方案四:第三方库

  • Apache Commons Daemon:功能强大但配置复杂
  • Airline:轻量级但功能有限

经过综合评估,决定基于JVM Shutdown Hook构建自定义解决方案,它能够在保证功能完整性的同时,提供最大的灵活性和可控性。

实现思路

初步功能设计

设计一个分层架构的关闭管理系统:

  1. 注册层:提供简洁的API用于注册关闭操作
  2. 管理层:维护关闭操作的有序集合,处理并发安全
  3. 执行层:在适当时机顺序执行所有关闭操作
  4. 容错层:确保单个操作的失败不影响整体关闭流程

关键设计决策

1. 单例模式确保全局唯一

java 复制代码
// 确保整个JVM中只有一个关闭管理器实例
public class ShutdownManager {
    private static final ShutdownManager INSTANCE = new ShutdownManager();
}

2. 线程安全的数据结构

java 复制代码
// 使用CopyOnWriteArrayList保证读写线程安全
private final List<Runnable> shutdownHooks = new CopyOnWriteArrayList<>();

3. 幂等性设计

java 复制代码
// 防止重复执行关闭操作
private volatile boolean isShuttingDown = false;

4. 异常隔离机制

java 复制代码
// 单个关闭操作的异常不应影响其他操作
try {
    hook.run();
} catch (Exception e) {
    log.error("Error executing shutdown hook", e);
    // 继续执行下一个hook
}

实现步骤与代码

第一步:核心关闭管理器实现

java 复制代码
package com.dtstack.knowledge.ai.server.manager;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.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);
 *
 */
public class ShutdownManager {

    private static final Logger log = LoggerFactory.getLogger(ShutdownManager.class);

    /**
     * 单例实例,采用饿汉式实现确保线程安全
     */
    private static final ShutdownManager INSTANCE = new ShutdownManager();

    /**
     * 关闭操作映射表,使用ConcurrentHashMap保证线程安全
     * key: 资源标识, value: 关闭操作
     */
    private final Map<String, Runnable> shutdownHookMap = new ConcurrentHashMap<>();

    /**
     * 关闭状态标志,volatile确保多线程环境下的可见性
     */
    private volatile boolean isShuttingDown = false;

    /**
     * 私有构造函数,注册JVM关闭钩子
     */
    private ShutdownManager() {
        registerJvmShutdownHook();
    }

    /**
     * 注册JVM关闭钩子
     */
    private void registerJvmShutdownHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            log.info("JVM shutdown hook triggered, preparing to execute {} shutdown operations",
                    shutdownHookMap.size());
            executeShutdown();
        }));
        log.debug("JVM shutdown hook registered successfully");
    }

    /**
     * 获取单例实例
     *
     * @return ShutdownManager单例实例
     */
    public static ShutdownManager getInstance() {
        return INSTANCE;
    }

    /**
     * 注册关闭操作
     *
     * <p>使用指定的key注册关闭操作,相同的key会覆盖之前的操作</p>
     *
     * @param key 资源唯一标识
     * @param closeAction 关闭操作
     */
    public void registerShutdownHook(String key, Runnable closeAction) {
        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
     */
    public boolean unregisterShutdownHook(String key) {
        if (key == null) {
            log.warn("Attempt to unregister shutdown hook with null key");
            return false;
        }

        Runnable removed = shutdownHookMap.remove(key);
        if (removed != null) {
            log.debug("Shutdown hook unregistered successfully, key: {}, current total: {}",
                    key, shutdownHookMap.size());
            return true;
        } else {
            log.debug("Shutdown hook not found for unregister, key: {}", key);
            return false;
        }
    }

    /**
     * 检查指定key的关闭操作是否已注册
     *
     * @param key 资源标识
     * @return 如果已注册返回true,否则返回false
     */
    public boolean isRegistered(String key) {
        return shutdownHookMap.containsKey(key);
    }

    /**
     * 执行所有关闭操作
     */
    public void executeShutdown() {
        // 双重检查锁定,防止重复执行
        if (isShuttingDown) {
            return;
        }

        synchronized (this) {
            if (isShuttingDown) {
                return;
            }
            isShuttingDown = true;
        }

        log.info("Starting shutdown process, total operations to execute: {}",
                shutdownHookMap.size());

        long startTime = System.currentTimeMillis();
        int successCount = 0;
        int failureCount = 0;

        // 顺序执行所有关闭操作
        for (Map.Entry<String, Runnable> entry : shutdownHookMap.entrySet()) {
            String key = entry.getKey();
            Runnable hook = entry.getValue();

            try {
                log.debug("Executing shutdown hook for key: {}", key);
                hook.run();
                successCount++;
                log.debug("Shutdown hook executed successfully, key: {}", key);
            } catch (Exception e) {
                failureCount++;
                log.error("Error executing shutdown hook, key: {}", key, e);
            }
        }

        // 清空已执行的关闭操作
        shutdownHookMap.clear();

        long duration = System.currentTimeMillis() - startTime;
        log.info("Shutdown process completed in {}ms, success: {}, failure: {}",
                duration, successCount, failureCount);
    }

    /**
     * 手动触发关闭流程
     */
    public void shutdown() {
        log.info("Manual shutdown triggered");
        executeShutdown();
    }

    /**
     * 获取当前注册的关闭操作数量
     */
    public int getHookCount() {
        return shutdownHookMap.size();
    }

    /**
     * 检查是否正在关闭过程中
     */
    public boolean isShuttingDown() {
        return isShuttingDown;
    }

    /**
     * 获取所有已注册的key
     *
     * @return 已注册的key集合
     */
    public java.util.Set<String> getRegisteredKeys() {
        return java.util.Collections.unmodifiableSet(shutdownHookMap.keySet());
    }
}

第二步:集成到钉钉客户端管理器(注册关闭)

java 复制代码
public static void bindDingdingBot(String aid, DingdingBotConfig dingdingBotConfig,
                                 AiMessageProcessor aiMessageProcessor) {
    ...
    // 创建钉钉流式客户端
    OpenDingTalkClient openDingTalkClient = OpenDingTalkStreamClientBuilder
            .custom()
            .credential(new AuthClientCredential(appKey, appSecret))
            .registerCallbackListener(DingTalkStreamTopics.BOT_MESSAGE_TOPIC,
                new ChatBotCallbackListener(
                    new RobotPrivateMessageService(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 (Exception e) {
        log.error("Failed to bind DingTalk bot, aid: {}, appKey: {}", aid, appKey, e);
        throw new RuntimeException("Bind DingTalk bot failed", e);
    }
}



/**
 * 注册关闭钩子
 * @param aid 应用标识
 */
private void registerShutdownHook(String aid) {
    ShutdownManager.getInstance().registerShutdownHook(aid, () -> {
        try {
            log.info("Executing shutdown hook for DingTalk client, aid: {}", aid);
            boolean success = 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 (Exception e) {
            log.error("Error executing shutdown hook for DingTalk client, aid: {}", aid, e);
        }
    });
    log.debug("Shutdown hook registered for DingTalk client, aid: {}", aid);
}

/**
 * 取消注册关闭钩子
 * @param aid 应用标识
 */
private void unregisterShutdownHook(String aid) {
    boolean unregistered = 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机制为各种异常退出场景提供了安全网。在实际生产环境中,有效避免了连接泄漏和资源浪费,提升了系统的整体稳定性和可维护性。

资料获取

大家点赞、收藏、关注、评论啦~

精彩专栏推荐订阅:在下方专栏👇🏻

更多博客与资料可查看👇🏻获取联系方式👇🏻,🍅文末获取开发资源及更多资源博客获取🍅

相关推荐
做人不要太理性1 年前
【C++】指针与智慧的邂逅:C++内存管理的诗意
c++·智能指针·raii·资源泄漏