web-dev-qa-db-ja.com

Spring @SubscribeMappingは本当にクライアントをいくつかのトピックにサブスクライブしますか?

Spring WebsocketとSTOMP、Simple Message Brokerを使用しています。 @Controllerでは、メソッドレベルの@SubscribeMappingを使用します。これは、クライアントがトピックにメッセージを受信できるように、クライアントをトピックにサブスクライブする必要があります。たとえば、クライアントがトピック"chat"にサブスクライブするとします。

stompClient.subscribe('/app/chat', ...);

クライアントが"/ topic/chat"の代わりに"/ app/chat"にサブスクライブすると、このサブスクリプションは@SubscribeMappingを使用してマップされるメソッドに移動します。

@SubscribeMapping("/chat")
public List getChatInit() {
    return Chat.getUsers();
}

Spring refがここにあります。言う:

デフォルトでは、@ SubscribeMappingメソッドからの戻り値は、接続されたクライアントに直接メッセージとして送信され、ブローカーを通過しません。これは、要求と応答のメッセージ対話を実装するのに役立ちます。たとえば、アプリケーションUIが初期化されているときにアプリケーションデータを取得します。

さて、これは私が望むものでしたが、ただ部分的に !!サブスクライブ後にinit-dataを送信します。しかし、subscribeingについてはどうですか?ここで起こったことは、サービスのようにrequest-replyに過ぎないように思えます。サブスクリプションは消費されます。これが当てはまる場合は、明確にしてください。

  • ブローカーがこれに関与していない場合、クライアントはどこで購読しましたか?
  • 後で「チャット」サブスクリプトにメッセージを送信したい場合、クライアントはそれを受信しますか?そうではないようです。
  • サブスクリプションを本当に実現しているのは誰ですか?ブローカ?または他の誰か?

ここでクライアントがどこにもサブスクライブされていない場合、なぜこれを「サブスクライブ」と呼ぶのでしょうか。クライアントは将来のメッセージではなく1つのメッセージのみを受信するためです。

編集:

サブスクリプションが実現されたことを確認するために、私は次のことを試みました。

サーバー側:

構成:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/hello").withSockJS();
    }
}

コントローラー:

@Controller
public class GreetingController {

    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public Greeting greeting(HelloMessage message) throws Exception {
        System.out.println("inside greeting");
        return new Greeting("Hello, " + message.getName() + "!");
    }

    @SubscribeMapping("/topic/greetings")
    public Greeting try1() {
        System.out.println("inside TRY 1");
        return new Greeting("Hello, " + "TRY 1" + "!");
    }
}

クライアント側:

...
    stompClient.subscribe('/topic/greetings', function(greeting){
                        console.log('RECEIVED !!!');
                    });
    stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));
...

私がしたいこと:

  1. クライアントが「/topic/greetings」をサブスクライブすると、メソッドtry1が実行されます。
  2. クライアントがmsgを '/app/hello'に送信すると、@SendTo '/topic/greetings'の挨拶メッセージを受信する必要があります。

結果:

  1. クライアントが/topic/greetingsをサブスクライブしている場合、メソッドtry1はそれをキャッチできません。

  2. クライアントがmsgを「/app/hello」に送信すると、greetingメソッドが実行され、クライアントはグリーティングメッセージを受信しました。したがって、「/topic/greetings」に正しくサブスクライブされていることがわかりました。

  3. しかし、1が失敗したことを思い出してください。いくつかの試行の後、クライアントが'/app/topic/greetings'にサブスクライブしたときに可能になりました。つまり、/appというプレフィックスが付いています(これは設定で理解できます)。

  4. 現在は1.が動作していますが、今回は2.が失敗します。クライアントがmsgを '/app/hello'に送信すると、はい、greetingメソッドは実行されましたが、クライアントはグリーティングメッセージを受信しませんでした。 (おそらく、クライアントは「/app」という接頭辞が付いたトピックにサブスクライブされたため、これは望ましくありませんでした。)

だから、私が得たのは私が望むものの1つまたは2つですが、これら2つは一緒ではありません。

  • この構造でどのようにこれを達成するのですか(マッピングパスを正しく設定する)?
34
Mert Mertce

デフォルトでは、@ SubscribeMappingメソッドからのreturn valueは、接続されたクライアントにメッセージとして直接返され、doesブローカーを通過しません

(エンファシス鉱山)

ここで、Spring Frameworkのドキュメントは、着信SUBSCRIBEメッセージではなく、応答メッセージで何が起こるかを説明しています。

質問に答えるには:

  • はい、クライアントはトピックにサブスクライブしています
  • はい、そのトピックをサブスクライブしているクライアントは、そのトピックを使用して送信するとメッセージを受信します
  • メッセージブローカーがサブスクリプションの管理を担当しています

サブスクリプション管理の詳細

SimpleMessageBrokerを使用すると、メッセージブローカーの実装はアプリケーションインスタンスに存在します。サブスクリプションの登録は、DefaultSubscriptionRegistryによって管理されます。メッセージを受信すると、SimpleBrokerMessageHandlerSUBSCRIPTIONメッセージを処理し、サブスクリプションを登録します( 実装を参照 )。

RabbitMQのような「実際の」メッセージブローカーを使用して、メッセージをブローカーに転送するStompブローカーリレーを構成しました。その場合、SUBSCRIBEメッセージは、サブスクリプションの管理を担当するブローカーに転送されます( こちらの実装を参照 )。

更新-STOMPメッセージフローの詳細

STOMPメッセージフローのリファレンスドキュメント を見ると、次のことがわかります。

  • 「/ topic/greeting」へのサブスクリプションは「clientInboundChannel」を通過し、ブローカーに転送されます
  • 「/ app/greeting」に送信されたグリーティングは、「clientInboundChannel」を通過して、GreetingControllerに転送されます。コントローラーは現在の時刻を追加し、戻り値はメッセージとして「brokerChannel」を介して「/ topic/greeting」に渡されます(宛先は規則に基づいて選択されますが、@ SendToでオーバーライドできます)。

ここで、/topic/helloはブローカーの宛先です。そこで送信されたメッセージは、ブローカーに直接転送されます。 /app/helloはアプリケーションの宛先であり、/topic/helloが別の指示をしない限り、@SendToに送信されるメッセージを生成することになっています。

更新された質問はどういうわけか異なるものであり、より正確なユースケースがなければ、これを解決するのに最適なパターンを言うことは困難です。以下にいくつかを示します。

  • 何かが非同期に発生したときはいつでもクライアントに認識させたい場合:特定のトピック/topic/helloにサブスクライブします
  • メッセージをブロードキャストする場合:特定のトピックにメッセージを送信する/topic/hello
  • たとえば、アプリケーションの状態を初期化するために、何かに対する即時のフィードバックを取得したい場合:アプリケーション宛先/app/helloにSUBSCRIBEを送信し、コントローラーがすぐにメッセージで応答する
  • 任意のアプリケーション宛先に1つ以上のメッセージを送信する場合/app/hello@MessageMapping@SendToの組み合わせ、またはメッセージングテンプレートを使用します。

良い例が必要な場合は、 実際のユースケースでSpring websocket機能のログを示すこのチャットアプリケーション をチェックしてください。

17
Brian Clozel

両方を持っている:

  • トピックを使用してサブスクリプションを処理する
  • そのトピックで@SubscribeMappingを使用して接続応答を配信する

あなたが経験したように機能しません(私も同様)。

