35

I noticed that the @PreDestroy hooks of my prototype scoped Spring beans were not getting executed.

I have since read here that this is actually by design. The Spring container will destroy singleton beans but will not destroy prototype beans. It is unclear to me why. If the Spring container will create my prototype bean and execute its @PostConstruct hook, why will it not destroy my bean as well, when the container is closed? Once my Spring container has been closed, does it even make sense to continue using any of its beans? I cannot see a scenario where you would want to close a container before you have finished with its beans. Is it even possible to continue using a prototype Spring bean after its container has been closed?

The above describes the puzzling background to my primary question which is: If the Spring container is not destroying prototype beans, does that mean a memory leak could occur? Or will the prototype bean get garbage-collected at some point?

The Spring documentations states:

The client code must clean up prototype-scoped objects and release expensive resources that the prototype bean(s) are holding. To get the Spring container to release resources held by prototype-scoped beans, try using a custom bean post-processor, which holds a reference to beans that need to be cleaned up.

What does that mean? The text suggests to me that I, as the programmer am responsible for explicitly (manually) destroying my prototype beans. Is this correct? If so, how do I do that?

IqbalHamid
  • 2,324
  • 1
  • 18
  • 24
  • You don't need to do anything as long as you don't need to release resources used by your prototype bean. e.g. if you create DB connection pool in your prototype scoped bean, you probably need to close it :) – Michal Jun 04 '18 at 13:03
  • Spring doesn't know what the lifecycle of a prototype bean is, hence it will only call the initializers and not the destruction callbacks as it simply doesn't (nor can) now when you don't need it anymore. – M. Deinum Jun 04 '18 at 13:05
  • Thank you both. @M.Deinum, what you have stated, I did discuss in my question. Closing the spring container should give Spring a big hint that we have finished with our prototype beans. Once a container has been closed, is it even possible to continue using any spring bean (even a prototype bean)? Even if it is, the quotation from the spring documentation suggests that unless I destroy the bean, there will be a memory leak. That is what is panicking me. I hear Michal who suggests that the garbage collector will pick it up once the object variable to the prototype bean goes out of scope. – IqbalHamid Jun 04 '18 at 13:31
  • 2
    No it doesn't. How does Spring now that there are still prototype beans hanging around? Some use the context with prototype beans as a factory for short-lived beans (or to have a blue print of a bean). Theoretically there could have been thousands of prototype bean instances created which already have been garbage collected. Spring simply doesn't know that. There is only a memory leak if that prototype beans holds a reference to something which prevents it from being garbage collected. – M. Deinum Jun 04 '18 at 13:34
  • @Michal But how would you do that (release resources used by the bean) if the bean's destruction hook (@PreDestroy) is never called by Spring. I guess calling the destruction hook manually from my client code would be the only way? – IqbalHamid Jun 04 '18 at 13:37
  • Thank you @M.Deinum, your comment makes sense. – IqbalHamid Jun 04 '18 at 13:39

2 Answers2

43

For the benefit of others, I will present below what I have gathered from my investigations:

As long as the prototype bean does not itself hold a reference to another resource such as a database connection or a session object, it will get garbage collected as soon as all references to the object have been removed or the object goes out of scope. It is therefore usually not necessary to explicitly destroy a prototype bean.

However, in the case where a memory leak may occur as described above, prototype beans can be destroyed by creating a singleton bean post-processor whose destruction method explicitly calls the destruction hooks of your prototype beans. Because the post-processor is itself of singleton scope, its destruction hook will get invoked by Spring:

  1. Create a bean post processor to handle the destruction of all your prototype beans. This is necessary because Spring does not destroy prototype beans and so any @PreDestroy hooks in your code will never get called by the container.

  2. Implement the following interfaces:

    1.BeanFactoryAware
    This interface provides a callback method which receives a Beanfactory object. This BeanFactory object is used in the post-processor class to identify all prototype beans via its BeanFactory.isPrototype(String beanName) method.

    2. DisposableBean
    This interface provides a Destroy() callback method invoked by the Spring container. We will call the Destroy() methods of all our prototype beans from within this method.

    3. BeanPostProcessor
    Implementing this interface provides access to post-process callbacks from within which, we prepare an internal List<> of all prototype objects instantiated by the Spring container. We will later loop through this List<> to destroy each of our prototype beans.


3. Finally implement the DisposableBean interface in each of your prototype beans, providing the Destroy() method required by this contract.

To illustrate this logic, I provide some code below taken from this article:

/**
* Bean PostProcessor that handles destruction of prototype beans
*/
@Component
public class DestroyPrototypeBeansPostProcessor implements BeanPostProcessor, BeanFactoryAware, DisposableBean {

    private BeanFactory beanFactory;

