1.背景
目前项目上云之后从原有的服务注册中心consul改为nacos,因此希望实现优雅重启服务的方式,当服务重启时可以将服务从nacos下线,同时能通知到消费端服务更新Ribbon的注册列表,避免出现请求异常的情况,后来发现行不通,还是要依赖ribbon的重试机制进行重新发起请求,但是研究优雅停机时研究了下nacos是如何停止容器并销毁Bean的。
2.销毁Bean的三种方式
2.1.注解PreDestroy方式
通过停止服务发现Nacos销毁Bean的方式是通过@PreDestroy的方式进行销毁的。
通过日志输出可以看到Nacos调用了deregister方法进行销毁Bean,因此看下该方法源码如下:
public class NacosServiceRegistry implements ServiceRegistry<Registration> {
//...省略
@Override
public void deregister(Registration 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.");
}
}
现在来看deregistry的调用方,该方法的调用方其实是SpringCloud中的方法,SpringCloud中实现容器的关闭和JVM的关闭,只不过最终的实现是由Nacos实现。源码如下(该源码来源SpringCloud):
public abstract class AbstractAutoServiceRegistration<R extends Registration>
implements AutoServiceRegistration, ApplicationContextAware,
ApplicationListener<WebServerInitializedEvent> {
//...省略部分代码
//最初的调用是SpringCloud提供的方法,该方法使用了@PreDestroy注解
//该注解是JDK提供的一个注解,用于注销jvm的一种方式,Spring对该注解进行捕获处理
@PreDestroy
public void destroy() {
stop();
}
//调用deregistry的逻辑方法
public void stop() {
if (this.getRunning().compareAndSet(true, false) && isEnabled()) {
//调用deregistry方法
deregister();
if (shouldRegisterManagement()) {
//该方法最终也是调用deregistry方法
deregisterManagement();
}
this.serviceRegistry.close();
}
}
}
看完SpringCloud注销JVM后,现在看Nacos如何实现的deregister的,首先SpringCloud提供了ServiceRegistry接口,其他服务注册中心只要实现该接口,即可完成自己的服务注册,注销等操作。从源码中可以知道Consul和Nacos各自实现了该接口。
接口代码如下:
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);
}
Nacos实现代码如下:
public class NacosServiceRegistry implements ServiceRegistry<Registration> {
//...省略部分代码
//注册服务Nacos实现逻辑
@Override
public void register(Registration 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);
}
}
//注销服务Nacos实现逻辑
@Override
public void deregister(Registration 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.");
}
}
因此以上就是Nacos关闭Bean容器以及注销Bean的一种方式,该方式是最简洁的方式进行实现的,直接借助@PreDestroy注解即可实现。
现在写一个测试类测试下该注解,代码如下:
@Component
public class DestroyBean {
@PreDestroy
public void destroyBean() {
System.out.println("销毁bean! by @PreDestroy"+System.currentTimeMillis());
}
}
随便写一个测试类启动即可:
@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);
log.info("<<<<<<<开始执行测试:{}", description.getMethodName());
}
@Override
protected void finished(Description description) {
super.finished(description);
log.info("{}执行完毕>>>>>>>", description.getMethodName());
}
@Override
protected void failed(Throwable e, Description description) {
super.failed(e, description);
log.error("-----方法执行失败,异常:{}", e.getMessage());
}
};
@Test
public void testHandleTransport() throws Exception {
//TODO: Test goes here...
applicationContext.publishEvent(new CustomEvent(applicationContext));
}
}
运行测试类的方法后,可以看到服务关闭时最终执行了加了PreDestroy注解的方法,这样就可以知道只要加入该注解,那么就可以在其中进行销毁Bean的操作。
2.2.事件订阅方式
Spring中提供了事件订阅的功能,即在平时可以使用的实现ApplicationListener,即可以创建实现类实现ApplicationListener接口,然后重写其中的onApplicationEvent方法,具体实现如下:
@Component
public class ContextClosedEventListener implements ApplicationListener<ContextClosedEvent> {
@SneakyThrows
@Override
public void onApplicationEvent(ContextClosedEvent event) {
System.out.println("关闭事件:" + this.getClass().getName()+"时间:"+System.currentTimeMillis());
Thread.sleep(10000);
}
}
通过该方式注销Bean时,当容器关闭时会调用到该方法中,那么在该方法中可以自己实现注销Bean的逻辑。那么Spring时如何提供事件注销Bean的功能的呢?Spring提供了一种关闭容器的方式即ShutdownHook的方式,而事件订阅的方式就是通过调用shutdownHook的方式进行关闭的,shutdownHook是开启一个线程进行执行的。源码如下:
public abstract class AbstractApplicationContext extends DefaultResourceLoader
implements ConfigurableApplicationContext {
//执行shutdownhook
@Override
public void registerShutdownHook() {
if (this.shutdownHook == null) {
// No shutdown hook registered yet.
//初始化一个线程执行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 {
//发布ClosedEvent事件
publishEvent(new ContextClosedEvent(this));
}
//...省略
}
}
当发布ContextClosedEvent事件时,最终会调用到SimpleApplicationEventMulticaster实现类中,具体逻辑如下:
public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
//进入colsedEvent调用方
@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 {
//最终执行到实现ApplicationListener并重写onApplicationEvent方法中
listener.onApplicationEvent(event);
}
//...省略
}
}
执行demo代码,可以看到Spring调用了ContextClosedEventListener的方法逻辑。
但是这里还有个问题就是该逻辑被执行了两次,该问题还没定位到为啥会执行两次,后续会补充。
2.3.实现 DisposableBean方式
Spring中暴露了初始化Bean和销毁Bean的两个接口,分别是InitializingBean和DisposableBean,因此在平时开发中可以使用这两个接口进行初始化Bean和销毁Bean,那么DisposableBean的销毁Bean在Spring中如何实现的呢?
首先看实现了DisposableBean的实现类是如何被Spring捕捉的,源码如下:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
//...省略
try {
//将实现DisposableBean的实现类注入到disposableBeans容器中
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
catch (BeanDefinitionValidationException ex) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
}
return exposedObject;
}
该代码是创建Bean的逻辑,在创建Bean完成后执行的DisposableBean的注入,具体逻辑封装在抽象类中,如下:
public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
//执行注入DisposableBean的逻辑
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()) {
//注入时并不是直接使用DisposableBean,而是使用了DisposableBean的适配器,DisposableAdapter,注入时通过new DisposableAdapter的方式将DisposableBean的实现类注入到disposableBeans容器中。
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));
}
}
}
}
以上代码中通过组装需要注入的DisposableAdapter对象,该对象实现了DisposableBean,具体注入逻辑如下:
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
//注入逻辑,容器名称为disposableBeans
public void registerDisposableBean(String beanName, DisposableBean bean) {
synchronized (this.disposableBeans) {
this.disposableBeans.put(beanName, bean);
}
}
}
以上都是注入到容器中的逻辑,接下来销毁Bean时是如何处理的,销毁Bean的方式依然通过shutdownHook的方式,shutdownHook逻辑中通过执行destroyBeans执行销毁,逻辑代码如下:
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);
}
}
//执行销毁方式,该逻辑中封装了事件方式销毁和DisposableBean方式销毁
protected void doClose() {
//...省略部分代码
try {
//事件订阅方法执行销毁Bean的方式
publishEvent(new ContextClosedEvent(this));
}
//...省略
//销毁Bean
destroyBeans();
//...省略
}
//通过beanFactory调用销毁单例Bean
protected void destroyBeans() {
getBeanFactory().destroySingletons();
}
//销毁单例Bean的逻辑
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; i--) {
//通过遍历的方式销毁Bean,主要遍历disposableBeanNames
destroySingleton(disposableBeanNames[i]);
}
this.containedBeanMap.clear();
this.dependentBeanMap.clear();
this.dependenciesForBeanMap.clear();
clearSingletonCache();
}
//该逻辑中封装了移除容器中的Bean以及销毁DisposableBean容器
public void destroySingleton(String beanName) {
// Remove a registered 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);
}
//销毁Bean对象的逻辑,该逻辑中对循环依赖也同步做了处理
protected void destroyBean(String beanName, @Nullable DisposableBean bean) {
Set<String> dependencies;
synchronized (this.dependentBeanMap) {
dependencies = this.dependentBeanMap.remove(beanName);
}
if (dependencies != null) {
//循环依赖的时候的销毁逻辑
for (String dependentBeanName : dependencies) {
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);
}
}
}
//...省略部分代码
}
通过Demo验证如下:
@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");
}
}
以上是通过DisposableBean的方式进行注销Bean的,该过程中可以看到,Spring主要将实现了DisposableBean的Bean进行注入到专属容器中,当需要销毁时,通过获取容器中的bean,然后通过调用实现类的destroy方法进行注销,这样对外提供了对Bean注入以及销毁的扩展性,对内封装了各个执行逻辑以及确定了操作的容器。
3.总结
以上就是Spring销毁Bean的三种方式,三种方式各有春秋,平时开发中可以随意使用,我将三种方式进行测试。
作者:陈汤姆
链接:从源码分析Spring销毁Bean的三种方式 - 掘金