Merge pull request #1820, improve graceful shutdown.

diff --git a/dubbo-bootstrap/src/main/java/org/apache/dubbo/bootstrap/DubboBootstrap.java b/dubbo-bootstrap/src/main/java/org/apache/dubbo/bootstrap/DubboBootstrap.java
index 8694e48..37ec8a3 100644
--- a/dubbo-bootstrap/src/main/java/org/apache/dubbo/bootstrap/DubboBootstrap.java
+++ b/dubbo-bootstrap/src/main/java/org/apache/dubbo/bootstrap/DubboBootstrap.java
@@ -16,16 +16,11 @@
  */
 package org.apache.dubbo.bootstrap;
 
-import com.alibaba.dubbo.common.extension.ExtensionLoader;
-import com.alibaba.dubbo.common.logger.Logger;
-import com.alibaba.dubbo.common.logger.LoggerFactory;
+import com.alibaba.dubbo.config.DubboShutdownHook;
 import com.alibaba.dubbo.config.ServiceConfig;
-import com.alibaba.dubbo.registry.support.AbstractRegistryFactory;
-import com.alibaba.dubbo.rpc.Protocol;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * A bootstrap class to easily start and stop Dubbo via programmatic API.
@@ -33,35 +28,33 @@
  */
 public class DubboBootstrap {
 
-    private static final Logger logger = LoggerFactory.getLogger(DubboBootstrap.class);
-
     /**
      * The list of ServiceConfig
      */
     private List<ServiceConfig> serviceConfigList;
 
     /**
-     * Has it already been destroyed or not?
+     * Whether register the shutdown hook during start?
      */
-    private final AtomicBoolean destroyed;
+    private final boolean registerShutdownHookOnStart;
 
     /**
      * The shutdown hook used when Dubbo is running under embedded environment
      */
-    private Thread shutdownHook;
+    private DubboShutdownHook shutdownHook;
 
     public DubboBootstrap() {
+        this(true, DubboShutdownHook.getDubboShutdownHook());
+    }
+
+    public DubboBootstrap(boolean registerShutdownHookOnStart) {
+        this(registerShutdownHookOnStart, DubboShutdownHook.getDubboShutdownHook());
+    }
+
+    public DubboBootstrap(boolean registerShutdownHookOnStart, DubboShutdownHook shutdownHook) {
         this.serviceConfigList = new ArrayList<ServiceConfig>();
-        this.destroyed = new AtomicBoolean(false);
-        this.shutdownHook = new Thread(new Runnable() {
-            @Override
-            public void run() {
-                if (logger.isInfoEnabled()) {
-                    logger.info("Run shutdown hook now.");
-                }
-                destroy();
-            }
-        }, "DubboShutdownHook");
+        this.shutdownHook = shutdownHook;
+        this.registerShutdownHookOnStart = registerShutdownHookOnStart;
     }
 
     /**
@@ -69,13 +62,19 @@
      * @param serviceConfig the service
      * @return the bootstrap instance
      */
-    public DubboBootstrap regsiterServiceConfig(ServiceConfig serviceConfig) {
+    public DubboBootstrap registerServiceConfig(ServiceConfig serviceConfig) {
         serviceConfigList.add(serviceConfig);
         return this;
     }
 
     public void start() {
-        registerShutdownHook();
+        if (registerShutdownHookOnStart) {
+            registerShutdownHook();
+        } else {
+            // DubboShutdown hook has been registered in AbstractConfig,
+            // we need to remove it explicitly
+            removeShutdownHook();
+        }
         for (ServiceConfig serviceConfig: serviceConfigList) {
             serviceConfig.export();
         }
@@ -85,8 +84,10 @@
         for (ServiceConfig serviceConfig: serviceConfigList) {
             serviceConfig.unexport();
         }
-        destroy();
-        removeShutdownHook();
+        shutdownHook.destroyAll();
+        if (registerShutdownHookOnStart) {
+            removeShutdownHook();
+        }
     }
 
     /**
@@ -107,27 +108,4 @@
             // ignore - VM is already shutting down
         }
     }
-
-    /**
-     * Destroy all the resources, including registries and protocols.
-     */
-    private void destroy() {
-        if (!destroyed.compareAndSet(false, true)) {
-            return;
-        }
-        // destroy all the registries
-        AbstractRegistryFactory.destroyAll();
-        // destroy all the protocols
-        ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
-        for (String protocolName : loader.getLoadedExtensions()) {
-            try {
-                Protocol protocol = loader.getLoadedExtension(protocolName);
-                if (protocol != null) {
-                    protocol.destroy();
-                }
-            } catch (Throwable t) {
-                logger.warn(t.getMessage(), t);
-            }
-        }
-    }
 }
diff --git a/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/AbstractConfig.java b/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/AbstractConfig.java
index 84ad29e..782215d 100644
--- a/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/AbstractConfig.java
+++ b/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/AbstractConfig.java
@@ -71,6 +71,9 @@
         legacyProperties.put("dubbo.consumer.retries", "dubbo.service.max.retry.providers");

         legacyProperties.put("dubbo.consumer.check", "dubbo.service.allow.no.provider");

         legacyProperties.put("dubbo.service.url", "dubbo.service.address");

+

+        // this is only for compatibility

+        Runtime.getRuntime().addShutdownHook(DubboShutdownHook.getDubboShutdownHook());

     }

 

     protected String id;

