spring注解之@PreDestroy的实现原理

一、引言

在开发中我们如果要在关闭spring容器后释放一些资源,通常的做法有如下几种:
1.在方法上加上@PreDestroy注解
2.实现DisposableBean接口,实现其destroy方法

比较常用的是第一种实现,因为其足够简便。下面就来分析一下它的实现原理,看它是在哪一个环节被触发的。

二、开始分析

我们先移步到CommonAnnotationBeanPostProcessor这个类中,看如下一段代码:

public CommonAnnotationBeanPostProcessor() {
        setOrder(Ordered.LOWEST_PRECEDENCE - 3);
        setInitAnnotationType(PostConstruct.class);
        setDestroyAnnotationType(PreDestroy.class);
        ignoreResourceType("javax.xml.ws.WebServiceContext");
}

可见在CommonAnnotationBeanPostProcessor的无参构造函数中设置了一个默认的DestroyAnnotationType,即PreDestroy.在它的上方我们也看到了经常使用的PostConstruct,其实原理是一致的,只是spring帮我们控制了调用的顺序而已。
而CommonAnnotationBeanPostProcessor是实现了BeanFactoryAwareBeanFactoryAware的,也就是spring在启动的时候会找到这些扩展接口的子类型进行实例化。从而实现一些个性化的功能,例如:注解、配置注入、初始化、关闭操作等等。由于本文的重点在PreDestroy,所以不会过多的讲spring的加载过程。
接下来深入看看在哪里使用到了PreDestroy:

{
            if (destroyAnnotationType != null) {
                        if (method.getAnnotation(destroyAnnotationType) != null) {
                            currDestroyMethods.add(new LifecycleElement(method));
                            if (debug) {
                                logger.debug("Found destroy method on class [" + clazz.getName() + "]: " + method);
                            }
                        }
                    }
                }
            });

            initMethods.addAll(0, currInitMethods);
            destroyMethods.addAll(currDestroyMethods);
            targetClass = targetClass.getSuperclass();
        }
        while (targetClass != null && targetClass != Object.class);

        return new LifecycleMetadata(clazz, initMethods, destroyMethods);
    }

看这一段代码,首先判断destroyAnnotationType它是否为空,显然这里不为空,这里指定了PreDestroy,然后再判断这个注解上是否有@PreDestroy注解,如果有,就将该Method包装成一个LifecycleElement添加到一个List中,然后将其添加到了另外一个集合destroyMethods中。接下来看一下哪里在使用这个destroyMethods集合。
移步到InitDestroyAnnotationBeanPostProcessor中,看到如下代码:

public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {
        LifecycleMetadata metadata = findLifecycleMetadata(bean.getClass());
        try {
            metadata.invokeDestroyMethods(bean, beanName);
        }
        catch (InvocationTargetException ex) {
            String msg = "Invocation of destroy method failed on bean with name '" + beanName + "'";
            if (logger.isDebugEnabled()) {
                logger.warn(msg, ex.getTargetException());
            }
            else {
                logger.warn(msg + ": " + ex.getTargetException());
            }
        }
        catch (Throwable ex) {
            logger.error("Failed to invoke destroy method on bean with name '" + beanName + "'", ex);
        }
    }

这个findLifecycleMetadata方法通过调用buildLifecycleMetadata方法最终调用到了最上面的那段代码,该metadata也就持有了所有加了PreDestroy注解的方法列表。接下来就是利用反射invoke目标类即可实现。

public void invokeDestroyMethods(Object target, String beanName) throws Throwable {
            Collection<LifecycleElement> destroyMethodsToUse =
                    (this.checkedDestroyMethods != null ? this.checkedDestroyMethods : this.destroyMethods);
            if (!destroyMethodsToUse.isEmpty()) {
                boolean debug = logger.isDebugEnabled();
                for (LifecycleElement element : destroyMethodsToUse) {
                    if (debug) {
                        logger.debug("Invoking destroy method on bean '" + beanName + "': " + element.getMethod());
                    }
                    element.invoke(target);
                }
            }
        }
三、再深入一点

如果想要优雅的退出,@PreDestroy能否满足要求呢?因为我们常用的做法就是注册一个钩子程序,当我们kill进程时(非 kill -9).jvm会收到操作系统一个终端,来做一些资源收尾的操作。

    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                logger.info("shutdown hook run.");
                try {
                   
                } catch (Exception e) {
                }
            }
        }));

如果spring想要优雅退出,必要要借助于hook,不然是没法影响中断的。接下来看一眼spring是怎么实现的。其实在AbstractApplicationContext中有这样一个方法:

 public void registerShutdownHook() {
        if (this.shutdownHook == null) {
            this.shutdownHook = new Thread() {
                public void run() {
                    synchronized(AbstractApplicationContext.this.startupShutdownMonitor) {
                        AbstractApplicationContext.this.doClose();
                    }
                }
            };
            Runtime.getRuntime().addShutdownHook(this.shutdownHook);
        }

    }

springboot会通过启动main函数时调用refreshContext来注册钩子程序:

private void refreshContext(ConfigurableApplicationContext context) {
        refresh(context);
        if (this.registerShutdownHook) {
            try {
                context.registerShutdownHook();
            }
            catch (AccessControlException ex) {
                // Not allowed in some environments.
            }
        }
    }

所以要想实现优雅关闭资源,使用@PreDestroy注解即可。

四、钩子程序的实现原理
class ApplicationShutdownHooks {
    /* The set of registered hooks */
    private static IdentityHashMap<Thread, Thread> hooks;
    static {
        try {
            Shutdown.add(1 /* shutdown hook invocation order */,
                false /* not registered if shutdown in progress */,
                new Runnable() {
                    public void run() {
                        //被sequence方法调用
                        runHooks();
                    }
                }
            );
            hooks = new IdentityHashMap<>();
        } catch (IllegalStateException e) {
            // application shutdown hooks cannot be added if
            // shutdown is in progress.
            hooks = null;
        }
    }
}

看一下runHooks()方法:

static void runHooks() {
        Collection<Thread> threads;
        synchronized(ApplicationShutdownHooks.class) {
            threads = hooks.keySet();
            hooks = null;
        }

        for (Thread hook : threads) {
            hook.start();
        }
        for (Thread hook : threads) {
            while (true) {
                try {
                    hook.join();
                    break;
                } catch (InterruptedException ignored) {
                }
            }
        }
    }
}

其实就是把hooks里的线程全部拿到然后启动,并且等待执行结束。添加hook即把线程存放在IdentityHashMap中。当调用Shutdown.add()的时候其实是将该线程存放在Shutdown的成员变量数组中。

static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
        synchronized (lock) {
            if (hooks[slot] != null)
                throw new InternalError("Shutdown hook at slot " + slot + " already registered");

            if (!registerShutdownInProgress) {
                if (state > RUNNING)
                    throw new IllegalStateException("Shutdown in progress");
            } else {
                if (state > HOOKS || (state == HOOKS && slot <= currentRunningHook))
                    throw new IllegalStateException("Shutdown in progress");
            }

            hooks[slot] = hook;
        }
    }

接下来看一下Shutdown的sequence方法:

 private static void sequence() {
        synchronized (lock) {
            /* Guard against the possibility of a daemon thread invoking exit
             * after DestroyJavaVM initiates the shutdown sequence
             */
            if (state != HOOKS) return;
        }
        runHooks();
        boolean rfoe;
        synchronized (lock) {
            state = FINALIZERS;
            rfoe = runFinalizersOnExit;
        }
        if (rfoe) runAllFinalizers();
    }

它最终会调用runHooks方法,然后启动在上面静态块中添加的Runnable线程,最终启动所有已注册的钩子程序。

 private static void runHooks() {
        for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
            try {
                Runnable hook;
                synchronized (lock) {
                    currentRunningHook = i;
                    hook = hooks[i];
                }
                //该run方法最终执行静态块中的runHooks()方法。
                if (hook != null) hook.run();
            } catch(Throwable t) {
                if (t instanceof ThreadDeath) {
                    ThreadDeath td = (ThreadDeath)t;
                    throw td;
                }
            }
        }
    }

接下来找到Terminator中:

class Terminator {

    private static SignalHandler handler = null;

    /* Invocations of setup and teardown are already synchronized
     * on the shutdown lock, so no further synchronization is needed here
     */

    static void setup() {
        if (handler != null) return;
        SignalHandler sh = new SignalHandler() {
            public void handle(Signal sig) {
                Shutdown.exit(sig.getNumber() + 0200);
            }
        };
        handler = sh;

        // When -Xrs is specified the user is responsible for
        // ensuring that shutdown hooks are run by calling
        // System.exit()

       //这就是响应中断的方法
        try {
            Signal.handle(new Signal("INT"), sh);
        } catch (IllegalArgumentException e) {
        }
        try {
            //TERM对应15,即kill -9
            Signal.handle(new Signal("TERM"), sh);
        } catch (IllegalArgumentException e) {
        }
    }

其中Signal.handle()就是处理中断信号的方法,最终会通过sh回调Shutdown.exit()方法。最终触发sequence()方法被调用,然后调用所有注册的钩子程序。

五、最后

初步介绍了一下PreDestroy的原理和钩子程序的一些细节,由于标题只是讲PreDestroy,所以其中省略了不少spring的实现细节。谢谢大家~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容

  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,796评论 6 342
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,644评论 18 139
  • 1.1 spring IoC容器和beans的简介 Spring 框架的最核心基础的功能是IoC(控制反转)容器,...
    simoscode阅读 6,708评论 2 22
  • 1.1 Spring IoC容器和bean简介 本章介绍了Spring Framework实现的控制反转(IoC)...
    起名真是难阅读 2,579评论 0 8
  • 这样的自己什么都做不了,缺少正常的感官思维, 换个自我,礼貌与稳重兼备的自己 为什么总再见,因为你总在贱!为什么会...
    叶静生阅读 294评论 0 0