or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration.mdhttp-routing.mdhttp-services.mdhttp1-protocol.mdindex.mdrequest-response.mdserver-management.mdspi.md

spi.mddocs/

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

```