diff --git a/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/DubboShutdownHook.java b/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/DubboShutdownHook.java
new file mode 100644
index 0000000..348c5e6
--- /dev/null
+++ b/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/DubboShutdownHook.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.dubbo.config;
+
+import com.alibaba.dubbo.common.extension.ExtensionLoader;
+import com.alibaba.dubbo.common.logger.Logger;
+import com.alibaba.dubbo.common.logger.LoggerFactory;
+import com.alibaba.dubbo.registry.support.AbstractRegistryFactory;
+import com.alibaba.dubbo.rpc.Protocol;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * The shutdown hook thread to do the clean up stuff.
+ * This is a singleton in order to ensure there is only one shutdown hook registered.
+ * Because {@link ApplicationShutdownHooks} use {@link java.util.IdentityHashMap}
+ * to store the shutdown hooks.
+ */
+public class DubboShutdownHook extends Thread {
+
+    private static final Logger logger = LoggerFactory.getLogger(DubboShutdownHook.class);
+
+    private static final DubboShutdownHook dubboShutdownHook = new DubboShutdownHook("DubboShutdownHook");
+
+    public static DubboShutdownHook getDubboShutdownHook() {
+        return dubboShutdownHook;
+    }
+
+    /**
+     * Has it already been destroyed or not?
+     */
+    private final AtomicBoolean destroyed;
+
+    private DubboShutdownHook(String name) {
+        super(name);
+        this.destroyed = new AtomicBoolean(false);
+    }
+
+    @Override
+    public void run() {
+        if (logger.isInfoEnabled()) {
+            logger.info("Run shutdown hook now.");
+        }
+        destroyAll();
+    }
+
+    /**
+     * Destroy all the resources, including registries and protocols.
+     */
+    public void destroyAll() {
+        if (!destroyed.compareAndSet(false, true)) {
+            return;
+        }
+        // destroy all the registries
+        AbstractRegistryFactory.destroyAll();
+        // destroy all the protocols
+        ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
+        for (String protocolName : loader.getLoadedExtensions()) {
+            try {
+                Protocol protocol = loader.getLoadedExtension(protocolName);
+                if (protocol != null) {
+                    protocol.destroy();
+                }
+            } catch (Throwable t) {
+                logger.warn(t.getMessage(), t);
+            }
+        }
+    }
+
+
+}
diff --git a/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/ProtocolConfig.java b/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/ProtocolConfig.java
index f82fe04..b901bed 100644
--- a/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/ProtocolConfig.java
+++ b/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/ProtocolConfig.java
@@ -460,4 +460,12 @@
         }

     }

 

+    /**

+     * Just for compatibility.

+     * It should be deleted in the next major version, say 2.7.x.

+     */

+    @Deprecated

+    public static void destroyAll() {

+        DubboShutdownHook.getDubboShutdownHook().destroyAll();

+    }

 }
\ No newline at end of file
diff --git a/dubbo-config/dubbo-config-spring/src/main/java/com/alibaba/dubbo/config/spring/initializer/DubboApplicationListener.java b/dubbo-config/dubbo-config-spring/src/main/java/com/alibaba/dubbo/config/spring/initializer/DubboApplicationListener.java
index 43ee49d..c3d7298 100644
--- a/dubbo-config/dubbo-config-spring/src/main/java/com/alibaba/dubbo/config/spring/initializer/DubboApplicationListener.java
+++ b/dubbo-config/dubbo-config-spring/src/main/java/com/alibaba/dubbo/config/spring/initializer/DubboApplicationListener.java
@@ -31,7 +31,11 @@
     private DubboBootstrap dubboBootstrap;
 
     public DubboApplicationListener() {
-        dubboBootstrap = new DubboBootstrap();
+        dubboBootstrap = new DubboBootstrap(false);
+    }
+
+    public DubboApplicationListener(DubboBootstrap dubboBootstrap) {
+        this.dubboBootstrap = dubboBootstrap;
     }
 
     @Override
