読者です 読者をやめる 読者になる 読者になる

Spring Messaging でのWebSocketメモ

Spring MessagingでのSTOMPは以下の形で設定を行う。 これがなぜ動くのかを軽く探ってみたのでメモとして残しておく。

普通にSpringでWebsocketを使いたいならリファレンスのここ見ればよいかと。

なぜこのリファレンスにあるソースが動くのかを大まかに調べるためにこの記事にメモしておく。

import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

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

}

アノテーション@EnableWebSocketMessageBrokeには以下のアノテーションが付けられている

...
@Import({
  DelegatingWebSocketMessageBrokerConfiguration.class
})
public @interface EnableWebSocketMessageBroker {
}

DelegatingWebSocketMessageBrokerConfigurationが渡されている。

DelegatingWebSocketMessageBrokerConfigurationの中身

InjectionされたWebScoketMessageBrokerConfigurerのリストに対して 処理を移譲する形になっている。

親クラスWebSocketMessageBrokerConfigurationSupportにて
SimpAnnotationMethodMessageHandlerを返却するcreateAnnotationMethodMessageHandlerが実装されている。
このメソッド内部ではWebSocketAnnotationMethodMessageHandlerインスタンス化されて返却されている。
このメソッドはAbstractMessageBrokerConfigurationのsimpAnnotationMethodMessageHandlerから呼び出される。

WebSocketAnnotationMethodMessageHandlerSimpAnnotationMethodMessageHandlerの子クラスである。
SimpAnnotationMethodMessageHandlerにはinitReturnValueHandlersというメソッドが実装されており
これはこの親クラスのAbstractMethodMessageHandlerにて抽象メソッドとして定義されている。

また、このクラスAbstractMethodMessageHandlerから afterPropertiesSetメソッドにてinitReturnValueHandlersが呼び出されている。
afterPropertySetメソッドはInitializingBeanで定義されているメソッドである。

話を元に戻して、先ほどのSimpAnnotationMethodMessageHandlerのinitReturnValueHandlersではSendToMethodReturnValueHandlerSubscriptionMethodReturnValueHandler
その他のHandlerMethodReturnValueHandlerを実装しているクラスのリストを返却している。

HandlerMethodReturnValueHandlerは以下のようなインターフェースになっている。

package org.springframework.messaging.handler.invocation;

import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;

public interface HandlerMethodReturnValueHandler {
  boolean supportsReturnType(MethodParameter arg0);

  void handleReturnValue(Object returnValue, MethodParameter returnType, Message<?> message) throws Exception;
}

この実装の参考はSendToMethodReturnValueHandlerを見るとよいかと思われる。

HandlerMethodReturnValueHandlerのhandleReturnValueが呼ばれる個所

AbstractMethodMessageHandlerafterPropertiesSetにてinitReturnValueHandlersが呼ばれたのちHandlerMethodReturnValueHandlerCompositeのaddHandlersメソッドにそのまま渡されている
このクラスはHandlerMethodReturnValueHandlerを実装しており
実際にはaddHandlersで追加されたHandlerに処理を移譲するようなクラスになっている。

HandlerMethodReturnValueHandlerCompositegetReturnValueHandlerにてaddHandlersで追加されたHandlerのsupportsReturnTypeの戻り値を見て
実際に使うMethodHandlerの決定が行われる。

このメソッドは、 AbstractMethodMessageHandler#handleMessage (MessageHandler#handleMessage)
AbstractMethodMessageHandler#handleMessageInternal
AbstractMethodMessageHandler#handleMatch
HandlerMethodReturnValueHandlerComposite#handleReturnValue
HandlerMethodReturnValueHandlerComposite#getReturnValueHandler
の流れで呼ばれる。

Websocket Scopeの実装

