Skip to content

ConfigurationClassEnhancer should explicitly set custom ClassLoader on CGLIB Enhancer (aligned with CglibAopProxy) #34274

Description

@AlexanderShchelkunov

demo.zip

Description

Hello!

I have a Spring Boot application, that supports 3rd party plugins. Because of that I need to use a custom class loader. But if there is a @Configuration that has @Lazy annotation (when Spring has to create a proxy), the app will not start. See the possible way to fix at the bottom.

Environment

Spring Boot: 3.3.3
Spring Framework: 6.1.12
Java: JDK 17

Steps to Reproduce

Start the app

Error Message

Caused by: java.lang.ClassCastException: class com.example.demo.SomeConfig$$SpringCGLIB$$0 cannot be cast to class org.springframework.cglib.proxy.Factory (com.example.demo.SomeConfig$$SpringCGLIB$$0 and org.springframework.cglib.proxy.Factory are in unnamed module of loader org.springframework.boot.loader.launch.LaunchedClassLoader @433c675d)
        at org.springframework.aop.framework.ObjenesisCglibAopProxy.createProxyClassAndInstance(ObjenesisCglibAopProxy.java:91) ~[spring-aop-6.1.12.jar!/:6.1.12]
        at org.springframework.aop.framework.CglibAopProxy.buildProxy(CglibAopProxy.java:221) ~[spring-aop-6.1.12.jar!/:6.1.12]
        ... 30 common frames omitted

Minimal sample application

build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.3'
	id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}
configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}
repositories {
	mavenCentral()
}
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-aop'
	implementation 'org.springframework:spring-core'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
}
tasks.named('test') {
	useJUnitPlatform()
}

DemoApplication.java

@SpringBootApplication
public class DemoApplication {
  public static void main(String[] args) {
    URLClassLoader classLoader = new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader());
    SpringApplication app = new SpringApplication(new DefaultResourceLoader(classLoader), DemoApplication.class);
    app.run(args);
  }
}

SomeConfig.java

@Configuration
public class SomeConfig {
}

SomeService.java

@Service
public class SomeService {

  private final SomeConfig config;

  @Lazy
  public SomeService(SomeConfig config) {
    this.config = config;
  }

}

Possible way to fix

This happens because Enhancer, that creates Configuration does not respect provided custom class loader.

In ConfigurationClassEnhancer in method newEnhancer it does not call enhancer.setClassLoader(classLoader)
In the same time CglibAopProxy respects provided custom class loader, it sets classloader in buildProxy method

if (classLoader != null) {
  enhancer.setClassLoader(classLoader);
...

There is also cache in AbstractClassGenerator, where key is ClassLoader:
private static volatile Map<ClassLoader, ClassLoaderData> CACHE = new WeakHashMap<>();
When Spring loads configuration class the first time, it does not use custom classloader and key is the default classloader (method create in AbstractClassGenerator).
When Spring creates cglib proxy for the configuration class, it calles create again, but this time custom class loader set to classLoader field, so the key in the CACHE is different. And this causes the issue.
This probably could be fixed if enhanchers for the same class will always use the same class loader.

For example you can set classloader in ConfigurationClassEnhancer -> newEnhancer method. I tried to do it in debug and it seems to work.

Activity

  1. Vovch0 commented on Jan 20, 2025

    @Vovch0
  2. snicoll commented on Jan 20, 2025

    @snicoll
    Member

    @AlexanderShchelkunov Thanks for the report. Please move all that text into a sample we can actually run ourselves. You can attach a zip to this issue or push the code to a GitHub repository.

  3. AlexanderShchelkunov commented on Jan 20, 2025

    @AlexanderShchelkunov
    Author

    @AlexanderShchelkunov Thanks for the report. Please move all that text into a sample we can actually run ourselves. You can attach a zip to this issue or push the code to a GitHub repository.

    Hi @snicoll,
    Please see the attached file.

    demo.zip

  4. changed the title [-]java.lang.ClassCastException: class XXX$$SpringCGLIB$$0 cannot be cast to class org.springframework.cglib.proxy.Factory on app startup with custom classloader[/-] [+]`ClassCastException`: `XXX$$SpringCGLIB$$0` cannot be cast to `org.springframework.cglib.proxy.Factory` with custom `ClassLoader`[/+] on Jan 21, 2025
  5. added
    in: coreIssues in core modules (aop, beans, core, context, expression)
    on Feb 3, 2025
  6. added and removed on Feb 3, 2025
  7. self-assigned this
    on Feb 3, 2025
  8. added this to the 6.2.3 milestone on Feb 3, 2025
  9. changed the title [-]`ClassCastException`: `XXX$$SpringCGLIB$$0` cannot be cast to `org.springframework.cglib.proxy.Factory` with custom `ClassLoader`[/-] [+]`ConfigurationClassEnhancer` should explicitly set custom `ClassLoader` on CGLIB `Enhancer` (aligned with `CglibAopProxy`)[/+] on Feb 3, 2025
  10. added a commit that references this issue on Feb 3, 2025
    1b18928
  11. added a commit that references this issue on Feb 3, 2025
    a4fc68b
  12. jhoeller commented on Feb 10, 2025

    @jhoeller
    Contributor

    Reopening due to a side effect with package-visible superclasses not being accessible.

  13. added a commit that references this issue on Feb 11, 2025
    f53da04
  14. added a commit that references this issue on Feb 11, 2025
    24fd094
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

in: coreIssues in core modules (aop, beans, core, context, expression)status: backportedAn issue that has been backported to maintenance branchesstatus: feedback-providedFeedback has been providedtype: bugA general bug

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions