If Configuration class implements both ServletContextAware and DestructionAwareBeanPostProcessor then setServletContext will not be called.

Steps to reproduce:

  1. Init a project https://start.spring.io/
  2. Maven project, Spring boot 2.4.1, Java 8
  3. Add spring-boot-starter-web dependency
  4. Add two configuration classes
DemoConfig
package com.example.demo;

import javax.annotation.PostConstruct;
import javax.servlet.ServletContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.ServletContextAware;

@Configuration
public class DemoConfig implements DestructionAwareBeanPostProcessor, ServletContextAware {
    private static final Logger LOG = LoggerFactory.getLogger( DemoConfig.class );

    private ServletContext servletContext;

    @PostConstruct
    public void init() {
        LOG.warn( "init() {}", servletContext );
    }

    @Override
    public void postProcessBeforeDestruction( Object bean, String beanName ) throws BeansException {
    }

    @Override
    public void setServletContext( ServletContext servletContext ) {
        LOG.warn( "setServletContext() {}", servletContext );
        this.servletContext = servletContext;
    }

}
AnotherConfig
package com.example.demo;

import javax.annotation.PostConstruct;
import javax.servlet.ServletContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.ServletContextAware;

@Configuration
public class AnotherConfig implements ServletContextAware {
    private static final Logger LOG = LoggerFactory.getLogger( AnotherConfig.class );

    private ServletContext servletContext;

    @PostConstruct
    public void init() {
        LOG.warn( "init() {}", servletContext );
    }

    @Override
    public void setServletContext( ServletContext servletContext ) {
        LOG.warn( "setServletContext() {}", servletContext );
        this.servletContext = servletContext;
    }

}

Logs:

2020-12-18 22:17:01.742  INFO 331634 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication using Java 1.8.0_265 on zaynetro-thinkpad with PID 331634 (/home/zaynetro/Code/trial/demo-spring-boot/target/classes started by zaynetro in /home/zaynetro/Code/trial/demo-spring-boot)
2020-12-18 22:17:01.745 DEBUG 331634 --- [           main] com.example.demo.DemoApplication         : Running with Spring Boot v2.4.1, Spring v5.3.2
2020-12-18 22:17:01.746  INFO 331634 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2020-12-18 22:17:02.367  WARN 331634 --- [           main] com.example.demo.DemoConfig              : init() null
2020-12-18 22:17:02.614  INFO 331634 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-12-18 22:17:02.623  INFO 331634 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-12-18 22:17:02.623  INFO 331634 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.41]
2020-12-18 22:17:02.668  INFO 331634 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-12-18 22:17:02.668  INFO 331634 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 873 ms
2020-12-18 22:17:02.713  WARN 331634 --- [           main] com.example.demo.AnotherConfig           : setServletContext() org.apache.catalina.core.ApplicationContextFacade@299266e2
2020-12-18 22:17:02.713  WARN 331634 --- [           main] com.example.demo.AnotherConfig           : init() org.apache.catalina.core.ApplicationContextFacade@299266e2
2020-12-18 22:17:02.842  INFO 331634 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-12-18 22:17:02.985  INFO 331634 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''

setServletContext is called correctly for AnotherConfig but never called for DemoConfig.

Comment From: philwebb

Having a @Configuration class implement DestructionAwareBeanPostProcessor is quite an unusual pattern and not something I'd recommend. Have you tried creating a dedicated class for the DestructionAwareBeanPostProcessor functionality and creating it from a static @Bean method?

Comment From: zaynetro

Thank you for your quick response!

We are migrating our Spring (5.2) Jetty Servlet (9.4) application to spring boot. The same configuration class used to work in the previous setup for us hence I decided to create an issue. We are actually following a recommended way to configure CometD library with Spring.

I will try to figure out if I can use your suggestion to achieve the same.

Comment From: snicoll

We are actually following a recommended way to configure CometD library with Spring.

Unfortunately, that doesn't change the fact that this is unusual and should be avoided. I've created an issue to ask them to consider reviewing this part of the code and doc.

Comment From: zaynetro

Sadly, I am a bit of at loss in here.

My problem is that my class implementing DestructionAwareBeanPostProcessor depends on a bean that requires ServletContext. It seems that ServletContext is not initialized until DestructionAwareBeanPostProcessor bean is constructed.

Could you share more info regarding:

creating it from a static @Bean method

I don't think I fully understand how to utilize that to solve my problem.

Comment From: snicoll

@zaynetro thanks for the update.

When you start a web application with the Spring Framework, the server is already started by the time the ApplicationContext is bootstrapped. As such the ServletContext is set very early on and any ServletContextAware callback can be honoured as you've experienced.

