Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Constructor autowiring AOP broken when running build-image with JLink #432

Open
SanderKnauff opened this issue Oct 17, 2024 · 4 comments
Open

Comments

@SanderKnauff
Copy link

This has been first reported in spring-projects/spring-boot#42771, but I was asked to report the bug here.

When creating a Spring Boot application using spring-boot:build-image while also enabeling BP_JVM_JLINK_ENABLED, any constructor autowriting on components that are annotated with @Repository will throw an exception on startup. This only happens when JLink is enabled and specifically the @Repository stereotype annotation is used. @Component does not present the same issue.

A reproduction for this problem can be found here: https://github.com/SanderKnauff/buildpack-aop-jlink-broken

Expected Behavior

Startup should finish properly and all CGLib generated classes should contain their generated constructors, so Objenesis can create instances.

Current Behavior

The following exception occurs at startup:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'ooo.sansk.demo.buildpacks.BuildpacksApplication$BasicRepository': Unexpected AOP exception
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:607) ~[spring-beans-6.1.13.jar:6.1.13]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.13.jar:6.1.13]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.13.jar:6.1.13]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.13.jar:6.1.13]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.13.jar:6.1.13]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.13.jar:6.1.13]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975) ~[spring-beans-6.1.13.jar:6.1.13]
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:971) ~[spring-context-6.1.13.jar:6.1.13]
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:625) ~[spring-context-6.1.13.jar:6.1.13]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-3.3.4.jar:3.3.4]
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[spring-boot-3.3.4.jar:3.3.4]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:335) ~[spring-boot-3.3.4.jar:3.3.4]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363) ~[spring-boot-3.3.4.jar:3.3.4]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352) ~[spring-boot-3.3.4.jar:3.3.4]
        at ooo.sansk.demo.buildpacks.BuildpacksApplication.main(BuildpacksApplication.java:13) ~[classes/:0.0.1-SNAPSHOT]
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Unknown Source) ~[na:na]
        at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:102) ~[workspace/:na]
        at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:64) ~[workspace/:na]
        at org.springframework.boot.loader.launch.JarLauncher.main(JarLauncher.java:40) ~[workspace/:na]
Caused by: org.springframework.aop.framework.AopConfigException: Unexpected AOP exception
        at org.springframework.aop.framework.CglibAopProxy.buildProxy(CglibAopProxy.java:236) ~[spring-aop-6.1.13.jar:6.1.13]
        at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:163) ~[spring-aop-6.1.13.jar:6.1.13]
        at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110) ~[spring-aop-6.1.13.jar:6.1.13]
        at org.springframework.aop.framework.AbstractAdvisingBeanPostProcessor.postProcessAfterInitialization(AbstractAdvisingBeanPostProcessor.java:127) ~[spring-aop-6.1.13.jar:6.1.13]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:438) ~[spring-beans-6.1.13.jar:6.1.13]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1809) ~[spring-beans-6.1.13.jar:6.1.13]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600) ~[spring-beans-6.1.13.jar:6.1.13]
        ... 19 common frames omitted
Caused by: org.springframework.aop.framework.AopConfigException: Unable to instantiate proxy using Objenesis, and regular proxy instantiation via default constructor fails as well
        at org.springframework.aop.framework.ObjenesisCglibAopProxy.createProxyClassAndInstance(ObjenesisCglibAopProxy.java:86) ~[spring-aop-6.1.13.jar:6.1.13]
        at org.springframework.aop.framework.CglibAopProxy.buildProxy(CglibAopProxy.java:221) ~[spring-aop-6.1.13.jar:6.1.13]
        ... 25 common frames omitted
Caused by: java.lang.NoSuchMethodException: ooo.sansk.demo.buildpacks.BuildpacksApplication$BasicRepository$$SpringCGLIB$$0.<init>()
        at java.base/java.lang.Class.getConstructor0(Unknown Source) ~[na:na]
        at java.base/java.lang.Class.getDeclaredConstructor(Unknown Source) ~[na:na]
        at org.springframework.aop.framework.ObjenesisCglibAopProxy.createProxyClassAndInstance(ObjenesisCglibAopProxy.java:80) ~[spring-aop-6.1.13.jar:6.1.13]
        ... 26 common frames omitted