Spring MessagingとSpring Websocketの連携では、websocket scopedなBeanの定義が可能になっている。 これらのScopeの実装に関わるクラスは以下のクラスである。

  1. org.springframework.messaging.simp.SimpSessionScope
  2. org.springframework.messaging.simp.SimpAttributesContextHolder
  3. org.springframework.messaging.simp.SimpAttributes

org.springframework.messaging.simp.SimpAttributesContextHolderの内部にて
ThreadLocalな値を保持しており、その値の型はSimpAttributes型になっている。

SimpAttributesはただのMapである。
解説はここでは特にしない。

また、SimpAnnotationMethodMessageHandlerのhandleMatchにてSimpAttributesContextHolderのsetAttributesFromMessageとresetAttributesが呼び出されており
ここでWebsocket Scopeの実装が行われているように伺える。

まとめ

とりあえずこれまでのSpringはここまで。

Spring入門した。

遅まきながらSpringを入門した時のメモを書きます。 Spring Bootではありません。

経緯

  1. WebScoketやりたい。
  2. Spring使ってみよう。
  3. Spring WebScoketあるじゃん使おう。
  4. Spring Messagingの連携良さそう。
  5. Stomp、SockJS?よく分かりません。(socket.ioがいいですボソッ)
    ・StompはデータフォーマットとそのEncoder/Decoderっぽい
    ・SockJSはWebSocketのフォールバックでWebSocketがない環境でも
    http通信でWebSocketをエミュレーションするやつっぽい
  6. Socket.ioのJava Implementationないの・・・?
    Springじゃなくて別のフレームワークならあった。。 。
  7. とりあえずじゃあ、Spring WebSocketでSpring Messagingの連携書いてみよう
  8. WebSocketSessionの間で有効なWebSocketSessionScopeみたいなの欲しいよね。調べる。 OMG!WebSocketMessageBroker(Stompと連携するやつ)じゃないと動かない。却下。
  9. 独自Scope書いて切り替える処理書いてみるかー

というわけで独自Scope書きました。

まずはSpringの初期化から

web.xml書きたくないでござる!!書きたくないでござる!! ApplicationContext.xml書きたくないでござる!!書きたくないでござる!!

というわけでアプリケーションの初期化をコードで書きます。 SpringのWebアプリ―ケーションの初期化はこいつ(WebApplicationInitializer)を実装すればいいみたい。 なるほど。JAX-RSのApplicationクラスみたいですね。

WebApplicationInitializerの処理はこっちから呼び出されてるようです。 以前うらがみさんに教えてもらったServletContainerInitializerが呼ばれる仕組みの流れからWebApplicationInitializerが呼ばれているようですね。

WebApplciationInitializerを継承したクラスを書いてみます。

書きました。

public class ApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new Class[] {
      WebSocketConfig.class
    };
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class[] {};
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] {
      "/echo"
    };
  }
}

Springが用意している抽象クラスを使うと楽になりました。 WebApplciationInitializerを自分で実装すると ドキュメントの 「A 100% code-based approach to configuration」の部分になるようです。xml書くよりは楽みたいです。

WebSocket用の設定

WebSocketの設定クラスの中でHandlerの登録を行います。 また、独自Scopeもここで渡してます。 WebSocket向けにEnableWebSocket AspectJでトレースログを吐きたかったのでEnableAspectJAutoProxyを付けています。

@Configuration
@EnableWebSocket
@EnableAspectJAutoProxy
@ComponentScan
public class WebSocketConfig implements WebSocketConfigurer {
  @Autowired
  EchoHandler echoHandler;
  
  public static final String WEB_SOCKET_SCOPE_NAME = "test_webscoket";

  @Override
  public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry.addHandler(echoHandler, "/echo");
  }
  
  @Bean 
  public static CustomScopeConfigurer customScopeConfigurer(){
    CustomScopeConfigurer configurer = new CustomScopeConfigurer();
    configurer.addScope(WEB_SOCKET_SCOPE_NAME, new WebSocketScope());
    return configurer;
  }
}

Scopeの実装の前に

@Component
@Scope(scopeName = WebSocketConfig.WEB_SOCKET_SCOPE_NAME)
public class TestService {
  public String serve() {
    return this.toString();
  }
}

このTestServiceをWebSocketSessionの生存範囲で動かしたいわけですが Scopeアノテーションで独自スコープ名に変更してあります。

serveの処理でtoStringの値を返しているのはハッシュコードの比較して 適切なスコープが実装されているかどうか確認します。

WebSocketのハンドラ

今回はTextMessageだけ取り扱う形にして TextWebSocketHandlerを継承しています。 メッセージを扱える際にSocketSessionHolder.setSession(session);といった形で ThreadLocalにデータを格納して、セッション単位でスコープの切り替えを行っています。 拡張をここからするなら、JacksonとかでObject Mappingをする必要があったりEventListenerが必要になります。

@Component
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
public class EchoHandler extends TextWebSocketHandler {

  private Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();

  @Override
  public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    sessions.put(session.getId(), session);
  }

  @Override
  public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    sessions.remove(session.getId());
    SocketSessionHolder.removeSession(session);
  }

  @Override
  protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    SocketSessionHolder.setSession(session);
    System.out.println("start");
    try {
      WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();
      System.out.println(context.getBean(TestService.class)
        .serve());
      System.out.println(context.getBean(TestService.class)
        .serve());
      sessions.forEach((__, other) -> {
        try {
          other.sendMessage(new TextMessage("catch " + message.getPayload()));
        } catch (Exception e) {
          e.printStackTrace();
        }
      });
    } finally {
      System.out.println("end");
      SocketSessionHolder.removeContext();
    }
  }
}

Scopeの実装

SpringではScopeの実装のために、Scopeインターフェースが用意されています。 以下のようなコードで実装しました。

public class WebSocketScope implements Scope {

  @Override
  public Object get(String name, ObjectFactory<?> factory) {
    return SocketSessionHolder.getCurrentContainer().get(name, factory::getObject);
  }

  @Override
  public String getConversationId() {
    return SocketSessionHolder.current().getId();
  }

  @Override
  public void registerDestructionCallback(String name, Runnable arg1) {
    // TODO Auto-generated method stub

  }

  @Override
  public Object remove(String name) {
    return SocketSessionHolder.getCurrentContainer().remove(name);
  }

  @Override
  public Object resolveContextualObject(String name) {
    // TODO Auto-generated method stub
    return null;
  }

}

registerDestructionCallbackの実装は行っていません。 たぶんコネクションクローズした時にオブジェクトの破棄をする時のコールバックを保持しておいて Object破棄時に呼ぶ処理を実装するんだと思います。

resolveContextualObjectの実装はよく分からないのでまた今度調べます。 →いくつかサンプル見てもnull返すのでいいみたい・・・?

もう少しScopeアノテーションに関する実装の読み込みが必要なようです。

コード中に出てくるSocketSessionHolderは ThreadLocalとWebSocketSessionを紐づけて オブジェクトプールを管理するクラスです。

以下のようなコードです。

public class SocketSessionHolder {
  private static ThreadLocal<WebSocketSession> session = new NamedThreadLocal<>("WebScoketThreadLocal");
  // TODO ConcurrentMap?
  private static Map<WebSocketSession, WebSocketScopeContainer> pool = new HashMap<>();

  public static WebSocketSession setSession(WebSocketSession session) {
    SocketSessionHolder.session.set(session);
    if (pool.get(session) == null) {
      pool.put(session, new WebSocketScopeContainer());
    }
    return session;
  }

  public static WebSocketSession current() {
    return session.get();
  }

  public static WebSocketScopeContainer getCurrentContainer() {
    return pool.get(current());
  }

  public static void removeContext() {
    session.remove();
  }
  
  public static void removeSession(WebSocketSession session){
    pool.remove(session);
  }
}

EchoHandlerのhandleTextMessageのメソッドが セッション単位で並列に呼ばれることがあるのかがわからないので これは後で調べます。たぶん呼ばれる気がしますが。

WebSocketScopeContainerはただのオブジェクトプールです

public class WebSocketScopeContainer {
  private Map<String, Object> pool = new HashMap<>();

  public Object remove(String name) {
    return pool.remove(name);
  }

  public Object get(String name, Supplier<?> supplier) {
    Object object = pool.get(name);
    if (object == null) {
      Object newObject = supplier.get();
      pool.put(name, newObject);
      return newObject;
    }
    return object;
  }

}

動作確認

JavaScriptで動作確認してみます。(実際にはこれを読み込んだhtmlのページを開きます。) WebSocketのSessionが確立した後に メッセージを2回送っています。

これはオブジェクトの生存スコープを確認するためです。 sendでメッセージを送った時に、ログに同じハッシュ値のオブジェクトが出てくるはずです。

const url1 = `ws://${location.host}${location.pathname}echo`;
test(url1);
function test(url) {
  const ws= new WebSocket(url);
  const bind=(f, v)=> f.bind(null, v);
  const log=console.log.bind(console);

  ws.onclose=bind(log, "close");
  ws.onerror=bind(log, "error");
  ws.onmessage=bind(log, "message");
  ws.onopen=()=> {
      ws.send("send text1");
      ws.send("send text2");
  }
}

というわけで結果としては動きました。動作確認はTomcat v7.xで行いました。

f:id:reteria:20170203100245p:plain

まとめ

初めのモチベーションとしてはSpringでWebsocketするかーぐらいの気持ちでした。
しかし、StompjsとSockJSというものが出てきて
あまり使う気に慣れません。(Socket.ioを使ったことがあるので)
また、Stompjsのリポジトリを見たところ
no longer maintainedになっております。(あとCoffeeScriptで書かれてる。)

少なくともこれに乗っかる気にはなれませんでした。

そのため、WebSocketセッション単位でライフサイクルが扱えるなら
普通にSpringでStompjsを使わずにWebSocketすればええか、と思ったのですが・・・
ScopeはMessageBrokerを使う際のみ有効だということが分かったので
少し勉強のために書いた次第です。
なお、今回書いたコードはエラーハンドリングを除いた状態かつthread safeじゃない状態で記述しているので
もう少しコードを足す必要があります。

このコードを書くために、Spring Websocketの @EnableWebSocketMessageBrokerをアノテートして有効にした場合の動きを調べました。

github.com

spring-framework/SimpAttributesContextHolder.java at bc14c5ba83e1f211628456bbccce7b2531aac58c · spring-projects/spring-framework · GitHub

というわけでSpring入門しました。

リポジトリはこちらに置いてあります。

github.com

おまけ

Springのリポジトリを見たり検索していて気付いたのですが
spring-web-reactiveがspring-webfluxに替わってました。

この記事に関連してsocket.ioの動きを調べて メッセージのやり取りについて気になったので テストで試してみました。気になったら覗いてください。

github.com

socket.ioの動きの何が気になったかというと
WebSocketのエンドポイントとしては単一のはずなのに
あたかも、サブディレクトリがあるような動きをしています。
この動きはデータのエンコードの際にデータを付与して表現しているようです。
面白いですね。

新卒2年目のエンジニアの自分がやっている、心掛けていること

初めに

こんばんは。初めましての方は初めましてかもしれません。

少し新年明けて思うところがあったので書いてみます。
ネガティブな話はできるだけ避けて。

まず、自分の素性を軽く説明しておきます。

新卒で小さいSIerに就職しました。
今は現場に常駐して作業を行っています。チームリーダーみたいなことをやっています。
仕事では主にJavaやったりJavaScriptやったり、してます。
趣味ではなんとなく触りたいものから、仕事の調査みたいなことをやってます。

一つ前提として

この記事には根拠も全然ありません。その辺は妄想で補ってください。

