0
# Service Provider Interface (SPI)
1
2
Extension points for custom protocols, features, connection handling, and streaming capabilities. Enables third-party integrations and custom server functionality through well-defined service provider interfaces.
3
4
## Capabilities
5
6
### Server Features SPI
7
8
Service provider interface for extending server functionality with custom features.
9
10
```java { .api }
11
/**
12
* SPI for server features and extensions.
13
*/
14
interface ServerFeature {
15
/**
16
* Setup the server feature.
17
* @param serverContext server context for configuration
18
*/
19
void setup(ServerFeatureContext serverContext);
20
21
/**
22
* Get feature name.
23
* @return feature name
24
*/
25
String name();
26
27
/**
28
* Get feature weight for ordering.
29
* @return feature weight (higher weight = later initialization)
30
*/
31
default double weight() {
32
return 100.0;
33
}
34
35
/**
36
* Called before server starts.
37
* @param server the server instance
38
*/
39
default void beforeStart(WebServer server) {
40
// Default implementation does nothing
41
}
42
43
/**
44
* Called after server starts.
45
* @param server the server instance
46
*/
47
default void afterStart(WebServer server) {
48
// Default implementation does nothing
49
}
50
51
/**
52
* Called before server stops.
53
* @param server the server instance
54
*/
55
default void beforeStop(WebServer server) {
56
// Default implementation does nothing
57
}
58
59
/**
60
* Called after server stops.
61
* @param server the server instance
62
*/
63
default void afterStop(WebServer server) {
64
// Default implementation does nothing
65
}
66
}
67
```
68
69
```java { .api }
70
/**
71
* Provider for server features.
72
*/
73
interface ServerFeatureProvider {
74
/**
75
* Create server feature instance.
76
* @param config feature configuration
77
* @return server feature instance
78
*/
79
ServerFeature create(Config config);
80
81
/**
82
* Get configuration type supported by this provider.
83
* @return configuration class
84
*/
85
Class<? extends Config> configType();
86
87
/**
88
* Get provider name.
89
* @return provider name
90
*/
91
String name();
92
}
93
```
94
95
**Usage Examples:**
96
97
```java
98
// Custom metrics feature
99
public class MetricsServerFeature implements ServerFeature {
100
private final MeterRegistry meterRegistry;
101
private Timer requestTimer;
102
103
public MetricsServerFeature(MeterRegistry meterRegistry) {
104
this.meterRegistry = meterRegistry;
105
}
106
107
@Override
108
public void setup(ServerFeatureContext context) {
109
this.requestTimer = Timer.builder("http.requests")
110
.description("HTTP request duration")
111
.register(meterRegistry);
112
113
// Add metrics filter to all HTTP routing
114
context.routing(HttpRouting.class, routing -> {
115
routing.addFilter(this::metricsFilter);
116
});
117
}
118
119
private void metricsFilter(FilterChain chain, RoutingRequest req, RoutingResponse res) {
120
Timer.Sample sample = Timer.start(meterRegistry);
121
try {
122
chain.proceed();
123
} finally {
124
sample.stop(requestTimer
125
.tag("method", req.method().text())
126
.tag("status", String.valueOf(res.status().code())));
127
}
128
}
129
130
@Override
131
public String name() {
132
return "metrics";
133
}
134
135
@Override
136
public double weight() {
137
return 50.0; // Early in the chain
138
}
139
}
140
141
// Feature provider
142
public class MetricsFeatureProvider implements ServerFeatureProvider {
143
@Override
144
public ServerFeature create(Config config) {
145
MeterRegistry registry = createMeterRegistry(config);
146
return new MetricsServerFeature(registry);
147
}
148
149
@Override
150
public Class<? extends Config> configType() {
151
return MetricsConfig.class;
152
}
153
154
@Override
155
public String name() {
156
return "metrics";
157
}
158
}
159
```
160
161
### Protocol Configuration SPI
162
163
Service provider interface for custom protocol implementations.
164
165
```java { .api }
166
/**
167
* Configuration for protocols.
168
*/
169
interface ProtocolConfig {
170
/**
171
* Get protocol name.
172
* @return protocol name
173
*/
174
String protocolName();
175
176
/**
177
* Get protocol version.
178
* @return protocol version
179
*/
180
String protocolVersion();
181
182
/**
183
* Get protocol configuration type.
184
* @return configuration type
185
*/
186
String configType();
187
188
/**
189
* Get protocol weight for ordering.
190
* @return protocol weight
191
*/
192
default double weight() {
193
return 100.0;
194
}
195
}
196
```
197
198
```java { .api }
199
/**
200
* Provider for protocol configurations.
201
*/
202
interface ProtocolConfigProvider {
203
/**
204
* Get configuration type supported by this provider.
205
* @return configuration class
206
*/
207
Class<? extends ProtocolConfig> configType();
208
209
/**
210
* Create protocol configuration from generic config.
211
* @param config configuration source
212
* @return protocol configuration
213
*/
214
ProtocolConfig create(Config config);
215
216
/**
217
* Get protocol type name.
218
* @return protocol type
219
*/
220
String protocolType();
221
}
222
```
223
224
**Usage Examples:**
225
226
```java
227
// Custom HTTP/2 protocol configuration
228
public class Http2Config implements ProtocolConfig {
229
private final int maxConcurrentStreams;
230
private final int initialWindowSize;
231
private final boolean enablePush;
232
233
private Http2Config(Builder builder) {
234
this.maxConcurrentStreams = builder.maxConcurrentStreams;
235
this.initialWindowSize = builder.initialWindowSize;
236
this.enablePush = builder.enablePush;
237
}
238
239
@Override
240
public String protocolName() {
241
return "HTTP";
242
}
243
244
@Override
245
public String protocolVersion() {
246
return "2.0";
247
}
248
249
@Override
250
public String configType() {
251
return "http2";
252
}
253
254
public int maxConcurrentStreams() { return maxConcurrentStreams; }
255
public int initialWindowSize() { return initialWindowSize; }
256
public boolean enablePush() { return enablePush; }
257
258
public static Builder builder() { return new Builder(); }
259
260
public static class Builder {
261
private int maxConcurrentStreams = 100;
262
private int initialWindowSize = 65535;
263
private boolean enablePush = true;
264
265
public Builder maxConcurrentStreams(int max) {
266
this.maxConcurrentStreams = max;
267
return this;
268
}
269
270
public Builder initialWindowSize(int size) {
271
this.initialWindowSize = size;
272
return this;
273
}
274
275
public Builder enablePush(boolean enable) {
276
this.enablePush = enable;
277
return this;
278
}
279
280
public Http2Config build() {
281
return new Http2Config(this);
282
}
283
}
284
}
285
286
// HTTP/2 protocol provider
287
public class Http2ProtocolProvider implements ProtocolConfigProvider {
288
@Override
289
public Class<? extends ProtocolConfig> configType() {
290
return Http2Config.class;
291
}
292
293
@Override
294
public ProtocolConfig create(Config config) {
295
return Http2Config.builder()
296
.maxConcurrentStreams(config.get("max-concurrent-streams").asInt().orElse(100))
297
.initialWindowSize(config.get("initial-window-size").asInt().orElse(65535))
298
.enablePush(config.get("enable-push").asBoolean().orElse(true))
299
.build();
300
}
301
302
@Override
303
public String protocolType() {
304
return "http2";
305
}
306
}
307
```
308
309
### Connection Management SPI
310
311
Service provider interfaces for custom connection handling and selection.
312
313
```java { .api }
314
/**
315
* Interface for server connections.
316
*/
317
interface ServerConnection {
318
/**
319
* Get connection channel.
320
* @return connection channel
321
*/
322
SocketChannel channel();
323
324
/**
325
* Get connection context.
326
* @return connection context
327
*/
328
ConnectionContext context();
329
330
/**
331
* Check if connection is secure.
332
* @return true if secure (TLS)
333
*/
334
boolean isSecure();
335
336
/**
337
* Get local address.
338
* @return local socket address
339
*/
340
SocketAddress localAddress();
341
342
/**
343
* Get remote address.
344
* @return remote socket address
345
*/
346
SocketAddress remoteAddress();
347
348
/**
349
* Close the connection.
350
*/
351
void close();
352
353
/**
354
* Check if connection is active.
355
* @return true if connection is active
356
*/
357
boolean isActive();
358
359
/**
360
* Handle connection processing.
361
*/
362
void handle();
363
364
/**
365
* Get connection protocol.
366
* @return protocol name
367
*/
368
String protocol();
369
}
370
```
371
372
```java { .api }
373
/**
374
* Selector for server connections.
375
*/
376
interface ServerConnectionSelector {
377
/**
378
* Select appropriate connection for request.
379
* @param context connection context
380
* @return selected connection
381
*/
382
ServerConnection select(ConnectionContext context);
383
384
/**
385
* Release connection after use.
386
* @param connection connection to release
387
*/
388
void release(ServerConnection connection);
389
390
/**
391
* Close all connections managed by this selector.
392
*/
393
void closeAll();
394
395
/**
396
* Get selector statistics.
397
* @return connection statistics
398
*/
399
ConnectionStats statistics();
400
}
401
```
402
403
```java { .api }
404
/**
405
* Provider for connection selectors.
406
*/
407
interface ServerConnectionSelectorProvider {
408
/**
409
* Create connection selector.
410
* @param config selector configuration
411
* @return connection selector
412
*/
413
ServerConnectionSelector create(ProtocolConfig config);
414
415
/**
416
* Get provider name.
417
* @return provider name
418
*/
419
String name();
420
421
/**
422
* Get configuration type.
423
* @return configuration class
424
*/
425
Class<? extends ProtocolConfig> configType();
426
427
/**
428
* Check if this provider supports the given protocol.
429
* @param protocol protocol name
430
* @return true if supported
431
*/
432
boolean supports(String protocol);
433
}
434
```
435
436
**Usage Examples:**
437
438
```java
439
// Custom WebSocket connection implementation
440
public class WebSocketConnection implements ServerConnection {
441
private final SocketChannel channel;
442
private final ConnectionContext context;
443
private final WebSocketHandler handler;
444
private volatile boolean active = true;
445
446
public WebSocketConnection(SocketChannel channel, ConnectionContext context,
447
WebSocketHandler handler) {
448
this.channel = channel;
449
this.context = context;
450
this.handler = handler;
451
}
452
453
@Override
454
public void handle() {
455
try {
456
handler.onConnect(this);
457
processWebSocketFrames();
458
} catch (Exception e) {
459
handler.onError(this, e);
460
} finally {
461
handler.onClose(this);
462
active = false;
463
}
464
}
465
466
@Override
467
public String protocol() {
468
return "websocket";
469
}
470
471
// Implementation of other ServerConnection methods...
472
}
473
474
// WebSocket connection selector
475
public class WebSocketConnectionSelector implements ServerConnectionSelector {
476
private final Set<WebSocketConnection> activeConnections = ConcurrentHashMap.newKeySet();
477
478
@Override
479
public ServerConnection select(ConnectionContext context) {
480
WebSocketConnection connection = new WebSocketConnection(
481
context.channel(), context, new DefaultWebSocketHandler());
482
activeConnections.add(connection);
483
return connection;
484
}
485
486
@Override
487
public void release(ServerConnection connection) {
488
if (connection instanceof WebSocketConnection wsConn) {
489
activeConnections.remove(wsConn);
490
}
491
}
492
493
@Override
494
public void closeAll() {
495
activeConnections.forEach(ServerConnection::close);
496
activeConnections.clear();
497
}
498
}
499
```
500
501
### HTTP Streaming SPI
502
503
Service provider interface for custom streaming response handlers.
504
505
```java { .api }
506
/**
507
* SPI for handling streaming responses.
508
*/
509
interface Sink {
510
/**
511
* Write data to sink.
512
* @param data data to write
513
*/
514
void writeData(DataChunk data);
515
516
/**
517
* Signal completion of stream.
518
*/
519
void complete();
520
521
/**
522
* Signal error in stream.
523
* @param throwable error that occurred
524
*/
525
void error(Throwable throwable);
526
527
/**
528
* Check if sink is closed.
529
* @return true if sink is closed
530
*/
531
boolean isClosed();
532
533
/**
534
* Close the sink.
535
*/
536
void close();
537
538
/**
539
* Get sink type.
540
* @return sink type identifier
541
*/
542
String type();
543
}
544
```
545
546
```java { .api }
547
/**
548
* Provider for sink implementations.
549
*/
550
interface SinkProvider {
551
/**
552
* Create sink for media type.
553
* @param context sink provider context
554
* @return sink instance
555
*/
556
Sink create(SinkProviderContext context);
557
558
/**
559
* Check if this provider supports the media type.
560
* @param mediaType media type to check
561
* @return true if supported
562
*/
563
boolean supports(MediaType mediaType);
564
565
/**
566
* Get provider name.
567
* @return provider name
568
*/
569
String name();
570
571
/**
572
* Get provider weight for ordering.
573
* @return provider weight
574
*/
575
default double weight() {
576
return 100.0;
577
}
578
}
579
```
580
581
```java { .api }
582
/**
583
* Context for sink providers.
584
*/
585
interface SinkProviderContext {
586
/**
587
* Get target media type.
588
* @return media type
589
*/
590
MediaType mediaType();
591
592
/**
593
* Get response headers.
594
* @return response headers
595
*/
596
WritableHeaders<?> headers();
597
598
/**
599
* Get response output stream.
600
* @return output stream
601
*/
602
OutputStream outputStream();
603
604
/**
605
* Get server response.
606
* @return server response
607
*/
608
ServerResponse response();
609
610
/**
611
* Get request context.
612
* @return request context
613
*/
614
Context context();
615
}
616
```
617
618
**Usage Examples:**
619
620
```java
621
// Custom JSON streaming sink
622
public class JsonStreamingSink implements Sink {
623
private final OutputStream outputStream;
624
private final ObjectMapper objectMapper;
625
private boolean closed = false;
626
private boolean firstWrite = true;
627
628
public JsonStreamingSink(OutputStream outputStream, ObjectMapper objectMapper) {
629
this.outputStream = outputStream;
630
this.objectMapper = objectMapper;
631
try {
632
outputStream.write('['); // Start JSON array
633
} catch (IOException e) {
634
throw new RuntimeException("Failed to initialize JSON stream", e);
635
}
636
}
637
638
@Override
639
public void writeData(DataChunk data) {
640
if (closed) throw new IllegalStateException("Sink is closed");
641
642
try {
643
if (!firstWrite) {
644
outputStream.write(',');
645
}
646
firstWrite = false;
647
648
objectMapper.writeValue(outputStream, data.data());
649
outputStream.flush();
650
} catch (IOException e) {
651
error(e);
652
}
653
}
654
655
@Override
656
public void complete() {
657
if (!closed) {
658
try {
659
outputStream.write(']'); // End JSON array
660
outputStream.flush();
661
} catch (IOException e) {
662
// Log error but don't throw
663
} finally {
664
close();
665
}
666
}
667
}
668
669
@Override
670
public String type() {
671
return "json-streaming";
672
}
673
674
// Implementation of other Sink methods...
675
}
676
677
// JSON streaming sink provider
678
public class JsonStreamingSinkProvider implements SinkProvider {
679
private final ObjectMapper objectMapper = new ObjectMapper();
680
681
@Override
682
public Sink create(SinkProviderContext context) {
683
context.headers().contentType(MediaType.APPLICATION_JSON);
684
context.headers().set("Transfer-Encoding", "chunked");
685
686
return new JsonStreamingSink(context.outputStream(), objectMapper);
687
}
688
689
@Override
690
public boolean supports(MediaType mediaType) {
691
return MediaType.APPLICATION_JSON.test(mediaType);
692
}
693
694
@Override
695
public String name() {
696
return "json-streaming";
697
}
698
699
@Override
700
public double weight() {
701
return 200.0; // Higher priority than default
702
}
703
}
704
```
705
706
### HTTP/1.1 Upgrade SPI
707
708
Service provider interface for HTTP/1.1 protocol upgrades (WebSocket, HTTP/2, etc.).
709
710
```java { .api }
711
/**
712
* SPI for HTTP/1.1 protocol upgrades.
713
*/
714
interface Http1Upgrader {
715
/**
716
* Check if this upgrader supports the requested protocol.
717
* @param protocol protocol name from Upgrade header
718
* @return true if supported
719
*/
720
boolean supports(String protocol);
721
722
/**
723
* Perform protocol upgrade.
724
* @param request HTTP/1.1 request with upgrade headers
725
* @param response HTTP/1.1 response for upgrade response
726
* @param connection underlying connection
727
* @return upgraded connection handler
728
*/
729
UpgradeResult upgrade(Http1ServerRequest request,
730
Http1ServerResponse response,
731
Http1Connection connection);
732
733
/**
734
* Get upgrade protocol name.
735
* @return protocol name
736
*/
737
String protocolName();
738
739
/**
740
* Get upgrader weight for selection priority.
741
* @return upgrader weight
742
*/
743
default double weight() {
744
return 100.0;
745
}
746
}
747
```
748
749
```java { .api }
750
/**
751
* Provider for HTTP/1.1 upgrade implementations.
752
*/
753
interface Http1UpgradeProvider {
754
/**
755
* Create upgrader instance.
756
* @param config upgrade configuration
757
* @return upgrader instance
758
*/
759
Http1Upgrader create(Config config);
760
761
/**
762
* Get supported protocol name.
763
* @return protocol name
764
*/
765
String protocolName();
766
767
/**
768
* Get provider name.
769
* @return provider name
770
*/
771
String name();
772
}
773
```
774
775
**Usage Examples:**
776
777
```java
778
// WebSocket upgrade implementation
779
public class WebSocketUpgrader implements Http1Upgrader {
780
@Override
781
public boolean supports(String protocol) {
782
return "websocket".equalsIgnoreCase(protocol);
783
}
784
785
@Override
786
public UpgradeResult upgrade(Http1ServerRequest request,
787
Http1ServerResponse response,
788
Http1Connection connection) {
789
// Validate WebSocket upgrade headers
790
Optional<String> key = request.headers().first("Sec-WebSocket-Key");
791
Optional<String> version = request.headers().first("Sec-WebSocket-Version");
792
793
if (key.isEmpty() || !"13".equals(version.orElse(""))) {
794
return UpgradeResult.failed("Invalid WebSocket upgrade request");
795
}
796
797
// Generate WebSocket accept key
798
String acceptKey = generateWebSocketAcceptKey(key.get());
799
800
// Send upgrade response
801
response.status(101)
802
.header("Upgrade", "websocket")
803
.header("Connection", "Upgrade")
804
.header("Sec-WebSocket-Accept", acceptKey)
805
.send();
806
807
// Return upgraded connection handler
808
return UpgradeResult.success(new WebSocketConnectionHandler(connection));
809
}
810
811
@Override
812
public String protocolName() {
813
return "websocket";
814
}
815
816
private String generateWebSocketAcceptKey(String key) {
817
// WebSocket key generation logic
818
String magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
819
return Base64.getEncoder().encodeToString(
820
MessageDigest.getInstance("SHA-1")
821
.digest((key + magic).getBytes()));
822
}
823
}
824
825
// WebSocket upgrade provider
826
public class WebSocketUpgradeProvider implements Http1UpgradeProvider {
827
@Override
828
public Http1Upgrader create(Config config) {
829
return new WebSocketUpgrader();
830
}
831
832
@Override
833
public String protocolName() {
834
return "websocket";
835
}
836
837
@Override
838
public String name() {
839
return "websocket-upgrade";
840
}
841
}
842
```
843
844
## SPI Registration and Discovery
845
846
### Service Loading
847
848
Helidon WebServer uses Java's ServiceLoader mechanism to discover SPI implementations:
849
850
```java
851
// META-INF/services/io.helidon.webserver.spi.ServerFeatureProvider
852
com.example.MetricsFeatureProvider
853
com.example.TracingFeatureProvider
854
855
// META-INF/services/io.helidon.webserver.spi.ProtocolConfigProvider
856
com.example.Http2ProtocolProvider
857
com.example.GrpcProtocolProvider
858
859
// META-INF/services/io.helidon.webserver.http.spi.SinkProvider
860
com.example.JsonStreamingSinkProvider
861
com.example.XmlStreamingSinkProvider
862
863
// META-INF/services/io.helidon.webserver.http1.spi.Http1UpgradeProvider
864
com.example.WebSocketUpgradeProvider
865
com.example.Http2UpgradeProvider
866
```
867
868
### Custom SPI Implementation
869
870
```java
871
// Example: Custom authentication feature
872
@AutoService(ServerFeatureProvider.class)
873
public class AuthFeatureProvider implements ServerFeatureProvider {
874
875
@Override
876
public ServerFeature create(Config config) {
877
return new AuthServerFeature(
878
config.get("auth.jwt-secret").asString().orElse("default-secret"),
879
config.get("auth.token-expiry").as(Duration.class).orElse(Duration.ofHours(1))
880
);
881
}
882
883
@Override
884
public Class<? extends Config> configType() {
885
return AuthConfig.class;
886
}
887
888
@Override
889
public String name() {
890
return "authentication";
891
}
892
}
893
894
// Usage in server configuration
895
WebServerConfig serverConfig = WebServerConfig.builder()
896
.port(8080)
897
.routing(HttpRouting.builder()
898
.get("/secure", (req, res) -> {
899
// This will be protected by the auth feature
900
res.send("Secure content");
901
})
902
.build())
903
.build();
904
905
// The auth feature is automatically loaded and applied
906
WebServer server = WebServer.create(serverConfig).start();
907
```