Skip to content

GH-267: Make ClassGraph work with Spring Boot DevTools / RestartClassLoader.#268

Merged
lukehutch merged 1 commit into
classgraph:masterfrom
michael-simons:issue/make-classgraph-compatible-with-springboot-devtools
Oct 26, 2018
Merged

GH-267: Make ClassGraph work with Spring Boot DevTools / RestartClassLoader.#268
lukehutch merged 1 commit into
classgraph:masterfrom
michael-simons:issue/make-classgraph-compatible-with-springboot-devtools

Conversation

@michael-simons

@michael-simons michael-simons commented Oct 26, 2018

Copy link
Copy Markdown
Contributor

This commit has to changes:

  • It introduces SpringBootRestartClassLoaderHandler and adds it to the registry. RestartClassLoader is parent last and such should be explicitly configured.
  • The codewise small but impact wise bigger change is instantiating ClassGraphClassLoader with a call of super(null), eliminating it’s parent.

The later one is done due to the following reason:
ClassGraphClassLoader goes through lengths in io.github.classgraph.ClassGraphClassLoader#findClass to determine which class loader has been the source of the scanned class and that works very well.

findClass is however called from loadClass only if there is no parent class loader or the parent class loader doesn't find the class.

In Spring Boot together with Spring Boots Restart Class Loader we have the following situation:

A launcher thread spawns the actually application using the RestartClassLoader described in #267.

From there on, all application classes (especially entities from OGM) live in the RestartClassLoader.

ClassGraph finds them as well and also notices that they are part of RestartClassLoader.

however, loadClass always uses the parent class loader first if any and none of the class loaders that are determined in findClass.

The main change here is not giving ClassGraphClassLoader a parent, thus forcing it to do the lookup by himself with the mechanism of findClass.

The rest are just tests.

  • Create a Thread using another class loader (knowing only 2 classes)
  • Run everything from that Thread and Class loader
  • Create a test stub that is runnable from the above thread and also in a normal way (Boot devtools on and off)
  • See if scanned classes are compatible with directly loaded classes.

If you have any questions or suggest that it would be better to solve this by also overwriting loadClass, I'm very happy to discuss.

This closes #267.

This adds SpringBootRestartClassLoaderHandler to make ClassGraph aware that Spring Boots RestartClassLoader is parent last. It also changes ClassGraphClassLoader to don’t use a parent withouth going through the classloader used for scanning classes.
@lukehutch

Copy link
Copy Markdown
Member

@michael-simons this is incredible -- I don't think I would have ever found the need to call super(null) to fix this -- and more importantly, your PR is (I think) the biggest and most complex PR I have ever received for ClassGraph! I really want to thank you for your contribution!

I'll pull in as-is, but I have one question -- is checking if the getSimpleName() of the initial context ClassLoader is "AppClassLoader" robust and future-proof? (The entire classloader hierarchy changed in JDK9, for example...)

@lukehutch lukehutch merged commit f686c64 into classgraph:master Oct 26, 2018
@lukehutch

Copy link
Copy Markdown
Member

PS @michael-simons I really could have used your help on ClassGraph a long time ago! :-) Supporting Spring Boot has been a major pain point for the project before now! (It all seems to be working OK now, but it took a lot of work to get to that point...)

@michael-simons

Copy link
Copy Markdown
Contributor Author

Luke, I'm super happy that I can and I'm allowed to contribute to projects. The pleasure is on my side!

is checking if the getSimpleName() of the initial context ClassLoader is "AppClassLoader" robust and future-proof? (The entire classloader hierarchy changed in JDK9, for example...)

That's a good catch and question! Give me a sec, that name shouldn't at least be hard coded.

@lukehutch

Copy link
Copy Markdown
Member

