Skip to content

Commit 693006c

Browse files
ctruedenclaude
andcommitted
Fix ServiceLoader class loader handling in Plugins
Use both the thread context class loader and the interface's own class loader when discovering plugins via ServiceLoader, deduplicating by class name. TCCL is consulted first so external implementations can override built-ins; the interface's class loader is added as a fallback so built-in implementations are always found even when the TCCL has no visibility into appose's JAR (e.g. in OSGi containers). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 36d2e89 commit 693006c

1 file changed

Lines changed: 31 additions & 6 deletions

File tree

src/main/java/org/apposed/appose/util/Plugins.java

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@
3434
import java.util.ArrayList;
3535
import java.util.Collection;
3636
import java.util.Comparator;
37+
import java.util.HashSet;
3738
import java.util.List;
3839
import java.util.ServiceLoader;
40+
import java.util.Set;
3941
import java.util.function.Function;
4042
import java.util.function.Predicate;
4143

@@ -61,9 +63,13 @@ private Plugins() {
6163
* @return List of discovered instances.
6264
*/
6365
public static <T> List<T> discover(Class<T> iface, Comparator<T> comparator) {
64-
ServiceLoader<T> loader = ServiceLoader.load(iface);
6566
List<T> singletons = new ArrayList<>();
66-
loader.forEach(singletons::add);
67+
Set<String> seen = new HashSet<>();
68+
for (ClassLoader cl : classLoaders(iface)) {
69+
for (T plugin : ServiceLoader.load(iface, cl)) {
70+
if (seen.add(plugin.getClass().getName())) singletons.add(plugin);
71+
}
72+
}
6773
if (comparator != null) singletons.sort(comparator);
6874
return singletons;
6975
}
@@ -75,11 +81,30 @@ public static <T> List<T> discover(Class<T> iface, Comparator<T> comparator) {
7581
}
7682

7783
public static <T, U> @Nullable U create(Class<T> iface, Function<T, U> creator) {
78-
ServiceLoader<T> loader = ServiceLoader.load(iface);
79-
for (T factory : loader) {
80-
U result = creator.apply(factory);
81-
if (result != null) return result;
84+
Set<String> seen = new HashSet<>();
85+
for (ClassLoader cl : classLoaders(iface)) {
86+
for (T factory : ServiceLoader.load(iface, cl)) {
87+
if (!seen.add(factory.getClass().getName())) continue;
88+
U result = creator.apply(factory);
89+
if (result != null) return result;
90+
}
8291
}
8392
return null;
8493
}
94+
95+
/**
96+
* Returns the class loaders to consult when discovering plugins.
97+
* The thread context class loader is tried first so that external
98+
* implementations (e.g. in a separate JAR) can override built-ins.
99+
* The interface's own class loader is appended if different, ensuring
100+
* built-in implementations are always found even when the TCCL has no
101+
* visibility into appose's JAR (e.g. in OSGi containers).
102+
*/
103+
private static <T> List<ClassLoader> classLoaders(Class<T> iface) {
104+
List<ClassLoader> loaders = new ArrayList<>();
105+
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
106+
if (tccl != null) loaders.add(tccl);
107+
if (iface.getClassLoader() != tccl) loaders.add(iface.getClassLoader());
108+
return loaders;
109+
}
85110
}

0 commit comments

Comments
 (0)