Open brunoborges opened 3 years ago
I guess this is more on the Framework side of things at https://github.com/spring-projects/spring-framework. The java.beans
package is quite a central piece if you ask me. Boot has a couple of dependencies to java.beans
& java.awt
as well, but the heavy lifting must be done on the framework side. Let's see what the team has to say on this and if they want to move the ticket there.
Having that said - I'd love to see this in a perfect world, but I doubt it's easy or straightforward, if at all possible.
We are aware of our rather wide-spread dependencies across the traditional JDK-scoped libraries. Some of those are entirely optional (e.g. JNDI, JMX, RMI and JSR-223 Scripting are only used on demand and do not have to be present at runtime - at least from the core Spring Framework perspective) but the java.beans
package is indeed a hard case. The JDK's own historic misdesign with the unnecessary AWT dependencies in that package caused its inclusion in java.desktop
, unfortunately ignoring the common use of the JavaBeans introspector and some of its API types in server-side libraries.
Our only way out is to reimplement the introspection algorithm ourselves and to replace java.beans.PropertyDescriptor
and co with corresponding API types of our own, so that's what we're considering for Spring Framework 6.0 (along with the general JDK baseline upgrade and the introduction of module-info
definitions across the codebase). This will cause binary compatibility breakage in a few places, but so will the Jakarta EE 9 API migration with its namespace change (also planned for 6.0). So anything we do in that respect, we'll definitely do it for 6.0 (and might then further refine it in 6.x releases).
Thanks a lot Juergen for sharing the roadmap for 6.0. This will certainly speed up modernization of Java applications with latest JDK releases, as Spring has become a de facto standard of server side development.
In the meantime, is there official documentation and/or tooling to help developers produce customized jlink runtimes for 5 and older?
Your comment that some modules are optional is key, but I haven't found this in the docs.
That will help on native side as well.
Here are some more data points. I found that with a vanilla Spring Boot webflux app jdeps
will report that it needs
java.base,java.desktop,java.instrument,java.management,java.naming,java.prefs,java.rmi,java.scripting,java.sql,jdk.httpserver,jdk.jfr,jdk.unsupported
(so no XML modules). If you remove the snakeyaml
dependency (which needs java.logging
) you can run the app with just
java.base,java.desktop,java.naming
You have to add back java.management
to get the full logs including JVM uptime and avoid a warning about the PID not being
discovered.
If Spring 6 could shed java.desktop
and java.naming
that would be cool. The naming dependency comes from CommonAnnotationBeanPostProcessor
(there's a reference to a SimpleJndiBeanFactory
which is probably never used), so it seems like that could be removed.
I suppose it's an open question for the JDK why jdeps
thinks you need all those other modules when actually you don't, at least for the "normal" code paths.
The code I used to test: https://github.com/dsyer/sample-docker-microservice
UPDATE: you can run without java.naming
if you use Spring AOT and -DspringAot=true
.
Our JNDI support and therefore the java.naming
module is being referenced in two common places, it seems: CommonAnnotationBeanPostProcessor
and StandardServletEnvironment
. The JNDI support itself is totally optional, we're only really referring to API types such as javax.naming.NamingException
, so we could easily make this defensive even in 5.3.x. I'll see what I can do for 5.3.11 there :-)
I suppose it's an open question for the JDK why jdeps thinks you need all those other modules when actually you don't, at least for the "normal" code paths.
jdeps --print-module-deps
transitively analyzes libraries on the class path and module path if referenced (see jdeps -h output. I included it below). It finds the compile-time view of the transitive dependences. You can use jdeps --compile-view
to look at the dependencies (package-level or class-level to understand what depends on what).
Just spring-boot-2.5.5.jar
itself requires java.base
, java.desktop
, java.logging
, java.management
, java.naming
, java.sql
and java.xml
This command will show package-level dependences that you can find out what classes references these modules.
$ jdeps --multi-release 17 -cp $CP ${MAVEN_REPOSITORY}/org/springframework/boot/spring-boot/2.5.5/spring-boot-2.5.5.jar
(You can use --verbose:class
to see the class-level dependencies).
Hope this helps.
$ jdeps --help
:
--list-deps Lists the module dependences. It also prints
any internal API packages if referenced.
This option transitively analyzes libraries on
class path and module path if referenced.
Use --no-recursive option for non-transitive
dependency analysis.
--list-reduced-deps Same as --list-deps with not listing
the implied reads edges from the module graph.
If module M1 reads M2, and M2 requires
transitive on M3, then M1 reading M3 is implied
and is not shown in the graph.
--print-module-deps Same as --list-reduced-deps with printing
a comma-separated list of module dependences.
This output can be used by jlink --add-modules
in order to create a custom image containing
those modules and their transitive dependences.
Thanks @mlchung that's a useful summary and a good example. The point really is that spring-boot-*.jar
has mandatory and optional dependencies and jdeps
doesn't know how to tell the difference. I think there might be a way to distinguish if we had module-info
in our jars. The module system itself has requires static
for optional dependencies at runtime, so that might be the way forward for Spring 6.
Just tried to run Spring without java.desktop and bumped into AnnotationBeanNameGenerator dependency on java.beans.Introspector - just for the sake of using Introspector.decapitalize() in buildDefaultBeanName() :).
As a workaround, could you provide the "optimal" module configuration for spring with jdeps
and jlink
?
For example:
--add-modules $(jdeps --ignore-missing-deps --print-module-deps application.jar),java.xml,java.desktop,java.instrument,java.management,java.naming,java.prefs,java.rmi,java.scripting,java.sql,jdk.httpserver,jdk.jfr,jdk.unsupported,java.security.jgss
Am I missing something, or is anything obsolete?
It depends which features you need at runtime. E.g. see my examples above https://github.com/spring-projects/spring-framework/issues/26884#issuecomment-928948327 which both have fewer modules than yours.
I suppose it's an open question for the JDK why
jdeps
thinks you need all those other modules when actually you don't, at least for the "normal" code paths.
@dsyer this is because jdeps
doesn't look at codepath, but at class library dependencies, which means "we don't know, this might need this class, because the import is there, so there goes the module!"
It depends which features you need at runtime. E.g. see my examples above #26884 (comment) which both have fewer modules than yours.
Would the docker build fail if a module is missing that my application would required? I mean, is it guaranteed the missing module is not just discovered later during runtime?
Pretty sure it’s a runtime error. Why don’t you try it and report back?
I can report back missing modules result in runtime errors, which is really bad.
I discovered it when using a DataSourceUtils.getConnection(ds);
call. As I did not catch exceptions at this stage, the app always exited with statuscode=0 inside a docker
container, but worked without problems locally.
Took me several hours to identify the spot, and fix it by including jdeps java.sql.rowset
module additionally to java.sql
.
Beware that obviously building spring-boot apps with a jdeps minimal jre modules configuration might lead to unpredictable results.
I am running spring boot 3 (spring framework 6), and still can't run it without java.desktop dependency. I thought the plan was to remove the unnecessary dependencies in spring framework 6. This has not been done yet or the plan was abandoned? Thanks for any info.
@sid-hbm The issue is still open and hasn't been done yet. The target is for M6, but that's not a cast-iron guarantee.
@jhoeller After some additional analysis, I found that our use of java.beans.Introspector
in CachedIntrospectionResults
, ExtendedBeanInfo
and ExtendedBeanInfoFactory
is the biggest source of increased footprint for command-line-runner
Spring AOT smoke test sample compared to Spring Native where we had a substitution to avoid getting those dependencies.
The 288 additional classes related to AWT shipped in a native image with our current arrangement is available here which increases the RSS footprint by 3.35M
. I suspect that this refactoring will allow even bigger footprint reduction since other classes are likely used transitively (the substitution just removes AWT dependency).
So strong +1 from me to fix this issue that significantly impacts Spring native application efficiency.
Maybe in context of this issue, it might be possible for Spring to validate the included jlink
modules, and alert if one is missing, regarding to the used classes?
I debugged my application for several days due to a SSLHandshakeException
that only look place in production.
Turned out that because I used jlink, I was missing the jdk.crypto.ec
and jdk.crypto.cryptoki
module to use HTTPS.
Unfortunately, if the module is missing, the error only occurs at runtime, which makes it even worse...
As per our 6.0 wrap-up discussions and my recent comment on #18079, the module system has not been a priority for 6.0 (for reasons explained in that comment). Not least of it all, we are not shipping module descriptors for jlink usage yet.
There would be some value in removing/reducing our dependency on the java.desktop
module or rather the java.beans
package specifically. However, this is not just about our internal delegation to the java.beans.Introspector
, it is also about the API exposure of the common java.beans.PropertyEditor
and java.beans.PropertyDescriptor
types. Since there is plenty of third-party code (not just applications but also libraries) depending on those Spring beans APIs, we cannot easily replace them completely; we'd rather have to phase them out over a longer period and deal with the widespread disruption caused there.
At the same time, the strategic value of not requiring the presence of the java.desktop
module is also being challenged. GraalVM's native images are based on a reachability algorithm which selectively includes types as they are actually being referenced in application and framework code, independent from deployment-level module boundaries. The proposed notion of "static images" in OpenJDK's Project Leyden might follow a similar approach. The benefits of such specifically tailored images outweigh the limited benefits of a custom module selection for jlink, with JDK module boundaries becoming less relevant.
We are considering a reimplementation of the beans introspection algorithm to not have to call the java.beans.Introspector
anymore. Even that is not to be taken lightly since there are many subtleties in the JDK's algorithm there. Also, a lot of other libraries and frameworks (as commonly found in Spring-based application stacks) also use the java.beans.Introspector
; only if all of those removed all of their usage of the java.beans
package, the java.desktop
module could actually be omitted. This is not likely to happen in the near term, as there has been very little movement in that direction up to now.
Last but not least, we are going to revisit our module system alignment in the context of Project Leyden which intends to build on module system concepts and tools to some degree. From that perspective, deeper module system alignment remains part of our technology strategy for the Spring Framework 6.x generation.
We are considering a reimplementation of the beans introspection algorithm to not have to call the java.beans.Introspector anymore.
That's great to hear, @jhoeller . As we're talking mostly about the modular aspect of this here and the impact on additional classes being loaded in Native, I thought I might give another perspective on this particular sentence. I recently profiled a fairly vanilla Spring-Boot test suite (the project uses Data JPA, Flyway, Web - nothing fancy) and noticed that java.beans.Introspector
pops up fairly often.
All these purple blocks show the usage of java.beans.Introspector
. The madness behind that is that a large chunk of time is spent on finding the Customizer
classes that 99% of people don't really have. Or looking up java.lang.ObjectCustomizer
because it looks up superclasses internally. Including throwing & catching ClassNotFoundExceptions
that are not really exceptional. Etc. As you can hopefully see there are quite a lot of small to big tinted blocks. Optimizing this via Spring's own functionality could be quite substantial for certain projects, so you brought me some joy with your plans.
On the reversed allocation profile for the test suite I get almost 25% of allocations that are only caused by calls to Introspector
and everything that comes along with it. (Roughly 20% of which are calls to find the Customizer
classes)
But: in all fairness. In our huge projects I only see 1% impact in production, rather than 13% in the mentioned vanilla project test suite. As tests start several application contexts usually it's not far fetched that the whole beans infrastructure - including Introspector
- is more common in these profiles like I showed here. The truth is probably somewhere in the middle.
Maybe this particular aspect is worth its own ticket, though?
@dreis2211 We decided to move forward with the Introspector bypass for 6.0 still, in time for Boot 3.0 RC1 next week: #29320 The implementation that is about to be pushed there passes the entire core test suite already.
That's great news, thanks @jhoeller . Unfortunately, the projects I'm profiling these days are far away from being able to test this easily though. I will report back as soon as I get hold of an internal project here that is willing to experiment with Spring-Boot 3 :)
@dreis2211 see my comment on #29320 - we could potentially backport an optional variant of this to 5.3.x. Let's continue the conversation over there, we've been hijacking this thread enough already :-)
Thanks @philwebb for the info. Can't wait the day when spring-boot is completely free from java.desktop and many other unnecessary modules. A simple hello-word spring boot app (just printing a hello) is taking around 170 MB of size for a docker container based on Alpine (the smallest you can use). A similar hello-word in go language would have a very small docker image (with alpine as base). It would be a big celebration when a spring boot docker image becomes small :-) All of the following modules should be removed from spring boot dependencies: java.management, java.security.jgss, java.naming, java.instrument, java.desktop, jdk.unsupported, java.rmi, java.compiler. Just for your info, here is base docker I use currently
FROM alpine/java:22-jdk AS builder
RUN ["jlink", "--compress=2", "--module-path", \ "$JAVA_HOME/jmods/", "--add-modules", \ "java.base,java.sql,java.logging,java.management,java.security.jgss,java.naming,java.instrument,java.desktop,jdk.unsupported,java.rmi,java.compiler", \ "--no-header-files", "--no-man-pages", "--strip-java-debug-attributes", "--output", "/opt/runtime"]
FROM alpine:3.20.1
RUN mkdir -p /opt/jdk
COPY --from=builder /opt/runtime /opt/jdk ENV JAVA_HOME=/opt/jdk \ PATH=${PATH}:/opt/jdk/bin
Spring requires the
java.desktop
module to be present at Java runtimes only so that it can use the classes in the java.beans package.Would be a good start for modernizing Spring apps on a post Java 9+ era of modules if Spring could at least work on slim Java runtimes produced with
jlink
.Other modules to consider adjusting the dependency are
java.naming
,java.xml
,java.sql
,jdk.*
,java.instrument
,java.management
,java.rmi
,java.scripting
.This is not about making Spring compatible with Java SE modules. This is only to allow developers to have smaller Java runtimes created with jlink.
In an ideal world, a Java runtime, created with the following
jlink
command should be sufficient to run a Spring Boot Hello World with the Web dependency: