This issue was originally created in Sleuth: https://github.com/spring-cloud/spring-cloud-sleuth/issues/1630 I investigated it a little bit, you can see my findings here: https://github.com/spring-cloud/spring-cloud-sleuth/issues/1630#issuecomment-756491649
Description:
It seems if the system class loader is wrapped (e.g.: spring-boot-devtools restarts the app using a RestartClassLoader
), cglib produces incorrect ClassLoaderData
causing proxied package-private methods returning null (public/protected is fine); please see the original issue and investigation details.
Here's a sample project (uses 2.3.6.RELEASE
) that reproduces the issue: https://github.com/jonatan-ivanov/sleuth-gh-1630-minimal Please check the readme, the issue is hiding.
Comment From: wilkinsona
When the target class is loaded by one class loader and the proxy class is loaded by another, the proxy can't access anything on the target that's package-private. This happens because package-private access requires code to be located in the same package and to have been loaded by the same class loader. To fix this automatically, I wonder if proxy creation needs to be a bit smarter about the ClassLoader that is uses when DevTools is in the picture.
It's AnnotationAwareAspectJAutoProxyCreator
that's in the picture here and it uses the same ClassLoader for all the proxies that it creates. When you're using Devtools, this is the RestartClassLoader
. The sample can be made to work by using a custom ProxyCreator
that uses the target class's ClassLoader
to create the proxy. Doing so requires some abuse of AnnotationAwareAspectJAutoProxyCreator
's extensibility as it wasn't really designed for this sort of customization:
static class BeanClassClassLoaderAnnotationAwareAspectJAutoProxyCreator extends AnnotationAwareAspectJAutoProxyCreator {
@Override
protected Object createProxy(Class<?> beanClass, String beanName, Object[] specificInterceptors,
TargetSource targetSource) {
if (getBeanFactory() instanceof ConfigurableListableBeanFactory) {
// Package-private so can't call this
// AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) getBeanFactory(), beanName, beanClass);
}
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
if (!proxyFactory.isProxyTargetClass()) {
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);
proxyFactory.setFrozen(isFrozen());
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}
ClassLoader classLoader = beanClass.getClassLoader();
System.out.println("Proxying " + beanClass + " using ClassLoader " + classLoader);
return proxyFactory.getProxy(classLoader);
}
};
It can then be hacked into place using a BFPP:
@Bean
static BeanFactoryPostProcessor hack() {
return (beanFactory) -> {
BeanDefinition definition = beanFactory.getBeanDefinition(AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME);
if (AnnotationAwareAspectJAutoProxyCreator.class.getName().equals(definition.getBeanClassName())) {
definition.setBeanClassName(BeanClassClassLoaderAnnotationAwareAspectJAutoProxyCreator.class.getName());
}
};
}
With these changes in place, the sample works with DemoService
being proxied using the RestartClassLoader
and the circuit break factory being proxied using the app class loader:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.3.6.RELEASE)
2021-02-19 10:45:22.976 INFO [,,,] 62607 --- [ restartedMain] com.example.demo.DemoApplication : No active profile set, falling back to default profiles: default
2021-02-19 10:45:23.479 INFO [,,,] 62607 --- [ restartedMain] o.s.cloud.context.scope.GenericScope : BeanFactory id=35769cb9-81fd-3224-81f3-bacd1a194697
Proxying class com.example.demo.DemoService using ClassLoader org.springframework.boot.devtools.restart.classloader.RestartClassLoader@6292fa3e
2021-02-19 10:45:24.068 INFO [,,,] 62607 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
Proxying class org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory using ClassLoader sun.misc.Launcher$AppClassLoader@2a139a55
2021-02-19 10:45:24.435 INFO [,,,] 62607 --- [ restartedMain] com.example.demo.DemoApplication : Started DemoApplication in 1.997 seconds (JVM running for 2.426)
2021-02-19 10:45:24.438 INFO [,,,] 62607 --- [ restartedMain] com.example.demo.DemoAspect : beforeNow
2021-02-19 10:45:24.445 INFO [,,,] 62607 --- [ restartedMain] com.example.demo.DemoApplication : now: 2021-02-19T10:45:24.445Z
2021-02-19 10:45:24.448 INFO [,,,] 62607 --- [ restartedMain] com.example.demo.DemoAspect : beforeStartedAt
2021-02-19 10:45:24.448 INFO [,,,] 62607 --- [ restartedMain] com.example.demo.DemoApplication : startedAt: 2021-02-19T10:45:23.731Z
Comment From: wilkinsona
The problem can also be avoided by pulling spring-cloud-circuitbreaker-resilience4j up into the restart class loader using a META-INF/spring-devtools.properties
file:
restart.include.cbr4j=.*/spring-cloud-circuitbreaker-resilience4j-[\\w\\d-\\.]+\\.jar
Ideally, users shouldn't have to know about this and it's only necessary when Sleuth is in the picture as it triggers the proxying of Resilience4JCircuitBreakerFactory
. If a general purpose solution in Boot isn't possible, Sleuth could, in theory, package a META-INF/spring-devtools.properties
file with the necessary restart.include
configuration. That would allow the method's package-private scope to be restored.