原文はこちら。
The article was written by Santiago Pericas-Geertsen (Engineer, Oracle).
https://medium.com/helidon/websockets-in-helidon-se-1f5d7c220ccd
https://medium.com/helidon/websockets-in-helidon-mp-48259cf808f1
HelidonはTyrusを統合しており、Jakarta WebSocket APIをサポートします。WebSocket APIを使うとJavaアプリケーションがWebSocketを使った対話にサーバーもしくはクライアントとして参加できます。
Eclipse Tyrus
https://projects.eclipse.org/projects/ee4j.tyrus
Jakarta WebSocket
https://projects.eclipse.org/projects/ee4j.websocket
以下では、Helidon SE、Helidon MPについて説明しますが、いずれもRESTリソースを使ってメッセージを共有Queueにプッシュし、WebSocketエンドポイントを使ってメッセージを1個ずつQueueからダウンロードする、というサンプルアプリケーションを例にして、REST接続とWebSocket接続がどのようにして Helidon SE/Helidon MPアプリケーションにシームレスに組み合わされるかを紹介しています。
Helidon SE
Helidon SEでのサポートは、JerseySupportに似たTyrusSupportクラスに基づいており、これを使ってHelidonアプリケーションは注釈付きWebSocketエンドポイントとプログラムで記述するWebSocketエンドポイントの両方を定義できるようにしています。
Project Dependencies
WebSocket拡張をHelidon SEアプリケーションで利用するには、以下の依存関係をpom.xmlに追加する必要があります。
<dependency> | |
<groupId>io.helidon.webserver</groupId> | |
<artifactId>helidon-webserver-tyrus</artifactId> | |
</dependency> |
Example
このサンプルアプリケーションのソースコードは以下の場所にあります。
WebSocket Example
https://github.com/oracle/helidon/tree/master/examples/webserver/websocket
まずMessageQueueServiceを見てみましょう。
public class MessageQueueService implements Service { | |
private final MessageQueue messageQueue = MessageQueue.instance(); | |
@Override | |
public void update(Routing.Rules routingRules) { | |
routingRules.post("/board", this::handlePost); | |
} | |
private void handlePost(ServerRequest request, ServerResponse response) { | |
request.content().as(String.class).thenAccept(messageQueue::push); | |
response.status(204).send(); | |
} | |
} |
このクラスはメッセージのポスト先であるRESTリソースを公開します。メッセージを受け取ると、共有Queueにプッシュし、HTTP 204 (No content) を返します。QueueはシングルトンクラスのMessageQueueによって実装されていることに着目してください。
QueueにプッシュされたメッセージをMessageBoardEndpointが提供するWebSocket接続を開くことで取得できます。
public class MessageBoardEndpoint extends Endpoint { | |
private final MessageQueue messageQueue = MessageQueue.instance(); | |
@Override | |
public void onOpen(Session session, EndpointConfig endpointConfig) { | |
session.addMessageHandler(new MessageHandler.Whole<String>() { | |
@Override | |
public void onMessage(String message) { | |
try { | |
if (message.equals("SEND")) { | |
while (!messageQueue.isEmpty()) { | |
session.getBasicRemote().sendObject(messageQueue.pop()); | |
} | |
} | |
} catch (Exception e) { | |
// ... | |
} | |
} | |
}); | |
} | |
} |
このプログラム的なエンドポイントはEndPointクラスを拡張しており、onOpenメソッドを新規接続の都度呼び出します。この例では、アプリケーションは文字列用のメッセージハンドラを登録しています。特別なSENDメッセージを受け取ると、WebSocket 接続を介してメッセージを送信し、共有Queueを空にします。
Helidon SEでは、RESTクラスとWebSocketクラスは手作業でWebサーバーに登録する必要がありますが、以下のようにRoutingインスタンス作成時に実施します。
List<Class<? extends Encoder>> encoders = | |
Collections.singletonList(UppercaseEncoder.class); | |
Routing.builder() | |
.register("/rest", new MessageQueueService()) | |
.register("/websocket", | |
TyrusSupport.builder().register( | |
ServerEndpointConfig.Builder.create( | |
MessageBoardEndpoint.class, "/").encoders( | |
encoders).build()).build()) | |
.build(); |
このコードスニペットではRouting、TyrusSupportとServerEndpointConfigのために複数のビルダーを使っています。特に、/websocket にMessageBoardEndpoint.classを登録し、メッセージエンコーダを関連付けています。メッセージエンコーダとデコーダの詳細については、以下を参照してください。この例では、UppercaseEncoder.classはサーバから送信されるすべてのメッセージを単純に大文字にします。
Jakarta WebSocket
https://projects.eclipse.org/projects/ee4j.websocket
Helidon SEのエンドポイントのメソッドをNettyのワーカースレッドプールで実行されます。このプール内のスレッドは非ブロッキングであることを意図しているため、エンドポイントメソッドを起点とするブロッキングや長時間の操作は、別のスレッドプールを使用して実行することをお勧めします。Helidonには、このような場合のための設定可能なスレッドプールのサプライヤが含まれています。詳細は以下をご覧ください。
ThreadPoolSupplier (io.helidon.common.configurable.ThreadPoolSupplier)
https://helidon.io/docs/latest/apidocs/io.helidon.common.configurable/io/helidon/common/configurable/ThreadPoolSupplier.html
Helidon MP
Helidon MPでのサポートは、CDIを使ったアノテーションやbean discoveryが中心です。開発者は注釈付きWebSocketエンドポイントとプログラムで記述するWebSocketエンドポイントのどちらかを選択、もしくは両者の組み合わせを利用することもできますが、Helidon MPでは、通常より簡潔で読みやすいコードになるため、注釈付きエンドポイントを推奨します。
Project Dependencies
WebSocket拡張をHelidon MPアプリケーションで利用するには、以下の依存関係をpom.xmlに追加する必要があります。
<dependency> | |
<groupId>io.helidon.microprofile.websocket</groupId> | |
<artifactId>helidon-microprofile-websocket</artifactId> | |
</dependency> |
Example
このサンプルアプリケーションのソースコードは以下の場所にあります。
Helidon MP WebSocket Example
https://github.com/oracle/helidon/tree/master/examples/microprofile/websocket
ここで紹介するHelidon MPアプリケーションはCDIとクラススキャンをフル活用しており、必要な情報は全てコードへの注釈から得られるため、追加コードは必要ありません。
RESTエンドポイントはJAX-RSリソースとして実装されており、Queueが直接注入されています。
@Path("rest") | |
public class MessageQueueResource { | |
@Inject | |
private MessageQueue messageQueue; | |
@POST | |
@Consumes("text/plain") | |
public void push(String s) { | |
messageQueue.push(s); | |
} | |
} |
アプリケーションスコープにはMessageQueueという1個のインスタンスがあり、これをRESTとWebSocketのエンドポイントで共有します。REST エンドポイントはメッセージをQueueにプッシュし、WebSocket エンドポイントはメッセージをQueueから取り出します。
それでは、注釈付き API を使用するWebSocketエンドポイントの実装を探ってみましょう。
@ServerEndpoint( | |
value = "/websocket", | |
encoders = { UppercaseEncoder.class }) | |
public class MessageBoardEndpoint { | |
@Inject | |
private MessageQueue messageQueue; | |
@OnMessage | |
public void onMessage(Session session, String message) { | |
if (message.equals("SEND")) { | |
while (!messageQueue.isEmpty()) { | |
session.getBasicRemote() | |
.sendObject(messageQueue.pop()); | |
} | |
} | |
} | |
} |
@ServerEndopointの注釈がエンドポイントを /websocket パスに固定し、メッセージエンコーダーを定義しています(詳細はのちほど説明します)。MessageBoardEndpointは単なるPOJOなので、イベントハンドラーに @OnMessage のような追加の注釈を使っています。このアプローチの利点の1つは、JAX-RS APIのように、メソッドシグネチャが固定されていない点です。上のコードスニペットでは、パラメータ(任意の順序で指定できます)には、WebSocket セッションと、呼び出しのトリガーとなった受信メッセージが含まれています。
onMessageの実装は文字列メッセージSENDを待ち、受信したら、各エントリを別個のWebSocketメッセージとしてその接続を使って送信し、Queueを空にします。
WebSocketエンドポイントはゼロ個以上のメッセージエンコーダーとデコーダーを指定できます。これらのコーデックはメッセージがエンドポイント(デコーダー)にルーティングされる前、もしくはメッセージがエンドポイント(エンコーダー)から送信された後に呼び出されます。このサンプルでは、UppercaseEncoderがクライアントに送信される各メッセージを大文字に変更しています。
public class UppercaseEncoder implements Encoder.Text<String> { | |
@Override | |
public String encode(String s) { | |
return s.toUpperCase(); | |
} | |
@Override | |
public void init(EndpointConfig config) { | |
} | |
@Override | |
public void destroy() { | |
} | |
} |
では、このHelidon MPアプリを実行するためには、他に何が必要なのでしょうか?サポートするクラスMessageQueue以外には何もありません。Helidon MP は @Path と @ServerEndpoint の両方を CDI Bean 定義アノテーションとして宣言しているので、必要なのは CDI ディスカバリを有効にすることだけです。
デフォルトでは、JAX-RSリソースとWebSocketエンドポイントの両方がルートパス “/” の下で利用可能になります。このデフォルト値は、それぞれ jakarta.ws.rs.Application と jakarta.websocket.server.ServerApplicationConfig のサブクラスおよび実装をそれぞれ提供すればオーバーライドできます。JAX-RSはアプリケーションのサブクラスで @ApplicationPath を使ってこのルートパスを提供しますが、WebSocket API には同等のものがないため、Helidon MPでは jakarta.websocket.server.ServerApplicationConfig の実装である独自のアノテーション @RoutingPath を使用しています。
Class Application (jakarta.ws.rs-api 2.1.6 API)
https://jakarta.ee/specifications/restful-ws/2.1/apidocs/javax/ws/rs/core/Application.html
Interface ServerApplicationConfig (Jakarta WebSocket API documentation)
https://jakarta.ee/specifications/websocket/1.1/apidocs/javax/websocket/server/ServerApplicationConfig.html
Annotation Type RoutingPath (Helidon Project 2.0.1 API)
https://helidon.io/docs/latest/apidocs/io.helidon.microprofile.server/io/helidon/microprofile/server/RoutingPath.html
Helidon MP内のすべてのエンドポイントメソッドは、Nettyとは独立した別のスレッドプールで実行されます。したがって、これらは通信データを処理するNettyの能力に影響を与えないため、ブロッキングや長時間実行される操作のために追加のスレッドを作成する必要はありません。