この記事で書いていることは特に何の意味も役に立つこともないかもしれません。
それでも自分の思いとして、ここに残しておきます。
ポエムです。技術の話はたぶんありません。

1. 声をかけるときには名前を呼ぶ。

「〇〇さん、今、少しお時間よろしいですか?」

これは僕自身、名前が覚えられない・思い出せないことが前提にあります。

また、人の名前を呼ばれると承認欲求が満たされるみたいな話を見た覚えがあります。
なので、気持ちよく会話を始めるために、声を掛けるときには名前を呼んでいます。

気持ちよく仕事したいですね。

2. 相手の顔・目を見て話す。

相手の話を聞いてますよ、という意思表示ですね。

人間はボディランゲージや第一印象が与える影響は大きいという話がありまして
この辺は就活本とかにも載っていたかと思います。
個人的にはメラビアンの法則みたいな話を学生の時に聞いた覚えがあります。
相槌とかも打つといいかもしれませんね。

また、顔を見て話す理由としてもう一つありまして
これは、自分が話している内容が
相手に伝わっているかどうかを確認しています。

気をつけたいところなのですが、なかなか難しいです。

3. 声を掛けた際に簡単に要件を話す

「(以前お話をさせていただいた|先ほどメールさせていただいた)
 〇〇の××に関してお話させていただきたいのですが」

この時点で相手が「あ、はい。なるほど。」と言えば
要件を話せばよいと思いますが
たまに顔にはてなマークが浮かんでる時があります。

これに関しては次の話とまとめて話をします。

4. 話が伝わっていない時は一旦話を止めて、確認して、もう少し詳しく説明する

「(ほげほげほげ)・・・えーっと、なんか分からないことあります?」

話が伝わらないことには先に進めません。
一旦、相手に喋ってもらって、コンテキストの共有に努めます。
また、無駄に刺激してしまうので話を聞いてもらえないことがあります。
顔がうんざりしてたら、一旦止めて確認しています。

5. 曖昧なところはすぐに確認する。

要望と対応内容が噛み合ってない時がたまにあったりします。
そういった場合に、曖昧な要望と曖昧な期待する対応内容が与えられることがあります。
すぐに確認しています。(メールでも電話でもいい。)

6. 笑顔で話す

これは難しいです。職場には嫌いな人間は多少なりともいます。
でも気持ちよく仕事したいので笑顔で接します。

7. 間違いを認める。

会議中に議論している時に、自分が間違ったことを言っている時があります。
というか、設計上の問題に気づく時があります。
こういうときは正直恥ずかしいのですが、すぐに謝っています。

「こうこうこうしたほうが・・・あ、申し訳ありません。これじゃやっぱり動かないです。◯○という理由で××で(略)」

8. 辛い時は辛いと言う

辛いと言わないと辛いんだ、と思ってもらえません。
上司に相談しましょう。
「しんどいです。」みたいな感じで丸投げしてます。
上司さまさまです。感謝しています。

9. 新人にはしっかり喋ってもらう

新人のカテゴリに入ると思われる自分がこれを書くのは少し可笑しい話ですが書いておきます。
これはあんまり個人的な話からは外れます。

自分のタスクの話は自分で喋ってもらいます。(フォローはします。)

何故かと言うとぶっちゃけると新人は喋ることに慣れていません。
しかし、現場・会社の都合上、他の会社の人と喋る機会が多いです。
距離感はなかなか掴みかねるところがあります。
その為に慣れてもらうためにも話をしてもらいます。

慣れてくると自信も付いてくるかと思われます。

あとあまり面倒見れないこともあるので、という話もあるのですが
ここだけの秘密です。(ごめんね後輩)

10. 話が噛み合ってない時は間に一旦入る

話が伝わっていないまま喋っている人がいます。一旦話を仲裁して
もう少し手前から聞いてみます。話を遡ります。

「○○機能の××なんですけどdさおいひゃskljdさkljだs(勢いで喋ってる)」(相手、首をかしげる)
(間に入ります。)「ごめん、えーと、何作ってるんだっけ?」「エラー表示機能です」
「なんで作るんだっけ?」「エラーを表示するために」
「ちょっと待って、なんの為にエラーを表示したいんだっけ?」「入力ミスを防ぐためです」
「で、今どういう状態だっけ?」「エラー表示機能の中でエラー表示機能をどうすればいいのか悩んでます」

質問ベースで会話をして、トラブルシュート的な形で話を聞いたりしています。

個人的に仕様決める時はなんか話を一応聞いておきたいみたいなところで間に入ったりしてます。

11. 会話をする際に内部の技術的な(実装に近い)単語は避けて、資料を見て分かるもので説明する。

現場の都合も大きく関わってくるのですが
現状、コンテキストが分断されがちな状態になっています。
そのために、技術的な単語は避けて、見て分かる物を出したりしています。

「HogeViewからHogeActionが発火して、そこから書き換えが起きてー、画面が少し崩れてー」

上の例はひどいですね。

下のような形で喋ることを意識しています。実装の話は聞かれたら答える感じで。
「画面クリックした時に、別の処理が呼ばれて予期せぬ動作をしています。スクリーンショットとしてはこんな感じです。」

12. 他人の作業を手伝ってあげる。

信頼感構築できます。次からは作業を振っても怒られません(怒られる。)

人間、面倒なことは嫌いです。手伝ってあげると割と好かれます。
手伝いすぎにだけ注意しましょう。

特に人とコミュニケーションを取るのを避ける人がいます。
そういう人の代わりにパッと聞いてきましょう。

(追記)13. わからないことはわからないと言う。

すぐに言わないとろくなことはありません。
また、大抵の人は尋ねると答えてくれます。

(追記)14. チームの誰が何をやっているのか把握しておく。

管理的な側面もあるのですが
これは次の内容に掛かってきます。

(追記)15. 近くにいる人に聞けることで分からないことはできるだけすぐ聞く。

他人が何をやっているのか、やっていたのかを聞いたり知っておくとよいかと思われます。

なにかの案件で改修する話がある場合
仕様や設計については実装担当者に聞いたほうが早いと思います。
実装に関してはコードを見れば分かると思いますが
一旦ザクッと説明してもらうと読みやすいかと思います。

せっかく隣にいるんだから。パッとシュッとお仕事したいですね。

最後に

とりあえず思いつく限りでエンジニアの自分がやっている、心掛けていることを書いてきました。
エンジニアとして働く傍ら、チームリーダーとしての役割をこなしています。
そんなおかげもあってか、契約は長いこと続いています。1年半でしょうか。

メールの文言は少し気を付けよう(文面で見るとやや冷たく見えるので)
みたいな話もあるのですが、これはまぁ置いときます。(割と痛い目見た)

どこまで行っても、僕は、僕達はエンジニアと言えども、人間です。

どれだけ正しくても、受け入れられないタイミングはあります。

僕だって、この記事を読んでいるエンジニアでない方もエンジニアの方もそうかと思います。

逆に人間である以上、嫌な話ですが感情面に漬け込む隙がどこかであるはずです。
少し、うまく使ってみてはどうでしょうか。

新卒2年目のエンジニアの自分がやっている、少し泥臭い話でした。

(追記)
 他人をないがしろにしているわけではなくて
 気持ちよくパッとシュッとお仕事をするための気持ちを書きました。

追記のおまけ

自分は学生のうちにある程度技術を触っていたこともあり
ある程度自信があったため、顧客と話していました。

教育係の方は新人に技術をしっかり教えて
自信をつけさせてあげてはいかがでしょうか。

技術ありきの自信だと思います。

追記のおまけ2

新人には裁量を与えてあげてください。勝手に伸びると思います。
ケアだけはしてあげてください。