Quarkus: Building a Quarkus application with Java 12+ can result in failure to start it in Java 8 runtime

Created on 31 Oct 2019  Â·  33Comments  Â·  Source: quarkusio/quarkus

Quarkus applications are generated to be runnable against a minimum Java 8 version. However, there appears to be an issue where if a Quarkus application is built using Java 8+ (especially Java 12 or higher) and then that application is started on Java 8, then it runs into an exception. One such exception is:

Exception in thread "main" java.lang.ExceptionInInitializerError
    at io.quarkus.runner.GeneratedMain.main(GeneratedMain.zig:27)
Caused by: java.lang.RuntimeException: Failed to start quarkus
    at io.quarkus.runner.ApplicationImpl1.<clinit>(ApplicationImpl1.zig:325)
    ... 1 more
Caused by: java.lang.NoClassDefFoundError: java/lang/constant/ConstantDesc
    at io.quarkus.arc.runtime.QuarkusConfigProducer_ProducerMethod_produceStringConfigProperty_75511873be6a3e578c4555b07b03c7231f99c7bc_Bean.<init>(QuarkusConfigProducer_ProducerMethod_produceStringConfigProperty_75511873be6a3e578c4555b07b03c7231f99c7bc_Bean.zig:257)
    at io.quarkus.arc.setup.Default_ComponentsProvider.getComponents(Default_ComponentsProvider.zig:81)
    at io.quarkus.arc.impl.ArcContainerImpl.<init>(ArcContainerImpl.java:102)
    at io.quarkus.arc.Arc.initialize(Arc.java:19)
    at io.quarkus.arc.runtime.ArcRecorder.getContainer(ArcRecorder.java:34)
    at io.quarkus.deployment.steps.ArcProcessor$generateResources15.deploy_0(ArcProcessor$generateResources15.zig:72)
    at io.quarkus.deployment.steps.ArcProcessor$generateResources15.deploy(ArcProcessor$generateResources15.zig:36)
    at io.quarkus.runner.ApplicationImpl1.<clinit>(ApplicationImpl1.zig:295)
    ... 1 more
Caused by: java.lang.ClassNotFoundException: java.lang.constant.ConstantDesc
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 9 more

It looks like we are missing the use of -release option that should be configured in our Maven (and Gradle?) compiler plugins to prevent javac from using Java 8+ APIs in the generated class files.

arearc kinbug

All 33 comments

Simplest way to reproduce this is. Have a trivial application with REST endpoint which has this:

    @org.eclipse.microprofile.config.inject.ConfigProperty(name="greeting.message")
    String config;

then in application.properties:

greeting.message=hello

Build the application using Java 12 or higher:

export JAVA_HOME=<path to java 12 or higher>
mvn clean install -DskipTests=true

Now run the application using Java 8:

export JAVA_HOME=<path to Java 8>
export PATH=$JAVA_HOME/bin:$PATH
java -jar target/<appname>-runner.jar

you should see the exception stacktrace like the one in the description of this issue.

The more I look into this, the more this looks related to gizmo where it uses ASM to generate the class files. Although gizmo does use OpCodes.V1_8 for class file generation using ASM https://github.com/quarkusio/gizmo/blob/master/src/main/java/io/quarkus/gizmo/ClassCreator.java#L151, I don't know if that will prevent ASM from using APIs in the generated class file from a higher version of Java (if compiled against the higher version of Java like in this issue). @stuartwdouglas, before I dig deeper in ASM and gizmo, would you happen to know about whether or not ASM has a -release equivalent of javac for class file generation?

I am not sure. This would be an ASM issue rather than a Gizmo one.

I would grep for ConstantDesc in the ASM source code and see what shows up.

This would be an ASM issue rather than a Gizmo one.

Looks like it. I'll try to see if I can reproduce this in a non-Quarkus use case and then present it to ASM project as a issue/question.

After some debugging, it now looks like we might need @mkouba's help to sort this out.

What seems to be happening is, we have io.quarkus.arc.runtime.QuarkusConfigProducer which has (among other methods)

@Dependent
    @Produces
    @ConfigProperty
    String produceStringConfigProperty(InjectionPoint ip) {
        return ConfigProducerUtil.getValue(ip, String.class, getConfig(ip));
    }

During deployment processing, the io.quarkus.arc.processor.BeanDeployment in its findBeans does a call to Beans.createProducerMethod after finding the @Produces on that above method in QuarkusConfigProducer. Within the implementation of Beans.createProducerMethod it ends up identifying java.lang.String as a bean (because the produceStringConfigProperty is returning a String type) and not just that, it also identifies (via Types.getProducerMethodTypeClosure) various interfaces that java.lang.String implements as the types exposed by this bean. These types are then added into a HashSet in the generated code here https://github.com/quarkusio/quarkus/blob/master/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java#L618 and thus making its way all the way into the runtime code that will potentially be loaded in a Java 8 runtime.