@michael-simons Actually I have another question -- why do you delegate from SpringBootRestartClassLoaderHandler to URLClassLoaderHandler? URLClassLoaderHandler is supposed to handle all subclasses of URLClassLoader already. Was that not working?`

https://github.com/classgraph/classgraph/blob/master/src/main/java/io/github/classgraph/utils/ClasspathFinder.java#L133

@michael-simons

Copy link
Copy Markdown
Contributor Author

I think that the handler as I understood it should use the same semantics in looking for classes than the class loader itself. RestartClassLoader does PARENT_LAST. If there's another way to specify it, the better.

@lukehutch

Copy link
Copy Markdown
Member

Oh, good point, I saw that in the code and then promptly forgot about that difference :-) And you even explained this in the first line of your PR comment! So that it's clear for future reference, what could be added in a comment explaining why this classloader is PARENT_LAST? What exactly goes wrong if that is PARENT_FIRST? Maybe all subclasses of URLClassLoader should be handled PARENT_LAST, since they are overriding the default mechanism? And actually, this is the first time PARENT_LAST has ever been tested, so I'm glad it works!

@michael-simons

Copy link
Copy Markdown
Contributor Author

Something along the line

"Spring Boot's RestartClassLoader sits in front of the parent class loader and watches a given set of directories for changes. While those classes are reachable from the parent class loader directly, they should always be loaded through direct access from the RestartClassLoader until it's completely turned of by means of Spring Boot Developer tools."

The RestartClassLoader shades only the project classes and additional directories that are configurable, so itself needs access to parent, but last.

@lukehutch

Copy link
Copy Markdown
Member

Sorry, ignore that last comment, I somehow missed the fact that you answered all my questions in #267. I'll put a link to that in the PARENT_LAST line of new ClassLoaderHandler.

@lukehutch

Copy link
Copy Markdown
Member

Great, I'll add that, thanks!

@GedMarc

GedMarc commented Oct 26, 2018

Copy link
Copy Markdown

This is very interesting for me -

I wonder how with the modular system this would take effect - I've had to implement auto restarts with SPI and on clearing and updating I get the results correctly from ClassGraph, but the modular reloading I haven't found a solution to it yet,

I've been a bit worried about Spring and the modular system, FAT jar's aren't exactly JPMS compatible at all, and the classloader as far as I'm aware doesn't really play a big role anymore (outside of classpath mode obviously)

Wonder what the impact is going to be for me

@lukehutch

Copy link
Copy Markdown
Member

Released in 4.4.9: https://github.com/classgraph/classgraph/releases/tag/classgraph-4.4.9

Thanks again for your contributions, @michael-simons!

Please let me know whether you think all subclasses of URLClassLoader should default to PARENT_LAST. I'm having a hard time getting my head around whether that would be a sensible default, because (despite working on ClassGraph for three years now) I still don't know enough about all the different ClassLoaders that are found out there in the wild.

@michael-simons

Copy link
Copy Markdown
Contributor Author

Fantastic. I'll reach out to the Pivotal people about the order.

@lukehutch

Copy link
Copy Markdown
Member

@GedMarc I was wondering about this recently too -- specifically the part "the classloader as far as I'm aware doesn't really play a big role anymore (outside of classpath mode obviously)". I think that actually what we'll see happening in the future is a lot of these large runtime environments will modify their classloaders to define their own ModuleLayer and associated configuration, i.e. they will actually upgrade themselves to work more like the built in JPMS classloaders.

@lukehutch

lukehutch commented Dec 2, 2018

Copy link
Copy Markdown
Member

@michael-simons I made a ton of changes to ClassGraph recently, and in the process managed to break your test (io.github.classgraph.issues.issue267), and I'm not sure how!

The first call to ClassLoadingWorksWithParentLastLoaders::assertCorrectClassLoaders(), from ClassLoadingWorksWithParentLastLoadersStub::sameClassLoaderThatFoundAClassShouldLoadIt(), passes. However, the second call to ClassLoadingWorksWithParentLastLoaders::assertCorrectClassLoaders(), from TestLauncher::run(), causes an assertion failure. (It doesn't cause a JUnit failure, because the failure is in a separate launched thread.)

Exception in thread "Thread-1" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at io.github.classgraph.issues.issue267.TestLauncher.run(ClassLoadingWorksWithParentLastLoadersStub.java:71)
Caused by: org.junit.ComparisonFailure: expected:<"[FakeRestart]ClassLoader"> but was:<"[App]ClassLoader">
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at io.github.classgraph.issues.issue267.ClassLoadingWorksWithParentLastLoaders.assertCorrectClassLoaders(ClassLoadingWorksWithParentLastLoaders.java:72)
	... 5 more

I would appreciate your help looking into this, since I'm not sure what's wrong...

lukehutch added a commit that referenced this pull request Dec 2, 2018
@lukehutch

Copy link
Copy Markdown
Member

OK, I figured it out -- FakeRestartClassLoader needed to have its own ClassLoaderHandler that specified PARENT_LAST delegation mode, otherwise the parent classloader (AppClassLoader) would resolve the class (since the default delegation mode is PARENT_FIRST). I updated the test in the above commit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Class loading doesn't work reliable with Spring Boot's DevTools

3 participants