Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[java] Fix #4953: Deprecate methods of PMDConfiguration that use ClassLoader#6312

Open
oowekyala wants to merge 23 commits intopmd:mainfrom
oowekyala:issue4953-deprecate-classloader
Open

[java] Fix #4953: Deprecate methods of PMDConfiguration that use ClassLoader#6312
oowekyala wants to merge 23 commits intopmd:mainfrom
oowekyala:issue4953-deprecate-classloader

Conversation

@oowekyala
Copy link
Member

@oowekyala oowekyala commented Dec 7, 2025

Describe the PR

Use the new type PmdClasspathConfig to 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:

ClassLoader myCustomClassLoader = ...;
PMDConfiguration config = ...;
config.setAnalysisClasspath(PmdClasspathConfig.thisClassLoaderWillNotBeClosedByPmd(myCustomClassLoader));

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:

PMDConfiguration config =...
config.prependAuxClasspath("A");
config.prependAuxClasspath("B");

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:

PMDConfiguration config =...
config.prependAuxClasspath("A");
try (PmdAnalysis pmd1 = PmdAnalysis.create(config)) {
   // ...
}
try (PmdAnalysis pmd2 = PmdAnalysis.create(config)) {
   // ...
}

The classloader within the configuration will be closed at the end of pmd1, and will therefore be in an invalid state when starting pmd2.
Lastly if you do

PMDConfiguration config =...
config.prependAuxClasspath("A");

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?

  • Added unit tests for fixed bug/feature
  • Passing all unit tests
  • Complete build ./mvnw clean verify passes (checked automatically by github actions)
  • Added (in-code) documentation (if needed)

…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.
@oowekyala oowekyala added this to the 7.20.0 milestone Dec 7, 2025
@oowekyala oowekyala added an:enhancement An improvement on existing features / rules is:deprecation The main focus is deprecating public APIs or rules, eg to make them internal, or removing them labels Dec 7, 2025
@pmd-actions-helper
Copy link
Contributor

pmd-actions-helper bot commented Dec 7, 2025

Documentation Preview

Compared to main:
This changeset changes 0 violations,
introduces 0 new violations, 0 new errors and 0 new configuration errors,
removes 0 violations, 0 errors and 0 configuration errors.

Regression Tester Report

(comment created at 2026-01-01 12:11:53+00:00 for 531ae77)

@adangel adangel changed the title [java] Fix #4953 - deprecate methods of PMDConfiguration that use ClassLoader [java] Fix #4953: Deprecate methods of PMDConfiguration that use ClassLoader Dec 22, 2025
Copy link
Member

@adangel adangel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param auxclasspath The class loader for auxclasspath configured
* @param auxclasspath The auxiliary classpath configured

* @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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why repeated? Is this test flaky?

Copy link
Member Author

@oowekyala oowekyala Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@adangel adangel modified the milestones: 7.20.0, 7.21.0 Dec 28, 2025
@oowekyala
Copy link
Member Author

Thank you for your review Andreas.

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.

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 Function<String, @Nullable URL>, or maybe more a Classpath instance that we have in pmd-java, it's a functional interface with the same signature. But currently we need an actual ClassLoader instance to support the (deprecated) PMDConfiguration#getClassLoader() method. We could do away with this in PMD 8.

I agree the default should probably be just an empty classpath (or a filtered Classpath that uses the boot classpath but only finds java.* classes).

So what I think we should do here is

  1. Keep the API as I suggest here until PMD 8
  2. In PMD 8
    • remove PMDConfiguration#getClassLoader and other deprecated methods
    • move the Classpath interface to pmd-core, and change the fallback strategy of PmdClasspathConfig to use a Classpath instead of a ClassLoader. This just means providing another creator method than PmdClasspathConfig#thisClassloaderWillNotBeClosedByPMD, one that takes a Classpath instance.
    • change the default fallback to be just finding java.* classes, but issue a warning if it is not overridden

Once we have removed PMDConfiguration#getClassLoader, we won't need to be able to convert the PmdClasspathConfig to a ClassLoader anymore. We can implement another class that parses the classpath and fetches resources and class files in classpath order. Maybe this will trump the performance issues that are there when you create many classloaders, but we don't know that yet.

@adangel
Copy link
Member

adangel commented Jan 13, 2026

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.
While experimenting with my own solution, I noticed that even the apex rule tests slowed down when we create a new ClasspathClassLoader for every single rule tests. I didn't look into the details, but I guess, loading the jrt-fs is costly: we immediatley load the list of modules and the packages->modules mapping. This happens even before the classloader is used (and apex never uses it). Just creating a classloader is probably not very expensive, as it is usually using lazy loading (accesses the URLs not upfront).

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.

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

Labels

an:enhancement An improvement on existing features / rules is:deprecation The main focus is deprecating public APIs or rules, eg to make them internal, or removing them

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[core] Deprecate PMDConfiguration#setClassloader and #getClassloader [doc] Improve doc around PMDConfiguration#prependAuxclasspath #setClassloader

2 participants