Help us understand the problem. What is going on with this article?

JavaでWebSocket使い方メモ

More than 5 years have passed since last update.

Java で WebSocket を実装してみる。
クライアント側は JavaScript で実装。

環境

AP サーバー

Tomcat 8.0.15

ブラウザ

  • IE11
  • Google Chrome 38
  • Firefox 33

HelloWorld

build.gradle

build.gradle
apply plugin: 'war'

war.baseName = 'websocket'

repositories {
    mavenCentral()
}

dependencies {
    providedCompile 'javax.websocket:javax.websocket-api:1.1'
}

フォルダ構成

|-build.gradle
`-src/main/
  |-java/sample/websocket/
  |  `-SampleWebSocket.java
  `-webapp/
     |-index.html
     `-script.js

サーバー側実装

SampleWebSocket.java
package sample.websocket;

import javax.websocket.OnMessage;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/echo")
public class SampleWebSocket {

    @OnMessage
    public String echo(String message) {
        System.out.println(message);
        return message;
    }
}

クライアント側実装

index.html
<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Sample</title>
    <script src="jquery.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <span id="message"></span>
  </body>
</html>
script.js
$(function() {
    var url = 'ws://localhost:8080/websocket/echo';
    var ws = new WebSocket(url);

    ws.onmessage = function(receive) {
        $('#message').text(receive.data);
    };

    ws.onopen = function() {
        ws.send('Hello WebSocket');
    }
});

動作確認

war ファイルを作成して tomcat にデプロイし、ブラウザで http://localhost:8080/websocket/ にアクセス。

websocket.JPG

サーバーコンソール出力
Hello WebSocket

説明

サーバー側

@ServerEndpoint("/echo")
public class SampleWebSocket {

    @OnMessage
    public String echo(String message) {
        System.out.println(message);
        return message;
    }
}
  • @ServerEndpoiint でアノテートしたクラスが、サーバー側の WebSocket のエンドポイントになる。
    • value 属性でパスを定義する。
  • @OnMessage でメソッドをアノテートすると、クライアントからメッセージを受信したときに、そのメソッドが実行される。
    • 引数に受信したメッセージを受け取れる。
    • 戻り値が、そのままクライアントに返信される。

クライアント側

$(function() {
    var url = 'ws://localhost:8080/websocket/echo';
    var ws = new WebSocket(url);

    ws.onmessage = function(receive) {
        $('#message').text(receive.data);
    };

    ws.onopen = function() {
        ws.send('Hello WebSocket');
    }
});
  • WebSocket のインスタンスを生成するとサーバーへの接続を開始する。
  • 接続が完了すると、 onopen に渡した関数がコールバックされる。
  • send() メソッドでサーバーにメッセージを送信する。
  • サーバーからメッセージが送られると、 onmessage に設定した関数がコールバックされる。

サーバーからクライアントにメッセージを送信する

サーバーからクライアントにメッセージを送る方法は、大きく2つある。
1つは Hello World のところの実装のように、 @OnMessage でアノテートしたメソッドの戻り値を使う方法。
もう1つは、 RemoteEndpoint を使う方法。

package sample.websocket;

import java.io.IOException;

import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/echo")
public class SampleWebSocket {

    @OnMessage
    public void echo(String message, Session session) throws IOException {
        session.getBasicRemote().sendText(message);
    }
}
  • javax.websocket.Session を引数に追加して、 getBasicRemote() メソッドで RemoteEndpoint のインスタンスを取得し、 sendText() メソッドでクライアントにメッセージを送信する。

非同期でメッセージを送信する

getBasicRemote() で取得できる RemoteEndpoint は、メッセージの送信が完了するまで処理を待機する。
終了を待ちたくない場合は、 getAsyncRemote()RemoteEndpoint を取得する。

package sample.websocket;

import java.io.IOException;

import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/echo")
public class SampleWebSocket {

    @OnMessage
    public void echo(String message, Session session) throws IOException {
        session.getAsyncRemote().sendText(message);
    }
}

接続中の全てのクライアントにメッセージを送信する

実装

package sample.websocket;

import java.io.IOException;

import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/broadcast")
public class SampleWebSocket {

