Skip to content

Commit bc9478d

Browse files
authored
Add generic interface for loading service providers from plugins (#88082)
Relates to 86224
1 parent 0eb35b3 commit bc9478d

File tree

4 files changed

+110
-2
lines changed

4 files changed

+110
-2
lines changed

server/src/main/java/org/elasticsearch/plugins/PluginsService.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import java.util.Locale;
5353
import java.util.Map;
5454
import java.util.Objects;
55+
import java.util.ServiceLoader;
5556
import java.util.Set;
5657
import java.util.function.Consumer;
5758
import java.util.function.Function;
@@ -279,7 +280,6 @@ private Map<String, LoadedPlugin> loadBundles(Set<PluginBundle> bundles) {
279280

280281
// package-private for test visibility
281282
static void loadExtensions(Collection<LoadedPlugin> plugins) {
282-
283283
Map<String, List<Plugin>> extendingPluginsByName = plugins.stream()
284284
.flatMap(t -> t.descriptor().getExtendedPlugins().stream().map(extendedPlugin -> Tuple.tuple(extendedPlugin, t.instance())))
285285
.collect(Collectors.groupingBy(Tuple::v1, Collectors.mapping(Tuple::v2, Collectors.toList())));
@@ -293,6 +293,28 @@ static void loadExtensions(Collection<LoadedPlugin> plugins) {
293293
}
294294
}
295295

296+
/**
297+
* SPI convenience method that uses the {@link ServiceLoader} JDK class to load various SPI providers
298+
* from plugins/modules.
299+
* <p>
300+
* For example:
301+
*
302+
* <pre>
303+
* var pluginHandlers = pluginsService.loadServiceProviders(OperatorHandlerProvider.class);
304+
* </pre>
305+
* @param service A templated service class to look for providers in plugins
306+
* @return an immutable {@link List} of discovered providers in the plugins/modules
307+
*/
308+
public <T> List<? extends T> loadServiceProviders(Class<T> service) {
309+
List<T> result = new ArrayList<>();
310+
311+
for (LoadedPlugin pluginTuple : plugins()) {
312+
ServiceLoader.load(service, pluginTuple.loader()).iterator().forEachRemaining(c -> result.add(c));
313+
}
314+
315+
return Collections.unmodifiableList(result);
316+
}
317+
296318
private static void loadExtensionsForPlugin(ExtensiblePlugin extensiblePlugin, List<Plugin> extendingPlugins) {
297319
ExtensiblePlugin.ExtensionLoader extensionLoader = new ExtensiblePlugin.ExtensionLoader() {
298320
@Override

server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,32 @@
1212
import org.apache.lucene.util.Constants;
1313
import org.elasticsearch.Version;
1414
import org.elasticsearch.common.settings.Settings;
15+
import org.elasticsearch.core.Strings;
1516
import org.elasticsearch.env.Environment;
1617
import org.elasticsearch.env.TestEnvironment;
1718
import org.elasticsearch.index.IndexModule;
19+
import org.elasticsearch.plugins.spi.TestService;
1820
import org.elasticsearch.test.ESTestCase;
21+
import org.elasticsearch.test.compiler.InMemoryJavaCompiler;
22+
import org.elasticsearch.test.jar.JarUtils;
1923

2024
import java.io.IOException;
2125
import java.io.InputStream;
2226
import java.lang.reflect.InvocationTargetException;
27+
import java.net.URL;
28+
import java.net.URLClassLoader;
29+
import java.nio.charset.StandardCharsets;
2330
import java.nio.file.FileSystemException;
2431
import java.nio.file.Files;
2532
import java.nio.file.NoSuchFileException;
2633
import java.nio.file.Path;
2734
import java.util.ArrayList;
2835
import java.util.Arrays;
2936
import java.util.Collection;
37+
import java.util.HashMap;
3038
import java.util.List;
3139
import java.util.Locale;
40+
import java.util.Map;
3241
import java.util.Set;
3342

3443
import static org.hamcrest.Matchers.containsInAnyOrder;
@@ -613,6 +622,70 @@ public void testThrowingConstructor() {
613622
assertThat(e.getCause().getCause(), hasToString(containsString("test constructor failure")));
614623
}
615624

625+
private ClassLoader buildTestProviderPlugin(String name) throws Exception {
626+
Map<String, CharSequence> sources = Map.of("r.FooPlugin", """
627+
package r;
628+
import org.elasticsearch.plugins.ActionPlugin;
629+
import org.elasticsearch.plugins.Plugin;
630+
public final class FooPlugin extends Plugin implements ActionPlugin { }
631+
""", "r.FooTestService", Strings.format("""
632+
package r;
633+
import org.elasticsearch.plugins.spi.TestService;
634+
public final class FooTestService implements TestService {
635+
@Override
636+
public String name() {
637+
return "%s";
638+
}
639+
}
640+
""", name));
641+
642+
var classToBytes = InMemoryJavaCompiler.compile(sources);
643+
644+
Map<String, byte[]> jarEntries = new HashMap<>();
645+
jarEntries.put("r/FooPlugin.class", classToBytes.get("r.FooPlugin"));
646+
jarEntries.put("r/FooTestService.class", classToBytes.get("r.FooTestService"));
647+
jarEntries.put("META-INF/services/org.elasticsearch.plugins.spi.TestService", "r.FooTestService".getBytes(StandardCharsets.UTF_8));
648+
649+
Path topLevelDir = createTempDir(getTestName());
650+
Path jar = topLevelDir.resolve(Strings.format("custom_plugin_%s.jar", name));
651+
JarUtils.createJarWithEntries(jar, jarEntries);
652+
URL[] urls = new URL[] { jar.toUri().toURL() };
653+
654+
URLClassLoader loader = URLClassLoader.newInstance(urls, this.getClass().getClassLoader());
655+
return loader;
656+
}
657+
658+
public void testLoadServiceProviders() throws Exception {
659+
ClassLoader fakeClassLoader = buildTestProviderPlugin("integer");
660+
@SuppressWarnings("unchecked")
661+
Class<? extends Plugin> fakePluginClass = (Class<? extends Plugin>) fakeClassLoader.loadClass("r.FooPlugin");
662+
663+
ClassLoader fakeClassLoader1 = buildTestProviderPlugin("string");
664+
@SuppressWarnings("unchecked")
665+
Class<? extends Plugin> fakePluginClass1 = (Class<? extends Plugin>) fakeClassLoader1.loadClass("r.FooPlugin");
666+
667+
assertFalse(fakePluginClass.getClassLoader().equals(fakePluginClass1.getClassLoader()));
668+
669+
getClass().getModule().addUses(TestService.class);
670+
671+
PluginsService service = newMockPluginsService(List.of(fakePluginClass, fakePluginClass1));
672+
673+
List<? extends TestService> providers = service.loadServiceProviders(TestService.class);
674+
assertEquals(2, providers.size());
675+
assertThat(providers.stream().map(p -> p.name()).toList(), containsInAnyOrder("string", "integer"));
676+
677+
service = newMockPluginsService(List.of(fakePluginClass));
678+
providers = service.loadServiceProviders(TestService.class);
679+
680+
assertEquals(1, providers.size());
681+
assertThat(providers.stream().map(p -> p.name()).toList(), containsInAnyOrder("integer"));
682+
683+
service = newMockPluginsService(new ArrayList<>());
684+
providers = service.loadServiceProviders(TestService.class);
685+
686+
assertEquals(0, providers.size());
687+
}
688+
616689
private static class TestExtensiblePlugin extends Plugin implements ExtensiblePlugin {
617690
private List<TestExtensionPoint> extensions;
618691

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.plugins.spi;
10+
11+
public interface TestService {
12+
String name();
13+
}

test/framework/src/main/java/org/elasticsearch/plugins/MockPluginsService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public MockPluginsService(Settings settings, Environment environment, Collection
5858
if (logger.isTraceEnabled()) {
5959
logger.trace("plugin loaded from classpath [{}]", pluginInfo);
6060
}
61-
pluginsLoaded.add(new LoadedPlugin(pluginInfo, plugin));
61+
pluginsLoaded.add(new LoadedPlugin(pluginInfo, plugin, pluginClass.getClassLoader(), ModuleLayer.boot()));
6262
}
6363

6464
this.classpathPlugins = List.copyOf(pluginsLoaded);

0 commit comments

Comments
 (0)