spring-projects / spring-framework

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

Document limitations of CGLIB proxy class generation in JPMS module setups #32671

Open xenoterracide opened 1 week ago

xenoterracide commented 1 week ago

arguably a duplicate of #24922 but since that wasn't fixed and actually causes real errors when using JPMS. It's not a warning. From the looks of it you're seeing if you can proxy java.lang

/home/xeno/.asdf/installs/java/temurin-17.0.8+7/bin/java -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -javaagent:/home/xeno/.local/share/JetBrains/Toolbox/apps/intellij-idea-ultimate/lib/idea_rt.jar=41197:/home/xeno/.local/share/JetBrains/Toolbox/apps/intellij-idea-ultimate/bin -Dfile.encoding=UTF-8 -classpath /home/xeno/IdeaProjects/bug-spring-security-jpms/build/resources/main:/home/xeno/.gradle/caches/modules-2/files-2.1/org.springframework/spring-context/6.1.5/735d1bd7372d7c53e7b31b4a9c980ce2e0b26424/spring-context-6.1.5.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.springframework/spring-core/6.1.5/6dae1b06ffacbb9abab636be2dbc6acd3b6e5d68/spring-core-6.1.5.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.springframework/spring-aop/6.1.5/a4f596bd3c55b6cec93f0e2e7245dd0bab8afec3/spring-aop-6.1.5.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.springframework/spring-beans/6.1.5/9ae967f467281c9bb977585ef4d5ea7351704d60/spring-beans-6.1.5.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.springframework/spring-expression/6.1.5/7e21cb1c6bbef1509e12d485b75ffc61278d9fa7/spring-expression-6.1.5.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/io.micrometer/micrometer-observation/1.12.4/492deebbd9b8ab23f588428f66578e21af266e01/micrometer-observation-1.12.4.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.springframework/spring-jcl/6.1.5/896ae3519327731589c6e77521656b50ae32d5b3/spring-jcl-6.1.5.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/io.micrometer/micrometer-commons/1.12.4/a57f10c78956b38087f97beae66cf14cb8b08d34/micrometer-commons-1.12.4.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-web/3.2.4/a74df12b71060da7c8e87f9a8c2ef4ea43fc8017/spring-boot-starter-web-3.2.4.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-json/3.2.4/ef3f72369ce7f6f7a7b02c0b23e60ef5bdf581b1/spring-boot-starter-json-3.2.4.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter/3.2.4/842cf7f0ed2ecfef3011f3191fc53c59ceed752/spring-boot-starter-3.2.4.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-tomcat/3.2.4/ffa632eeaaf1a4e807ec4bbcc1938f7d43096472/spring-boot-starter-tomcat-3.2.4.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.springframework/spring-webmvc/6.1.5/92809fce136e0b662dc9325529443386ba5ec2c6/spring-webmvc-6.1.5.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.springframework/spring-web/6.1.5/4f4e92cc52ee33260f1ee0cdc7b7a2f22d49708c/spring-web-6.1.5.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.15.4/7de629770a4559db57128d35ccae7d2fddd35db3/jackson-datatype-jsr310-2.15.4.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.module/jackson-module-parameter-names/2.15.4/e654497a08359db2521b69b5f710e00836915d8c/jackson-module-parameter-names-2.15.4.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.datatype/jackson-datatype-jdk8/2.15.4/694777f182334a21bf1aeab1b04cc4398c801f3f/jackson-datatype-jdk8-2.15.4.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-databind/2.15.4/560309fc381f77d4d15c4a4cdaa0db5025c4fd13/jackson-databind-2.15.4.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-logging/3.2.4/32616f4a33ec0fda0c54aaa67ab10dc78df3fd78/spring-boot-starter-logging-3.2.4.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/jakarta.annotation/jakarta.annotation-api/2.1.1/48b9bda22b091b1f48b13af03fe36db3be6e1ae3/jakarta.annotation-api-2.1.1.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.yaml/snakeyaml/2.2/3af797a25458550a16bf89acc8e4ab2b7f2bfce0/snakeyaml-2.2.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-websocket/10.1.19/adf4710fac2471236f8a466ca678cdf7e6a8257c/tomcat-embed-websocket-10.1.19.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-core/10.1.19/3dbbca8acbd4dd6a137c3d6f934a2931512b42ce/tomcat-embed-core-10.1.19.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-el/10.1.19/c61a582c391aca130884a5421deedfe1a96c7415/tomcat-embed-el-10.1.19.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-annotations/2.15.4/5223ea5a9bf52cdc9c5e537a0e52f2432eaf208b/jackson-annotations-2.15.4.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-core/2.15.4/aebe84b45360debad94f692a4074c6aceb535fa0/jackson-core-2.15.4.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.4.14/d98bc162275134cdf1518774da4a2a17ef6fb94d/logback-classic-1.4.14.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-to-slf4j/2.21.1/d77b2ba81711ed596cd797cc2b5b5bd7409d841c/log4j-to-slf4j-2.21.1.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.slf4j/jul-to-slf4j/2.0.12/eb5f48f782b41cc881b0bf1fb4d88ae2ff6d5b93/jul-to-slf4j-2.0.12.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-core/1.4.14/4d3c2248219ac0effeb380ed4c5280a80bf395e8/logback-core-1.4.14.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/2.0.12/48f109a2a6d8f446c794f3e3fa0d86df0cdfa312/slf4j-api-2.0.12.jar:/home/xeno/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-api/2.21.1/74c65e87b9ce1694a01524e192d7be989ba70486/log4j-api-2.21.1.jar -p /home/xeno/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-autoconfigure/3.2.4/b3f481aff8f0775f44d78399c804a8c52d75b971/spring-boot-autoconfigure-3.2.4.jar:/home/xeno/IdeaProjects/bug-spring-security-jpms/build/classes/java/main:/home/xeno/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot/3.2.4/ccb7cbb30dcf1d91dbbf20a3219a457eead46601/spring-boot-3.2.4.jar -m bug.spring.security.jpms.main/org.example.bugspringsecurityjpms.BugSpringSecurityJpmsApplication

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.4)

