@WebListener does not work in a native image without additional reflection hints
oliveryasuna opened this issue · 6 comments
Reproducible with Spring Boot 3.1.0.
- Define a minimal servlet listener:
@WebListener
public class MyServletContextListener implements ServletContextListener {
}
- Add
@ServletComponentScan
to a configuration.
If you run the application from main
, everything works as expected. However, if you build a native image with GraalVM, you get an exception:
java.lang.ClassNotFoundException: com.oliveryasuna.MyServletContextListener
at org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader.loadClass(TomcatEmbeddedWebappClassLoader.java:72)
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1245)
at org.apache.catalina.core.DefaultInstanceManager.loadClass(DefaultInstanceManager.java:490)
at org.apache.catalina.core.DefaultInstanceManager.loadClassMaybePrivileged(DefaultInstanceManager.java:472)
at org.apache.catalina.core.DefaultInstanceManager.newInstance(DefaultInstanceManager.java:142)
at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4381)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4892)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1332)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1322)
at java.base@17.0.5/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base@17.0.5/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:871)
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:846)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1332)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1322)
at java.base@17.0.5/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.base@17.0.5/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:871)
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:241)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:428)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:913)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
at org.apache.catalina.startup.Tomcat.start(Tomcat.java:485)
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:123)
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.<init>(TomcatWebServer.java:104)
at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:489)
at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:211)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:183)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:161)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:602)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:733)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:435)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:311)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1305)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1294)
at com.oliveryasuna.Application.main(Application.java:21)
I assume that this issue would present itself on all servlet component types: @WebFilter
, @WebServlet
, and @WebListener
. I tried adding a reflection runtime hint, but this didn't work:
hints.reflection()
.registerType(
TypeReference.of(MyServletContextListener.class),
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS
);
This works for me with the following configuration:
@SpringBootApplication
@ServletComponentScan
@ImportRuntimeHints(TomcatnativeApplication.WebListenerHintsRegistrar.class)
public class TomcatnativeApplication {
public static void main(String[] args) {
SpringApplication.run(TomcatnativeApplication.class, args);
}
static class WebListenerHintsRegistrar implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.reflection().registerType(MyServletContextListener.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}
}
}
Additional reflection might be required (typically, invocation of public methods) - this depends on how the Servlet container is calling those methods.
Arguably, @ServletComponentScan
and the ServletComponentScanRegistrar
could automatically register runtime hints for those classes. I'm moving this issue to Spring Boot so this enhancement can be considered.
@bclozel Thank you. Not sure why wrapping in TypeReference.of
didn't work.
@oliveryasuna most probably your hint was not called at all during the AOT phase and there's a setup error that doesn't register it properly.
I don't think @WebServlet
and @WebFilter
are affected due to the way that they're registered. Their registration is strongly typed so there's no reflection involved and Graal can discover the classes during image compilation. @WebListener
is different as the listeners are handled as string class names. For example, this is how they're registered with Tomcat:
It then loads the classes using reflection.
We could probably rework ServletComponentScanRegistrar
to be an AOT contributor. That would prevent the need for a dedicated exclude filter.
Is this planned?