spring-projects / spring-framework

Spring Framework
https://spring.io/projects/spring-framework
Apache License 2.0
56.57k stars 38.13k forks source link

Remove dependency on java.desktop and other modules #26884

Open brunoborges opened 3 years ago

brunoborges commented 3 years ago

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:

$ jlink \
        --add-modules java.base,java.logging \
        --strip-debug \
        --no-man-pages \
        --no-header-files \
        --compress=2 \
        --output /javaruntime
dreis2211 commented 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.

jhoeller commented 3 years ago

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).

brunoborges commented 3 years ago

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.

sdeleuze commented 3 years ago

That will help on native side as well.

dsyer commented 3 years ago

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.

jhoeller commented 3 years ago

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 :-)

mlchung commented 3 years ago

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.
dsyer commented 3 years ago

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.

oleg-alexeyev commented 2 years ago

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() :).

membersound commented 2 years ago

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?

dsyer commented 2 years ago

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.

brunoborges commented 2 years ago

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!"

membersound commented 2 years ago

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?

dsyer commented 2 years ago

Pretty sure it’s a runtime error. Why don’t you try it and report back?

membersound commented 2 years ago

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.

sid-hbm commented 2 years ago

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.

philwebb commented 2 years ago

@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.

sdeleuze commented 2 years ago

@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.

membersound commented 2 years ago

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...

jhoeller commented 2 years ago

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.

dreis2211 commented 2 years ago

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.

introspector-impact

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)

image

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?

jhoeller commented 2 years ago

@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.

dreis2211 commented 2 years ago

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 :)

jhoeller commented 2 years ago

@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 :-)

sid-hbm commented 3 months ago

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"]

Second stage: Copies the custom Java RuntimeImage into a bare alpine

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