Fix Maven 3.9.12 + Develocity compatibility: declare checkPrerequisites override#855
Conversation
Root cause and where this should be fixedThree parties are involved; each one is doing something a little brittle. 1. Maven made a backwards-incompatible API change in a patch release.
2. The Develocity Maven extension uses 3. In Maven 3.9.11 the same anti-pattern would have applied to Why fix it hereDevelocity could harden the reflective lookup (use
If/when Develocity loosens their lookup, this override becomes a no-op safety net rather than load-bearing. |
|
|
||
| // Declared so Class.getDeclaredMethod on this subclass finds it — | ||
| // getDeclaredMethod does not walk the superclass chain. | ||
| @Override |
There was a problem hiding this comment.
checkPrerequisites doesn't exist in DefaultMavenPluginManager before Maven 3.9.12. On Maven 3.8.x without Develocity this is fine (method is never called). But with Develocity, getDeclaredMethod now finds this override and invokes it — causing super.checkPrerequisites() to throw NoSuchMethodError at runtime.
This replaces one error with another for users still on Maven 3.8.x + Develocity.
There was a problem hiding this comment.
Good catch — fixed in 6a24180. Switched to a MethodHandle.findSpecial lookup resolved at class-load time so the bytecode no longer embeds a hard invokespecial reference to a method that may not exist on the parent.
- Maven ≥ 3.9.12: handle is non-null → our override delegates to super.
- Maven < 3.9.12 (3.8.x / 3.9.0 – 3.9.11): static initializer catches
NoSuchMethodExceptionfromfindSpecial, handle stays null, the override returns a no-op. NoNoSuchMethodErrorat invoke time.
Verified locally by loading the same uber jar against both Maven 3.9.11 maven-core and 3.9.12 maven-core on the classpath:
=== Maven 3.9.11 (no checkPrerequisites on parent) ===
STEP 1 OK: getDeclaredMethod returned ...checkPrerequisites...
STEP 2 OK: checkPrerequisites returned without NoSuchMethodError
=== Maven 3.9.12 (parent has checkPrerequisites) ===
STEP 1 OK: getDeclaredMethod returned ...checkPrerequisites...
STEP 2 OK: super was invoked, threw NullPointerException (expected on 3.9.12+ with empty descriptor)
| import static org.junit.Assert.assertEquals; | ||
| import static org.junit.Assert.assertNotNull; |
There was a problem hiding this comment.
This test runs under TestNG (@org.testng.annotations.Test) but uses JUnit 4 assertions. This only works incidentally because TestNG has an optional transitive dependency on junit:4.13.2. Please use TestNG's own assertions instead:
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
There was a problem hiding this comment.
Done in 6a24180 — swapped to TestNG's assertEquals / assertNotNull and flipped the argument order to TestNG's (actual, expected) convention. Note: the existing ArtifactoryResolutionTest in the same package uses JUnit assertions too, so we may want a follow-up to clean that up — but I left it out of this PR's scope.
| jdomVersion = '2.0.6.1' | ||
| jsrVersion = '3.0.2' | ||
| mavenVersion = '3.8.6' | ||
| mavenVersion = '3.9.12' |
There was a problem hiding this comment.
mavenVersion is used in 3 places, not just maven-core:
// build.gradle lines 492-503
"org.apache.maven:maven-repository-metadata:$mavenVersion", // bumped 3.8.6 → 3.9.12
"org.apache.maven:maven-model:$mavenVersion", // bumped 3.8.6 → 3.9.12
...
implementation("org.apache.maven:maven-core:$mavenVersion") // bumped 3.8.6 → 3.9.12
PR intended to only bump maven-core to get access to checkPrerequisites, but it accidentally also bumped maven-repository-metadata and maven-model as a side effect since they share the same variable.
maybe we can split the variable:
mavenVersion = '3.8.6' // for maven-repository-metadata, maven-model
mavenCoreVersion = '3.9.12' // for maven-core only
wdyt @bhanurp ?
There was a problem hiding this comment.
I would keep the single version for simplicity.
On some point you want to upgrade everything.
CI triage — none of the red checks are caused by this PRCompared this PR's runs against the master HEAD CI run ( Pre-existing on master HEAD (and in 5 of 5 recent master pushes):
Initially red on this PR but green on rerun (transient flakes, coincided with a GitHub Actions cache outage —
Stable green across both runs — the jobs this change actually exercises:
The mavenVersion bump (3.8.6 → 3.9.12) is consumed only by |
Concern raised by @agrasth (PR jfrog#855): with the previous direct super.checkPrerequisites() call, bytecode embeds an invokespecial reference to DefaultMavenPluginManager.checkPrerequisites that does not exist on Maven 3.8.x / 3.9.0 - 3.9.11. Develocity's reflective lookup now finds our override on those runtimes too, invokes it, and the super call throws NoSuchMethodError — replacing one error with another for users still on Maven 3.8.x + Develocity. Switch to a MethodHandle.findSpecial lookup resolved at class-load time: - Maven >= 3.9.12: handle is non-null, our override delegates to super correctly. - Maven < 3.9.12: handle stays null (NoSuchMethodException caught in static initializer); the override is a safe no-op; no NoSuchMethodError at invoke time. Verified locally with the same uber jar against both Maven 3.9.11 and 3.9.12 maven-core on the classpath. Also: switch the test from JUnit assertions (working incidentally via TestNG's transitive junit dep) to TestNG's own assertions, and swap arg order to match TestNG's (actual, expected) convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update: also delegate on Maven < 3.9.12 (per @galusben suggestion)@galusben raised: when called by Develocity on Maven < 3.9.12, the previous version of the override silently no-op'd — bypassing whatever prerequisites check the caller expected. New commit Subtle trap worth flaggingFirst attempt looked up the old name first, falling back to the new one. That hit a deprecation shim: on Maven 3.9.12 the Verified with Fix: try the new name first. On 3.9.12 we hit the real concrete impl on End-to-end verificationSame uber jar, two Maven runtimes: The NPE is from the parent touching uninitialized state on a stand-alone |
Concern raised by @agrasth (PR #855): with the previous direct super.checkPrerequisites() call, bytecode embeds an invokespecial reference to DefaultMavenPluginManager.checkPrerequisites that does not exist on Maven 3.8.x / 3.9.0 - 3.9.11. Develocity's reflective lookup now finds our override on those runtimes too, invokes it, and the super call throws NoSuchMethodError — replacing one error with another for users still on Maven 3.8.x + Develocity. Switch to a MethodHandle.findSpecial lookup resolved at class-load time: - Maven >= 3.9.12: handle is non-null, our override delegates to super correctly. - Maven < 3.9.12: handle stays null (NoSuchMethodException caught in static initializer); the override is a safe no-op; no NoSuchMethodError at invoke time. Verified locally with the same uber jar against both Maven 3.9.11 and 3.9.12 maven-core on the classpath. Also: switch the test from JUnit assertions (working incidentally via TestNG's transitive junit dep) to TestNG's own assertions, and swap arg order to match TestNG's (actual, expected) convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Concern raised by @agrasth (PR jfrog#855): with the previous direct super.checkPrerequisites() call, bytecode embeds an invokespecial reference to DefaultMavenPluginManager.checkPrerequisites that does not exist on Maven 3.8.x / 3.9.0 - 3.9.11. Develocity's reflective lookup now finds our override on those runtimes too, invokes it, and the super call throws NoSuchMethodError — replacing one error with another for users still on Maven 3.8.x + Develocity. Switch to a MethodHandle.findSpecial lookup resolved at class-load time: - Maven >= 3.9.12: handle is non-null, our override delegates to super correctly. - Maven < 3.9.12: handle stays null (NoSuchMethodException caught in static initializer); the override is a safe no-op; no NoSuchMethodError at invoke time. Verified locally with the same uber jar against both Maven 3.9.11 and 3.9.12 maven-core on the classpath. Also: switch the test from JUnit assertions (working incidentally via TestNG's transitive junit dep) to TestNG's own assertions, and swap arg order to match TestNG's (actual, expected) convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4b82c53 to
fbf0b01
Compare
…es override Maven 3.9.12 renamed DefaultMavenPluginManager#checkRequiredMavenVersion to checkPrerequisites. The Develocity Maven extension reflectively looks the new method up via Class.getDeclaredMethod on the Plexus-bound DefaultMavenPluginManager implementation, which is ArtifactoryEclipsePluginManager in this project. getDeclaredMethod does not walk the superclass, so the lookup throws NoSuchMethodException and blocks any Maven >= 3.9.12 build that also loads Develocity's extension. PR jfrog#844 (in v2.43.6) added the prerequisitesCheckers Plexus field requirement, which addressed the NPE variant on Maven 3.9.12 but did not declare the method, so the Develocity path stayed broken. Bumps maven-core build dep from 3.8.6 to 3.9.12 so the override compiles cleanly, and adds a regression unit test that mirrors Develocity's exact reflective lookup (Class.getDeclaredMethod on the subclass). Reproduced locally against Maven 3.9.12 + JDK 11 with the published v2.43.6 jar and confirmed the same stack trace as the failing CI log; the fix makes the NoSuchMethodException go away. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the third-party-specific framing from the source and test comments — the contract (declared on the subclass so getDeclaredMethod can find it) is the load-bearing fact. The why-it-matters discussion lives on the PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Concern raised by @agrasth (PR jfrog#855): with the previous direct super.checkPrerequisites() call, bytecode embeds an invokespecial reference to DefaultMavenPluginManager.checkPrerequisites that does not exist on Maven 3.8.x / 3.9.0 - 3.9.11. Develocity's reflective lookup now finds our override on those runtimes too, invokes it, and the super call throws NoSuchMethodError — replacing one error with another for users still on Maven 3.8.x + Develocity. Switch to a MethodHandle.findSpecial lookup resolved at class-load time: - Maven >= 3.9.12: handle is non-null, our override delegates to super correctly. - Maven < 3.9.12: handle stays null (NoSuchMethodException caught in static initializer); the override is a safe no-op; no NoSuchMethodError at invoke time. Verified locally with the same uber jar against both Maven 3.9.11 and 3.9.12 maven-core on the classpath. Also: switch the test from JUnit assertions (working incidentally via TestNG's transitive junit dep) to TestNG's own assertions, and swap arg order to match TestNG's (actual, expected) convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ia override Previously, when Maven < 3.9.12 invoked our checkPrerequisites override (e.g. via Develocity's reflective lookup), the MethodHandle was null and we silently no-op'd. That weakened behavior on those runtimes — the prerequisites check the caller expected to run was skipped. Now the static initializer also looks up the older method name (checkRequiredMavenVersion) so the check runs on every supported Maven version: - Maven >= 3.9.12: handle resolves to DefaultMavenPluginManager#checkPrerequisites (the real concrete impl on the abstract class, not the deprecated default method on the interface — the order of the lookup matters: trying the new name first hits the concrete impl directly). - Maven < 3.9.12 (3.8.x / 3.9.0 - 3.9.11): new name doesn't exist on the parent, fall back to checkRequiredMavenVersion which is a real concrete impl. - If neither exists (no known Maven version): handle stays null, override is a safe no-op. Verified locally with the same uber jar against both Maven 3.9.11 and 3.9.12 maven-core jars on the classpath; the call lands in the correct parent method on each runtime (no recursion, no NoSuchMethodError, no silent skip). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the local handle variable and collapse the multi-catch with ExceptionUtils.rethrow (commons-lang3, already on the classpath). Same runtime behavior — original exception type is preserved (no wrapping in RuntimeException), and PluginIncompatibleException still propagates as a real checked exception. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fbf0b01 to
be32cd5
Compare
Summary
DefaultMavenPluginManager#checkRequiredMavenVersiontocheckPrerequisites. The Develocity Maven extension reflectively looks the new method up viaClass.getDeclaredMethodon the Plexus-boundDefaultMavenPluginManagerimplementation, which in this project isArtifactoryEclipsePluginManager.getDeclaredMethoddoes not walk the superclass, so the lookup throwsNoSuchMethodExceptionand blocks any Maven >= 3.9.12 build that also loads Develocity's extension.prerequisitesCheckersPlexus field requirement, which addressed the NPE variant on Maven 3.9.12 — but did not declare the method, so the Develocity path stayed broken.maven-corebuild dep from 3.8.6 to 3.9.12 so the override compiles cleanly, declarescheckPrerequisiteson the subclass (delegating tosuper), and adds a regression unit test that mirrors Develocity's exact reflective lookup.Failing stack trace this fixes (from a real CI log)
Repro confirmed locally
Loading the master HEAD uber jar and running
Class.forName("…ArtifactoryEclipsePluginManager").getDeclaredMethod("checkPrerequisites", PluginDescriptor.class)reproduces the exact sameNoSuchMethodException. With this branch, the lookup returns the declared method and the exception goes away.Test plan
ArtifactoryEclipsePluginManagerTest.testCheckPrerequisitesIsDeclaredOnSubclassmirrors Develocity's exact reflective contract — fails on master, passes here../gradlew :build-info-extractor-maven3:compileJava :build-info-extractor-maven3:testclean against JDK 11 + the bumped maven-core 3.9.12 build dep..github/workflows/maven-compatibility-test.ymlto install Develocity's Maven extension in the test pom and fail onNoSuchMethodException, so this regression class is caught by the nightly job.🤖 Generated with Claude Code