jetty / jetty.project

Eclipse Jetty® - Web Container & Clients - supports HTTP/2, HTTP/1.1, HTTP/1.0, websocket, servlets, and more
https://eclipse.dev/jetty
Other
3.86k stars 1.91k forks source link

Jetty's use of `java.util.ServiceLoader$Provider` breaks on Android (as it is not present) #8912

Closed chefchefchef closed 2 months ago

chefchefchef commented 1 year ago

Jetty version(s) Jetty 11

Java version/vendor (use: java -version) Dalvik

OS type/version Android SDK 33

Description With Javalin 4 (Jetty 9) and Android I had no problems. After migrating to Javalin 5 (Jetty 11) my server doesn't startup anymore. in JDK 11 I can find the missing class (ServiceLoader$Provider) but in android sdk I cannot (https://developer.android.com/reference/java/util/ServiceLoader). I use Java 11 and compileSdkVersion 33. I cannot find ServiceLoader$Provider in SDK of Android API 33 (when decompiled).

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: ch.xxx.yyy, PID: 6634
    java.lang.NoClassDefFoundError: Failed resolution of: Ljava/util/ServiceLoader$Provider;
        at org.eclipse.jetty.util.TypeUtil.serviceProviderStream(TypeUtil.java:800)
        at org.eclipse.jetty.http.PreEncodedHttpField.(PreEncodedHttpField.java:42)
        at org.eclipse.jetty.http.MimeTypes$Type.(MimeTypes.java:91)
        at org.eclipse.jetty.http.MimeTypes$Type.(MimeTypes.java:49)
        at org.eclipse.jetty.http.MimeTypes$Type.values(MimeTypes.java:47)
        at org.eclipse.jetty.http.MimeTypes.lambda$static$0(MimeTypes.java:169)
        at org.eclipse.jetty.http.MimeTypes$$ExternalSyntheticLambda4.get(Unknown Source:0)
        at org.eclipse.jetty.util.Index$Builder.withAll(Index.java:346)
        at org.eclipse.jetty.http.MimeTypes.(MimeTypes.java:166)
        at org.eclipse.jetty.server.handler.ContextHandler.doStart(ContextHandler.java:883)
        at org.eclipse.jetty.servlet.ServletContextHandler.doStart(ServletContextHandler.java:306)
        at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:93)
        at org.eclipse.jetty.util.component.ContainerLifeCycle.start(ContainerLifeCycle.java:171)
        at org.eclipse.jetty.server.Server.start(Server.java:470)
        at org.eclipse.jetty.util.component.ContainerLifeCycle.doStart(ContainerLifeCycle.java:114)
        at org.eclipse.jetty.server.handler.AbstractHandler.doStart(AbstractHandler.java:89)
        at org.eclipse.jetty.server.Server.doStart(Server.java:415)
        at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:93)
        at io.javalin.jetty.JettyServer.start(JettyServer.kt:74)
        at io.javalin.Javalin.start(Javalin.java:171)
        at io.javalin.Javalin.start(Javalin.java:148)
        at ch.xxx.yyy.server.YYYServer.start(YYYServer.kt:237)
        at ch.xxx.yyy.server.ServerBinder.start(ServerBinder.kt:31)
        at ch.xxx.yyy.startup.ServerStateHandler$manage$1$1.invokeSuspend(ServerStateHandler.kt:30)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.internal.LimitedDispatcher.run(LimitedDispatcher.kt:42)
        at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:95)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
        Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@1766ac3, Dispatchers.Main.immediate]
    Caused by: java.lang.ClassNotFoundException: Didn't find class "java.util.ServiceLoader$Provider" on path: DexPathList[[zip file "/data/app/~~vM8Ki57CqJhIAaTm3OsTqw==/ch.xxx.yyy-S1y5IOg5qayhzEybJnMheg==/base.apk"],nativeLibraryDirectories=[/data/app/~~vM8Ki57CqJhIAaTm3OsTqw==/ch.xxx.yyy-S1y5IOg5qayhzEybJnMheg==/lib/arm64, /system/lib64, /system_ext/lib64]]
        at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:218)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
            ... 32 more

How to reproduce? Run Javalin 5 / Jetty 11 on Android

joakime commented 1 year ago

We cannot fix this.

java.util.ServiceLoader.Provider was added in OpenJDK 9.

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/ServiceLoader.Provider.html

There many components that rely on this, and Jetty is only a small part of this reliance. This is problem outside of our control, and seems to be entirely within Android, or your use of the Android SDK.

Looking at the android source tree, their JDK 11 has the above class. https://android.googlesource.com/platform/libcore/+/refs/tags/jdk11u/jdk-11.0.18+3/src/java.base/share/classes/java/util/ServiceLoader.java#440

joakime commented 1 year ago

Closing, this works for others on Android. You have to configure your project to compile on Java 11. This error only shows up if you haven't done that properly.

zugazagoitia commented 1 year ago