2024-04-18T15:38:49.330-04:00  INFO 1910319 --- [bug-spring-security-jpms] [           main] o.e.b.BugSpringSecurityJpmsApplication   : Starting BugSpringSecurityJpmsApplication using Java 17.0.8 with PID 1910319 (/home/xeno/IdeaProjects/bug-spring-security-jpms/build/classes/java/main started by xeno in /home/xeno/IdeaProjects/bug-spring-security-jpms)
2024-04-18T15:38:49.333-04:00  INFO 1910319 --- [bug-spring-security-jpms] [           main] o.e.b.BugSpringSecurityJpmsApplication   : No active profile set, falling back to 1 default profile: "default"
2024-04-18T15:38:49.730-04:00  WARN 1910319 --- [bug-spring-security-jpms] [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.cglib.core.CodeGenerationException: java.lang.reflect.InaccessibleObjectException-->Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @5a4041cc
2024-04-18T15:38:49.738-04:00  INFO 1910319 --- [bug-spring-security-jpms] [           main] .s.b.a.l.ConditionEvaluationReportLogger : 

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2024-04-18T15:38:49.755-04:00 ERROR 1910319 --- [bug-spring-security-jpms] [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.cglib.core.CodeGenerationException: java.lang.reflect.InaccessibleObjectException-->Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @5a4041cc
    at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:547) ~[spring-core-6.1.5.jar:6.1.5]
    at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:371) ~[spring-core-6.1.5.jar:6.1.5]
    at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:575) ~[spring-core-6.1.5.jar:6.1.5]
    at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.lambda$new$1(AbstractClassGenerator.java:107) ~[spring-core-6.1.5.jar:6.1.5]
    at org.springframework.cglib.core.internal.LoadingCache.lambda$createEntry$1(LoadingCache.java:52) ~[spring-core-6.1.5.jar:6.1.5]
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
    at org.springframework.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:57) ~[spring-core-6.1.5.jar:6.1.5]
    at org.springframework.cglib.core.internal.LoadingCache.get(LoadingCache.java:34) ~[spring-core-6.1.5.jar:6.1.5]
    at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:130) ~[spring-core-6.1.5.jar:6.1.5]
    at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:317) ~[spring-core-6.1.5.jar:6.1.5]
    at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:562) ~[spring-core-6.1.5.jar:6.1.5]
    at org.springframework.cglib.proxy.Enhancer.createClass(Enhancer.java:407) ~[spring-core-6.1.5.jar:6.1.5]
    at org.springframework.context.annotation.ConfigurationClassEnhancer.createClass(ConfigurationClassEnhancer.java:138) ~[spring-context-6.1.5.jar:6.1.5]
    at org.springframework.context.annotation.ConfigurationClassEnhancer.enhance(ConfigurationClassEnhancer.java:109) ~[spring-context-6.1.5.jar:6.1.5]
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.enhanceConfigurationClasses(ConfigurationClassPostProcessor.java:533) ~[spring-context-6.1.5.jar:6.1.5]
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanFactory(ConfigurationClassPostProcessor.java:310) ~[spring-context-6.1.5.jar:6.1.5]
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:363) ~[spring-context-6.1.5.jar:6.1.5]
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:153) ~[spring-context-6.1.5.jar:6.1.5]
    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:788) ~[spring-context-6.1.5.jar:6.1.5]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:606) ~[spring-context-6.1.5.jar:6.1.5]
    at spring.boot@3.2.4/org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.2.4.jar:na]
    at spring.boot@3.2.4/org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-3.2.4.jar:na]
    at spring.boot@3.2.4/org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[spring-boot-3.2.4.jar:na]
    at spring.boot@3.2.4/org.springframework.boot.SpringApplication.run(SpringApplication.java:334) ~[spring-boot-3.2.4.jar:na]
    at spring.boot@3.2.4/org.springframework.boot.SpringApplication.run(SpringApplication.java:1354) ~[spring-boot-3.2.4.jar:na]
    at spring.boot@3.2.4/org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) ~[spring-boot-3.2.4.jar:na]
    at bug.spring.security.jpms.main/org.example.bugspringsecurityjpms.BugSpringSecurityJpmsApplication.main(BugSpringSecurityJpmsApplication.java:10) ~[main/:na]
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @5a4041cc
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354) ~[na:na]
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297) ~[na:na]
    at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199) ~[na:na]
    at java.base/java.lang.reflect.Method.setAccessible(Method.java:193) ~[na:na]
    at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:505) ~[spring-core-6.1.5.jar:6.1.5]
    ... 26 common frames omitted