That said, this is mixing two different phase of the application. A @Configuration is meant to defines the components of the application and should not implement any "runtime" business logic. With Spring Boot and its embedded server support, the server is starting as part of the application startup and the ServletContext may or may not available. As startup happens asynchronously, you have no guarantee that the context is available if you don't express a dependency on it.

Steps to reproduce:

Going forward, could you please move that to an actual GitHub repository? This is what I had to do to reproduce the problem and can be a good base for discussion. I've pushed your change and then an additional commit that describes what we were discussing here. Please note in particular that the access to ServletContext is delayed to the actual use rather than storing the variable on startup.

Can you please give that DemoPostProcessor a try and let us know how it goes?

Comment From: zaynetro

As startup happens asynchronously, you have no guarantee that the context is available if you don't express a dependency on it.

How can you express a dependency on ServletContext? ServletContextAware interface isn't really a precondition. Is there other ways?

I need for a DestructionAwareBeanPostProcessor to depend on a bean that depends on ServletContext. Unfortunately, when I do that currently my bean is initialized before ServletContext is set.

I have tried to express my problem here: https://github.com/snicoll-scratches/spring-boot-issue-24561/pull/1

Comment From: snicoll

How can you express a dependency on ServletContext? ServletContextAware interface isn't really a precondition.

That is a good point. ServletContextAware will be honoured if the servlet context is available by the time the bean is actually created. We've already discussed you can't really do this as part of the context initialisation since the servlet context itself is set when starting the server.

Rather, you should get a callback once the server has started (and the servlet context is set). You could do so via ApplicationListener<ApplicationStartedEvent> for instance.

I need for a DestructionAwareBeanPostProcessor to depend on a bean that depends on ServletContext. Unfortunately, when I do that currently my bean is initialized before ServletContext is set.

With an embedded server support, I don't see a way for a bean to reliably depend on the ServletContext using regular dependency injection style. Perhaps that bean could have the servletContext injected at a later stage (via the listener I've just expressed above?).

I have tried to express my problem here: snicoll-scratches/spring-boot-issue-24561#1

Great. Let's continue the brainstorm over there.

Comment From: zaynetro

Alright, I think I have found a workaround solution that works for me. I create my bean that depends on servlet context and create post processor as a static bean.

AnotherConfig.java
@Configuration
public class AnotherConfig implements ServletContextAware {
    private static final Logger LOG = LoggerFactory.getLogger( AnotherConfig.class );

    private ServletContext servletContext;

    @Override
    public void setServletContext( ServletContext servletContext ) {
        this.servletContext = servletContext;
    }

    @Bean
    public Hello hello() {
        return new Hello(servletContext);
    }

    @Bean
    public static DemoPostProcessor demoPostProcessor() {
        return new DemoPostProcessor();
    }
}

And then in my post processor I try to get a bean after it has been initialized.

DemoPostProcessor.java
public class DemoPostProcessor implements DestructionAwareBeanPostProcessor, ApplicationContextAware {

    private static final Logger LOG = LoggerFactory.getLogger( DemoPostProcessor.class );

    private WebApplicationContext applicationContext;

    Optional<Hello> getHello() {
        if( applicationContext.getServletContext() != null ) {
            // Once we have servletContext I want to instantiate my Hello bean. This could fail with:
            //
            // > Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException:
            // > Error creating bean with name 'hello':
            // > Requested bean is currently in creation: Is there an unresolvable circular reference?

            try {
                return Optional.of( applicationContext.getBean( Hello.class ) );
            } catch( BeanCurrentlyInCreationException e ) {
                // Ignore
            }
        }

        return Optional.empty();
    }

    @Override
    public Object postProcessBeforeInitialization( Object bean, String name ) throws BeansException {
        Optional<Hello> found = getHello();
        if( found.isPresent() ) {
            found.get().process( bean, name );
        }
        return bean;
    }

    @Override
    public void postProcessBeforeDestruction( Object bean, String name ) throws BeansException {
        LOG.warn("Destruction of {} with servletContext {}", name, getHello().get().servletContext);
    }

    @Override
    public void setApplicationContext( ApplicationContext applicationContext ) throws BeansException {
        this.applicationContext = (WebApplicationContext) applicationContext;
    }
}

Note that I don't particularly like this solution but it works for my use case. There is an option to use CometD library without processing the annotations so I might switch to that if this solution becomes unreliable.

As a final note it would be nice for a bean to be able to depend on a servlet context but I understand that it might be undesirable with post processor beans.

Feel free to close this issue.

UPD: this solution is ugly and not recommended. I have switched to post processing beans manually instead.