quarkus: Dev mode failing to compile changes on external JAR dependencies

I’m developing a web application using Quarkus, it’s a normal Maven project, but part of my business code is developed on another project that uses Gradle as it’s a requirement for building a Kotlin MPP library, so I configured this library written in Kotlin to compile for the following platforms: JVM (Quarkus), Android SDK (Android app) and Swift (iOS app). For JVM, the kotlin-multiplatform gradle plugin generates a JAR that works as expected on any JVM platform.

The idea behind this is a shared library to avoid duplicate code, I know that Kotlin MPP is more common to be used only for mobile apps, but I need to share unit code between client and server because my app also works offline and process the same logic used on the backend in some situations.

Quarkus behavior The issue is that Quarkus dev mode is unable to reload changes on this external library. Doing some research on existent issues, I found a closed issue #2865 and @stuartwdouglas says “If you have a multi module maven project this should work, as it will monitor the source directories of all the modules in the project.”.

I know that Quarkus dev mode can reloads a multi-module Maven project, but as sayed before, my business library cannot be compiled on Maven as kotlin multiplatform plugin only works with Gradle. There’s no magic to build the multiplatform artifacts, I just execute: $ ./gradlew build publishToMavenLocal.

I also attempted to migrate my Quarkus project to use Gradle as build tool instead of Maven and uses the business library as a Gradle module, but when I did it, I found a similiar problem that is already reported on issue #1098, then I prefered to just use Maven for Quarkus as it’s more stable.

NOTE: there is no need to configure Kotlin multiplatform specific features to reproduce the issue, you can do it with a normal Java project that generates a JAR.

Expected behavior

Quarkus dev mode should also reload code changes made to external JAR artifacts if configured to do so.

Actual behavior

Quarkus dev mode fails to compile changes made in a external JAR artifact, forcing me to stop and start dev mode again after each build of the external JAR.

How to Reproduce?

How to Reproduce

  1. Get the sample business project that generates JAR artifact. business-sample.zip

  2. Get the sample Quarkus project. quarkus-service.zip

  3. Build the business project: mvn install

  4. Run Quarkus Dev Mode: mvn compile quarkus:dev

  5. Send a request to http://localhost:8080/uuid endpoint.

  6. Look that the UUID was successful generated using the external business code

  7. Now, uncomment the randomUpperCase() method on the external business project.

public class JavaUUID {
  public String random() {
      return String.format("JAVA-%s",
          UUID.randomUUID().toString());
  }
  
  public String randomUpperCase() {
      return random().toUpperCase();
  }
}
  1. Build the business project: mvn clean install
  2. Change JAX-RS GET endpoint to use the external randomUpperCase() method instead of random():
@Path("/uuid")
@Produces(MediaType.APPLICATION_JSON)
public class UUIDResource {

  @GET
  public Response generate() {
      final String uuid = new JavaUUID().randomUpperCase();
      final LocalDateTime dateTime = LocalDateTime.now();
  
      return Response.ok(new UUIDResponse(uuid, dateTime)).build();
  }
}
  1. Send a request http://localhost:8080/uuid endpoint again.
  2. Look that Quarkus throws a RuntimeException and fails to compile the changes.
java.lang.NoSuchMethodError: 'java.lang.String org.acme.business.JavaUUID.randomUpperCase()'
	at org.acme.UUIDResource.generate(UUIDResource.java:18)
	at org.acme.UUIDResource_Subclass.generate$$superforward1(Unknown Source)
	at org.acme.UUIDResource_Subclass$$function$$2.apply(Unknown Source)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:53)
	at io.quarkus.arc.runtime.devconsole.InvocationInterceptor.proceed(InvocationInterceptor.java:62)
	at io.quarkus.arc.runtime.devconsole.InvocationInterceptor.monitor(InvocationInterceptor.java:49)
	at io.quarkus.arc.runtime.devconsole.InvocationInterceptor_Bean.intercept(Unknown Source)
	at io.quarkus.arc.impl.InterceptorInvocation.invoke(InterceptorInvocation.java:41)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.perform(AroundInvokeInvocationContext.java:40)
	at io.quarkus.arc.impl.InvocationContexts.performAroundInvoke(InvocationContexts.java:32)
	at org.acme.UUIDResource_Subclass.generate(Unknown Source)
  1. Comment any reference and import of the external business class on the JAX-RS resource.
  2. Send the request to the endpoint and see that the exception goes away.
  3. Restart Quarkus by pressing “S” on the terminal.
  4. Uncomment business code on the JAX-RS resource.
  5. Send the request again and you’ll see the same exception as before.
  6. Repeat steps 1 to 10 and see that this approach works only if you stop dev mode and starts again after each business project build. I think there could be a way to restart the application from scratch.

Output of uname -a or ver

Fernandos-iMac-Pro.local 21.5.0 Darwin Kernel Version 21.5.0: Tue Apr 26 21:08:22 PDT 2022; root:xnu-8020.121.3~4/RELEASE_X86_64 x86_64

Output of java -version

java version “17.0.3” 2022-04-19 LTS Java™ SE Runtime Environment (build 17.0.3+8-LTS-111) Java HotSpot™ 64-Bit Server VM (build 17.0.3+8-LTS-111, mixed mode, sharing)

GraalVM version (if different from Java)

No response

Quarkus version or git rev

Tested on 2.11 (stable) and master (999-SNAPSHOT)

Build tool (ie. output of mvnw --version or gradlew --version)

Maven home: /Users/fernando/.sdkman/candidates/maven/current Java version: 17.0.3, vendor: Oracle Corporation, runtime: /Users/fernando/.sdkman/candidates/java/17.0.3-oracle Default locale: en_US, platform encoding: UTF-8 OS name: “mac os x”, version: “12.4”, arch: “x86_64”, family: “mac”

Additional information

I research about Quarkus ClassLoader and I found a article written by @gsmet on the Quarkus Blog and it says “An important assumption of Quarkus is that the application lives in a closed world. You cannot dynamically add a jar at runtime to your Quarkus application and expect it to work.”

So, this is a limitation or it’s possible to do a workaround to enable a JAR redeployment at runtime? I’m happy with the development experience that Quarkus provide us, is fast even when I need to restart the dev mode from scratch…just curious about this behaviour.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 22 (21 by maintainers)

Commits related to this issue

Most upvoted comments

Thanks @fercomunello I’ll have a closer look next week.

I created a branch with a test for this last week. It’s waiting for the fix.

Also if we are going to support this we should add some tests for it.

In principal I am ok with the fix. Although maybe we should just be creating a new file manager each time anyway? I can’t actually remember if it was the file manager or compiler that was the bit that was slowing down reload time.

The class actually isn’t in the system classloader, so it’s something else. However, if I make a copy of the original external JAR right before compilation and use that copy of the JAR for the compiler’s classpath, it works.

I suspect the issue isn’t in the Quarkus code but is related to the fact that the external lib is in the system classloader and the Java compiler is somehow affected by that.

Got it, thanks.

Still not seeing it. Here is what I was trying:

  1. mvn clean quarkus:dev
  2. Load localhost:8080/uuid
  3. add method randomUpperCase in the JavaUUID
  4. modify the UUIDResource to use randomUpperCase
  5. Re-load localhost:8080/uuid and make sure it’s in upper case
  6. modify the generate method by adding -UUID suffix to the generated uuid
  7. Re-load localhost:8080/uuid and make sure the suffix is added

Does the above sequence fail in your environment?

The issue is that only modules of the project the application module originates from are live-reloadable by default. External modules aren’t. However, you could configure quarkus.class-loading.reloadable-artifacts if you really have to. Let us know if that worked for you. Thanks.