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

Allow AOP proxies to be created using the original ClassLoader #26601

Closed
wilkinsona opened this issue Feb 24, 2021 · 10 comments
Closed

Allow AOP proxies to be created using the original ClassLoader #26601

wilkinsona opened this issue Feb 24, 2021 · 10 comments
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement
Milestone

Comments

@wilkinsona
Copy link
Member

Affects: 5.3

There's a problem in Spring Boot's DevTools that's caused by an AOP proxy being created using the restart class loader rather than the class loader of the proxy's target. This causes problem when calling package-private methods on the proxy.

I've hacked together something that overrides the proxy creator to use the target's class loader and it fixes the problem. I'd now like to explore how we could make the solution more robust and I think some Framework changes are required. The override of AbstractAutoProxyCreator.createProxy is only changing the behaviour of a single line:

Rather than using getProxyClassLoader(), my override uses beanClass.getClassLoader().

The other part of the problem is then configuring the context to use the customized AnnotationAwareAspectJAutoProxyCreator. I'm changing the class of its bean definition at the moment. Perhaps there's already a more elegant way to do this?

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Feb 24, 2021
@jhoeller
Copy link
Contributor

jhoeller commented Feb 25, 2021

This is related to the recent #26403 where we were trying to prevent an illegal access warning for defining proxy classes in Boot's RestartClassLoader when the original class lives in the parent ClassLoader. Ironically by not using that recent SmartClassLoader.publicDefineClass mechanism and preventing illegal access completely on JVM startup, we'd end up defining the proxy class in the original bean ClassLoader anyway since that's the only thing the JDK 9+ Lookup.defineClass API can do for us. That said, of course we should be aiming for consistent behavior here and not rely on JDK configuration differences.

Looking at it the other way round, when do we have to define the proxy class in the restart loader in such a scenario? Only really when the original class has also been loaded there, so that'd be covered by using beanClass.getClassLoader() already. However, the latter isn't appropriate in other scenarios: Whenever the AOP proxy refers to aspects from a more specific class loader than the original bean class came from, we should be defining the proxy in that specific class loader. Even worse, when the original bean class came from a low-level class loader which doesn't even know about Spring's AOP implementation classes, we have to use a more specific class loader as well. That's why we use getProxyClassLoader() there, typically referring to the common application class loader (usually the same as the bean class loader in the bean factory).

I tend to see the restart class loader as a special case here. For general class loaders, we cannot reasonably infer whether the proxy class loader is necessary or the original bean class loader is sufficient. However, for restart class loaders, we should only really use the restart loader if the original bean class has also been loaded there. In all other scenarios, the application's bean class loader is sufficient or even preferable (given the package-private method scenario). We could simply introduce such an indication in the SmartClassLoader interface, to be implemented by Boot's RestartClassLoader accordingly, and automatically take it into acocunt in AbstractAutoProxyCreator.

@jhoeller jhoeller self-assigned this Feb 25, 2021
@jhoeller jhoeller added in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Feb 25, 2021
@jhoeller jhoeller added this to the 5.3.5 milestone Feb 25, 2021
@jhoeller
Copy link
Contributor

Let's assume the following code in AbstractAutoProxyCreator:

		ClassLoader targetClassLoader = getProxyClassLoader();
		if (targetClassLoader != beanClass.getClassLoader() && targetClassLoader instanceof SmartClassLoader) {
			targetClassLoader = ((SmartClassLoader) targetClassLoader).getOriginalClassLoader();
		}
		return proxyFactory.getProxy(targetClassLoader);

Boot would then override RestartClassLoader.getOriginalClassLoader() to return the original bean class loader (maybe a straightforward getParent()?) whereas a regular non-DevTools application ClassLoader would get used as-is (even if implements SmartClassLoader, it should simply leave the default getOriginalClassLoader() method returning this). This would cover all scenarios as far as I can see, effectively matching beanClass.getClassLoader() for regular application classes while still picking up the application class loader for classes from lower-level loaders (such as JDK library classes).

@wilkinsona
Copy link
Member Author

Thanks very much, Juergen. This all sounds good to me. In particular, I agree that the restart class loader is a special case here. Our intention would be to implement this fix in DevTools. Doing that by making RestartClassLoader a SmartClassLoader and a new getOriginalClassLoader method would be pleasingly elegant.

We also have spring-projects/spring-boot#19010 which I think is another variant of the same problem. In that scenario, the restart class loader was being used to create a proxy that's sub-classing a private class (I mistakenly said package-private in the issue). The super-class is part of Spring Security and was loaded by the app class loader. This causes the proxying attempt to fail with an IllegalAccessError occurs. Applying the same hack to change the class loader fixes this problem too with the proxy now being created using the app class loader.

@jhoeller
Copy link
Contributor

jhoeller commented Feb 25, 2021

Even more elegantly, RestartClassLoader seems to be a SmartClassLoader already :-) Based on #26403, @snicoll implemented publicDefineClass to enforce a warning-free definition in the restart class loader... which seems to be contradicting this issue a bit but is still worth having for cases where we do need to define a class in the restart class loader. Actually, such a publicDefineClass method was meant to be made available in the regular non-DevTools ClassLoader implementation in Boot as well, preventing an illegal access warning e.g. for proxying JDK library classes.

@wilkinsona
Copy link
Member Author

Actually, such a publicDefineClass method was meant to be made available in the regular non-DevTools ClassLoader in Boot as well, preventing an illegal access warning e.g. for proxying JDK library classes.

Unfortunately, outside of DevTools, we don't always control the ClassLoader. For example, when an application's launched in your IDE or run via Maven or Gradle, the ClassLoader will be the JVM's system class loader.

@jhoeller
Copy link
Contributor

Being at it, could Boot allow for setting up a custom SmartClassLoader in such IDE/Maven/Gradle scenarios even outside of DevTools? With custom user configuration, just in case anyone complains about remaining illegal access warnings in their scenario?

It probably will be less of an issue in JDK 16+, actually, since denying illegal access by default there leads to us silently defining the class in the original bean's ClassLoader via Lookup.defineClass anyway... which works for everything but the JDK library loader. In that sense, it's also a solution for people to bootstrap their JVM with --illegal-access=deny in order to get rid of those warnings, except for defining proxies for JDK library classes where we really need a publicDefineClass-enabled ClassLoader. The latter is exactly the scenario where a SmartClassLoader for non-DevTools scenarios still makes sense as an option to provide.

@wilkinsona
Copy link
Member Author

wilkinsona commented Feb 25, 2021

Being at it, could Boot allow for setting up a custom SmartClassLoader in such IDE/Maven/Gradle scenarios even outside of DevTools? With custom user configuration, just in case anyone complains about remaining illegal access warnings in their scenario?

That's something that I'd quite like us to be able to do, but I haven't had the time to figure out how we could do it in a consistent manner. It would also help in instrumentation scenarios, such as spring-projects/spring-boot#22109, where users would rely on things like Tomcat's InstrumentableClassLoader in a more traditional deployment.

@jhoeller jhoeller changed the title Allow AOP proxies to be created using the target's ClassLoader Allow AOP proxies to be created using the original ClassLoader Feb 25, 2021
@wilkinsona
Copy link
Member Author

@jhoeller Would it be possible to make a similar change to AbstractAdvisingBeanPostProcessor?

@jhoeller
Copy link
Contributor

Indeed, that's pretty much the same case there. Done!

@wilkinsona
Copy link
Member Author

Thanks!

This was referenced Mar 17, 2021
lxbzmy pushed a commit to lxbzmy/spring-framework that referenced this issue Mar 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

3 participants