diff --git a/CHANGES.md b/CHANGES.md
index b170b7086d..6e706fc8e4 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -10,7 +10,9 @@ This document is intended for Spotless developers.
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
## [Unreleased]
+### Added
* Support for`clang-format` on maven-plugin ([#2406](https://github.com/diffplug/spotless/pull/2406))
+* Allow overriding classLoader for all `JarState`s to enable spotless-cli ([#2427](https://github.com/diffplug/spotless/pull/2427))
## [3.0.2] - 2025-01-14
### Fixed
diff --git a/lib/src/main/java/com/diffplug/spotless/JarState.java b/lib/src/main/java/com/diffplug/spotless/JarState.java
index 8680932b9e..76dee4f438 100644
--- a/lib/src/main/java/com/diffplug/spotless/JarState.java
+++ b/lib/src/main/java/com/diffplug/spotless/JarState.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2024 DiffPlug
+ * Copyright 2016-2025 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,6 +28,11 @@
import java.util.Set;
import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
/**
* Grabs a jar and its dependencies from maven,
* and makes it easy to access the collection in
@@ -37,6 +42,21 @@
* catch changes in a SNAPSHOT version.
*/
public final class JarState implements Serializable {
+
+ private static final Logger logger = LoggerFactory.getLogger(JarState.class);
+
+ // Let the classloader be overridden for tools using different approaches to classloading
+ @Nullable
+ private static ClassLoader forcedClassLoader = null;
+
+ /** Overrides the classloader used by all JarStates. */
+ public static void setForcedClassLoader(@Nullable ClassLoader forcedClassLoader) {
+ if (!Objects.equals(JarState.forcedClassLoader, forcedClassLoader)) {
+ logger.info("Overriding the forced classloader for JarState from {} to {}", JarState.forcedClassLoader, forcedClassLoader);
+ }
+ JarState.forcedClassLoader = forcedClassLoader;
+ }
+
/** A lazily evaluated JarState, which becomes a set of files when serialized. */
public static class Promised implements Serializable {
private static final long serialVersionUID = 1L;
@@ -125,26 +145,36 @@ URL[] jarUrls() {
}
/**
- * Returns a classloader containing the only jars in this JarState.
+ * Returns either a forcedClassloader ({@code JarState.setForcedClassLoader()}) or a classloader containing the only jars in this JarState.
* Look-up of classes in the {@code org.slf4j} package
* are not taken from the JarState, but instead redirected to the class loader of this class to enable
* passthrough logging.
*
* The lifetime of the underlying cacheloader is controlled by {@link SpotlessCache}.
+ *
+ * @see com.diffplug.spotless.JarState#setForcedClassLoader(ClassLoader)
*/
public ClassLoader getClassLoader() {
+ if (forcedClassLoader != null) {
+ return forcedClassLoader;
+ }
return SpotlessCache.instance().classloader(this);
}
/**
- * Returns a classloader containing the only jars in this JarState.
+ * Returns either a forcedClassloader ({@code JarState.setForcedClassLoader}) or a classloader containing the only jars in this JarState.
* Look-up of classes in the {@code org.slf4j} package
* are not taken from the JarState, but instead redirected to the class loader of this class to enable
* passthrough logging.
*
- * The lifetime of the underlying cacheloader is controlled by {@link SpotlessCache}.
+ * The lifetime of the underlying cacheloader is controlled by {@link SpotlessCache}
+ *
+ * @see com.diffplug.spotless.JarState#setForcedClassLoader(ClassLoader)
*/
public ClassLoader getClassLoader(Serializable key) {
+ if (forcedClassLoader != null) {
+ return forcedClassLoader;
+ }
return SpotlessCache.instance().classloader(key, this);
}
}
diff --git a/lib/src/test/java/com/diffplug/spotless/JarStateTest.java b/lib/src/test/java/com/diffplug/spotless/JarStateTest.java
new file mode 100644
index 0000000000..f44aa4a0b3
--- /dev/null
+++ b/lib/src/test/java/com/diffplug/spotless/JarStateTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2025 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.spotless;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.util.stream.Collectors;
+
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class JarStateTest {
+
+ @TempDir
+ java.nio.file.Path tempDir;
+
+ File a;
+
+ File b;
+
+ Provisioner provisioner = (withTransitives, deps) -> deps.stream().map(name -> name.equals("a") ? a : b).collect(Collectors.toSet());
+
+ @BeforeEach
+ void setUp() throws IOException {
+ a = Files.createTempFile(tempDir, "a", ".class").toFile();
+ Files.writeString(a.toPath(), "a");
+ b = Files.createTempFile(tempDir, "b", ".class").toFile();
+ Files.writeString(b.toPath(), "b");
+ }
+
+ @AfterEach
+ void tearDown() {
+ JarState.setForcedClassLoader(null);
+ }
+
+ @Test
+ void itCreatesClassloaderWhenForcedClassLoaderNotSet() throws IOException {
+ JarState state1 = JarState.from(a.getName(), provisioner);
+ JarState state2 = JarState.from(b.getName(), provisioner);
+
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(state1.getClassLoader()).isNotNull();
+ softly.assertThat(state2.getClassLoader()).isNotNull();
+ });
+ }
+
+ @Test
+ void itReturnsForcedClassloaderIfSetNoMatterIfSetBeforeOrAfterCreation() throws IOException {
+ JarState stateA = JarState.from(a.getName(), provisioner);
+ ClassLoader forcedClassLoader = new URLClassLoader(new java.net.URL[0]);
+ JarState.setForcedClassLoader(forcedClassLoader);
+ JarState stateB = JarState.from(b.getName(), provisioner);
+
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(stateA.getClassLoader()).isSameAs(forcedClassLoader);
+ softly.assertThat(stateB.getClassLoader()).isSameAs(forcedClassLoader);
+ });
+ }
+
+ @Test
+ void itReturnsForcedClassloaderEvenWhenRountripSerialized() throws IOException, ClassNotFoundException {
+ JarState stateA = JarState.from(a.getName(), provisioner);
+ ClassLoader forcedClassLoader = new URLClassLoader(new java.net.URL[0]);
+ JarState.setForcedClassLoader(forcedClassLoader);
+ JarState stateB = JarState.from(b.getName(), provisioner);
+
+ JarState stateARoundtripSerialized = roundtripSerialize(stateA);
+ JarState stateBRoundtripSerialized = roundtripSerialize(stateB);
+
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(stateARoundtripSerialized.getClassLoader()).isSameAs(forcedClassLoader);
+ softly.assertThat(stateBRoundtripSerialized.getClassLoader()).isSameAs(forcedClassLoader);
+ });
+ }
+
+ private JarState roundtripSerialize(JarState state) throws IOException, ClassNotFoundException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ try (ObjectOutputStream oOut = new ObjectOutputStream(outputStream)) {
+ oOut.writeObject(state);
+ }
+ try (ObjectInputStream oIn = new ObjectInputStream(new java.io.ByteArrayInputStream(outputStream.toByteArray()))) {
+ return (JarState) oIn.readObject();
+ }
+ }
+
+}