    private final List<Object> prototypeBeans = new LinkedList<>();

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (beanFactory.isPrototype(beanName)) {
            synchronized (prototypeBeans) {
                prototypeBeans.add(bean);
            }
        }
        return bean;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    @Override
    public void destroy() throws Exception {
        synchronized (prototypeBeans) {
            for (Object bean : prototypeBeans) {
                if (bean instanceof DisposableBean) {
                    DisposableBean disposable = (DisposableBean)bean;
                    try {
                        disposable.destroy();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
            prototypeBeans.clear();
        }
    }
}
IqbalHamid
  • 2,324
  • 1
  • 18
  • 24
  • Does the prototype bean get garbage collected if it does not hold the reference to session or pool but it hold references to other services (stateless beans). The prototype bean calls the method of the services which are transactional. So the bean is not itself transactions or has session but the injected beand does. – Akshay Oct 26 '18 at 13:22
  • For some reason `beanFactory.isPrototype()` breaks in integration tests with a `NoSuchBeanDefinitionException` on the integration-test-bean itself. Other than that this approach is awesome. – sjngm Nov 11 '19 at 09:29
  • On an additional note it's better to do `Deque prototypeBeans = new ArrayDeque<>();` and `push()` instead of `add()` so that the loop then is performed in reverse order. You never know if there are additional dependencies among the beans... – sjngm Nov 11 '19 at 11:22
  • And again integration tests: Don't forget to add `@DirtiesContext` to the integration test in case it instantiates the prototype-scoped bean. – sjngm Dec 17 '19 at 13:17
  • If we are using rest based spring boot application, In which situation DestroyPrototypeBeansPostProcessor destroy method will be called ? – Suneet Khurana Sep 13 '20 at 11:56
  • I get the how part but I didn't get the why part. you're saying since there is a case where the prototype beans have db, session references in it , the burden should fall on client and not on the framework to destroy it with a custom processor. But Why? – susheelbhargavk Dec 01 '20 at 22:02
  • "As long as the prototype bean does not itself hold a reference to another resource such as a database connection or a session object, it will get garbage collected as soon as all references to the object have been removed or the object goes out of scope." Why do you think a bean will not be collected if it hold a db connection? – Dragos Ionut Feb 16 '21 at 18:12
  • 1
    Why are we clearing a class variable in this example? Why did we call the clear method of the list? Should we clear all local variables in a bean? Would it cause a memory leak? – omerstack Feb 22 '22 at 11:32
1

Your answer is great. I'd also like to share some notes on an alternative solution that allows prototype members which are natively managed by the Spring IoC container lifecycle through the use of the inner beans.

I recently wrote an answer to a separate question on inner beans. Inner beans are created by assigning bean property values as BeanDefinition objects. Bean definition property values are automatically resolved to (inner) instances (as managed singleton beans) of the bean that they define.

The following XML context configuration element can be used to create distinct autowireable ForkJoinPool beans for each reference which will be managed (@PreDestroy will be called on context shutdown):

<!-- Prototype-scoped bean for creating distinct FJPs within the application -->
<bean id="forkJoinPool" class="org.springframework.beans.factory.support.GenericBeanDefinition" scope="prototype">
    <property name="beanClass" value="org.springframework.scheduling.concurrent.ForkJoinPoolFactoryBean" />
</bean>

This behavior is contingent upon the reference being assigned as a property value of a bean definition, though. This means that @Autowired- and constructor-injection do not work with this by default, since these autowiring methods resolve the value immediately rather than using the property value resolution in AbstractAutowireCapableBeanFactory#applyPropertyValues. Autowiring by type will also not work, as type-resolution will not propagate through beans that are BeanDefinitions to find the produced type.

This method will only work if either of the two conditions are true:

  • The dependent beans are also defined in XML
  • Or if the autowire mode is set to AutowireCapableBeanFactory#AUTOWIRE_BY_NAME

<!-- Setting bean references through XML -->
<beans ...>
    <bean id="myOtherBean" class="com.example.demo.ForkJoinPoolContainer">
        <property name="forkJoinPool" ref="forkJoinPool" />
    </bean>
</beans>

<!-- Or setting the default autowire mode -->
<beans default-autowire="byName" ...>
    ...
</beans>

Two additional changes could likely be made to enable constructor-injection and @Autowired-injection.

  • Constructor-injection:

    The bean factory assigns an AutowireCandidateResolver for constructor injection. The default value (ContextAnnotationAutowireCandidateResolver) could be overridden (DefaultListableBeanFactory#setAutowireCandidateResolver) to apply a candidate resolver which seeks eligible beans of type BeanDefinition for injection.

  • @Autowired-injection:

    The AutowiredAnnotationBeanPostProcessor bean post processor directly sets bean values without resolving BeanDefinition inner beans. This post processor could be overridden, or a separate bean post processor could be created to process a custom annotation for managed prototype beans (e.g., @AutowiredManagedPrototype).

Mike Hill
  • 3,622
  • 23
  • 27