1. The background

At present, the original service registry consul has been changed from Consul to NACOS. Therefore, we hope to implement an elegant restart of the service. When the service is restarted, the service can be offline from NacOS and the consumer service can be notified to update the Ribbon registry to avoid request exceptions. Again, the ribbon’s retry mechanism is used to re-initiate requests, but for elegant downtime we looked at how NacOS stops containers and destroys beans.

2. Three ways to destroy beans

2.1. Annotate PreDestroy

By stopping the service, it was discovered that the way Nacos destroyed the Bean was destroyed by @predestroy.

Nacos called the deregister method to destroy the Bean.

public class NacosServiceRegistry implements ServiceRegistry<Registration> { //... Leave @override public void deregister(Registration) {// The output log is this log.info(" de-registering from Nacos Server now..." ); if (StringUtils.isEmpty(registration.getServiceId())) { log.warn("No dom to de-register for nacos client..." ); return; } NamingService namingService = nacosDiscoveryProperties.namingServiceInstance(); String serviceId = registration.getServiceId(); String group = nacosDiscoveryProperties.getGroup(); try { namingService.deregisterInstance(serviceId, group, registration.getHost(), registration.getPort(), nacosDiscoveryProperties.getClusterName()); } catch (Exception e) { log.error("ERR_NACOS_DEREGISTER, de-register failed... {},", registration.toString(), e); } log.info("De-registration finished."); }}Copy the code

Now look at the callers of deregistry. The callers of this method are actually methods in the SpringCloud that implement container closure and JVM closure, but the final implementation is implemented by Nacos. The source code is as follows (from SpringCloud) :

public abstract class AbstractAutoServiceRegistration<R extends Registration> implements AutoServiceRegistration, ApplicationContextAware, ApplicationListener<WebServerInitializedEvent> { //... // The original call is a method provided by SpringCloud that uses the @predestroy annotation // an annotation provided by the JDK as a way to log out of the JVM, @predestroy public void destroy() {stop(); } public void stop() {if (this.geTrunning ().compareandSet (true, False) &&isEnabled ()) {// Call deregister(); If (shouldRegisterManagement()) {// This method also calls deregistry (); } this.serviceRegistry.close(); }}}Copy the code

SpringCloud provides a ServiceRegistry interface that allows other service registries to register and unregister their own services. It can be seen from the source code that Consul and Nacos implement this interface respectively.

The interface code is as follows:

public interface ServiceRegistry<R extends Registration> {

    void register(R registration);

    void deregister(R registration);

    void close();

    void setStatus(R registration, String status);

