1

I am trying to get access to the beans instantiated in my Spring Boot ApplicationContext from within static code for Java service provider (META-INF/services). I am using Java 14 (openjdk:14-jdk-slim).

In the remote debugger session, I can see that the static variable for the application context CONTEXT is getting set to the right value:

@Component
@Slf4j
public class ApplicationContextListener implements ApplicationContextAware {
    private static volatile ApplicationContext CONTEXT;

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

Next time it is getting accessed at runtime later (from another thread), the variable CONTEXT is null, e.g. here:

    public static MyBean getMyBean() {
        return CONTEXT.getBean(MyBean.class);
    }

I suspect that the problem is that the class holding the static variable gets "unloaded" in between. But 1) why would that happen to a class defining a valid bean in the application context 2) how can I avoid that?

What can be the reason for the static variable to become null later during the application execution? There is no code setting the variable except for the one listed above. The setApplicationContext is only gets invoked once.

If the issue is related to the class unloading, how can I avoid that?

Maybe my initialization code is executed against static variable in a class loaded by classloader A and access is taking place to the not initialized static variable in the same class loaded by the classlaoder B? Assuming this is the reason, how could I fix this problem?

Thank you!

Update

The classloader of the class ApplicationContextListener instance when in the method setApplicationContext is org.springframework.boot.devtools.restart.classloader.RestartClassLoader as returned by this.getClass().getClassLoader().

When the debugger stops later in the ApplicationContextListener class static methods the debugger shows ClassLoaders$AppClassLoader for the ApplicationContextListener.class.getClassLoader()

The Thread.currentThread().getContextClassLoader() in the latter case (in static get) shows org.springframework.boot.devtools.restart.classloader.RestartClassLoader too.

Sergey Shcherbakov
  • 4,534
  • 4
  • 40
  • 65
  • 1
    When you hit the breakpoint in `setApplicationContext`, which classloader has loaded `ApplicationContextListener`? Which classloader has loaded `ApplicationContextListener` in `getMyBean`? – tgdavies Nov 16 '21 at 03:45
  • Apparently, those are two distinct class loaders. Updating the question with additional info. – Sergey Shcherbakov Nov 16 '21 at 19:09

1 Answers1

0

The reason is the Spring Boot Automatic Restart feature enabled by default via dependency to the spring-boot-devtools added by the Spring Initializer that I used to create the project.

That feature enables loading bean classes via additional class loader.

In this situation one way to get access to the static variable is like follows:

private static ApplicationContext getContext() {
    try {
        return (ApplicationContext) getClass(ApplicationListenerContext.class.getName())
            .getField("CONTEXT").get(null);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

private static Class getClass(String classname) throws ClassNotFoundException {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    if(classLoader == null) {
        classLoader = ApplicationListenerContext.class.getClassLoader();
    }
    return classLoader.loadClass(classname);
}

On the other hand, that workaround is not really needed. It is possible to switch off Automatic Restart feature by passing -Dspring.devtools.restart.enabled=false to the application startup parameters.

Even simpler solution is to exclude spring-boot-devtools dependency from the exploded libraries that I package into the service Docker image:

        <plugin>
            <artifactId>maven-dependency-plugin</artifactId>
            <executions>
                <execution>
                    <phase>install</phase>
                    <goals>
                        <goal>copy-dependencies</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>${project.build.directory}/lib</outputDirectory>
                        <excludeArtifactIds>spring-boot-devtools</excludeArtifactIds>
                    </configuration>
                </execution>
            </executions>
        </plugin>

The following SO questions provide more details on this topic:

Class loader error - unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader

What is a NoSuchBeanDefinitionException and how do I fix it?

Sergey Shcherbakov
  • 4,534
  • 4
  • 40
  • 65