spring-projects / spring-boot

Spring Boot helps you to create Spring-powered, production-grade applications and services with absolute minimum fuss.
https://spring.io/projects/spring-boot
Apache License 2.0
75.43k stars 40.76k forks source link

Empty spring-boot-loader Directory Causing ClassNotFoundException in Dockerized Spring Boot Application #41014

Closed refeccd closed 5 months ago

refeccd commented 5 months ago

Packing the docker image according to the spring-boot 3.3.0 documentation I find that spring-boot-loader is empty in the extracted directories

➜  distributor-starter-1.0.0-SNAPSHOT git:(main) tree
.
├── application
│   ├── distributor-starter-1.0.0-SNAPSHOT.jar
│   └── lib
│       ├── distributor-api-1.0.0-SNAPSHOT.jar
│       ├── distributor-api-impl-1.0.0-SNAPSHOT.jar
│       ├── distributor-common-1.0.0-SNAPSHOT.jar
│       ├── distributor-dal-1.0.0-SNAPSHOT.jar
│       ├── distributor-facade-1.0.0-SNAPSHOT.jar
│       ├── distributor-service-1.0.0-SNAPSHOT.jar
│       └── distributor-web-1.0.0-SNAPSHOT.jar
├── dependencies
│   └── lib
│       ├── HdrHistogram-2.2.1.jar
│       ├── HikariCP-5.1.0.jar
│       ├── LatencyUtils-2.0.3.jar
│       ├── android-json-0.0.20131108.vaadin1.jar
│       ├── aopalliance-1.0.jar
│       ├── apollo-client-2.2.0.jar
│       ├── apollo-core-2.2.0.jar
│       ├── aspectjweaver-1.9.22.jar
│       ├── bcprov-jdk18on-1.78.1.jar
│       ├── checker-qual-3.42.0.jar
│       ├── classmate-1.7.0.jar
│       ├── commons-codec-1.16.1.jar
│       ├── commons-collections4-4.4.jar
│       ├── commons-compress-1.26.2.jar
│       ├── commons-io-2.16.1.jar
│       ├── commons-lang3-3.14.0.jar
│       ├── commons-pool2-2.12.0.jar
│       ├── error_prone_annotations-2.26.1.jar
│       ├── failureaccess-1.0.2.jar
│       ├── gson-2.10.1.jar
│       ├── guava-33.2.1-jre.jar
│       ├── guice-5.0.1.jar
│       ├── hibernate-validator-8.0.1.Final.jar
│       ├── hutool-all-5.8.28.jar
│       ├── j2objc-annotations-3.0.0.jar
│       ├── jackson-annotations-2.17.1.jar
│       ├── jackson-core-2.17.1.jar
│       ├── jackson-databind-2.17.1.jar
│       ├── jackson-datatype-jdk8-2.17.1.jar
│       ├── jackson-datatype-jsr310-2.17.1.jar
│       ├── jackson-module-blackbird-2.17.1.jar
│       ├── jackson-module-parameter-names-2.17.1.jar
│       ├── jakarta.annotation-api-2.1.1.jar
│       ├── jakarta.validation-api-3.0.2.jar
│       ├── javax.inject-1.jar
│       ├── jboss-logging-3.5.3.Final.jar
│       ├── jsqlparser-4.9.jar
│       ├── jsr305-3.0.2.jar
│       ├── jul-to-slf4j-2.0.13.jar
│       ├── kafka-clients-3.7.0.jar
│       ├── lettuce-core-6.3.2.RELEASE.jar
│       ├── listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar
│       ├── log4j-api-2.23.1.jar
│       ├── log4j-to-slf4j-2.23.1.jar
│       ├── logback-classic-1.5.6.jar
│       ├── logback-core-1.5.6.jar
│       ├── logstash-logback-encoder-7.4.jar
│       ├── lombok-1.18.32.jar
│       ├── lz4-java-1.8.0.jar
│       ├── micrometer-commons-1.13.0.jar
│       ├── micrometer-core-1.13.0.jar
│       ├── micrometer-jakarta9-1.13.0.jar
│       ├── micrometer-observation-1.13.0.jar
│       ├── micrometer-registry-prometheus-1.13.0.jar
│       ├── mybatis-3.5.16.jar
│       ├── mybatis-plus-3.5.6.jar
│       ├── mybatis-plus-annotation-3.5.6.jar
│       ├── mybatis-plus-core-3.5.6.jar
│       ├── mybatis-plus-extension-3.5.6.jar
│       ├── mybatis-plus-spring-boot-autoconfigure-3.5.6.jar
│       ├── mybatis-plus-spring-boot3-starter-3.5.6.jar
│       ├── mybatis-spring-3.0.3.jar
│       ├── mysql-connector-j-8.3.0.jar
│       ├── netty-buffer-4.1.110.Final.jar
│       ├── netty-codec-4.1.110.Final.jar
│       ├── netty-common-4.1.110.Final.jar
│       ├── netty-handler-4.1.110.Final.jar
│       ├── netty-resolver-4.1.110.Final.jar
│       ├── netty-transport-4.1.110.Final.jar
│       ├── netty-transport-native-unix-common-4.1.110.Final.jar
│       ├── prometheus-metrics-config-1.2.1.jar
│       ├── prometheus-metrics-core-1.2.1.jar
│       ├── prometheus-metrics-exposition-formats-1.2.1.jar
│       ├── prometheus-metrics-model-1.2.1.jar
│       ├── prometheus-metrics-shaded-protobuf-1.2.1.jar
│       ├── prometheus-metrics-tracer-common-1.2.1.jar
│       ├── reactive-streams-1.0.4.jar
│       ├── reactor-core-3.6.6.jar
│       ├── slf4j-api-2.0.13.jar
│       ├── snakeyaml-2.2.jar
│       ├── snappy-java-1.1.10.5.jar
│       ├── spring-aop-6.1.8.jar
│       ├── spring-beans-6.1.8.jar
│       ├── spring-boot-3.3.0.jar
│       ├── spring-boot-actuator-3.3.0.jar
│       ├── spring-boot-actuator-autoconfigure-3.3.0.jar
│       ├── spring-boot-autoconfigure-3.3.0.jar
│       ├── spring-boot-configuration-metadata-3.3.0.jar
│       ├── spring-boot-jarmode-tools-3.3.0.jar
│       ├── spring-boot-properties-migrator-3.3.0.jar
│       ├── spring-context-6.1.8.jar
│       ├── spring-context-support-6.1.8.jar
│       ├── spring-core-6.1.8.jar
│       ├── spring-data-commons-3.3.0.jar
│       ├── spring-data-keyvalue-3.3.0.jar
│       ├── spring-data-redis-3.3.0.jar
│       ├── spring-expression-6.1.8.jar
│       ├── spring-jcl-6.1.8.jar
│       ├── spring-jdbc-6.1.8.jar
│       ├── spring-kafka-3.2.0.jar
│       ├── spring-messaging-6.1.8.jar
│       ├── spring-oxm-6.1.8.jar
│       ├── spring-retry-2.0.6.jar
│       ├── spring-tx-6.1.8.jar
│       ├── spring-web-6.1.8.jar
│       ├── spring-webmvc-6.1.8.jar
│       ├── tomcat-embed-core-10.1.24.jar
│       ├── tomcat-embed-el-10.1.24.jar
│       ├── tomcat-embed-websocket-10.1.24.jar
│       └── zstd-jni-1.5.5-6.jar
├── snapshot-dependencies
│   └── lib
│       ├── common-1.0.0-SNAPSHOT.jar
│       ├── common-util-1.0.0-SNAPSHOT.jar
│       ├── common-web-1.0.0-SNAPSHOT.jar
│       ├── spring-boot-starter-apollo-1.0.0-SNAPSHOT.jar
│       ├── spring-boot-starter-cache-1.0.0-SNAPSHOT.jar
│       ├── spring-boot-starter-kafka-1.0.0-SNAPSHOT.jar
│       ├── spring-boot-starter-logger-1.0.0-SNAPSHOT.jar
│       ├── spring-boot-starter-mybatis-1.0.0-SNAPSHOT.jar
│       ├── spring-boot-starter-starrocks-1.0.0-SNAPSHOT.jar
│       └── spring-support-1.0.0-SNAPSHOT.jar
└── spring-boot-loader