Now it so happens that, starting Java 12+, java.lang.String has started implementing new interfaces (java.lang.constant.ConstantDesc, java.lang.constant.Constable and such) that have been introduced since Java 12. As a result, the class generated by the BeanGenerator is no longer loadable in Java 8.

I don't have much insight into Arc, so I'm not sure whether java.lang.String should be consider a bean type (does CDI allow String to be a bean type?) and even if it does if there should be a way to filter out types that don't belong to the target language runtime. Although this right now is specific to java.lang.String, I think we might run into this with other classes too. I don't know if there's a generic way to deal with this or if we should just deal with the String type for now and see how to fix it.

I'm going to unassign myself from this one for now, since this has now turned out to be related to Arc, which I don't have much knowledge of.

@jaikiran Thanks for the insightful analysis and for your time on this!

Yeah, so, for me it's not top priority on the shopping list to get applications built with JDK 12+ to run on Java 8.

I think we can live with it for now and get back to it when we have more time.

I agree

Wondering though what's the deal about String being considered a bean. Would this imply we have tons of unused useless beins being registered?

It's considered a bean for injecting config properties (and JWT claims).

JWT ?

On Thu, 31 Oct 2019, 11:08 Stuart Douglas, notifications@github.com wrote:

It's considered a bean for injecting config properties (and JWT claims).

—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/quarkusio/quarkus/issues/5062?email_source=notifications&email_token=AAAKMTLCSICW4W3DDSNJ423QRK4CJA5CNFSM4JHEFMI2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOECXLCTA#issuecomment-548319564,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAAKMTJGTMVDDMDABHWAOE3QRK4CJANCNFSM4JHEFMIQ
.

Well basically anything that does:

@SomeQualifier
@Produces
String stringProducer() {
 // produce a string
}

The main ones in the base platform are microprofile config which lets you inject config as a String, and Json Web Token support (smallrye-jwt) that allow you to inject claims from the token as a string.

Ah thanks for the explanation, that makes sense.

But I'd say I'd expect the qualifier to be mandatory.. I hope we don't
register and inspect just any and all String fields :)

On Thu, 31 Oct 2019, 11:31 Stuart Douglas, notifications@github.com wrote:

Well basically anything that does:

@SomeQualifier
@Produces
String stringProducer() {
// produce a string
}

The main ones in the base platform are microprofile config which lets you
inject config as a String, and Json Web Token support (smallrye-jwt) that
allow you to inject claims from the token as a string.

—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/quarkusio/quarkus/issues/5062?email_source=notifications&email_token=AAAKMTJMQQXFNNGOMBRWCSLQRK6YXA5CNFSM4JHEFMI2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOECXM7BY#issuecomment-548327303,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAAKMTIQJ4UMLVJZEYTZSHDQRK6YXANCNFSM4JHEFMIQ
.

@Sanne @Produces is mandatory ;-).

@jaikiran So the set of bean types of a producer method is defined by the spec - the return type, every superclass and all interfaces (see https://docs.jboss.org/cdi/spec/2.0/cdi-spec.html#producer_method_types). I think we can easily fix this one with @Typed(String.class) added to the producer method. The only problem would be that you wouldn't be able to resolve @ConfigProperty ConstantDesc injection points. And that's probably acceptable, or?

@jaikiran So this branch should fix this particular issue: https://github.com/mkouba/quarkus/tree/issue-5062

But I'm not sure we could fix this in a general way...

I think we can easily fix this one with @Typed(String.class) added to the producer method. The only problem would be that you wouldn't be able to resolve @ConfigProperty ConstantDesc injection points. And that's probably acceptable, or?

I don't have too much CDI usage knowledge nor have I interacted much with CDI users. So I'm not the best person to answer that question, but I think it's probably acceptable and I don't think there will be users injecting a ConstantDesc or even Constable and some other interfaces that String implements (perhaps CharSequence would be an exception?).

@jaikiran So this branch should fix this particular issue: https://github.com/mkouba/quarkus/tree/issue-5062

I built that locally against latest upstream master and gave it a try. The same use case which fails in upstream master and 0.27 of quarkus, now succeeds without any issues with this fix. I even tested some REST endpoints to make sure the value is injected and nothing else is broken. All seemed fine.

I'll let @stuartwdouglas and @gsmet decide when they might want to include that fix and whether or not they think this is a OK fix.

Thank you for looking into it.

@mkouba did you create a PR for this one, in the end?

Nope, I didn't receive a confirmation that it's an acceptable solution ;-). Shall I?

BTW once https://github.com/quarkusio/quarkus/pull/5387 is merged my workaround cannot be used anymore and we'll have to send a PR to SmallRye instead.

I get this as well -- having Java13, but using maven-compiler-plugin::release=11, and then running it inside Docker with "FROM fabric8/java-alpine-openjdk11-jre"

Exception in thread "main" java.lang.ExceptionInInitializerError
at io.quarkus.runner.GeneratedMain.main(GeneratedMain.zig:27)
Caused by: java.lang.RuntimeException: Failed to start quarkus
at io.quarkus.runner.ApplicationImpl.(ApplicationImpl.zig:455)
... 1 more
Caused by: java.lang.RuntimeException: Failed to initialize Arc
at io.quarkus.arc.Arc.initialize(Arc.java:26)
at io.quarkus.arc.runtime.ArcRecorder.getContainer(ArcRecorder.java:34)
at io.quarkus.deployment.steps.ArcProcessor$generateResources22.deploy_0(ArcProcessor$generateResources22.zig:72)
at io.quarkus.deployment.steps.ArcProcessor$generateResources22.deploy(ArcProcessor$generateResources22.zig:36)
at io.quarkus.runner.ApplicationImpl.(ApplicationImpl.zig:417)
... 1 more
Caused by: java.lang.ClassNotFoundException: java.lang.constant.ConstantDesc
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:583)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:398)
at io.smallrye.config.inject.ConfigProducer_ProducerMethod_produceStringConfigProperty_75511873be6a3e578c4555b07b03c7231f99c7bc_Bean.(ConfigProducer_ProducerMethod_produceStringConfigProperty_75511873be6a3e578c4555b07b03c7231f99c7bc_Bean.zig:308)
at io.quarkus.arc.setup.Default_ComponentsProvider.addBeans1(Default_ComponentsProvider.zig:386)
at io.quarkus.arc.setup.Default_ComponentsProvider.getComponents(Default_ComponentsProvider.zig:38)
at io.quarkus.arc.impl.ArcContainerImpl.(ArcContainerImpl.java:102)
at io.quarkus.arc.Arc.initialize(Arc.java:20)
... 5 more

Hi guys. I'm new to Quarkus and have just stumbled upon this problem. For me, I was building using JDK 13 and running using JDK 11. If I understand this correctly, this is a general problem. If a Quarkus app is built with one version of library, then the same version of library must also be used at runtime or you risk errors like that when for example types in that library rearrange inheritance from one version to another, although the app itself would normally be binary compatible with such change in the library. In the above example, the app didn't use the ConstantDesc type at all. So normally it wouldn't matter if it was a supertype of String or not and whether such type was present at runtime or not. I think Quarkus should not blindly register the types from hierarchy of bean type at compile time unless such types are actually used (at injection points for example, etc.) in the app. Only in that case would Quarkus built artifacts have binary compatibility that is normally expected in a Java app. So what do you think?

I think Quarkus should not blindly register the types from hierarchy of bean type at compile time unless such types are actually used (at injection points for example, etc.) in the app. Only in that case would Quarkus built artifacts have binary compatibility that is normally expected in a Java app. So what do you think?

I am not a CDI expert, but I don't think we can impose such a restriction because of the fact that one can use Arc.container().instance(...) to get beans at runtime without there being an injection point.

What I propose we do for now, is that we just hard-code an exclusion for the two problematic interfaces (which are used by String in Java 12+) in the Arc code (I made a small modification to Arc to do this and it does fix the reported problem).
Of course it will mean that users won't be able to inject ConstantDesc and Constable but I don't think that will be such a common case anyway...
What you folks are reporting is a much more common case and that is the one I think we need to address.

WDYT @mkouba @stuartwdouglas ?

Ah, yes. That's what I realized soon after writing the comment. If a bean can be requested by its type programmatically, then Quarkus can't know at compile time what type will be requested.
I'm wondering why do all the super-types of a bean need to be resolved at compile time and then hard-coded into generated code? Is this just so that at run-time the generated code can "find" the bean as quickly as possible? Can't a lookup table be constructed at run-time? I think the fact that this problem was uncovered for String/ConstantDesc/Constable is just a coincidence and more similar problems might be expected in the future. Working around the problem for one case might not be enough.

Frankly, I’m not sure it’s worth the hassle. Is there a good reason to build with JDK 11 and run with JDK 8?

@gsmet isn't valid to a lower release? I mean it's probably not terribly common for an application, but it seems like the price to pay (banning these arcane interfaces from CDI injection is small)

I think we should just hard code restrictions around the problematic interfaces. Enough people seem to be affected by this that it is worth doing.

So the hard-coded restrictions are a superugly hack and as @plevart mentions there will be probably other similar use cases. On the other hand, I agree that we should fix the most common use case now and possibly work on a better solution in the future.

Agreed, I did that in #6517

Is there an issue already to track a more general solution?

@plevart There is not. Feel free to open one containing all the information you think is relevant.

Thanks

Was this page helpful?
0 / 5 - 0 ratings