
infra.beans.factory.support.DisposableBeanAdapter Maven / Gradle / Ivy
/*
* Copyright 2017 - 2024 the original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see [https://www.gnu.org/licenses/]
*/
package infra.beans.factory.support;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import java.io.Serial;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import infra.beans.factory.BeanDefinitionValidationException;
import infra.beans.factory.DisposableBean;
import infra.beans.factory.config.DestructionAwareBeanPostProcessor;
import infra.core.ReactiveAdapter;
import infra.core.ReactiveAdapterRegistry;
import infra.core.ReactiveStreams;
import infra.lang.Assert;
import infra.lang.Nullable;
import infra.logging.Logger;
import infra.logging.LoggerFactory;
import infra.util.CollectionUtils;
import infra.util.ObjectUtils;
import infra.util.ReflectionUtils;
import infra.util.StringUtils;
/**
* Adapter that implements the {@link DisposableBean} and {@link Runnable}
* interfaces performing various destruction steps on a given bean instance:
*
* - DestructionBeanPostProcessors;
*
- the bean implementing DisposableBean itself;
*
- a custom destroy method specified on the bean definition.
*
*
* @author Juergen Hoeller
* @author Costin Leau
* @author Stephane Nicoll
* @author Harry Yang
* @see AbstractBeanFactory
* @see DisposableBean
* @see DestructionAwareBeanPostProcessor
* @since 4.0 2021/10/24 22:56
*/
final class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
@Serial
private static final long serialVersionUID = 1L;
private static final Logger log = LoggerFactory.getLogger(DisposableBeanAdapter.class);
private static final String CLOSE_METHOD_NAME = "close";
private static final String DESTROY_METHOD_NAME = "destroy";
private static final String SHUTDOWN_METHOD_NAME = "shutdown";
private final Object bean;
private final String beanName;
private final boolean nonPublicAccessAllowed;
private final boolean invokeDisposableBean;
private boolean invokeAutoCloseable;
@Nullable
private String[] destroyMethodNames;
@Nullable
private transient Method[] destroyMethods;
@Nullable
private final List beanPostProcessors;
/**
* Create a new DisposableBeanAdapter for the given bean.
*
* @param bean the bean instance (never {@code null})
* @param beanName the name of the bean
* @param beanDefinition the merged bean definition
* @param postProcessors the List of BeanPostProcessors
* (potentially DestructionAwareBeanPostProcessor), if any
*/
public DisposableBeanAdapter(String beanName, Object bean, RootBeanDefinition beanDefinition,
List postProcessors) {
Assert.notNull(bean, "Disposable bean is required");
this.bean = bean;
this.beanName = beanName;
this.nonPublicAccessAllowed = beanDefinition.isNonPublicAccessAllowed();
this.invokeDisposableBean = bean instanceof DisposableBean
&& !beanDefinition.hasAnyExternallyManagedDestroyMethod(DESTROY_METHOD_NAME);
String[] destroyMethodNames = inferDestroyMethodsIfNecessary(bean.getClass(), beanDefinition);
if (ObjectUtils.isNotEmpty(destroyMethodNames)
&& !(invokeDisposableBean && DESTROY_METHOD_NAME.equals(destroyMethodNames[0]))
&& !beanDefinition.hasAnyExternallyManagedDestroyMethod(destroyMethodNames[0])) {
this.invokeAutoCloseable = bean instanceof AutoCloseable && CLOSE_METHOD_NAME.equals(destroyMethodNames[0]);
if (!invokeAutoCloseable) {
this.destroyMethodNames = destroyMethodNames;
var destroyMethods = new ArrayList(destroyMethodNames.length);
for (String destroyMethodName : destroyMethodNames) {
Method destroyMethod = determineDestroyMethod(destroyMethodName);
if (destroyMethod == null) {
if (beanDefinition.isEnforceDestroyMethod()) {
throw new BeanDefinitionValidationException(
"Could not find a destroy method named '%s' on bean with name '%s'".formatted(destroyMethodName, beanName));
}
}
else {
if (destroyMethod.getParameterCount() > 0) {
Class>[] paramTypes = destroyMethod.getParameterTypes();
if (paramTypes.length > 1) {
throw new BeanDefinitionValidationException(
"Method '%s' of bean '%s' has more than one parameter - not supported as destroy method"
.formatted(destroyMethodName, beanName));
}
else if (paramTypes.length == 1 && boolean.class != paramTypes[0]) {
throw new BeanDefinitionValidationException(
"Method '%s' of bean '%s' has a non-boolean parameter - not supported as destroy method"
.formatted(destroyMethodName, beanName));
}
}
destroyMethod = ReflectionUtils.getPubliclyAccessibleMethodIfPossible(destroyMethod, bean.getClass());
destroyMethods.add(destroyMethod);
}
}
this.destroyMethods = destroyMethods.toArray(new Method[0]);
}
}
this.beanPostProcessors = filterPostProcessors(postProcessors, bean);
}
/**
* Create a new DisposableBeanAdapter for the given bean.
*
* @param bean the bean instance (never {@code null})
* @param postProcessors the List of BeanPostProcessors
* (potentially DestructionAwareBeanPostProcessor), if any
*/
public DisposableBeanAdapter(Object bean, List postProcessors) {
Assert.notNull(bean, "Disposable bean is required");
this.bean = bean;
this.nonPublicAccessAllowed = true;
this.beanName = bean.getClass().getName();
this.invokeDisposableBean = bean instanceof DisposableBean;
this.beanPostProcessors = filterPostProcessors(postProcessors, bean);
}
/**
* Create a new DisposableBeanAdapter for the given bean.
*/
private DisposableBeanAdapter(Object bean, String beanName,
boolean nonPublicAccessAllowed, boolean invokeDisposableBean, boolean invokeAutoCloseable,
@Nullable String[] destroyMethodNames, @Nullable List postProcessors) {
this.bean = bean;
this.beanName = beanName;
this.nonPublicAccessAllowed = nonPublicAccessAllowed;
this.invokeDisposableBean = invokeDisposableBean;
this.invokeAutoCloseable = invokeAutoCloseable;
this.destroyMethodNames = destroyMethodNames;
this.beanPostProcessors = postProcessors;
}
@Override
public void run() {
destroy();
}
@Override
public void destroy() {
if (CollectionUtils.isNotEmpty(beanPostProcessors)) {
for (DestructionAwareBeanPostProcessor processor : beanPostProcessors) {
processor.postProcessBeforeDestruction(bean, beanName);
}
}
if (this.invokeDisposableBean) {
if (log.isTraceEnabled()) {
log.trace("Invoking destroy() on bean with name '{}'", beanName);
}
try {
((DisposableBean) this.bean).destroy();
}
catch (Throwable ex) {
if (log.isWarnEnabled()) {
String msg = "Invocation of destroy method failed on bean with name '%s'".formatted(beanName);
if (log.isDebugEnabled()) {
// Log at warn level like below but add the exception stacktrace only with debug level
log.warn(msg, ex);
}
else {
log.warn(msg + ": " + ex);
}
}
}
}
if (this.invokeAutoCloseable) {
if (log.isTraceEnabled()) {
log.trace("Invoking close() on bean with name '{}'", beanName);
}
try {
((AutoCloseable) this.bean).close();
}
catch (Throwable ex) {
if (log.isWarnEnabled()) {
String msg = "Invocation of close method failed on bean with name '%s'".formatted(beanName);
if (log.isDebugEnabled()) {
// Log at warn level like below but add the exception stacktrace only with debug level
log.warn(msg, ex);
}
else {
log.warn(msg + ": " + ex);
}
}
}
}
else if (this.destroyMethods != null) {
for (Method destroyMethod : this.destroyMethods) {
invokeCustomDestroyMethod(destroyMethod);
}
}
else if (this.destroyMethodNames != null) {
for (String destroyMethodName : this.destroyMethodNames) {
Method destroyMethod = determineDestroyMethod(destroyMethodName);
if (destroyMethod != null) {
destroyMethod = ReflectionUtils.getPubliclyAccessibleMethodIfPossible(destroyMethod, this.bean.getClass());
invokeCustomDestroyMethod(destroyMethod);
}
}
}
}
@Nullable
private Method determineDestroyMethod(String destroyMethodName) {
try {
Class> beanClass = this.bean.getClass();
MethodDescriptor descriptor = MethodDescriptor.create(this.beanName, beanClass, destroyMethodName);
String methodName = descriptor.methodName();
Method destroyMethod = findDestroyMethod(descriptor.declaringClass(), methodName);
if (destroyMethod != null) {
return destroyMethod;
}
for (Class> beanInterface : beanClass.getInterfaces()) {
destroyMethod = findDestroyMethod(beanInterface, methodName);
if (destroyMethod != null) {
return destroyMethod;
}
}
return null;
}
catch (IllegalArgumentException ex) {
throw new BeanDefinitionValidationException(
"Could not find unique destroy method on bean with name '%s': %s".formatted(beanName, ex.getMessage()));
}
}
@Nullable
private Method findDestroyMethod(Class> clazz, String name) {
return nonPublicAccessAllowed ?
ReflectionUtils.findMethodWithMinimalParameters(clazz, name) :
ReflectionUtils.findMethodWithMinimalParameters(clazz.getMethods(), name);
}
/**
* Invoke the specified custom destroy method on the given bean.
* This implementation invokes a no-arg method if found, else checking
* for a method with a single boolean argument (passing in "true",
* assuming a "force" parameter), else logging an error.
*/
private void invokeCustomDestroyMethod(Method destroyMethod) {
if (log.isTraceEnabled()) {
log.trace("Invoking custom destroy method '{}' on bean with name '{}': {}",
destroyMethod.getName(), beanName, destroyMethod);
}
int paramCount = destroyMethod.getParameterCount();
Object[] args = new Object[paramCount];
if (paramCount == 1) {
args[0] = Boolean.TRUE;
}
try {
ReflectionUtils.makeAccessible(destroyMethod);
Object returnValue = destroyMethod.invoke(this.bean, args);
if (returnValue == null) {
// Regular case: a void method
logDestroyMethodCompletion(destroyMethod, false);
}
else if (returnValue instanceof Future> future) {
// An async task: await its completion.
future.get();
logDestroyMethodCompletion(destroyMethod, true);
}
else if (!ReactiveStreams.isPresent ||
!ReactiveDestroyMethodHandler.await(this, destroyMethod, returnValue)) {
if (log.isDebugEnabled()) {
log.debug("Unknown return value type from custom destroy method '{}' on bean with name '{}': {}",
destroyMethod.getName(), beanName, returnValue.getClass());
}
}
}
catch (InvocationTargetException | ExecutionException ex) {
logDestroyMethodException(destroyMethod, ex.getCause());
}
catch (Throwable ex) {
if (log.isWarnEnabled()) {
log.warn("Failed to invoke custom destroy method '{}' on bean with name '{}'",
destroyMethod.getName(), beanName, ex);
}
}
}
void logDestroyMethodException(Method destroyMethod, Throwable ex) {
if (log.isWarnEnabled()) {
String msg = "Custom destroy method '%s' on bean with name '%s' propagated an exception"
.formatted(destroyMethod.getName(), beanName);
if (log.isDebugEnabled()) {
// Log at warn level like below but add the exception stacktrace only with debug level
log.warn(msg, ex);
}
else {
log.warn(msg + ": " + ex);
}
}
}
void logDestroyMethodCompletion(Method destroyMethod, boolean async) {
if (log.isDebugEnabled()) {
log.debug("Custom destroy method '{}' on bean with name '{}' completed{}",
destroyMethod.getName(), beanName, (async ? " asynchronously" : ""));
}
}
/**
* Serializes a copy of the state of this class,
* filtering out non-serializable BeanPostProcessors.
*/
@Serial
protected Object writeReplace() {
ArrayList serializablePostProcessors = null;
if (this.beanPostProcessors != null) {
serializablePostProcessors = new ArrayList<>();
for (DestructionAwareBeanPostProcessor postProcessor : this.beanPostProcessors) {
if (postProcessor instanceof Serializable) {
serializablePostProcessors.add(postProcessor);
}
}
}
return new DisposableBeanAdapter(this.bean, this.beanName, this.nonPublicAccessAllowed,
this.invokeDisposableBean, this.invokeAutoCloseable, this.destroyMethodNames, serializablePostProcessors);
}
/**
* Check whether the given bean has any kind of destroy method to call.
*
* @param bean the bean instance
* @param beanDefinition the corresponding bean definition
*/
public static boolean hasDestroyMethod(Object bean, RootBeanDefinition beanDefinition) {
return bean instanceof DisposableBean
|| inferDestroyMethodsIfNecessary(bean.getClass(), beanDefinition) != null;
}
/**
* If the current value of the given beanDefinition's "destroyMethodName" property is
* {@link AbstractBeanDefinition#INFER_METHOD}, then attempt to infer a destroy method.
* Candidate methods are currently limited to public, no-arg methods named "close" or
* "shutdown" (whether declared locally or inherited). The given BeanDefinition's
* "destroyMethodName" is updated to be null if no such method is found, otherwise set
* to the name of the inferred method. This constant serves as the default for the
* {@code @Bean#destroyMethod} attribute and the value of the constant may also be
* used in XML within the {@code } or {@code
* } attributes.
* Also processes the {@link java.io.Closeable} and {@link java.lang.AutoCloseable}
* interfaces, reflectively calling the "close" method on implementing beans as well.
*/
@Nullable
static String[] inferDestroyMethodsIfNecessary(Class> target, RootBeanDefinition beanDefinition) {
String[] destroyMethodNames = beanDefinition.getDestroyMethodNames();
if (destroyMethodNames != null && destroyMethodNames.length > 1) {
return destroyMethodNames;
}
String destroyMethodName = beanDefinition.resolvedDestroyMethodName;
if (destroyMethodName == null) {
destroyMethodName = beanDefinition.getDestroyMethodName();
boolean autoCloseable = AutoCloseable.class.isAssignableFrom(target);
if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName)
|| (destroyMethodName == null && autoCloseable)) {
// Only perform destroy method inference in case of the bean
// not explicitly implementing the DisposableBean interface
destroyMethodName = null;
if (!(DisposableBean.class.isAssignableFrom(target))) {
if (autoCloseable) {
destroyMethodName = CLOSE_METHOD_NAME;
}
else {
try {
destroyMethodName = target.getMethod(CLOSE_METHOD_NAME).getName();
}
catch (NoSuchMethodException ex) {
try {
destroyMethodName = target.getMethod(SHUTDOWN_METHOD_NAME).getName();
}
catch (NoSuchMethodException ex2) {
// no candidate destroy method found
}
}
}
}
}
beanDefinition.resolvedDestroyMethodName =
destroyMethodName != null ? destroyMethodName : "";
}
return StringUtils.isEmpty(destroyMethodName) ? null : new String[] { destroyMethodName };
}
/**
* Check whether the given bean has destruction-aware post-processors applying to it.
*
* @param bean the bean instance
* @param postProcessors the post-processor candidates
*/
public static boolean hasApplicableProcessors(Object bean, List postProcessors) {
if (CollectionUtils.isNotEmpty(postProcessors)) {
for (DestructionAwareBeanPostProcessor processor : postProcessors) {
if (processor.requiresDestruction(bean)) {
return true;
}
}
}
return false;
}
/**
* Search for all DestructionAwareBeanPostProcessors in the List.
*
* @param processors the List to search
* @return the filtered List of DestructionAwareBeanPostProcessors
*/
@Nullable
private static List filterPostProcessors(List processors, Object bean) {
ArrayList filtered = null;
if (CollectionUtils.isNotEmpty(processors)) {
filtered = new ArrayList<>(processors.size());
for (DestructionAwareBeanPostProcessor processor : processors) {
if (processor.requiresDestruction(bean)) {
filtered.add(processor);
}
}
}
return filtered;
}
/**
* Inner class to avoid a hard dependency on the Reactive Streams API at runtime.
*/
private static class ReactiveDestroyMethodHandler {
public static boolean await(DisposableBeanAdapter beanAdapter, Method destroyMethod, Object returnValue) throws InterruptedException {
ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(returnValue.getClass());
if (adapter != null) {
CountDownLatch latch = new CountDownLatch(1);
adapter.toPublisher(returnValue)
.subscribe(new DestroyMethodSubscriber(destroyMethod, latch, beanAdapter));
latch.await();
return true;
}
return false;
}
}
/**
* Reactive Streams Subscriber for destroy method completion.
*/
private static class DestroyMethodSubscriber implements Subscriber