8 directories, 128 files

This causes an error on startup

➜  distributor-starter-1.0.0-SNAPSHOT git:(main) java -jar application/distributor-starter-1.0.0-SNAPSHOT.jar
Exception in thread "main" java.lang.NoClassDefFoundError: org/springframework/boot/SpringApplication
        at com.xxx.canal.distributor.Application.main(Application.java:12)
Caused by: java.lang.ClassNotFoundException: org.springframework.boot.SpringApplication
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
        ... 1 more

I tried the latest Spring-Boo-3.3.1-SNAPSHOT and the error is still there

refeccd commented 5 months ago

Also I seem to have found a problem in the documentation. If you do a docker multi-stage build as per the docs, shouldn't there be no application.jar in the final ENTRYPOINT?

wilkinsona commented 5 months ago

I can't reproduce the behavior that you have described. The empty spring-boot-loader directory is to be expected as the loader is not used with the CDS-friendly layout in Spring Boot 3.3. The failure looks to me like the application's dependencies are not in the correct place relative to the application.jar file that refers to them from its Class-Path manifest entry.

If you do a docker multi-stage build as per the docs, shouldn't there be no application.jar in the final ENTRYPOINT?

No, there should be. It contains the application's own classes and, through its Class-Path manifest entry, adds all of the application's dependencies to the classpath.

If you would like us to spend some more time investigating, please spend some time providing a complete yet minimal sample that reproduces the problem. You can share it with us by pushing it to a separate repository on GitHub or by zipping it up and attaching it to this issue.

andre161292 commented 5 months ago

We were just running into this error too. After some digging, https://github.com/spring-projects/spring-boot/commit/c22548982abf0b9c5c02406fe789b0423fcfa02d seems to be the breaking change here, as the JarLauncher was relocated from org.springframework.boot.loader.JarLauncher to org.springframework.boot.loader.launch.JarLauncher.

The fix for us was to change the docker entrypoint from

ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

to

ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
wilkinsona commented 5 months ago

Thanks, @andre161292, but that's not the same problem. The change to the name of the JarLauncher was made in Spring Boot 3.2. The change described in this issue is specific to Spring Boot 3.3 and its CDS support. You can also see above that the application is being launched using java -jar where the launcher's class name is not specified.

refeccd commented 5 months ago

I'm sorry it was due to my own negligence. I wrongly assumed that the structure after copying was the same as the structure after extracting. So I ignored the part of the dockerfile that was copied from builder, and directly executed application.jar with java -jar, which reported a ClassNotFoundException. I wrote the entire dockerfile in its entirety and it runs fine

wilkinsona commented 5 months ago

Thanks for letting us know.

azalesky commented 5 months ago

The empty spring-boot-loader directory is to be expected

Why spring-boot-loader directory is created and mentioned in documentation if it is expected to be empty?

wilkinsona commented 5 months ago

Because it's one of the default layers. Whether or not it has any content depends on the specific type of extraction that you use. This is similar to the snapshot-dependencies directory which may also be empty if the application does not have any snapshot dependencies.

azalesky commented 5 months ago

Thank you for fast response. Extraction with option --launcher created non empty spring-boot-loader directory.