Process finished with exit code 1
------------------------------------------------------------
Gradle 8.7
------------------------------------------------------------

Build time:   2024-03-22 15:52:46 UTC
Revision:     650af14d7653aa949fce5e886e685efc9cf97c10

Kotlin:       1.9.22
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          17.0.10 (Eclipse Adoptium 17.0.10+7)
OS:           Linux 6.6.26-1-MANJARO amd64

bug-spring-framework-jpms-32671.tar.gz

blocks #18079

side comment: can we please stop the pattern of closing with "it's just a warning"? especially if it's a warning from java which means it's someone's error.

philwebb commented 1 week ago

Some debugging shows that the classloader used when trying to enhance org.example.bugspringsecurityjpms.BugSpringSecurityJpmsApplication is the AppClassLoader, despite the RestartClassLoader being set here.

philwebb commented 1 week ago

@xenoterracide If you're not directly calling bean methods in your @Configuration class you can try doing @SpringBootApplication(proxyBeanMethods = false) so that the proxy isn't created.

jhoeller commented 1 week ago

Please understand that JPMS simply does not allow for defining new classes in unrelated ClassLoaders: There is intentionally no Java platform API that lets us do this. This is not a CGLIB incompatibility or a legacy warning that we don't care about, it is rather a fundamental consequence of the module system being designed to prevent such runtime definitions in distinct ClassLoaders.

As for that fallback code path in ReflectUtils, we just retain that for compatibility with --add-opens=java.base/java.lang=ALL-UNNAMED. This will always fail in a strict module system setup but only after having tried the JDK 9+ Lookup.defineClass API first. Unfortunately Lookup.defineClass only works for the original ClassLoader, not for a separate ClassLoader that we may want to define the proxy class in. This prevents certain kinds of use cases that used to work fine in a classpath setup, e.g. proxies for certain core Java types.

There is one escape hatch in a Spring setup: SmartClassLoader.publicDefineClass which the Boot RestartClassLoader implements. Such explicit support for externally provided class definitions is the only way to make a separate ClassLoader work in a JPMS setup at all. If https://github.com/spring-projects/spring-boot/issues/40434 reveals anything concrete that can be improved for that escape hatch, I'm happy to consider it. Beyond that, I'm afraid we are not able to do anything about the fundamental limitations in the platform module system.

wilkinsona commented 1 week ago

@jhoeller this isn't a Devtools problem as the attached sample isn't using Devtools and RestartClassLoader isn't involved. Unfortunately, that means that there's no opportunity in Boot for us to do anything that requires a custom class loader.

I think this is the same as or very similar to the problem reported in https://github.com/spring-projects/spring-boot/issues/26578. I've just closed that issue as there's nothing we can do in Boot as we don't have any control over the class loader.

I think the proxyBeanMethods = false workaround, either on @SpringBootApplication or @Configuration, is the best we can offer at the moment for this particular case. It may be that this just needs to be documented as a possible escape hatch for the fundamental limitations of the platform module system. For other Framework features that rely on proxying, such as the use of AOP seen in https://github.com/spring-projects/spring-boot/issues/26578, there may be no workaround.

jhoeller commented 1 week ago

@wilkinsona thanks for the update, I'll turn this issue into a documentation ticket then. There is indeed nothing we can do in scenarios where we don't control the ClassLoader, and I'm not expecting any Devtools-driven refinement on the core side either.

philwebb commented 1 week ago

FWIW the sample in https://github.com/spring-projects/spring-boot/issues/26578 does use devtools but for some reason the RestartClassLoader isn't being used when the enhancer runs.

xenoterracide commented 1 week ago

questions:

  1. Has anyone whined at Java about this? I'd do it but I A don't know what I'm talking about, and B it'd be a lot more meaningful coming from Spring which is huge.
  2. Is it possible to auto-magically detect being run on the module path and either disable the feature auto-magically or in addition to documentation throw a better error pointing to the documentation/fix?
  3. Tangent, I notice the error itself is around java.lang classes, whenever I've defined a @Bean of type String (etc), that's had problems where I've had to disable the proxy on it anyways... from the discussion I'm certain that avoiding that wouldn't fix anything but maybe it could be avoided?