[java] Fix #4953: Deprecate methods of PMDConfiguration that use ClassLoader#6312
[java] Fix #4953: Deprecate methods of PMDConfiguration that use ClassLoader#6312
Conversation
…ader Add new methods that just set/get the string classpath. ClasspathClassLoader is now created not in PMD core but within JavaLanguageProcessor. One of the main differences with main is that we accept file:// URLs as valid classpath entries. They may refer to directories or to jar files. This is consistent with how java.net.URLClassLoader treats classpath entries. If you want to use a classpath file (listing classpath entries one per line) you need to use the corresponding, separate method in PMDConfiguration.
This clears up possible confusions and enables new use cases. For instance, we need to allow running different analyses with the same classpath object (classloader). This is crucial eg in tests, where we cannot afford to create thousands of identical classloaders. The new API is more explicit about whether the classloaders are closed by PMD or are expected to be closed by the user. This removes a risk of memory leak caused by calling prependAuxClasspath repeatedly. Previously this would have created several ClasspathClassLoader, but only the last one would be closed. TODO: are there still risks of memory leaks? - Calling setClasspathWrapper moves the previous CPW out of scope. This is a potential memory leak -> it is likely we should close it and log it. We expect people to call this method with their own custom classloader. Technically they could also do config.setClasspathWrapper(config.getClasspathWrapper()) so we cannot close the CP wrapper early. We actually cannot build it early. The prependclasspath should not create any closeable resources.
Ref pmd#4953 This is a huge simplification and also fixes many of the usability issues with the previous iteration. For instance it doesn't matter if the classpath wrapper goes out of scope because it doesn't actually manage any resources. Also if you think about it you don't need a real chain of classloaders with a boolean to know if you can close it or not. You just need a fallback classloader and the classpath as a string (or even better, a pre-parsed list of URLs). This is really the "graceful collapse of complexity" Brian Goetz is talking about.
|
Compared to main: (comment created at 2026-01-01 12:11:53+00:00 for 531ae77) |
adangel
left a comment
There was a problem hiding this comment.
Thanks for working on this!
I left a couple of comments (don't take it personally). I think, the main point I want to have is, that the new API we provide, doesn't reference any classloader. So that you can configure PMD's auxclasspath without needing to get a classloader from anywhere and that we discourage using the default (PMD's boot classpath). Maybe the default should be just empty.
Internally, we probably still fall back on the classloader. When we implement/fix #6010 / #6099, we might need for compatibility two completely separate codepaths down the line...
| * | ||
| * @param ruleSets The rulesets configured for this analysis. | ||
| * @param auxclassPathClassLoader The class loader for auxclasspath configured for this analysis. | ||
| * @param auxclasspath The class loader for auxclasspath configured |
There was a problem hiding this comment.
| * @param auxclasspath The class loader for auxclasspath configured | |
| * @param auxclasspath The auxiliary classpath configured |
pmd-core/src/main/java/net/sourceforge/pmd/lang/JvmLanguagePropertyBundle.java
Show resolved
Hide resolved
pmd-core/src/main/java/net/sourceforge/pmd/lang/JvmLanguagePropertyBundle.java
Show resolved
Hide resolved
pmd-core/src/main/java/net/sourceforge/pmd/lang/JvmLanguagePropertyBundle.java
Show resolved
Hide resolved
pmd-core/src/main/java/net/sourceforge/pmd/lang/JvmLanguagePropertyBundle.java
Show resolved
Hide resolved
pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java
Outdated
Show resolved
Hide resolved
pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java
Outdated
Show resolved
Hide resolved
| * @see <a href="https://github.com/pmd/pmd/issues/4899">[java] Parsing failed in ParseLock#doParse() java.io.IOException: Stream closed #4899</a> | ||
| */ | ||
| @Test | ||
| @RepeatedTest(100) |
There was a problem hiding this comment.
Why repeated? Is this test flaky?
There was a problem hiding this comment.
It's a concurrency test. If you break the synchronisation logic the test will only fail if you are very lucky... or if you run it several times eventually it will fail
When I broke the synchronisation logic the test only failed about 10% of the time, so I think it's safer to repeat it
There was a problem hiding this comment.
If the test doesn't reproduce the problem always, then it's not a very useful test. Maybe I have written the test myself? Don't remember anymore. Anyway, having such a test gives a false sense of safety...
There was a problem hiding this comment.
It is useful though. Concurrency issues are unpredictable, that's just the way it is. Repeating it gives us high confidence that it is actually asserting the behavior we want.
pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeSystem.java
Outdated
Show resolved
Hide resolved
pmd-java/src/test/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/ClassStubTest.java
Outdated
Show resolved
Hide resolved
|
Thank you for your review Andreas.
I don't think we can get around using a classloader in the API for now. One reason is that we don't want the PMDConfiguration to own closeable resources, so the lifetime of those resources (OpenClasspath) must be the LanguageProcessor. But if you want to run many analyses with the same configuration, it is very expensive to create and close an OpenClasspath for each of them. This is what happens in our tests: we cannot create thousands of classloaders (one per test) without having a very significant performance hit. Other people might have similar use cases, like in IDE integrations or such. I think for that use case we need a fallback strategy that is not based on the string classpath, but that uses preexisting resources that are managed by the caller of PMD. For now that means using a ClassLoader that is created and closed by the caller, according to their needs. Technically this fallback doesn't need to be a classloader, it could be just a I agree the default should probably be just an empty classpath (or a filtered Classpath that uses the boot classpath but only finds So what I think we should do here is
Once we have removed |
Co-authored-by: Andreas Dangel <[email protected]>
… into issue4953-deprecate-classloader
|
I'm still not convinced we need to make it possible, to provide a classloader instance in the long term. I've checked the following PMD clients:
So - except for our own pmd-eclipse-plugin and PMD Designer - everyone else uses prependAuxClasspath. I'd try to keep the whole change as simple as possible - after all, the issue we want to fix here is only: Deprecated set/getClassloader. I would move the new Java Language Property PMD_JAVA_ENABLE_CLASSPATH_DIAGNOSTICS out to a different change, that tackles #5064, as this is unrelated. See #6398 for my own attempt on this. |
Describe the PR
Use the new type
PmdClasspathConfigto replace those methods. This is conceptually a fallback classloader + a prepended classpath. The fallback classloader is never created or owned by the PMD analysis so it is not our responisibility to close it. If the prepended classpath is not empty then we create a ClasspathClassLoader when starting the analysis and close it when we're done.This is meant to support the use case where you still want to manage the lifecycle of the classloader manually. For instance in our tests, it would be very slow to create one separate classloader per test, so we can create the classloader once and share it between tests. This is also useful in integrations that use a custom classloader (Maven/Gradle presumably). The new API makes it intentionally very obvious who is responsible for managing the classloader lifecycle:
The type PmdClasspathConfig does not own any resources. This removes the possibility for a lot of bugs and resource leaks, compared to current main. For instance in main you can write:
The first call creates a ClasspathClassLoader. The second creates another ClasspathClassLoader and replaces the first one in the configuration. Here the first ClasspathClassLoader will never be closed and leaks resources, as nobody has a reference to it anymore.
Another example:
The classloader within the configuration will be closed at the end of
pmd1, and will therefore be in an invalid state when startingpmd2.Lastly if you do
and never run a PmdAnalysis with this configuration, the classloader will also never be closed and leak resources.
With the new model, a PMDConfiguration never contains any owned resources like a classloader. It may contain a reference to a borrowed classloader, which it isn't responsible for closing. Any owned resources are created when the PmdAnalysis is created and released when it is destroyed. This means the API can now be used as shown above without issue.
Related issues
Ready?
./mvnw clean verifypasses (checked automatically by github actions)