diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index 174ac4eff0..b9ba22ccf4 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -31,6 +31,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.aop.scope.ScopedProxyFactoryBean; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.annotation.AnnotationUtils; @@ -64,6 +65,7 @@ class ConfigurationClassEnhancer { Assert.notNull(beanFactory, "BeanFactory must not be null"); this.callbackInstances.add(new BeanMethodInterceptor(beanFactory)); + this.callbackInstances.add(new DisposableBeanMethodInterceptor()); this.callbackInstances.add(NoOp.INSTANCE); for (Callback callback : this.callbackInstances) { @@ -74,7 +76,13 @@ class ConfigurationClassEnhancer { // handling a @Bean-annotated method; otherwise, return index of the NoOp callback. callbackFilter = new CallbackFilter() { public int accept(Method candidateMethod) { - return (BeanAnnotationHelper.isBeanAnnotated(candidateMethod) ? 0 : 1); + if (BeanAnnotationHelper.isBeanAnnotated(candidateMethod)) { + return 0; + } + if (DisposableBeanMethodInterceptor.isDestroyMethod(candidateMethod)) { + return 1; + } + return 2; } }; } @@ -104,6 +112,7 @@ class ConfigurationClassEnhancer { // any performance problem. enhancer.setUseCache(false); enhancer.setSuperclass(superclass); + enhancer.setInterfaces(new Class[] {DisposableBean.class}); enhancer.setUseFactory(false); enhancer.setCallbackFilter(this.callbackFilter); enhancer.setCallbackTypes(this.callbackTypes.toArray(new Class[this.callbackTypes.size()])); @@ -145,6 +154,29 @@ class ConfigurationClassEnhancer { } + /** + * Intercepts the invocation of any {@link DisposableBean#destroy()} on @Configuration + * class instances for the purpose of de-registering CGLIB callbacks. This helps avoid + * garbage collection issues See SPR-7901. + */ + private static class DisposableBeanMethodInterceptor implements MethodInterceptor { + + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + Enhancer.registerStaticCallbacks(obj.getClass(), null); + if (DisposableBean.class.isAssignableFrom(obj.getClass().getSuperclass())) { + return proxy.invokeSuper(obj, args); + } + return null; + } + + public static boolean isDestroyMethod(Method candidateMethod) { + return candidateMethod.getName().equals("destroy") && + candidateMethod.getParameterTypes().length == 0 && + DisposableBean.class.isAssignableFrom(candidateMethod.getDeclaringClass()); + } + } + + /** * Intercepts the invocation of any {@link Bean}-annotated methods in order to ensure proper * handling of bean semantics such as scoping and AOP proxying. diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassCglibCallbackDeregistrationTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassCglibCallbackDeregistrationTests.java new file mode 100644 index 0000000000..1d21211ad7 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassCglibCallbackDeregistrationTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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 org.springframework.context.annotation.configuration; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; + +/** + * Tests ensuring that @Configuration-related CGLIB callbacks are de-registered + * at container shutdown time, allowing for proper garbage collection. See SPR-7901. + * + * @author Chris Beams + */ +public class ConfigurationClassCglibCallbackDeregistrationTests { + + /** + * asserting that the actual callback is deregistered is difficult, + * but we can at least assert that the @Configuration class is enhanced + * to implement DisposableBean. The enhanced implementation of destroy() + * will do the de-registration work. + */ + @Test + public void destroyContext() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class); + Config config = ctx.getBean(Config.class); + assertThat(config, instanceOf(DisposableBean.class)); + ctx.destroy(); + } + + /** + * The DisposableBeanMethodInterceptor in ConfigurationClassEnhancer + * should be careful to invoke any explicit super-implementation of + * DisposableBean#destroy(). + */ + @Test + public void destroyExplicitDisposableBeanConfig() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(DisposableConfig.class); + DisposableConfig config = ctx.getBean(DisposableConfig.class); + assertThat(config.destroyed, is(false)); + ctx.destroy(); + assertThat("DisposableConfig.destroy() was not invoked", config.destroyed, is(true)); + } + + + @Configuration + static class Config { + } + + + @Configuration + static class DisposableConfig implements DisposableBean { + boolean destroyed = false; + public void destroy() throws Exception { + this.destroyed = true; + } + } +}