diff --git a/dubbo-config/dubbo-config-spring/src/test/java/com/alibaba/dubbo/config/spring/initializer/DubboApplicationListenerTest.java b/dubbo-config/dubbo-config-spring/src/test/java/com/alibaba/dubbo/config/spring/initializer/DubboApplicationListenerTest.java
new file mode 100644
index 0000000..fdfe87a
--- /dev/null
+++ b/dubbo-config/dubbo-config-spring/src/test/java/com/alibaba/dubbo/config/spring/initializer/DubboApplicationListenerTest.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.dubbo.config.spring.initializer;
+
+import com.alibaba.dubbo.config.DubboShutdownHook;
+import org.apache.dubbo.bootstrap.DubboBootstrap;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+
+public class DubboApplicationListenerTest {
+
+    @Test
+    public void testTwoShutdownHook() {
+        DubboShutdownHook spyHook = Mockito.spy(DubboShutdownHook.getDubboShutdownHook());
+        ClassPathXmlApplicationContext applicationContext = getApplicationContext(spyHook, true);
+        applicationContext.refresh();
+        applicationContext.close();
+        // shutdown hook can't be verified, because it will executed after main thread has finished.
+        // so we can only verify it by manually run it.
+        try {
+            spyHook.start();
+            spyHook.join();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        Mockito.verify(spyHook, Mockito.times(2)).destroyAll();
+    }
+
+    @Test
+    public void testOneShutdownHook() {
+        DubboShutdownHook spyHook = Mockito.spy(DubboShutdownHook.getDubboShutdownHook());
+        ClassPathXmlApplicationContext applicationContext = getApplicationContext(spyHook, false);
+        applicationContext.refresh();
+        applicationContext.close();
+        Mockito.verify(spyHook, Mockito.times(1)).destroyAll();
+    }
+
+    private ClassPathXmlApplicationContext getApplicationContext(DubboShutdownHook hook, boolean registerHook) {
+        DubboBootstrap bootstrap = new DubboBootstrap(registerHook, hook);
+        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext();
+        applicationContext.addApplicationListener(new DubboApplicationListener(bootstrap));
+        return applicationContext;
+    }
+}
diff --git a/dubbo-container/dubbo-container-api/src/main/java/com/alibaba/dubbo/container/Main.java b/dubbo-container/dubbo-container-api/src/main/java/com/alibaba/dubbo/container/Main.java
index 266ddc1..36eb9ae 100644
--- a/dubbo-container/dubbo-container-api/src/main/java/com/alibaba/dubbo/container/Main.java
+++ b/dubbo-container/dubbo-container-api/src/main/java/com/alibaba/dubbo/container/Main.java
@@ -61,7 +61,7 @@
             logger.info("Use container type(" + Arrays.toString(args) + ") to run dubbo serivce.");

 

             if ("true".equals(System.getProperty(SHUTDOWN_HOOK_KEY))) {

-                Runtime.getRuntime().addShutdownHook(new Thread() {

+                Runtime.getRuntime().addShutdownHook(new Thread("dubbo-container-shutdown-hook") {

                     @Override

                     public void run() {

                         for (Container container : containers) {

diff --git a/dubbo-container/dubbo-container-spring/pom.xml b/dubbo-container/dubbo-container-spring/pom.xml
index 8221adf..fc385dc 100644
--- a/dubbo-container/dubbo-container-spring/pom.xml
+++ b/dubbo-container/dubbo-container-spring/pom.xml
@@ -39,5 +39,10 @@
             <groupId>org.springframework</groupId>

             <artifactId>spring-context</artifactId>

         </dependency>

+        <dependency>

+            <groupId>com.alibaba</groupId>

+            <artifactId>dubbo-config-spring</artifactId>

+            <version>${project.parent.version}</version>

+        </dependency>

     </dependencies>

 </project>
\ No newline at end of file
diff --git a/dubbo-container/dubbo-container-spring/src/main/java/com/alibaba/dubbo/container/spring/SpringContainer.java b/dubbo-container/dubbo-container-spring/src/main/java/com/alibaba/dubbo/container/spring/SpringContainer.java
index d21f3a5..d4c03c6 100644
--- a/dubbo-container/dubbo-container-spring/src/main/java/com/alibaba/dubbo/container/spring/SpringContainer.java
+++ b/dubbo-container/dubbo-container-spring/src/main/java/com/alibaba/dubbo/container/spring/SpringContainer.java
@@ -19,6 +19,7 @@
 import com.alibaba.dubbo.common.logger.Logger;

 import com.alibaba.dubbo.common.logger.LoggerFactory;

 import com.alibaba.dubbo.common.utils.ConfigUtils;

+import com.alibaba.dubbo.config.spring.initializer.DubboApplicationListener;

 import com.alibaba.dubbo.container.Container;

 

 import org.springframework.context.support.ClassPathXmlApplicationContext;

@@ -43,7 +44,10 @@
         if (configPath == null || configPath.length() == 0) {

             configPath = DEFAULT_SPRING_CONFIG;

         }

-        context = new ClassPathXmlApplicationContext(configPath.split("[,\\s]+"));

+        context = new ClassPathXmlApplicationContext(configPath.split("[,\\s]+"), false);

+        context.addApplicationListener(new DubboApplicationListener());

+        context.registerShutdownHook();

+        context.refresh();

         context.start();

     }