Possible Solution

N/A

Steps to Reproduce

  1. Clone the repository from https://github.com/SanderKnauff/buildpack-aop-jlink-broken
  2. Run ./mvnw -DskipTests package spring-boot:process-aot spring-boot:build-image
  3. Then run the created image using docker run buildpacks:0.0.1-SNAPSHOT.

The repository contains configuration for Podman, I have not tested this against Docker, but I expect the results to be the same.
The Podman configuration will only activate when the podman profile is active during the Maven lifecylces (-Ppodman).

Motivations

I am trying to create smaller container images for our Spring boot projects. The JLink option has a large impact on the result and as such would be a great way to improve the sizes. This issue however is blocking progress, as the application will not function this way.

@wilkinsona
Copy link

Something else to try, that I should have mentioned earlier, is to try using JLink locally so that buildpacks aren't involved. That would help to further narrow down the likely cause.

@dmikusa
Copy link
Contributor

dmikusa commented Oct 18, 2024

+1 to what @wilkinsona suggested. Run the same commands outside of buildpacks and see if you also have issues. The buildpack is just automating the run of jlink for you.

It'll do this:

  1. Run java --list-modules and fetch the list of modules
  2. Gets a list of all the modules that start with java. (as opposed to jdk.). The intent is to remove JDK functionality since most users won't need that at runtime.
  3. Run jlink --no-man-pages --no-header-files --strip-debug --compress=1 --add-modules with the list of modules from 2.).

If that is removing too much stuff for your app, you can change what is removed by setting BP_JVM_JLINK_ARGS to the list of args that should get passed into jlink. This will completely override what's set by default, so you need to give it the full list of args to use.

Hope that helps!

@SanderKnauff
Copy link
Author

SanderKnauff commented Oct 18, 2024

You're right! Buildpacks itself is not the problem, but JLink with insufficient modules is!

jlink --no-man-pages --no-header-files --strip-debug --compress=1 --add-modules "$(java --list-modules | grep '^java' | sed 's/@21.0.5//' | paste -s -d, -)" --output /tmp/jre
java -jar target/buildpacks-1.0.0-SNAPSHOT.jar

Also displays the same problem.

I've gradually included more jdk. modules And I've come to the conclusion that creating a JRE without jdk.unsupported is causing the issue.

Try running

rm -rf /tmp/jre && jlink --no-man-pages --no-header-files --strip-debug --compress=1 --add-modules "$(java --list-modules | grep '^java' | sed 's/@21.0.5//' | paste -s -d, -)" --output /tmp/jre && /tmp/jre/bin/java -jar target/buildpacks-0.0.1-SNAPSHOT.jar

vs

rm -rf /tmp/jre && jlink --no-man-pages --no-header-files --strip-debug --compress=1 --add-modules "$(java --list-modules | grep '^java' | sed 's/@21.0.5//' | paste -s -d, -),jdk.unsupported" --output /tmp/jre && /tmp/jre/bin/java -jar target/buildpacks-0.0.1-SNAPSHOT.jar

I understand that including JDK modules is not intended by default. Most Java applications probably do not need that level of runtime manipulation. The question now remains is, is this an issue? Should spring-boot:build-image automatically configure the correct module list depending on the included dependencies? Is this an issue with CGLib/Objenesis that is relying on JDK modules at runtime?

Personally I am glad to have a workaround and know what the problem is, but having this case work out of the box would be nice, as it is not directly obvious from the error messages what the issue is.

@dmikusa
Copy link
Contributor

dmikusa commented Oct 18, 2024

Hey, that's great you got to the bottom of this and thanks for sharing your findings.

  1. The current buildpacks implementation does not take the app type or code into account when making the decision above. it is making the default decision based on the fact that we usually only install a JRE, so with jlink we figured a good place would be to do something similar by default. (side note: I'd be curious to know if your app runs with just a JRE as well. If you have a sec to try)

  2. It's possible we could do something more sophisticated. Right now the logic doesn't look at your app/code, but it's possible to do that. Before we make any changes, I think we'd want to see if this is expected behavior from the Java libraries being used here and under what circumstances (i.e. all users of these libraries or only when used under certain conditions). The idea is to gauge how much of an impact this is going to have on users.

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants