vaadin / quarkus

An extension to Quarkus to support Vaadin Flow
Apache License 2.0
28 stars 3 forks source link

Using custom servlet in Quarkus app fails with ClassNotFoundException #19

Open mvysny opened 3 years ago

mvysny commented 3 years ago

Description of the bug / feature

Using custom servlet in a Quarkus+Vaadin app causes DevModeInitializer to fail with ClassNotFoundException.

The reason is that Quarkus uses multiple classloaders; it loads Vaadin jars in one classloader and the app code (including the Servlet class) in another classloader. The "Vaadin classloader" is then unable to load the Servlet class.

The exception stacktrace follows:

2021-01-07 09:52:04,044 ERROR [io.qua.run.boo.StartupActionImpl] (Quarkus Main Thread) Error running Quarkus: java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at io.quarkus.runner.bootstrap.StartupActionImpl$3.run(StartupActionImpl.java:134)
    at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.ExceptionInInitializerError
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490)
    at java.base/java.lang.Class.newInstance(Class.java:584)
    at io.quarkus.runtime.Quarkus.run(Quarkus.java:61)
    at io.quarkus.runtime.Quarkus.run(Quarkus.java:38)
    at io.quarkus.runtime.Quarkus.run(Quarkus.java:104)
    at io.quarkus.runner.GeneratedMain.main(GeneratedMain.zig:29)
    ... 6 more
Caused by: java.lang.RuntimeException: Failed to start quarkus
    at io.quarkus.runner.ApplicationImpl.<clinit>(ApplicationImpl.zig:178)
    ... 15 more
Caused by: java.lang.RuntimeException: java.lang.RuntimeException: javax.servlet.ServletException: java.lang.reflect.InvocationTargetException
    at io.quarkus.undertow.runtime.UndertowDeploymentRecorder.bootServletContainer(UndertowDeploymentRecorder.java:519)
    at io.quarkus.deployment.steps.UndertowBuildStep$build-649634386.deploy_0(UndertowBuildStep$build-649634386.zig:1948)
    at io.quarkus.deployment.steps.UndertowBuildStep$build-649634386.deploy(UndertowBuildStep$build-649634386.zig:40)
    at io.quarkus.runner.ApplicationImpl.<clinit>(ApplicationImpl.zig:158)
    ... 15 more
Caused by: java.lang.RuntimeException: javax.servlet.ServletException: java.lang.reflect.InvocationTargetException
    at io.undertow.servlet.core.DeploymentManagerImpl.deploy(DeploymentManagerImpl.java:255)
    at io.quarkus.undertow.runtime.UndertowDeploymentRecorder.bootServletContainer(UndertowDeploymentRecorder.java:508)
    ... 18 more
Caused by: javax.servlet.ServletException: java.lang.reflect.InvocationTargetException
    at com.vaadin.flow.server.startup.ClassLoaderAwareServletContainerInitializer.onStartup(ClassLoaderAwareServletContainerInitializer.java:99)
    at io.undertow.servlet.core.DeploymentManagerImpl$1.call(DeploymentManagerImpl.java:205)
    at io.undertow.servlet.core.DeploymentManagerImpl$1.call(DeploymentManagerImpl.java:187)
    at io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:42)
    at io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
    at io.quarkus.undertow.runtime.UndertowDeploymentRecorder$10$1.call(UndertowDeploymentRecorder.java:554)
    at io.undertow.servlet.core.DeploymentManagerImpl.deploy(DeploymentManagerImpl.java:253)
    ... 19 more
Caused by: java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at com.vaadin.flow.server.startup.ClassLoaderAwareServletContainerInitializer.onStartup(ClassLoaderAwareServletContainerInitializer.java:94)
    ... 25 more
Caused by: javax.servlet.ServletException: Servlet class name (org.acme.servlet.MyServlet) can't be found!
    at com.vaadin.flow.server.startup.DevModeInitializer.process(DevModeInitializer.java:198)
    ... 30 more
Caused by: java.lang.ClassNotFoundException: org.acme.servlet.MyServlet
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
    at io.quarkus.bootstrap.classloading.QuarkusClassLoader.loadClass(QuarkusClassLoader.java:412)
    at io.quarkus.bootstrap.classloading.QuarkusClassLoader.loadClass(QuarkusClassLoader.java:365)
    at java.base/java.lang.Class.forName0(Native Method)
    at java.base/java.lang.Class.forName(Class.java:315)
    at com.vaadin.flow.server.startup.DevModeInitializer.isVaadinServletSubClass(DevModeInitializer.java:218)
    at com.vaadin.flow.server.startup.DevModeInitializer.process(DevModeInitializer.java:191)
    ... 30 more

Minimal reproducible example

Please see the attached app.

vaadin-quarkus.zip

  1. Unzip the app
  2. Run mvn -C clean package quarkus:dev

Expected behavior

Vaadin should use Thread's context class loader to load the classes

Actual behavior

Vaadin uses Class.forName() which uses DevModeInitializer.class.getClassLoader().

Versions:

- Vaadin / Flow version: 14.4.4/2.4.3
- Java version: 11
- Application Server (if applicable): Quarkus+Vertx+Undertow
denis-anisimov commented 3 years ago

The problem is in fundamentally broken code in DevModeInitializer which tries to use DeploymentConfiguration when there is no any DeploymentConfiguration: deployment configuration is intended to be used for a servlet. DevModeInitializer is invoked before any other initialization and it may not expect that there are some servlets at all. (and if there are several servlets then whose DeploymentConfiguration to use ?).

DevModeInitializer.isVaadinServletSubClass is used to find the Vaadin servlet to get its DeploymentConfiguration .

It's absolutely wrong to use DeploymentConfiguration in any ServletContextListener. The only config which can be available in the ServletContextListener is an application configuration. There is no any application configuration in V14 (and may be never be).

But this is already properly done in the master : Flow 6.0. If you are able to use V19/Flow 6.0 then please use it and there should not be the issue at all.

mvysny commented 3 years ago

Thank you ;) Yeah I've already took advantage of the com.vaadin.flow.di.ResourceProvider and implemented my own QuarkusResourceProvider which uses Thread.currentThread().getContextClassLoader() to load flow-build-info.json properly - excellent work! (sorry - not really related to this ticket; BUT using custom ResourceProvider fixes the original issue vaadin/flow#9713, which causes no need for a custom servlet, which renders this ticket unnecessary).

Unfortunately this kind of solution is not applicable to Vaadin 14 (since there is nothing similar to Lookup). Any chance of a backport of Lookup/ResourceProvider to Vaadin 14?

denis-anisimov commented 3 years ago

Lookup is only in the master. The changes related to Lookup impl and usage are quite breaking. I don't think they will be backported to the v14....... At least I have no intention to do this.

But you may create a ticket about this: let's redirect this question to the decision makers.