    @OnMessage
    public void broadcast(String message, Session session) throws IOException {
        session.getOpenSessions().forEach(s -> {
            s.getAsyncRemote().sendText(message);
        });
    }
}
<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Sample</title>
    <script src="jquery.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <input type="text" id="message" />
    <button id="send">Send</button>

    <hr>

    <span id="receive"></span>
  </body>
</html>
$(function() {
    $('#send').attr('disabled', true);
    var ws = new WebSocket('ws://localhost:8080/websocket/broadcast');

    ws.onopen = function() {
        $('#send').attr('disabled', false);
    };

    ws.onmessage = function(receive) {
        $('#receive').text(receive.data);
    };

    $('#send').on('click', function() {
        var sendMessage = $('#message').val();
        ws.send(sendMessage);
    });
});

動作確認

IE, Chrome, Firefox でページを開いて、 IE でテキストを入力、ボタンをクリックする。

websocket.JPG

すると、 Chrome, Firefox に IE で入力した文字が出力される。

説明

  • Session.getOpenSessions() で、現在接続中の全てのセッションを取得できる。

問題点

この実装方法で、ブロードキャストは問題なく実装できる。

しかし、この実装には無駄が多い。
クライアントに送信するデータは全て同じ内容だが、送信するための形式にデータを変換する処理が毎回行われている。

同じデータは使いまわせるようにすると効率が良いか、現状 WebSocket の API にはこれを実現するものが定義されていない。

リファレンス実装である Tyrus を使っていれば、以下のように実装することでこの無駄を回避できる。

@OnMessage
public void onMessage(Session s, String m) {
  ((TyrusSession) s).broadcast(m);
}

今後のバージョンアップで改善されるかもしれない。

参考
- Oracle Blogs 日本語のまとめ: [Java] Optimized WebSocket broadcast

イベントハンドリング

実装

package sample.websocket;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/event")
public class SampleWebSocket {

    @OnOpen
    public void onOpen(Session session) {
        System.out.println("open : " + session.getId());
    }

    @OnClose
    public void onClose(Session session) {
        System.out.println("close : " + session.getId());
    }

    @OnError
    public void onError(Session session, Throwable cause) {
        System.out.println("error : " + session.getId() + ", " + cause.getMessage());
    }
}
new WebSocket('ws://localhost:8080/websocket/event');

動作確認

ブラウザで http://localhost:8080/websocket/ を表示。

サーバー側コンソール出力
open : 0

F5 で画面を再描画。

サーバー側コンソール出力
close : 0
open : 1

ブラウザを閉じる。

サーバー側コンソール出力
error : 1, java.util.concurrent.ExecutionException: java.io.IOException: 確立された接続がホスト コンピューターのソウトウェアによって中止されました。
close : 1

説明

  • @OnOpen で接続開始時、 @OnClose で接続終了時、 @OnError で通信エラー時の処理をハンドリングできる。
  • WebSocket のセッションと HttpServletSession は完全に別物。
  • ブラウザが再描画されれば JavaScript も実行され直すので、前の接続(セッション)は切断される。

パスパラメータを使用する

実装

package sample.websocket;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/pathparam/{string}/{number}")
public class SampleWebSocket {

    @OnOpen
    public void open(@PathParam("string") String string, @PathParam("number") int number) {
        System.out.printf("OnOpen : string=%s, number=%d%n", string, number);
    }

    @OnMessage
    public void message(String message, @PathParam("string") String string, @PathParam("number") int number) {
        System.out.printf("Message : message=%s, string=%s, number=%d%n", message, string, number);
    }

    @OnClose
    public void close(@PathParam("string") String string, @PathParam("number") int number) {
        System.out.printf("Close : string=%s, number=%d%n", string, number);
    }
}
<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Sample</title>
    <script src="jquery.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <button id="hoge">hoge</button>
    <button id="fuga">fuga</button>
  </body>
</html>
$(function() {
    $('#hoge').on('click', function() {
        var ws = new WebSocket('ws://localhost:8080/websocket/pathparam/hoge/11');
        ws.onopen = function() {
            ws.send('hoge message');
            ws.close();
        };
    });

    $('#fuga').on('click', function() {
        var ws = new WebSocket('ws://localhost:8080/websocket/pathparam/fuga/22');
        ws.onopen = function() {
            ws.send('fuga message');
            ws.close();
        };
    });
});

動作確認

ブラウザでページを開いて、「hoge」ボタンをクリック。

サーバー側コンソール出力
OnOpen : string=hoge, number=11
Message : message=hoge message, string=hoge, number=11
Close : string=hoge, number=11

「fuga」ボタンをクリック。

サーバー側コンソール出力
OnOpen : string=fuga, number=22
Message : message=fuga message, string=fuga, number=22
Close : string=fuga, number=22

説明

  • @ServerEndpointvalue 属性に指定するパスに、 {パラメータ名} という形式でパスパラメータを定義することができる。
  • パスパラメータは、 @OnOpen, @OnMessage, @OnClose, @OnError などの各メソッドで引数として受け取ることができる。
  • メソッド引数には、 @PathParam("パラメータ名") という形で受け取るパラメータを指定する。
  • 基本は String だが、 int などのプリミティブ型で受け取ることも可能。

接続を閉じる

サーバー側から閉じる

実装

package sample.websocket;

import java.io.IOException;

import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/close")
public class SampleWebSocket {

    @OnMessage
    public void close(String message, Session session) throws IOException {
        session.close();
    }
}
var ws = new WebSocket('ws://localhost:8080/websocket/close');

ws.onopen = function() {
    ws.send(null);
}

ws.onclose = function(closeEvent) {
    console.log('code = ' + closeEvent.code + ', reason = ' + closeEvent.reason);
};

動作確認

ブラウザ側コンソール出力
code = 1000, reason =  

ブラウザ側から閉じる

実装

var ws = new WebSocket('ws://localhost:8080/websocket/close');

ws.onopen = function() {
    ws.close();
}
package sample.websocket;

import java.io.IOException;

import javax.websocket.CloseReason;
import javax.websocket.OnClose;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/close")
public class SampleWebSocket {

    @OnClose
    public void onClose(Session session, CloseReason reason) throws IOException {
        System.out.println("code = " + reason.getCloseCode().getCode() + ", reason = " + reason.getReasonPhrase());
    }
}

動作確認

サーバー側コンソール出力
code = 1000, reason = null

説明

  • サーバーから切断する場合は、 Session#close() メソッドを使用する。
  • クライアントから切断する場合は、 WebSocket#close() メソッドを使用する。

ServerEndpoint のインスタンスはセッションごとに生成される

実装

package sample.websocket;

import javax.websocket.OnMessage;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/instance")
public class SampleWebSocket {

    @OnMessage
    public void message(String message) {
        System.out.println(this.hashCode());
    }
}
<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Sample</title>
    <script src="jquery.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <button id="send">Send</button>
  </body>
</html>
$(function() {
    var ws = new WebSocket('ws://localhost:8080/websocket/instance');

    ws.onopen = function() {
        $('#send').on('click', function() {
            ws.send('message');
        });
    }
});

動作確認

ブラウザ(Chrome)を開き、「Send」ボタンを何回かクリック。

サーバー側コンソール出力
1962593639
1962593639
1962593639
1962593639

新しいタブでページを開き、そちらで「Send」ボタンを何回かクリック。

サーバー側コンソール出力
2050833800
2050833800
2050833800
2050833800

F5で画面を再描画してから、「Send」ボタンを何回かクリック。

サーバー側
1334674039
1334674039
1334674039

説明

ServerEndpoint のインスタンスは、セッションごと(コネクションごと)に作成される。

1つのセッションの中では、常に同じ ServerEndpoint のインスタンスが利用される。
また、 ServerEndpoint のインスタンスは常に1つのスレッドとだけ関連付けられる。

つまり、セッションごとの情報を保持しておきたい場合は、 ServerEndpoint のインスタンスフィールドに保持しておけば良い。

Note:
As opposed to servlets, WebSocket endpoints are instantiated multiple times.
(略)
Each instance is associated with one and only one connection. This facilitates keeping user state for each connection and makes development easier, because there is only one thread executing the code of an endpoint instance at any given time.

The Java EE 7 Tutorial:Creating WebSocket Applications in the Java EE Platform | Java EE Documentation

サーバーから自発的にメッセージを送信する

ここまでの実装方法だと、サーバー側から自発的にクライアントにメッセージを送ることができない。

サーバー側から自発的にメッセージを送信するには、 ServerEndpoint の static フィールドに Session を保持しておき、 static メソッドを介して Session にアクセスする。

実装

SampleWebSocket.java
package sample.websocket;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.websocket.OnClose;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/broadcast")
public class SampleWebSocket {

    private static final Queue<Session> sessions = new ConcurrentLinkedQueue<>();

    static {
        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
        service.scheduleWithFixedDelay(SampleWebSocket::broadcast, 5, 5, TimeUnit.SECONDS);
    }

    @OnOpen
    public void connect(Session session) {
        sessions.add(session);
    }

    @OnClose
    public void remove(Session session) {
        sessions.remove(session);
    }

    public static void broadcast() {
        Date now = new Date();
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");

        sessions.forEach(session -> {
            session.getAsyncRemote().sendText("Broadcast : " + formatter.format(now));
        });
    }
}
<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Sample</title>
    <script src="jquery.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <span id="message"></span>
  </body>
</html>
$(function() {
    var ws = new WebSocket('ws://localhost:8080/websocket/broadcast');

    ws.onmessage = function(e) {
        $('#message').text(e.data);
    };
});

動作確認

ブラウザをいくつか立ち上げて、 http://lcoalhost:8080/broadcast を開く。

最初のアクセスから5秒ほど経過すると、各ブラウザに現在時刻が出力される。

websocket.JPG

以後、5秒ごとに時刻の表示が更新される。

説明

  • @OnOpen のときに Session を static 宣言した ConcurrentLinkedQueue に格納している。
  • また、 @OnClose のときに Session をキューから削除している。

自作の static フィールドに保存して管理するというのが、なんだか大丈夫なのかなと不安になるけど、 公式のチュートリアル がこうなっているのだから、きっと大丈夫なのだろう。

ちなみに、 JavaEE 環境なら CDI の Event を使ってもうちょっとスマート?に実装する方法が Stack Overflow で紹介されている。

java - How to get an existing websocket instance - Stack Overflow

メッセージを Java オブジェクトにマッピングするための仕組みを利用する

デコーダー(メッセージ→Javaオブジェクト)

実装

Hoge.java
package sample.websocket;

public class Hoge {

    public int id;
    public String name;

    @Override
    public String toString() {
        return "Hoge [id=" + id + ", name=" + name + "]";
    }
}
HogeDecoder.java
package sample.websocket;

import javax.websocket.DecodeException;
import javax.websocket.Decoder;
import javax.websocket.EndpointConfig;

public class HogeDecoder implements Decoder.Text<Hoge> {

    @Override
    public void init(EndpointConfig config) {
        System.out.println("init");
    }

    @Override
    public boolean willDecode(String s) {
        System.out.println("willDecode");
        return true;
    }

    @Override
    public Hoge decode(String s) throws DecodeException {
        System.out.println("decode");

        String[] tokens = s.split(":");

        Hoge hoge = new Hoge();
        hoge.id = Integer.parseInt(tokens[0]);
        hoge.name = tokens[1];

        return hoge;
    }

    @Override
    public void destroy() {
        System.out.println("destroy");
    }
}
SampleWebSocket.java
package sample.websocket;

import javax.websocket.OnMessage;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint(value="/decode", decoders=HogeDecoder.class)
public class SampleWebSocket {

    @OnMessage
    public void message(Hoge hoge) {
        System.out.println(hoge);
    }
}
<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Sample</title>
    <script src="jquery.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <button id="send">Send</button>
    <button id="close">Close</button>
  </body>
</html>
$(function() {
    var ws = new WebSocket('ws://localhost:8080/websocket/decode');

    ws.onopen = function(e) {
        $('#send').on('click', function() {
            ws.send('12:Hoge');
        });

        $('#close').on('click', function() {
            ws.close();
        });
    };
});

動作確認

Web ブラウザでページを表示。

