Discovered whilst looking into https://github.com/spring-projects/spring-boot/issues/39733
The following test will pass with Spring Framework 5.3.36 but fail with Spring Framework 6.0.12:
package org.springframework.context.annotation;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureClassLoader;
import org.junit.jupiter.api.Test;
import org.springframework.util.StreamUtils;
class ConfigurationClassEnhancerTests {
@Test
void test() throws Exception {
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
ClassLoader parentClassLoader = getClass().getClassLoader();
MyClassLoader classLoader = new MyClassLoader(parentClassLoader);
Class<?> myClass = parentClassLoader.loadClass(MyConfig.class.getName());
configurationClassEnhancer.enhance(myClass, parentClassLoader);
Class<?> myReloadedClass = classLoader.loadClass(MyConfig.class.getName());
Class<?> enhancedReloadedClass = configurationClassEnhancer.enhance(myReloadedClass, classLoader);
assertThat(enhancedReloadedClass.getClassLoader()).isEqualTo(classLoader);
}
@Configuration
static class MyConfig {
@Bean
public String myBean() {
return "bean";
}
}
static class MyClassLoader extends SecureClassLoader {
MyClassLoader(ClassLoader parent) {
super(parent);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.contains("MyConfig")) {
String path = name.replace('.', '/').concat(".class");
try (InputStream in = super.getResourceAsStream(path)) {
byte[] bytes = StreamUtils.copyToByteArray(in);
if (bytes.length > 0) {
return defineClass(name, bytes, 0, bytes.length);
}
} catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
return super.loadClass(name, resolve);
}
}
}
I haven't managed to exactly track down why, but I suspect that b31a15851e7aaabf3629cc101d285e751e535927 might have introduced the problem. Perhaps this key?
Comment From: jhoeller
This is caused by the enhancer.setAttemptLoad(true)
setting which we apply as of 6.0 for AOT purposes. Without that flag, the test passes on 6.x as well.
AttemptLoad seems to load a class with the same name irrespective of the specified ClassLoader. Maybe we need to conditionally set it to false depending on the ClassLoader arrangement.
Comment From: philwebb
I guess we're setting setAttemptLoad(true)
to pick up any class files that we generated during the AOT phase. In this case, I think the first run is generating class bytecode in the parent loader and the second run is then picking that up. Ideally we want a way to pick up classes that were AOT generated but not those that were generated in-memory.
Comment From: jhoeller
Since we are primarily talking about reload scenarios, maybe we should simply set AttemptLoad to false if the current configuration class passes SmartClassLoader.isClassReloadable
. That would cover Boot's scenario, wouldn't it?
In general, cleanly differentiating between pre-loaded classes and pre-defined classes is impossible for an arbitrary target ClassLoader. The ClassLoader API is trying hard to treat those the same, with only the protected findLoadedClass
allowing to differentiate within the ClassLoader implementation itself. We got that problem in ContextTypeMatchClassLoader
already, and it would be good to avoid any further variations of this... not least of it all: for compatibility with the module system.
Comment From: jhoeller
A modified test case where the custom ClassLoader implements SmartClassLoader.isClassReloadable
passes fine with a corresponding check in ConfigurationClassEnhancer
. I intend to go with this for 6.1.10 and 6.0.23.
Comment From: philwebb
Sounds good. Once that's in I can update Boot and test the read sample again.
Comment From: philwebb
Actually we already implement that method so we may not need any Boot changes.
Comment From: jhoeller
I would expect Boot's RestartClassLoader
to be naturally compliant already, indeed. Alright, about to push this to 6.1.x here.
Comment From: philwebb
Tested and all looks good 👍