I can't get it working any way, using JDK11 and Android SDK33.

I tested for JDK 11 using [this API](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#isBlank()): https://github.com/zugazagoitia/jett11-android13-mpoc/blob/6bda1e48c7dcaeedde9a88b9d378a5c2ddc0894d/app/src/main/java/com/example/sample/MainActivity.java#L34

You can find a minimal working example here: https://github.com/zugazagoitia/jett11-android13-mpoc

The java.lang.NoClassDefFoundError is raised as soon as a request is made. Javalin just used another Jetty class to trigger the same result earlier.

The reason, as stated earlier, is clear: the ServiceLoader class in the android SDK is missing the Provider interface used by Jetty.

My question is will this be addressed or is it a priority at all?

sbordet commented 1 year ago

@zugazagoitia did you open an issue to the Android SDK to add the Provider interface?

zugazagoitia commented 1 year ago

@zugazagoitia did you open an issue to the Android SDK to add the Provider interface?

I've just commented on this issue, which seems they were considering.

zugazagoitia commented 1 year ago

Update: They implemented the rest of the ServiceLoader class into the main working tree. This change is however not included in Android 14 (API Level 34) [1][2], likely in a future Android 15 and/or API Level 35 release scheduled for Q3 2024.

LukeXeon commented 1 year ago

This works for me, you can create a new Android module and make sure its build.gradle uses Java 1.8,here we temporarily call it "ponyfill".

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

Then create a placeholder ServiceLoader type under this module (you don’t need to actually implement it), and add the Provider interface definition inside it. Android will always load the system class first when loading a class, so it It will not use the ServiceLoader we used to place the placeholder, but when it cannot find the Provider, it will use the class we provided.

package java.util;

import java.util.function.Supplier;

public final class ServiceLoader<S> {
    public interface Provider<S> extends Supplier<S> {
        Class<? extends S> type();

        S get();
    }
}

Finally, you also need to "desugar" Java in your application module

android{

    compileOptions {
        coreLibraryDesugaringEnabled true
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_11
    }

}

dependencies {
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.3'
    implementation project(":ponyfill")
}
LukeXeon commented 1 year ago

However, things are always full of compromises, and I found a case where Jetty called a method that didn't exist on Android. On Android's non-standard jdk, ServiceLoader has no stream() method no matter what version it is, and Google's "desugar" doesn't handle this, which is frustrating and causes crashes org/eclipse/jetty/util/security/Credential.java Ideally it would be great if Jetty also used ServiceLoaderSpliterator to implement Credential org/eclipse/jetty/util/ServiceLoaderSpliterator.java This is even just a piece of cake, maybe I can raise a PR if needed?

iammmmmmm commented 8 months ago

I successfully ran Javalin (depends on Jetty11) 6.0.1 on Android (SDK 33)! Although it is just a simple “Hello World,” it returned as expected.

I embedded Javalin in a JavaFX program and then successfully packaged the APK through the Maven plugin and GraalVM provided by Gluon. It successfully ran on both Android 9 and Android 13.

The entire test project’s code is available at:here

Packaging Environment Information:

GraalVM CE Gluon: GraalVM CE Gluon 22.1.0.1-Final Platform: Kali Linux x86 image Build Tools: Maven and Gluon Maven Plugin

G%33TO8@Z%WTUS25 JYI3O3 image image

Snipaste_2024-08-08_11-22-32

image image

Snipaste_2024-08-08_11-26-24
emartynov commented 2 months ago

This works for me, you can create a new Android module and make sure its build.gradle uses Java 1.8,here we temporarily call it "ponyfill".

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

Then create a placeholder ServiceLoader type under this module (you don’t need to actually implement it), and add the Provider interface definition inside it. Android will always load the system class first when loading a class, so it It will not use the ServiceLoader we used to place the placeholder, but when it cannot find the Provider, it will use the class we provided.

package java.util;

import java.util.function.Supplier;

public final class ServiceLoader<S> {
    public interface Provider<S> extends Supplier<S> {
        Class<? extends S> type();

        S get();
    }
}

Finally, you also need to "desugar" Java in your application module

android{

    compileOptions {
        coreLibraryDesugaringEnabled true
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_11
    }

}

dependencies {
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.3'
    implementation project(":ponyfill")
}

Interesting that this class and method are in desugaring library https://github.com/google/desugar_jdk_libs/tree/master/jdk11/src/java.base/share/classes/java/util

emartynov commented 2 months ago

Not sure if desugaring is also applied for the test app (I assume that WireMock is running in context of the test app).

zugazagoitia commented 2 months ago

Update: They implemented the rest of the ServiceLoader class into the main working tree. This change is however not included in Android 14 (API Level 34) [1][2], likely in a future Android 15 and/or API Level 35 release scheduled for Q3 2024.

Since Android 15 has been released, I think this issue can be closed. @joakime @sbordet

sbordet commented 2 months ago

Thanks for the feedback!