サーバー側コンソール出力
init

「Send」ボタンをクリック。

サーバー側コンソール出力
willDecode
decode
Hoge [id=12, name=Hoge]

もう一回クリック。

サーバー側コンソール出力
willDecode
decode
Hoge [id=12, name=Hoge]

「Close」ボタンをクリック。

サーバー側コンソール出力
destroy

説明

  • Decoder を実装したクラスを作成し、 @ServerEndpointdecoders に指定する。
  • Decoder にはテキストを変換する Text<T> や、バイナリデータを変換する Binary<T> というサブインターフェースが用意されており、実際はこれらを実装したクラスを作成する。
  • デコーダーが指定されていると、クライアントからのメッセージは一旦このデコーダーに渡される。
  • デコーダーでは、以下のメソッドを実装する。
メソッド 説明
init 初期化処理。セッションが確立されたときに1度だけ実行される。
willDecode メッセージをデコードするかどうかを判定する。falseを返した場合、 decode() メソッドは実行されない。
decode メッセージを Java オブジェクトに変換するデコード処理を実装する。
destroy セッションが破棄されるときに1度だけ実行される。
  • @OnMessage でアノテートした ServerEndpoint のメソッドには、デコーダーが作成した Java オブジェクトが引数に渡される。

エンコーダー(Javaオブジェクト→メッセージ)

実装

HogeEncoder.java
package sample.websocket;

import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;

public class HogeEncoder implements Encoder.Text<Hoge>{

    @Override
    public void init(EndpointConfig config) {
        System.out.println("init");
    }

    @Override
    public String encode(Hoge hoge) throws EncodeException {
        System.out.println("encode");
        return hoge.id + ":" + hoge.name;
    }

    @Override
    public void destroy() {
        System.out.println("destroy");

    }
}
SampleWebSocket.java
package sample.websocket;

import javax.websocket.OnMessage;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint(value="/encode", encoders=HogeEncoder.class)
public class SampleWebSocket {

    @OnMessage
    public Hoge message(String message) {
        System.out.println("message");
        Hoge hoge = new Hoge();
        hoge.id = 20;
        hoge.name = "HOGE";

        return hoge;
    }
}
$(function() {
    var ws = new WebSocket('ws://localhost:8080/websocket/encode');

    ws.onopen = function() {
        $('#send').on('click', function() {
            ws.send(null);
        });

        $('#close').on('click', function() {
            ws.close();
        });
    };

    ws.onmessage = function(e) {
        console.log(e.data);
    };
});

動作確認

ブラウザでページを表示。

サーバー側コンソール出力
init

「Send」ボタンをクリック。

サーバー側コンソール出力
message
encode
クライアント側コンソール出力
20:HOGE

「Close」ボタンをクリック。

サーバー側コンソール出力
destroy

説明

  • デコーダーの逆。変換の向きが逆になったこと以外、特に大きな違いはない。

おまけ

Encoder と Decoder は、そのままだとパラメータで使う型の数だけ作成しないといけないため、ちょっとイケてない。

ということで、 json と任意の型とを変換する Encoder と Decoder を実装してみた。

build.gradle

build.gradle
dependencies {
    compile 'com.fasterxml.jackson.core:jackson-databind:2.4.3'
    providedCompile 'javax.websocket:javax.websocket-api:1.1'
}

json の変換には Jackson を利用する。

実装

JsonConverter.java
package sample.websocket;

import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Stream;

import javax.websocket.DecodeException;
import javax.websocket.Decoder;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.server.PathParam;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonConverter implements Encoder.Text<Object>, Decoder.Text<Object> {

    public static final String ENDPOINT_CLASS = "json.endpoint.class";

    private ObjectMapper mapper = new ObjectMapper();
    private Map<String, Object> userProperties;
    private Class<?> messageClass;

    @Override
    public void init(EndpointConfig config) {
        this.userProperties = config.getUserProperties();
    }

    @Override
    public Object decode(String s) throws DecodeException {
        try {
            Class<?> messageClass = this.getMessageClass();
            return this.mapper.readValue(s, messageClass);
        } catch (IOException e) {
            throw new DecodeException(s, "failed to decode message.", e);
        }
    }

    private Class<?> getMessageClass() {
        if (this.messageClass == null) {
            this.initMessageClass();
        }

        return this.messageClass;
    }

    private void initMessageClass() {
        Class<?> endpointClass = (Class<?>)userProperties.get(ENDPOINT_CLASS);

        Method onMessageMethod = this.findOnMessageMethod(endpointClass);
        Parameter messageParameter = this.findMessageParameter(onMessageMethod);

        this.messageClass = messageParameter.getType();
    }

    private Method findOnMessageMethod(Class<?> endpointClass) {
        return this.findFromArray(endpointClass.getMethods(), this::isOnMessageMethod, "method annotated by @OnMessage is not found.");
    }

    private boolean isOnMessageMethod(Method method) {
        return method.isAnnotationPresent(OnMessage.class);
    }

    private Parameter findMessageParameter(Method onMessageMethod) {
        return this.findFromArray(onMessageMethod.getParameters(), this::isMessageParameter, "message parameter is not found.");
    }

    private boolean isMessageParameter(Parameter parameter) {
        return this.isNotSession(parameter) && this.isNotPathParam(parameter);
    }

    private boolean isNotSession(Parameter parameter) {
        return !Session.class.isAssignableFrom(parameter.getType());
    }

    private boolean isNotPathParam(Parameter parameter) {
        return !parameter.isAnnotationPresent(PathParam.class);
    }

    private <T> T findFromArray(T[] array, Predicate<T> condition, String exceptionMessage) {
        return Stream.of(array)
                     .filter(condition)
                     .findFirst()
                     .orElseThrow(() -> new RuntimeException(exceptionMessage));
    }

    @Override
    public String encode(Object object) throws EncodeException {
        try {
            return this.mapper.writeValueAsString(object);
        } catch (JsonProcessingException e) {
            throw new EncodeException(object, "failed to encode message.", e);
        }
    }

    @Override
    public boolean willDecode(String s) {
        return true;
    }

    @Override public void destroy() {/*no use*/}
}
package sample.websocket;

import javax.websocket.EndpointConfig;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint(value="/json", encoders=JsonConverter.class, decoders=JsonConverter.class)
public class SampleWebSocket {

    @OnOpen
    public void open(EndpointConfig config) {
        config.getUserProperties().put(JsonConverter.ENDPOINT_CLASS, SampleWebSocket.class);
    }

    @OnMessage
    public Hoge message(Hoge message, Session session) {
        System.out.println("message = " + message);

        Hoge hoge = new Hoge();
        hoge.id = 33;
        hoge.name = "fuga";

        return hoge;
    }
}
<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Sample</title>
    <script src="jquery.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <button id="send">Send</button>
    <button id="close">Close</button>
  </body>
</html>
$(function() {
    var ws = new WebSocket('ws://localhost:8080/websocket/json');

    ws.onopen = function() {
        $('#send').on('click', function() {
            var message = {id: 15, name: "Hoge"};
            ws.send(JSON.stringify(message));
        });

        $('#close').on('click', function() {
            ws.close();
        });
    };

    ws.onmessage = function(e) {
        console.log(e.data);
    };
});

動作確認

ブラウザでページを表示して、「Send」ボタンをクリック。

サーバー側コンソール出力
message = Hoge [id=15, name=Hoge]
クライアント側コンソール出力
{"id":33,"name":"fuga"}

説明

  • EndpointConfig#getUserProperties() で取得できる Map (UserProperties)を介して、 ServerEndpoint の型の情報をやり取りしている。
  • SampleWebSocketopen() メソッド内で、 ServerEndpoint の型を UserProperties に設定する。
  • JsonConverter 側は、 init() メソッドで UserProperties を取得してインスタンス変数に保存しておく。
    • SampleWebSocketopen() メソッドより JsonConverterinit() メソッドが先に呼ばれる。
    • このため、この時点では UserProperties に型情報は保存されていない。
  • そして、 decode() メソッドが呼ばれたときに変換後の型情報をリフレクションで取得し、 Jackson でデコードしている。

これで JsonConverter は使い回ししやすくなるはず。

参考

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away