(私がやったように)あなたの状況を解決する方法は:

  1. @SubscribeMappingを削除します-/ appプレフィックスでのみ機能します
  2. 当然のように/ topicをサブスクライブします(/ appプレフィックスなし)
  3. ApplicationListenerを実装する

    1. 単一のクライアントに直接返信したい場合は、ユーザーの宛先を使用します( websocket-stomp-user-destination を参照するか、/ topic/my-id-42などのサブパスをサブスクライブすることもできます)このサブトピックにメッセージを送信できます(正確なユースケースについてはわかりませんが、私は専用のサブスクリプションを持っているので、broadcast

    2. StompCommand.SUBSCRIBEを受け取ったらすぐに、ApplicationListenerのonApplicationEventメソッドでメッセージを送信します

サブスクリプションイベントハンドラー:

@Override
  public void onApplicationEvent(SessionSubscribeEvent event) {
      Message<byte[]> message = event.getMessage();
      StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
      StompCommand command = accessor.getCommand();
      if (command.equals(StompCommand.SUBSCRIBE)) {
          String sessionId = accessor.getSessionId();
          String stompSubscriptionId = accessor.getSubscriptionId();
          String destination = accessor.getDestination();
          // Handle subscription event here
          // e.g. send welcome message to *destination*
       }
  }
15
Pauli

私は同じ問題に直面し、クライアントで/topic/appの両方にサブスクライブすると、最終的にソリューションに切り替わり、/topicバインドまで/appハンドラーで受信したすべてをバッファリングしましたすべてのチャット履歴をダウンロードします。つまり、@SubscribeMappingが返します。次に、最近のすべてのチャットエントリを/topicで受信したものとマージします。私の場合は重複している可能性があります。

別の作業アプローチは、宣言することでした

registry.enableSimpleBroker("/app", "/topic");
registry.setApplicationDestinationPrefixes("/app", "/topic");

明らかに、完璧ではありません。しかし、働いた:)

5

こんにちはMertさん、質問は4年以上前に聞かれますが、最近同じ問題に頭を悩まして最終的に解決したので、答えようとします。

ここで重要なのは@SubscribeMappingone-timeリクエスト-レスポンス交換であるため、コントローラーのtry1()メソッドはクライアントコードの実行直後に1回だけトリガーされる

stompClient.subscribe('/topic/greetings', callback)

その後、try1()によってstompClient.send(...)をトリガーする方法はありません

ここでのもう1つの問題は、コントローラーがアプリケーションメッセージハンドラーの一部であり、宛先がプレフィックス/appでリッピングされているため、@SubscribeMapping("/topic/greetings")に到達するには、実際にこのようなクライアントコードを記述する必要があることです

stompClient.subscribe('/app/topic/greetings', callback)

従来のtopicは曖昧さを避けるためにブローカーにマッピングされるため、コードを次のように変更することをお勧めします。

@SubscribeMapping("/greetings")

stompClient.subscribe('/app/greetings', callback)

そして今console.log('RECEIVED !!!')は動作するはずです。

official doc は、初期UIレンダリングでの@SubscribeMappingのユースケースシナリオも推奨しています。

これはいつ便利ですか?ブローカーは/ topicおよび/ queueにマッピングされ、アプリケーションコントローラーは/ appにマッピングされると仮定します。この設定では、ブローカーは繰り返しのブロードキャストを目的とする/ topicおよび/ queueへのすべてのサブスクリプションを保存し、アプリケーションが関与する必要はありません。クライアントはまた、/ app宛先にサブスクライブすることもできます。コントローラーは、サブスクリプションを格納または使用せずにブローカーを関与させることなく、そのサブスクリプションに応じて値を返すことができます(事実上、1回限りの要求応答交換)。この使用例の1つは、起動時にUIに初期データを入力することです。

1
mzoz

完全に関連しているわけではないかもしれませんが、「app/test」に登録しているとき、「app/test」に送信されたメッセージを受信することはできませんでした。

ブローカーを追加することが問題であることがわかりました(理由はわかりません)。

だからここに前の私のコードがあります:

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app");
        config.enableSimpleBroker("/topic");
    }

後:

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app");
        // problem line deleted
    }

さて、 'app/test'にサブスクライブすると、これは機能します:

    template.convertAndSend("/app/test", stringSample);

私の場合、これ以上は必要ありません。

1
FloFlow