Skip to content

Commit 5567e0d

Browse files
authored
Introduce ServerPlugin (#6825)
Motivation: When integrating cross-cutting server concerns (e.g., xDS-driven TLS, service decorators, server listeners), users currently need to call multiple `ServerBuilder` methods individually. There's no way to bundle these concerns into a reusable unit that also participates in `Server.reconfigure()` and gets cleaned up on server stop. Modifications: - Added `ServerPlugin` interface (extends `SafeCloseable`) with a single `install(ServerBuilder)` method. - Added `ServerBuilder.plugin(ServerPlugin)` to register plugins at build time. - In `ServerBuilder.build()`, call `plugin.install(this)` for each registered plugin before building the server config. - In `Server.reconfigure()`, re-install all plugins on the new `ServerBuilder` after the `ServerConfigurator` runs. - In `Server.finishDoStop()`, close plugins via the existing `ShutdownSupport` machinery. - Added `ServerPluginTest` covering install-on-build, install-on-reconfigure, close-on-stop, ordering, and builder modification. Result: - Users can encapsulate multi-concern server configuration into a single `ServerPlugin` and register it with `ServerBuilder.plugin(plugin)`. - Plugins are automatically re-installed during `Server.reconfigure()` and closed when the server stops.
1 parent 87a2533 commit 5567e0d

4 files changed

Lines changed: 344 additions & 2 deletions

File tree

core/src/main/java/com/linecorp/armeria/server/Server.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,17 +113,19 @@ public static ServerBuilder builder() {
113113
@GuardedBy("lock")
114114
private final Map<InetSocketAddress, ServerPort> activePorts = new LinkedHashMap<>();
115115
private final ConnectionLimitingHandler connectionLimitingHandler;
116+
private final List<ServerPlugin> plugins;
116117
private boolean hasWebSocketService;
117118

118119
@Nullable
119120
@VisibleForTesting
120121
ServerBootstrap serverBootstrap;
121122

122-
Server(DefaultServerConfig serverConfig) {
123+
Server(DefaultServerConfig serverConfig, List<ServerPlugin> plugins) {
123124
serverConfig.setServer(this);
124125
config = new UpdatableServerConfig(requireNonNull(serverConfig, "serverConfig"));
125126
startStop = new ServerStartStopSupport(config.startStopExecutor());
126127
connectionLimitingHandler = new ConnectionLimitingHandler(config.maxNumConnections());
128+
this.plugins = requireNonNull(plugins, "plugins");
127129

128130
// Server-wide metrics.
129131
RequestTargetCache.registerServerMetrics(config.meterRegistry());
@@ -418,6 +420,7 @@ public void reconfigure(ServerConfigurator serverConfigurator) {
418420
requireNonNull(serverConfigurator, "serverConfigurator");
419421
final ServerBuilder sb = builder();
420422
serverConfigurator.reconfigure(sb);
423+
plugins.forEach(plugin -> plugin.install(sb));
421424
final ImmutableList<ServerPort> serverPorts;
422425
lock.lock();
423426
try {
@@ -720,6 +723,9 @@ private void finishDoStop(CompletableFuture<Void> future) {
720723
serverChannels.clear();
721724

722725
final Builder<ShutdownSupport> builder = ImmutableList.builder();
726+
for (ServerPlugin plugin : plugins) {
727+
builder.add(ShutdownSupport.of(plugin));
728+
}
723729
builder.addAll(config.delegate().shutdownSupports());
724730
for (VirtualHost virtualHost : config.virtualHosts()) {
725731
builder.addAll(virtualHost.shutdownSupports());

core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@
4444
import java.util.ArrayList;
4545
import java.util.Collection;
4646
import java.util.Collections;
47+
import java.util.Comparator;
4748
import java.util.List;
4849
import java.util.Map;
4950
import java.util.Map.Entry;
5051
import java.util.Optional;
52+
import java.util.ServiceLoader;
5153
import java.util.concurrent.Executor;
5254
import java.util.concurrent.ExecutorService;
5355
import java.util.concurrent.Executors;
@@ -245,6 +247,9 @@ public final class ServerBuilder implements TlsSetters, ServiceConfigsBuilder<Se
245247
private final ServerTlsProviderBuilder serverTlsProviderBuilder = new ServerTlsProviderBuilder();
246248
private Function<? super String, ? extends EventLoopGroup> bossGroupFactory = DEFAULT_BOSS_GROUP_FACTORY;
247249
private ConnectionAcceptor connectionAcceptor = ConnectionAcceptor.always();
250+
private static final List<ServerPlugin> SPI_PLUGINS =
251+
ImmutableList.copyOf(ServiceLoader.load(ServerPlugin.class));
252+
private final List<ServerPlugin> plugins = new ArrayList<>();
248253

249254
ServerBuilder() {
250255
// Set the default host-level properties.
@@ -532,6 +537,25 @@ public ServerBuilder connectionAcceptor(ConnectionAcceptor connectionAcceptor) {
532537
return this;
533538
}
534539

540+
/**
541+
* Adds a {@link ServerPlugin} that will be installed during {@link Server} construction
542+
* and during {@link Server#reconfigure(ServerConfigurator)}.
543+
*
544+
* <p>Plugins are installed in insertion order. Each plugin's
545+
* {@link ServerPlugin#install(ServerBuilder)} is called before the server configuration
546+
* is built. Plugins are closed when the {@link Server} stops.
547+
*
548+
* <p>Note: Plugins can only be added at initial build time. Calling
549+
* {@link ServerBuilder#plugin(ServerPlugin)} inside a {@link ServerConfigurator} passed to
550+
* {@link Server#reconfigure(ServerConfigurator)} has no effect — the server uses the plugins
551+
* registered at construction. Existing plugins are re-installed automatically during reconfiguration.
552+
*/
553+
@UnstableApi
554+
public ServerBuilder plugin(ServerPlugin plugin) {
555+
plugins.add(requireNonNull(plugin, "plugin"));
556+
return this;
557+
}
558+
535559
/**
536560
* Sets the worker {@link EventLoopGroup} which is responsible for performing socket I/O and running
537561
* {@link Service#serve(ServiceRequestContext, Request)}.
@@ -2466,11 +2490,24 @@ public ServerBuilder unloggedExceptionsReportIntervalMillis(long unloggedExcepti
24662490
* Returns a newly-created {@link Server} based on the configuration properties set so far.
24672491
*/
24682492
public Server build() {
2469-
final Server server = new Server(buildServerConfig(ports));
2493+
final List<ServerPlugin> plugins = buildPlugins();
2494+
plugins.forEach(plugin -> plugin.install(this));
2495+
final Server server = new Server(buildServerConfig(ports), plugins);
24702496
serverListeners.forEach(server::addListener);
24712497
return server;
24722498
}
24732499

2500+
private List<ServerPlugin> buildPlugins() {
2501+
// SPI-discovered plugins first, then user-registered plugins
2502+
return ImmutableList.<ServerPlugin>builder()
2503+
.addAll(SPI_PLUGINS)
2504+
.addAll(this.plugins)
2505+
.build()
2506+
.stream()
2507+
.sorted(Comparator.comparingInt(ServerPlugin::order))
2508+
.collect(toImmutableList());
2509+
}
2510+
24742511
DefaultServerConfig buildServerConfig(List<ServerPort> serverPorts) {
24752512
final AnnotatedServiceExtensions extensions =
24762513
virtualHostTemplate.annotatedServiceExtensions();
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2026 LY Corporation
3+
*
4+
* LY Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package com.linecorp.armeria.server;
17+
18+
import com.linecorp.armeria.common.annotation.UnstableApi;
19+
20+
/**
21+
* A plugin that encapsulates multi-concern registration into a single
22+
* {@link ServerBuilder#plugin(ServerPlugin)} call.
23+
*
24+
* <p>The {@link #install(ServerBuilder)} method is called during {@link Server} construction
25+
* and during {@link Server#reconfigure(ServerConfigurator)}, allowing the plugin to register
26+
* any combination of server-level concerns (e.g., ports, TLS, service decorators).
27+
*
28+
* <p>Plugins are sorted by {@link #order()} before installation. Lower values are installed first.
29+
*
30+
* <p>The {@link #close()} method is called when the {@link Server} stops, allowing the plugin
31+
* to clean up resources such as subscriptions or background tasks.
32+
*
33+
* <h2>Example</h2>
34+
* <pre>{@code
35+
* Server server = Server.builder()
36+
* .http(8080)
37+
* .service("/", (ctx, req) -> HttpResponse.of(200))
38+
* .plugin(sb -> sb.decorator(LoggingService.newDecorator()))
39+
* .build();
40+
* }</pre>
41+
*/
42+
@FunctionalInterface
43+
@UnstableApi
44+
public interface ServerPlugin extends AutoCloseable {
45+
46+
/**
47+
* Installs this plugin into the given {@link ServerBuilder}. Called during
48+
* {@link Server} construction and during {@link Server#reconfigure(ServerConfigurator)}.
49+
*/
50+
void install(ServerBuilder sb);
51+
52+
/**
53+
* Returns the order of this plugin. Plugins with lower values are installed first.
54+
* Plugins with the same order preserve their insertion order (stable sort).
55+
* The default value is {@code 0}.
56+
*/
57+
default int order() {
58+
return 0;
59+
}
60+
61+
/**
62+
* Called when the {@link Server} stops, allowing the plugin to clean up resources.
63+
* Note that if the same plugin instance is registered with multiple servers,
64+
* this method will be called once for each server that stops.
65+
* The default implementation is a no-op.
66+
*/
67+
@Override
68+
default void close() {}
69+
}

0 commit comments

Comments
 (0)