    <T> T getStatus(R registration);

}
Copy the code

The implementation code of Nacos is as follows:

public class NacosServiceRegistry implements ServiceRegistry<Registration> { //... @override public void register(Registration) {if (StringUtils.isEmpty(registration.getServiceId())) { log.warn("No service to register for nacos client..." ); return; } String serviceId = registration.getServiceId(); String group = nacosDiscoveryProperties.getGroup(); Instance instance = getNacosInstanceFromRegistration(registration); try { namingService.registerInstance(serviceId, group, instance); log.info("nacos registry, {} {} {}:{} register finished", group, serviceId, instance.getIp(), instance.getPort()); } catch (Exception e) { log.error("nacos registry, {} register failed... {},", serviceId, registration.toString(), e); // rethrow a RuntimeException if the registration is failed. // issue : <https://github.com/alibaba/spring-cloud-alibaba/issues/1132> rethrowRuntimeException(e); }} // register service Nacos implementation logic @override public void deregister(Registration) {log.info(" de-registering from Nacos Server now..." ); if (StringUtils.isEmpty(registration.getServiceId())) { log.warn("No dom to de-register for nacos client..." ); return; } NamingService namingService = nacosDiscoveryProperties.namingServiceInstance(); String serviceId = registration.getServiceId(); String group = nacosDiscoveryProperties.getGroup(); try { namingService.deregisterInstance(serviceId, group, registration.getHost(), registration.getPort(), nacosDiscoveryProperties.getClusterName()); } catch (Exception e) { log.error("ERR_NACOS_DEREGISTER, de-register failed... {},", registration.toString(), e); } log.info("De-registration finished."); }}Copy the code

So this is a way for Nacos to close the Bean container and unregister the Bean, which is the simplest way to do it, directly with the @predestroy annotation.

Now write a test class to test the annotation as follows:

@component public class DestroyBean {@predestroy public void DestroyBean () {system.out.println (" DestroyBean! by @PreDestroy"+System.currentTimeMillis()); }}Copy the code

Write a random test class to start:

@RunWith(SpringRunner.class) @Slf4j @SpringBootTest(classes = TestApplication.class) public class DestroyTest implements  ApplicationContextAware { private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public ApplicationContext getApplicationContext() { return applicationContext; } @Before public void before() throws Exception { } @After public void after() throws Exception { } @Rule public TestWatcher watchman = new TestWatcher() { @Override protected void starting(Description description) { super.starting(description); The info (" < < < < < < < start executing test: {} ", the description, getMethodName ()); } @Override protected void finished(Description description) { super.finished(description); The info (" > {} has been completed > > > > > > ", the description, getMethodName ()); } @Override protected void failed(Throwable e, Description description) { super.failed(e, description); Log.error ("----- fails, exception :{}", LLDB etMessage()); }}; @Test public void testHandleTransport() throws Exception { //TODO: Test goes here... applicationContext.publishEvent(new CustomEvent(applicationContext)); }}Copy the code

When you run the method of the test class, you can see that the PreDestroy annotated method is finally executed when the service is shut down, so you know that once you add the annotation, you can do Bean destruction in it.

2.2. Event subscription

You can create an implementation class that implements the ApplicationListener interface and override the onApplicationEvent method. The implementation is as follows:

@Component public class ContextClosedEventListener implements ApplicationListener<ContextClosedEvent> { @SneakyThrows @override public void onApplicationEvent(ContextClosedEvent event) {system.out.println (" ContextClosedEvent event "); "+ this.getClass().getName()+" "+ system.currentTimemillis ()); Thread.sleep(10000); }}Copy the code

When you unregister a Bean in this way, the method is called when the container is closed, so you can implement the logic of unregistering the Bean yourself in this method. So how does Spring provide event logout Bean functionality? Spring provides a way to close a container called ShutdownHook, and the event subscription method is closed by calling ShutdownHook, which opens a thread for execution. The source code is as follows:

public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext Override public void registerShutdownHook() {if (this.shutdownHook == null) {// No shutdownhook Registered yet. // Initialize a Thread by doClose this.shutdownhook = new Thread() {@override public void run() {synchronized (startupShutdownMonitor) { doClose(); }}}; Runtime.getRuntime().addShutdownHook(this.shutdownHook); } } protected void doClose() { // Check whether an actual close attempt is necessary... if (this.active.get() && this.closed.compareAndSet(false, true)) { if (logger.isDebugEnabled()) { logger.debug("Closing " + this); } LiveBeansView.unregisterApplicationContext(this); Try {// Publish ClosedEvent event publishEvent(new ContextClosedEvent(this)); } / /... Omit}}Copy the code

When released ContextClosedEvent events, will eventually call to SimpleApplicationEventMulticaster implementation class, specific logic is as follows:

Public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {/ / enter colsedEvent the caller @Override public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) { ResolvableType type = (eventType ! = null ? eventType : resolveDefaultEventType(event)); for (final ApplicationListener<? > listener : getApplicationListeners(event, type)) { Executor executor = getTaskExecutor(); if (executor ! = null) { executor.execute(() -> invokeListener(listener, event)); } else { invokeListener(listener, event); } } } // protected void invokeListener(ApplicationListener<? > listener, ApplicationEvent event) { ErrorHandler errorHandler = getErrorHandler(); if (errorHandler ! = null) { try { doInvokeListener(listener, event); } catch (Throwable err) { errorHandler.handleError(err); } } else { doInvokeListener(listener, event); }} private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {try {/ / execution to finally realize ApplicationListener rewrite onApplicationEvent method listener. OnApplicationEvent (event);  } / /... Omit}}Copy the code

To perform the demo code, can see the Spring calls the ContextClosedEventListener method logic.

However, there is a problem that this logic is executed twice. The problem is not fixed as to why this logic is executed twice.

2.3. Implement the DisposableBean mode

Spring exposes two interfaces for InitializingBean and DisposableBean, which are InitializingBean and DisposableBean, so you can use these two interfaces to initialize Bean and DisposableBean in normal development. So how is the destruction Bean to DisposableBean implemented in Spring?

To see how Spring captures the implementation class that implements DisposableBean, the source code is as follows:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException { //... Omit the try {/ / will implement DisposableBean class into disposableBeans container registerDisposableBeanIfNecessary (beanName, bean, MBD); } catch (BeanDefinitionValidationException ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex); } return exposedObject; }Copy the code

This code is the logic to create the Bean, which executes the injection of DisposableBean after it has been created. The specific logic is encapsulated in an abstract class, as follows:

public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory { / / execution into DisposableBean logic protected void registerDisposableBeanIfNecessary (String beanName, Object bean, RootBeanDefinition mbd) { AccessControlContext acc = (System.getSecurityManager() ! = null ? getAccessControlContext() : null); if (! Mbd.isprototype () && requiresDestruction(bean, MBD)) {if (mbd.issingleton ()) { It uses the DisposableBean's adapter, DisposableAdapter, and injects the DisposableBean implementation class to the disposableBeans container by means of New DisposableAdapter. registerDisposableBean(beanName, new DisposableBeanAdapter(bean, beanName, mbd, getBeanPostProcessors(), acc)); } else { // A bean with a custom scope... Scope scope = this.scopes.get(mbd.getScope()); if (scope == null) { throw new IllegalStateException("No Scope registered for scope name '" + mbd.getScope() + "'"); } scope.registerDestructionCallback(beanName, new DisposableBeanAdapter(bean, beanName, mbd, getBeanPostProcessors(), acc)); }}}}Copy the code

DisposableAdapter object which needs to be injected is assembled in the above code, which implements DisposableBean. The specific injection logic is as follows:

Public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {/ / injection logic, Public void registerDisposableBean(String beanName, DisposableBean bean) { synchronized (this.disposableBeans) { this.disposableBeans.put(beanName, bean); }}}Copy the code

ShutdownHook is used to destroy the Bean. In The shutdownHook logic, destroyBeans will be used to destroy the Bean. The logic code is as follows:

public void registerShutdownHook() { if (this.shutdownHook == null) { // No shutdown hook registered yet. this.shutdownHook = new Thread() { @Override public void run() { synchronized (startupShutdownMonitor) { doClose(); }}}; Runtime.getRuntime().addShutdownHook(this.shutdownHook); } // Execute destruction mode, which encapsulate event destruction and DisposableBean destruction. // The event subscription method executes the destruction Bean publishEvent(new ContextClosedEvent(this)); } / /... Omit // Destroy Bean destroyBeans(); / /... Protected void destroyBeans() {getBeanFactory().Destroysingletons (); Public void destroySingletons() {if (Logger.istraceEnabled ()) {logger.trace("Destroying Singletons in " + this); } synchronized (this.singletonObjects) { this.singletonsCurrentlyInDestruction = true; } String[] disposableBeanNames; synchronized (this.disposableBeans) { disposableBeanNames = StringUtils.toStringArray(this.disposableBeans.keySet()); } for (int i = disposableBeanNames.length - 1; i >= 0; DisposableBeanNames destroySingleton(disposableBeanNames[I]); // disposableBeanNames[I]; } this.containedBeanMap.clear(); this.dependentBeanMap.clear(); this.dependenciesForBeanMap.clear(); clearSingletonCache(); Public void destroySingleton(String beanName) {// Remove a registered Bean singleton of the given name, if any. removeSingleton(beanName); // Destroy the corresponding DisposableBean instance. DisposableBean disposableBean; synchronized (this.disposableBeans) { disposableBean = (DisposableBean) this.disposableBeans.remove(beanName); } destroyBean(beanName, disposableBean); } // Destroy the Bean object's logic, Protected void destroyBean(String beanName, @nullable DisposableBean bean) {Set<String> dependencies; synchronized (this.dependentBeanMap) { dependencies = this.dependentBeanMap.remove(beanName); } if (dependencies ! If (String dependentBeanName: dependentBeanName) {destroySingleton(dependentBeanName); } } if (bean ! = null) { try { bean.destroy(); } catch (Throwable ex) { if (logger.isInfoEnabled()) { logger.info("Destroy method on bean with name '" + beanName + "' threw an exception", ex); }}} / /... Omit some code}Copy the code

The Demo verification is as follows:

@Component public class DestroyBeanImpl implements InitializingBean,DisposableBean { @Override public void destroy() throws Exception { System.out.println("destroy bean by DisposableBean"); } @Override public void afterPropertiesSet() throws Exception { System.out.println("Initializing bean by InitializingBean"); }}Copy the code

As you can see from this process, Spring mainly injects the Bean that DisposableBean to its own container, and when it needs to be destroyed, by taking the Bean from the container, We then log out by calling the destroy method of the implementation class, which provides the extensibility to inject and destroy beans externally, and encapsulates the execution logic and the container that determines the operation internally.

3. Summary

These are the three ways that Spring destroys beans. The three ways are different and can be used freely in normal development. I will test them in three ways.