vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

AngularとSpringBootでWebページを作ってみたい その1

はじめに

ひょんなことから試しにWebサイトを作ってみることにしました。

フロントはAngular、バックはSpring bootで、以下の機能を持たせてみます。

  • ページは以下の3ページ
    • 1.トップ
    • 2.メニュー一覧
    • 3.お問い合わせ
  • 1.トップでは 1-1. バナー画像、1-2. ニュース を表示
  • 1-1.バナー画像は複数画像(アイテム数に合わせて動的に設定)を矢印キーまたはタイマーで切り替え
  • 1-2. ニュースはDBに登録されている内容を非同期で取得して表示する
  • 各画面へはヘッダーにボタンを設置し、それをクリックして遷移する

機能や 2.メニュー一覧 の子どものページなど追加したい部分はありますが、
まずは上記をやってみることにします。

ヘッダーを作る

※2017.06.23更新
ページ遷移の方法を、「location.href=“/”;」のようにするのではなく、
「< button routerLink=“/” class=“header-button” >Home< /button >」とするよう変更しました。

切り替えのたびにページ全体がリロードしてしまうためです。

global-header.component.html

< header id="global-header" >
  < img width="300" src="https://pbs.twimg.com/media/DClPJ4qUIAAROmQ.jpg" id="header-logo" >
  < button routerLink="/" class="header-button">Home< /button>
  < button routerLink="./menulist" class="header-button">Menu< /button>
  < button routerLink="./contact" class="header-button">お問い合わせ< /button>

global-header.component.css

#global-header{
    background-color: #2980B9;
    width: 100%;
    min-width: 1200px;
    height: 90px;
}
#header-logo{
    margin: 10px;
    width: 70px;
    height: 70px;
    float: left;
}
.header-button{
    background-color: #2980B9;
    border-style: none;
    color: #FFFFFF;
    font-size: 18px;
    width: 160px;
    height: 60px;
    margin: 30px 20px 0px 0px;
    float: left;
}
.header-button:hover{
    background-color: #FFF;
    color: #000;
    font-weight: bold;
}

global-header.component.ts

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-global-header',
  templateUrl: './global-header.component.html',
  styleUrls: ['./global-header.component.css']
})
export class GlobalHeaderComponent implements OnInit {
  constructor() { }
  ngOnInit() {
  }
}

ページ遷移はHTML側で行うため、Typescriptでは何もしていません。

ルーティング

(開発環境では)localhost:4200以下の各URLにアクセスした時に、
該当のページを表示するため、ルーティングを行います。

まずはsrc/app以下にapp.routing.tsを作成します
(これはangular.cliを使うのではなく、エディタから作成するようです)。

app.routing.ts

import { ModuleWithProviders }  from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { MainPageComponent } from './main-page/main-page.component';
import { ContactPageComponent } from './contact-page/contact-page.component';

const appRoutes: Routes = [
    {
        path: '',
        component: MainPageComponent
    },
    {
        path: 'contact',
        component: ContactPageComponent
    }
];
export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);
  • まだ2.メニュー一覧は追加されていません。

これをapp.module.tsに登録して、
ルーティング処理を有効にします。

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { SlideAnimeComponent } from './slide-anime/slide-anime.component';
import { FormsModule } from '@angular/forms';
import { GlobalHeaderComponent } from './global-header/global-header.component';
import { ContactPageComponent } from './contact-page/contact-page.component';
import { routing } from './app.routing';
import { MainPageComponent } from './main-page/main-page.component';
import { TopBannerComponent } from './top-banner/top-banner.component';
import { TopNewsComponent } from './top-news/top-news.component';

@NgModule({
  declarations: [
    AppComponent,
    SlideAnimeComponent,
    GlobalHeaderComponent,
    ContactPageComponent,
    MainPageComponent,
    TopBannerComponent,
    TopNewsComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    FormsModule,
    routing
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

バナーを作る

トップページに、バナーを表示してみます。

top-banner.component.html

< section id="top-banner-area" >
  < img [src]="[imageSrcPaths[activeImageNum]]" class="top-banner" [@bannerState]="topBannerState" (@bannerState.done)="onBannerChanged()" />
< /section >
  • [@bannerState]と(@bannerState.done)は次の項目で触れます。
  • imgのsrcとして配列(今回は固定)に渡したURLのうち一つを渡し、表示しています。

top-banner.component.css

#top-banner-area{
    background-color: #000;
    padding-left:300px;
    width: 900px;
    height: 600px;
}
.top-banner{
    width: 600px;
    height: 600px;
}
  • 例によってサイズは適当です。

アニメーションをつける

アニメーションで画像を切り替えてみます。

基本的には以前やったものと同じような内容です。

top-banner.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';

@Component({
  selector: 'app-top-banner',
  templateUrl: './top-banner.component.html',
  styleUrls: ['./top-banner.component.css'],
  animations: [
    trigger('bannerState',
      [
        state('active', style({
          transform: 'scale(1.0)',
          filter: 'brightness(100%)'
        })),
        state('inactive', style({
          transform: 'scale(0.6)',
          filter: 'brightness(20%)'
        })),
        transition('active => inactive', animate('400ms ease-in')),
        transition('inactive => active', animate('10ms ease-in'))
      ]
    )]
})
export class TopBannerComponent implements OnInit {
  private imageSrcPaths: string[];
  private activeImageNum: number;
  private nextImageNum: number;
  private topBannerState: string;

  constructor() { }
  ngOnInit() {
    this.imageSrcPaths = [
      "https://pbs.twimg.com/media/DClPJ4qUIAAROmQ.jpg",
      "https://pbs.twimg.com/media/DCH75wfU0AAievH.jpg"
    ];
    this.activeImageNum = - 1;
    this.nextImageNum = 0;
    this.topBannerState = 'inactive';
    this.onBannerChanged();
  }
〜省略〜
  onBannerChanged(){
    // アニメーション完了後に次の画像に切り替え.
    // active->inactive、inactive->activeの両方で呼ばれてしまうので後者を無視.
    if(this.activeImageNum === this.nextImageNum){
      return;
    }
    this.activeImageNum = this.nextImageNum;
    this.topBannerState = 'active';
  }
  • あとはtopBannerStateを「active」「inactive」に切り替えてあげればOKです。
  • アニメーションでは画像のサイズを変える他、「filter: brightness」で明るさも変更しています。

注意点

注意すべき点として、Angular4からanimationが@angular/coreから分離されています。

ただ、まだ@angular/coreから削除はされていないため state、style、transition、trigger のimportができてしまうのですが、
@angular/animationsからimportしないとエラーが発生するようです。

また、app.module.tsにBrowserAnimationsModuleを追加する必要があります。

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { SlideAnimeComponent } from './slide-anime/slide-anime.component';
import { FormsModule } from '@angular/forms';
import { GlobalHeaderComponent } from './global-header/global-header.component';
import { ContactPageComponent } from './contact-page/contact-page.component';
import { routing } from './app.routing';
import { MainPageComponent } from './main-page/main-page.component';
import { TopBannerComponent } from './top-banner/top-banner.component';
import { TopNewsComponent } from './top-news/top-news.component';

@NgModule({
  declarations: [
    AppComponent,
    SlideAnimeComponent,
    GlobalHeaderComponent,
    ContactPageComponent,
    MainPageComponent,
    TopBannerComponent,
    TopNewsComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    FormsModule,
    routing
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

キーイベントを取る

さて、次はバナー画像の表示切り替えを、左右の矢印キーを押したときに行うようにしてみます。

キー入力のイベントは、例えばinputタグであれば下記のように書くことができます。

< input (keyup)="onArrowKeyUp($event)" >

これをTypeScript側でevent.keyCodeのようにすれば、押されたキーを判別できます。

top-banner.component.ts

〜省略〜
onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
        // 右矢印キーが押されたときの処理.
    }
    else if(event.keyCode === 37){ // LeftArrow
        // 左矢印キーが押されたときの処理.
    }
}
〜省略〜

が、inputではなくdivタグなどではうまくイベントを取ることができません。

document.onkeyupに、ngOnInit()などでメソッドを追加することはできるものの、
そのままではクラス内で定義している変数などにアクセスできない問題が起こります。

でどうするかというと、「HostListener」を使います。

top-banner.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';

〜省略〜

  @HostListener('document:keyup', ['$event'])
  onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
      // 右矢印キーが押されたときの処理.
    }
    else if(event.keyCode === 37){ // LeftArrow
      // 左矢印キーが押されたときの処理.
    }
  }
〜省略〜

Javaアノテーションのような感じで、取得したいイベント(document.onkeyup)とその引数($event)をそれぞれ指定すると、
直下(または直後?)のメソッドが、該当イベントの発生時に呼ばれるようになります。

top-banner.component.tsのコード

最後に、ここまでのtop-banner.component.tsのコードを載せておきます。

top-banner.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';

@Component({
  selector: 'app-top-banner',
  templateUrl: './top-banner.component.html',
  styleUrls: ['./top-banner.component.css'],
  animations: [
    trigger('bannerState',
      [
        state('active', style({
          transform: 'scale(1.0)',
          filter: 'brightness(100%)'
        })),
        state('inactive', style({
          transform: 'scale(0.6)',
          filter: 'brightness(20%)'
        })),
        transition('active => inactive', animate('400ms ease-in')),
        transition('inactive => active', animate('10ms ease-in'))
      ]
    )]
})
export class TopBannerComponent implements OnInit {
  private imageSrcPaths: string[];
  private activeImageNum: number;
  private nextImageNum: number;
  private topBannerState: string;

  constructor() { }
  ngOnInit() {
    this.imageSrcPaths = [
      "https://pbs.twimg.com/media/DClPJ4qUIAAROmQ.jpg",
      "https://pbs.twimg.com/media/DCH75wfU0AAievH.jpg"
    ];
    this.activeImageNum = - 1;
    this.nextImageNum = 0;
    this.topBannerState = 'inactive';
    this.onBannerChanged();
  }
  @HostListener('document:keyup', ['$event'])
  onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
      this.nextImageNum = (this.activeImageNum < this.imageSrcPaths.length - 1)? this.activeImageNum + 1 : 0;
      this.topBannerState = 'inactive';
    }
    else if(event.keyCode === 37){ // LeftArrow
      this.nextImageNum = (this.activeImageNum > 0)? this.activeImageNum - 1: this.imageSrcPaths.length - 1;
      this.topBannerState = 'inactive';
    }
  }
  onBannerChanged(){
    if(this.activeImageNum === this.nextImageNum){
      return;
    }
    this.activeImageNum = this.nextImageNum;
    this.topBannerState = 'active';
  }
}

おわりに

次はバナー画像をタイマーで切り替えるようにしたり、
非同期でニュースのデータを取得して表示できるようにしたいと思います。

参照

Angular

Animation

KeyEvent

CSS

PlayModeでUnity Test Toolsを使ってみた

はじめに

Unityでテストを書くのに使用するUnity Test Tools。
Unity5.3から組み込みとなり、AssetStoreで別途インストールする必要はなくなりました。

ただ、CoroutineやTween系(iTweenやDOTweenとか)は確認することができず、別の方法でテストを行う必要がありました。

が、5.6から強化され、PlayModeでテストを行うことができるようになりました。

ということで今回は、PlayModeでDOTweenを使って3Dモデルを移動させるコードをテストしてみることにしました。

準備

前述の通り、Unity Test Toolsを使うのに何かをインストールする必要はありません。

ただし、デフォルトではPlayModeでのテスト実行が無効になっているため、
これを有効にします。

  1. メニューの Window > Test Runner からTest Runnerウインドウを開く
  2. PlayMode タブを開き、 Enable playmode tests をクリックして有効にする
  3. UnityEditorを再起動する

f:id:mslGt:20170527010432j:plain

あとは PlayMode タブの Create PlayMode test または右クリック > Create > Testing > PlayMode Test C# Script からテスト用クラスを追加します。
(※PlayMode用のファイルは、Editorフォルダ内に入れてしまうとPlayMode用のTest Scriptと認識されないようなので、
それ以外の場所に保存する必要があります
)

テスト対象のコード

テスト対象のコードは、下記の関数 Move とします。

ObjectController.cs

using DG.Tweening;
using UnityEngine;

public class ObjectController : MonoBehaviour
{
    public GameObject EventHandleObject;
    protected ObjectCtrlEventHandler ObjectEventHandler;
    
    public Tweener Move(GameObject targetObject, Vector3 goalPosition, float duration, Ease easeType)
    {
        return targetObject.transform.DOMove(goalPosition, duration)
            .SetEase(easeType)
            .OnComplete(ObjectEventHandler.OnFinished);
    }
    private void Awake()
    {
        ObjectEventHandler = EventHandleObject.GetComponent();
    }
}

ObjectCtrlEventHandler.cs

using UnityEngine;

public class ObjectCtrlEventHandler : MonoBehaviour
{
    public void OnFinished()
    {
        Debug.Log("Finished");
    }
}
  • 引数として渡しているGameObject(targetObject)を、goalPositionの位置までduration秒で移動させる、という内容です。
  • 移動が完了したら ObjectCtrlEventHandler > OnFinished が呼ばれます。

テストを書く

この関数に対するテストを書きます。

ObjectControllerTest.cs

using System.Collections;
using DG.Tweening;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

namespace Tests
{
    public class ObjectControllerTest: ObjectController
    {
        private GameObject targetObject;
        
        [SetUp]
        public void Init()
        {
            // 初期化処理. JUnitでいう@Before.
            EventHandleObject = new GameObject();
            ObjectEventHandler = EventHandleObject.AddComponent();
            
            targetObject = new GameObject();
        }
        [Test]
        public void ObjectControllerTestSimplePasses()
        {
            // 待ち時間が不要な処理はTestを使う.
        }
        [UnityTest]
        public IEnumerator MoveIn0Sec()
        {
            var tween = Move(targetObject, Vector3.one, 0f, Ease.Flash);
            // Tweenerを返す場合OnCompleteの処理をダミーに置き換えることができる.
            tween.OnComplete(() => Debug.Log(""));
            
            // 結果が戻るのが次フレーム以降のため、少し待つ.
            yield return new WaitForSeconds(0.1f);
            
            Assert.AreEqual(targetObject.transform.position, Vector3.one);
        }

        [TearDown]
        public void Dispose()
        {
            // 終了処理. JUnitでいう@After.
        }
    }
}

Testの実行

上記のようなTest Scriptを作成すると、Test Runnerウィンドウ > PlayModeタブ に該当のTestが表示されます。
あとは「Run All」や「Run Selected」をクリックすればテストが実行されます。

NUnit

  • テストコードは、NUnit(ver.2.6.4)がベースになっており、初期化処理([SetUp])、終了処理([TearDown])が使用できます。

初期化処理

  • テスト対象である ObjectController のAwakeでGetComponentしている ObjectCtrlEventHandler は、
    Testでは(GameObjectにアタッチができないため)NullReferenceExceptionになるので SetUp で値をセットしています。

DOTweenの処理が反映されるタイミング

  • DOTweenで処理を行う場合、durationを0にしていても同フレーム内では座標値の変更が反映されないため、 WaitForSeconds で待ち時間を設けます。
  • [Test] では戻り値がvoidに限定されるため、 [UnityTest] を使って戻り値を IEnumerator にし、yield return new WaitForSeconds を使用しています。

f:id:mslGt:20170527010612j:plain

おわりに

これまで以上にテスト可能な箇所が増えたことで、ぐんとテストが書きやすくなったように思います。

一点気になっているのは、例えばJenkinsなどのCIツールを利用する場合にも、今回のテストは実行できるのか?ということです。

こちらについては近いうちに試してみたいと思います。

参照

JJUG CCC 2017 Springに行ってきました

はじめに

5/20に行われたJJUG CCC 2017 Springに参加してきました。

www.java-users.jp

感想を一言で言えば、行ってよかった楽しかった!ということに尽きます。
…まぁもう少したくさんの方と話できればよかったとか、後述する懇親会でのLTの話とか思うことも色々ありはしますが。

せっかくなので、各セッションの感想などを書いてみたいと思います。

なお、スライド資料は下記を参照することにします。

JJUG CCC 2017 Springスライドまとめ(随時更新) #jjug_ccc - メンチカツには醤油でしょ!!

※各感想は私の理解に基づくもので、今後資料を見直していく中で間違いを見つけましたら修正する予定です。

G+H 1: 非機能要件とSpring Boot

まず非機能要件とは何か、という話からでしたが、
よくよく考えてみると機能要件と非機能要件の分類って難しくね?という話に。

この分類についてIPAの非機能要求グレードが紹介され、
非機能要求を段階的に細分化していく様子を見せてくれていました。

後半は、それらをSpring Bootで実装する方法について。

ここで登場していたSpring Actuatorは、前から気になっているSpring Securityなどに加えて、自分でも触ってみたいと思いました。

G+H 2: Vue.js + Spring Bootで楽しくフルスタック開発やってみた

フロントエンドでVue.jsを使っていくための構成や、Spring Bootに組み込む上で効率的なビルド方法などについて紹介されました。

変更を即時反映するところなどはAngularなどでも同じような機能が用意されているため、
Vue.jsだけでなく、今のJSフレームワークを使って開発する場合に参考にできる、と感じました。

F 3: SpotBugs(FindBugs)による大規模ERPのコード品質改善

FindBugsをForkしたSpotBugsがなぜ生まれたのか、また数ある静的解析ツールの中から、
なぜSpotBugs(FindBugs)が選ばれたのかなどが紹介されました。

現状では、静的解析ツールは一つだけでなんでも賄うのではなく、
状況に合わせて組み合わせるしかないこと、また開発効率が落ちると使ってくれなくなるため、速度重視の解析と網羅性を高めた解析を組み合わせる、という話が印象的でした。

G+H 4:Javaエンジニアに知って欲しいRDBアンチパターン

DBというのはアプリケーションのコードやサービスより長く使われることが多いため、
最初の設計がまずいと後々大きな負債になってしまう、という話からスタート。

RDBアンチパターンを面白く紹介しつつ、しっかりと最初に設計を行うこと、
また状況に合わせてリファクタリング(移行期間はアプリケーションのコードなどと比べ長くかかる)を行う必要がある、ということを説明されていました。

また、モニタリングの監視を行うことで、
障害が発生する前にあらかじめ対処しておく必要がある、というところも印象的でした。

E5: Javaで実装して学ぶOAuth 2.0!

OAuth2.0の基礎からJava EEでどう実現するかを紹介されていました。

まずは認可と認証の違いや下記の4人の登場人物と、それぞれの役割について。

  1. リソースオーナー
  2. リソースサーバー
  3. クライアント
  4. 認可サーバー

そして認証、認可の流れを噛み砕いて説明、という内容でした。

OAuth2.0の仕様としてすべてが決まっているわけではなく、
実現しなければいけないことだけが決まっていて、具体的にどうするかは決まっていない、という部分があるのが印象的でした。

また、いくどとなく仕様を正しく理解する必要性を強調されていたのも印象的でした。

柔らかく解説されていたとはいえ、完全に理解するためにはもう少し今回のスライドも含めて資料を読み込む必要があると思いますが、
どういうことを理解していかなければいけないのか、という概要はわかったような気がしました。

また、合間合間の時間確認とともに差し込まれるCM(スポンサー枠の発表でした)が、
だんだん笑いを誘うようになってきたあたり、(狙っていたのかはともかく)とてもうまいと思いました。

対象が異なるとはいえ、どうしても同じような内容になってしまうCMを、楽しく聴くことができたので。

あと、最後のデモは見ているだけで胸が痛く。。。(´・ω・`)

A+B 6: Java8プログラミング ベストプラクティス & きしだが働いてるかどうかIDEのメモリ使用状況から機械学習で判定する

2本立ての内容で、まずはNetBeansのメモリ使用状況を元に、
どのようなデータを使ってNetBeansを活発に使用している・していないかを判断したのか、またその判定の方法が紹介されました。

諸事情から内容がコンパクトになったとのことでしたが、しっかりオチもあっておもしろかったですw

2つ目はプログラミングベストプラクティスで、全体的なところでは可能な限りImmutableにすること、
Java8以上の話ではStream、Optionalを中心に紹介されました。

下記は特に印象に残っています。

  • 変数にダミーで値を入れて、状況に応じてその変数に再代入するより、変数をカラにしておいて、条件に応じて値を入れる方が最適化されやすい
  • (StreamのforEachの中でif文を使う処理の話で)Optionalは、要素数が一つだけのStreamと見なして、同じような処理を行うことができる。

M 8: Ordinary Object Pointer について調べてみた

Javaのオブジェクトの内部表現であるOrdinary Object Pointerのお話。

例えばString型の変数は内部的にどう表現され、それは何Byteになるのか、というような内容。

正直なところ知識がなさすぎて、ただただなるほど〜と感心するばかりでした。

とはいえ、プログラムの中身が実際にはどうなっているのか、という興味はあるので、
今後もう少し時間をかけて学んでいってみたいと思います。

懇親会LT

懇親会では、飛び込みLTができる、ということでここ最近挑戦しているWebSocketをテーマにLTやってみました。

speakerdeck.com

しかし結果は。。。

5分を想定していたら、3分とそれより短かったことが最大の敗因ですが、
冒頭に余計な話をしすぎてしまったような気がします。

今後、LTの時間がわからない場合も資料としてはともかく、話す内容としてはもう少し的をしぼって3分で収まるように作りたいと思います。

また、PCをインターネットに接続し忘れていたため、上記資料が途中から表示されなくなる、という問題もありました。

おそらくキャッシュが途中で消された?のかと思いますが、
ローカル環境にPDF自体は保存していたため、画面に映す資料としてはそちらを使うようにしたいと思います。

ちなみに、Demoで使用した寿司の3Dモデルは下記を使用しています。

https://www.assetstore.unity3d.com/jp/#!/content/37401

懇親会冒頭で寿司で盛り上がる(寿司スポンサーのLINEさん、ありがとうございます!)など、
不安もいっぱいでしたが暖かい反応をいただけて嬉しかったです。

おわりに

気にはなっていたものの、手が出せていなかったこと(例:Spring Security、OAuth2.0)についての話が聴けたので、
どんどん触っていきたいという気持ちが強まりました。
(から回ってる気はしますが)

次回は(採用される・されないはともかく)CfPを出せるよう、もっと突っ込んで調べたり、作ったりしていきたいと思います。

JJUG CCCは初の参加でしたが、今回からは託児所が導入されたり、
参加人数が1000人を超えるなど、ただただ感心するばかりです。

これも幹事の方々や、ボランティアスタッフの方々、スポンサーの方々のおかげだと思います。 本当にありがとうございました!

SpringでSTOMPを使わずにWebSocket

はじめに

前回はサーバー側をGolangにしていましたが、やっぱりSpring Bootでも実現したい!ということで、
今回はSpring BootでSTOMPなしでWebSocketを使い、Unityからアクセスできるようにしてみます。

なお今回の内容は、下記を参考にしています。

最小(と思う)構成

まずUnityからのアクセスによって接続を確立し、メッセージを受け取ってみます。
これをできるだけ少ないコードで再現しようとすると、こんな感じに。

build.gradle

buildscript {
    ext {
        springBootVersion = '1.5.3.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}
dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-websocket')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}
  • spring-boot-starter-websocketを追加しています。

WebsocktestApplication.java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class WebsocktestApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebsocktestApplication.class, args);
    }
}
  • Applicationクラスです。SpringInitializrで生成したクラスから特に変更はしていません。

WebSocketConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(messageHandler(), "/ws");
    }
    @Bean
    public WebSocketHandler messageHandler() {
        return new MessageHandler();
    }
}

STOMPを使った場合と同様に設定クラスを用意します。

詳細は後述しますが、指定したURL(localhost:8080/ws)にWebSocketを使ってアクセスがあった場合の登録処理を行います。

MessageHandler.java

import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

public class MessageHandler extends TextWebSocketHandler {
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 接続が確立されたら呼ばれる.
    }
    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) {
        // WebSocketクライアントからメッセージを受信した時に呼ばれる.
    }
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // 接続が切られたら呼ばれる.
    }
}
  • こちらも詳細は後述しますが、接続を確立したときやクライアントからメッセージを受け取った場合のイベントハンドラーです。
    設定クラス(WebSocketConfig.java)から使用します。

メッセージの送受信

STOMPを使う場合は、接続確立時にSubscribeを行うことで、他のクライアントからメッセージを受信した場合にそれを受け取ることができていました。

ただ今回はその方法が使用できないため、接続確立時にWebSocketSessionの情報を保持しておき、
メッセージを受け取ったらメッセージの送信者以外にメッセージを送信するようにしてみます。
※下記よりスマートな方法があるような気はしますが、記事投稿時点で他に思いつきませんでしたorz

MessageHandler.java

import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.util.ArrayList;

public class MessageHandler extends TextWebSocketHandler {
    private ArrayList< WebSocketSession > users;
    public MessageHandler(){
        users = new ArrayList<>();
    }
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 接続確立時、配列にWebSocketSessionの情報を追加.
        if(users.stream()
                .noneMatch(user -> user.getId().equals(session.getId()))){
            users.add(session);
        }
    }
    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) {
        // メッセージを受け取ったら送信元以外にメッセージを送る.
        users.stream()
                .filter(user -> !user.getId().equals(session.getId()))
                .forEach(user -> {
                    try{
                        user.sendMessage(message);
                    }
                    catch (IOException ex){
                        System.out.println(ex.getLocalizedMessage());
                    }
                });
    }
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // 接続が切れたら配列から削除.
        users.stream()
                .filter(user -> user.getId().equals(session.getId()))
                .findFirst()
                .ifPresent(user -> users.remove(user));
    }
}

これでとりあえず複数アプリからlocalhost:8080/wsに接続すると、
メッセージを送り合うことができるようになりました。

次は設定クラスが継承しているWebSocketConfigurer.javaと、イベントハンドラーのTextWebSocketHandler.javaの処理を追ってみることにします。

WebSocketConfigurer

まず設定クラスから追いかけてみます。

WebSocketConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(messageHandler(), "/ws");
    }
    @Bean
    public WebSocketHandler messageHandler() {
        return new MessageHandler();
    }
}

ここでは後述するハンドラー(WebSocketHandler)の登録と、Bean定義(DIコンテナに追加する)を行っています。

「registry.addHandler(messageHandler(), “/ws”)」で、
localhost:8080/wsにアクセスした時のハンドラーを登録しています。

またBean定義についてですが、今回DIを使っていないので、下記のようにしても動いたりします。

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry.addHandler(new MessageHandler(), "/ws");
}

このクラスが継承しているWebSocketConfigurer.javaは、ハンドラー登録の関数(registerWebSocketHandlers)だけを持つインターフェイスです。

では、この引数であるWebSocketHandlerRegistryを追ってみましょう。

WebSocketHandlerRegistry

WebSocketHandlerRegistry.java

package org.springframework.web.socket.config.annotation;

import org.springframework.web.socket.WebSocketHandler;

public interface WebSocketHandlerRegistry {
    WebSocketHandlerRegistration addHandler(WebSocketHandler webSocketHandler, String... paths);
}

関数addHandlerの戻り値になっているWebSocketHandlerRegistryを見てみます。

package org.springframework.web.socket.config.annotation;

import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

public interface WebSocketHandlerRegistration {
    // ハンドラーの追加.第二引数でアクセスポイントを指定.
    WebSocketHandlerRegistration addHandler(WebSocketHandler handler, String... paths);
    // ハンドシェイク(WebSocketの接続開始時に行う通信)に対するHandlerのセット.
    WebSocketHandlerRegistration setHandshakeHandler(HandshakeHandler handshakeHandler);
    // ハンドシェイクの前後に処理を追加するためのインターセプター.
    WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors);
    // Originヘッダー値の設定.
    WebSocketHandlerRegistration setAllowedOrigins(String... origins);
    // Sock.jsを使用する.
    SockJsServiceRegistration withSockJS();
}

ということで、設定クラスでは主にハンドラーの設定を行っています。

TextWebSocketHandler

ではそのハンドラーについて。

まずはもとのコードから(Sessionの保持やメッセージの送信などは省略します)。

MessageHandler.java

import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

public class MessageHandler extends TextWebSocketHandler {
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 接続が確立されたら呼ばれる.
    }
    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) {
        // WebSocketクライアントからメッセージを受信した時に呼ばれる.
    }
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // 接続が切られたら呼ばれる.
    }
}

次に継承しているTextWebSocketHandler.javaを見てみます。

TextWebSocketHandler.java

package org.springframework.web.socket.handler;

import java.io.IOException;

import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;

public class TextWebSocketHandler extends AbstractWebSocketHandler {

    @Override
    protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
        try {
            session.close(CloseStatus.NOT_ACCEPTABLE.withReason("Binary messages not supported"));
        }
        catch (IOException ex) {
            // ignore
        }
    }
}

ここではバイナリメッセージが送信された場合に、そのセッションを閉じています。
このようにしている理由は、WebSocketHandlerが文字列にしか対応していないため、ということのようです。

最初から実装していないと、バイナリメッセージが送信された場合に受信できずエラーになる、ということなのでしょうか。

※2017.05.17更新
バイナリメッセージが送信された時にセッションを閉じるのは、
このクラスがTextWebSocketHandlerの名前の通りテキストデータをやりとりするためのものなので、
それ以外のデータは受け付けませんよ、ということのようです。

失礼しましたorz

続けてAbstractWebSocketHandler.javaを見てみます。

AbstractWebSocketHandler.java

package org.springframework.web.socket.handler;

import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.PongMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;

public abstract class AbstractWebSocketHandler implements WebSocketHandler {

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    }
    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage message) throws Exception {
        if (message instanceof TextMessage) {
            handleTextMessage(session, (TextMessage) message);
        }
        else if (message instanceof BinaryMessage) {
            handleBinaryMessage(session, (BinaryMessage) message);
        }
        else if (message instanceof PongMessage) {
            handlePongMessage(session, (PongMessage) message);
        }
        else {
            throw new IllegalStateException("Unexpected WebSocket message type: " + message);
        }
    }
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    }
    protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
    }
    protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
    }
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
    }
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    }
    @Override
    public boolean supportsPartialMessages() {
        return false;
    }
}

ほとんどカラですが、handleMessageで受け取ったメッセージの型に合わせて処理を振り分けていますね。

では大本のWebSocketHandler.javaです。

WebSocketHandler

WebSocketHandler.java

package org.springframework.web.socket;

public interface WebSocketHandler {
    // 接続確立後のイベント.
    void afterConnectionEstablished(WebSocketSession session) throws Exception;
    // メッセージを受け取ったときのイベント.
    void handleMessage(WebSocketSession session, WebSocketMessage message) throws Exception;
    // メッセージのやり取り中のエラーをハンドリングする.
    void handleTransportError(WebSocketSession session, Throwable exception) throws Exception;
    // 接続を切ったときのイベント.
    void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception;
    // サイズが大きすぎる場合などに分割されたメッセージを扱うかどうか.
    boolean supportsPartialMessages();
}

おわりに

Golangもそうですが、シンプルなコードで通信ができてしまうのはすごいと思いました(小並感)。

次回は下記のような部分をなんとかしたいと思います。 * セッション情報をハンドラークラスから分離する * Webブラウザから特定のページにアクセスしたときに、今接続しているアプリの情報(IDや数など)や送信しているメッセージの表示を行う

あとはUnity側から送信するメッセージの量をもっと増やした場合でも、問題が起きないかなども気になるところですね。

参照

UnityでWebSocketを使ってみたい

はじめに

前回に引き続きWebSocketについてのお話。

ですが、今回はUnityからwebsocket-sharpというライブラリを使ってWebSocket Clientとしてアクセスします。

そして、アクセスする先はgorilla/websocketのExamplesにあるチャットです。

Springはどうしたんじゃいという話なのですが、C#でSTOMPを使う方法がわからなかったため、
とりあえず今回はGolangのものを使用することにしました。

こちらについては問題が解決すれば別途ブログにまとめたいと思います。

websocket-sharp

まずUnityからWebsocketでアクセスするのに利用する、websocket-sharpの準備です。

Unityから呼び出せるようにするために、dllファイルを作成する必要があります。

上記GitHubのReadmeの通りなのですが、一応手順を載せておきます。
(Jetbrains Riderで実行しました)

  1. プロジェクトをダウンロードしてソリューションファイルを開きます。
  2. ソリューションが5つ(websocket-sharpとExample4つ)あり、その中の「websocket-sharp」の上で右クリック->「Build selected projects」でビルドします。
  3. ビルドに成功したら、websocket-sharp > websocket-sharp > bin > Debug(or Release)にwebsocket-sharp.dllが出力されるため、UnityプロジェクトのAssets以下に置きます。

  4. Step2で、プロジェクト全体に対してビルドすると、Exampleのどれかでエラーになります。

で、Websocketでアクセスするコードはこんな感じです。

WebsocketAccessor.cs

using System;
using System.Collections.Generic;
using UnityEngine;
using WebSocketSharp;

public class WebsocketAccessor : MonoBehaviour
{
    private void Start()
    {
        // サーバー側ではlocalhost:8088/wsにアクセスした時に登録処理を行う.
        using(WebSocket ws = new WebSocket("ws://localhost:8088/ws")){
            // 接続開始時のイベント.
            ws.OnOpen += (sender, e) =>
            {
                Debug.Log("Opended");
            };
            // メッセージ受信時のイベント.
            ws.OnMessage += (sender, e) =>
            {
                Debug.Log("Received " + e.Data);
            };
            // 接続.
            ws.Connect ();
            // メッセージ送信.
            ws.Send ("世界さん、チーッす!");
        }
    }
}

パスワードもないものにアクセスするだけとはいえ、シンプルですね。

サーバー側ではメッセージの送受信をbyte型のスライスで行っていますが、
Unity側からはstring型のデータを渡すことができます。

また、このUnityプロジェクトをビルドして複数起動すれば、Unityアプリ同士でメッセージの送受信も行うことができます。便利!

Json

さて、上記のように単純にメッセージを送り合うだけなら問題がないのですが、
例えば3Dモデルの座標値を送りたい場合。

前述の通りメッセージとしてstring型のデータを扱えるため、JsonUtilityを使ってJson形式に変換し、それを送信してみたいと思います。

まずJsonに変換するデータを格納するためのクラスを用意します。

ObjectStatus.cs

using System;

[Serializable]
public class ObjectStatus
{
    public float PositionX;
    public float PositionY;
    public float PositionZ;
}
  • クラス名については気にしない方向でお願いいたします(白目)

で、入れ物が用意できればあとはデータを詰めてJsonに変換するだけです。

WebsocketAccessor.cs

using System;
using System.Collections.Generic;
using UnityEngine;
using WebSocketSharp;

public class WebsocketAccessor : MonoBehaviour
{
    public GameObject TargetModel;
    private WebSocket ws;
    private ObjectStatus objectStatus;

    private void Start()
    {
        objectStatus = new ObjectStatus();

        ws = new WebSocket("ws://localhost:8088/ws");

        ws.OnOpen += (sender, e) =>
        {
            Debug.Log("Opended");
        };
        ws.OnMessage += (sender, e) =>
        {
            Debug.Log("Received " + e.Data);
        };
        ws.Connect ();
    }

    private void Update()
    {
        if (Input.GetMouseButtonUp(1))
        {
            // データをクラスにセット.
            objectStatus.PositionX = TargetModel.transform.localPosition.x;
            objectStatus.PositionY = TargetModel.transform.localPosition.y;
            objectStatus.PositionZ = TargetModel.transform.localPosition.z;

            // セットしたクラスを使ってJson形式に変換.
            var json = JsonUtility.ToJson(objectStatus);
            // 変換したデータを送信する.
            ws.Send (json);
        }
    }
    private void OnDestroy()
    {
        // 接続を切る.
        ws.Close();
    }
}
  • 今回は右クリック時にメッセージを送信しています。

送信したデータは下記のような内容となります。
特に送信するデータが多い場合など、要素名は短くしておいた方が良いかもしれません。

{"PositionX":1.84,"PositionY":1.28,"PositionZ":0.0}

おわりに

今回はとりあえずサンプルを試しただけで、あまり中身に触れられませんでした。

ということで、次回あたりでもう少し触れてみたいと思います。

また、サーバー側の処理についても少し追いかけてみたいと思います。

参照

SpringBootでWebSocketを使ってみたい(Sample Backend編)

はじめに

チャットのように、Webサーバーを通してデータの受け渡しをリアルタイムで実施するための技術にWebSocketがあります。

今回はこれをSpringBootから利用する方法として紹介されていた下記の内容について調べつつ、
いじってみたりすることにします。

Keywords

WebSocket

まずWebSocketについてですが、超乱雑にまとめると

  • 最初にクライアント側とサーバー側を連携し、その後は接続・切断をせず小さいオーバーヘッドでやり取りを行う
  • クライアント側からだけでなくサーバー側からもデータ(メッセージ)のやりとりをリクエストできる

といったところでしょうか。
頻繁なメッセージのやりとりが必要なチャットなどを作るときに利用されます。

詳しくは下記のようなページをご覧ください。(内容が間違っていたり、あまりにも説明が不足していることがわかれば後日修正します)

今回のサンプルではsockjsを利用しています。

STOMP

WebSocketでクライアント側とサーバー側でやりとりをするためには、
お互いに共通した方法(プロトコル)を持っている必要があります。

サンプルでは、このためのプロトコルとしてSTOMP(Simple (or Streaming) Text Orientated Messaging Protocol)を使用しています。

正式名称の通り、テキストベースのメッセージでやりとりを行います。

メッセージのやりとりの流れは最初にSubscribeを行い、
更新があればそれを受け取る、ということのようです。

このSTOMPをWebSocket上で使用するため、STOMP.jsを利用しています。
一点気がかりなのは、GitHubを見るとメンテナンスが止まってしまっていること。

みなさんどう対処しているのか(´・ω・`)

。。。とりあえず今回はStomp.jsを使用することとします。

クラス構成

上記のサンプルに含まれるクラスは以下の通りです。

  • src
    • main.java.hello
      • Application.java ・・・ アプリケーションのメインクラス
      • Greeting.java ・・・ Subscriberに送信するメッセージを保持するモデルクラス
      • GreetingController.java ・・・ メッセージのルーティングを行うコントローラークラス
      • HelloMessage.java ・・・ メッセージをクライアント側から受信する時に、メッセージ内容(名前)を格納するモデルクラス
      • WebSocketConfig.java ・・・ エンドポイントの登録、メッセージのやり取りを行うWebSocketMessageBrokerの設定を行う設定クラス

この内「Application.java」はSpringBootの開始を担うメインクラス、「Greeting.java」「HelloMessage.java」はデータを保持するモデルクラスです。

ここからは「GreetingController.java」「WebSocketConfig.java」について追いかけてみます。

GreetingController

コントローラークラスです。

GreetingController.java

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class GreetingController {
    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public Greeting greeting(HelloMessage message) throws Exception {
        // 1秒待ってから応答する.
        Thread.sleep(1000);
        // メッセージ内容を"/topic/greetings"のSubscriberに渡す.
        return new Greeting("Hello, " + message.getName() + "!");
    }
}
  • @Controllerアノテーションを付与することで、通常のルーティング処理に加えてメッセージのルーティングも行うことができます。
  • @MessageMappingアノテーションを付与することで、今回は「/hello」にメッセージが送信されたときに関数「greeting(HelloMessage message)」が呼ばれます。
    このときメッセージの内容は、「HelloMessage.java」に格納された形で引数として渡されます。
  • @SendToアノテーションを付与することで、戻り値であるメッセージの内容を「/topic/greetings」をSubscribeしているユーザーに伝えることができます。
  • 「Thread.sleep(1000);」は無くても動作します。

なおマッピングできるパスの形式は、「/hello」の他「/hello.message.*」や「/hello.message.{goodmorning}」といったものも指定できるようです(MessageMapping.classより)。

WebSocketConfig

設定クラスです。

WebSocketConfig.java

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@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("/gs-guide-websocket").withSockJS();
    }
}
  • @EnableWebSocketMessageBrokerアノテーションを付与することで、WebSocketのメッセージのやり取りを有効にします。

ここで実装している内容は2つです。

  1. MessageBrokerの設定
  2. STOMPのエンドポイントの登録

1. MessageBrokerの設定

メッセージのやりとりを仲介するMessageBrokerの設定を行います。

config.enableSimpleBroker(“/topic”);

「/topic」以下のURL(GreetingController.javaのメッセージ送信先である「/topic/greetings」)でメッセージを渡せるようにします。

メッセージを受け取ったら、あらかじめSubscribeしていたユーザーにメッセージが渡されます。

config.setApplicationDestinationPrefixes(“/app”);

「/app」以下のURLにメッセージを送ると、コントローラークラス(GreetingController.java)が呼ばれるようになります。

今回はクライアント側から「/app/hello」にメッセージを送信すると、
GreetingController.javaの「greeting(HelloMessage message)」が呼ばれます。

2. STOMPのエンドポイントの登録

sockjsを使ってWebSocketの接続処理を行うエンドポイントの登録を行います。

「withSockJS()」は今回sockjsを使っているため必要なのですが、
sockjsを使わない場合にWebSocketを使う処理が行えるのか、可能であればどのようにするのかは気になるところです。

AbstractWebSocketMessageBrokerConfigurer

設定クラス(WebSocketConfig.java)で継承しているAbstractWebSocketMessageBrokerConfigurerではどのようなことをしているのか、少し見てみることにしました。

AbstractWebSocketMessageBrokerConfigurer.java

package org.springframework.web.socket.config.annotation;

import java.util.List;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;

public abstract class AbstractWebSocketMessageBrokerConfigurer implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
    }
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
    }
    @Override
    public void configureClientOutboundChannel(ChannelRegistration registration) {
    }
    @Override
    public boolean configureMessageConverters(List messageConverters) {
        return true;
    }
    @Override
    public void addArgumentResolvers(List argumentResolvers) {
    }
    @Override
    public void addReturnValueHandlers(List returnValueHandlers) {
    }
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
    }
}

※コメント、Javadocsは省略しています。

WebSocketMessageBrokerConfigurer.javaのメソッドをOverrideしていますが、
中身はすべて空になっています。

ドキュメントを見ると、WebSocketMessageBrokerConfigurer.javaを実装しやすくするため、
オプショナルな関数を空で実装している抽象クラスのようです。

設定クラスで実装していた「configureMessageBroker(MessageBrokerRegistry registry)」も含まれているため、
実装が必須な関数というのは、STOMPのエンドポイントの登録を行う「registerStompEndpoints(StompEndpointRegistry registry)」だけのようですね。

では、WebSocketMessageBrokerConfigurer.javaも見てみます。

WebSocketMessageBrokerConfigurer.java

package org.springframework.web.socket.config.annotation;

import java.util.List;

import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;

public interface WebSocketMessageBrokerConfigurer {

    void registerStompEndpoints(StompEndpointRegistry registry);

    void configureWebSocketTransport(WebSocketTransportRegistration registry);

    void configureClientInboundChannel(ChannelRegistration registration);

    void configureClientOutboundChannel(ChannelRegistration registration);

    void addArgumentResolvers(List argumentResolvers);

    void addReturnValueHandlers(List returnValueHandlers);

    boolean configureMessageConverters(List messageConverters);

    void configureMessageBroker(MessageBrokerRegistry registry);
}

※コメント、Javadocsは省略しています。

オプショナルな関数の内容は下記を参照していただきたいところですが、
WebSocketでのメッセージの送受信を行う関数(MessageChannel)の設定などが行えるため、必要に応じてOverrideすることになりそうです。

なお、interfaceで共通の関数を指定して、共通の処理を抽象クラスで実装し、個別の処理を抽象クラスを継承したクラスで実装する、
という流れはまさにJava本格入門で読んだ内容だな〜、などと思いながら見ていました。

おわりに

細かく追ってはみたつもりですが、全体的にまだフワフワしているため、
実際にサンプルを作りながらもう少し突っ込んで触る必要がありそうです。

次回はクライアント側を追いかけてみます。

参照

Spring

WebSocket

STOMP

UbuntuのUnity上でUnity3Dを動かす

はじめに

以前Ubuntu向けのUnity3D(デスクトップ環境と混ざってややこしいので今回はゲームエンジンの方はUnity 3D表記とします)のEditorが開発されている、
という話は聞いていたのですが、その後どうなったのかなぁ〜と思ったらちゃんと開発が続いていました。

というわけで、Unity 3Dと、スクリプトを書くためのJetBrains Riderをインストールした話をまとめます。

※2017年4月現在、Ubuntu用のUnity 3Dはあくまで開発版であるため、予期せぬバグなどが存在する恐れがあります。

準備

gdebi

後述しますが、Ubuntu用のUnity 3Dは.debファイルとして入手できます。

この.debファイルをインストールするときに、依存するパッケージが不足している場合に自動でインストールしてくれる「gdebi」をインストールします。

sudo apt install gdebi

【参考】

Angularなどの開発のため、新しいバージョンのものを使いたかったのでNodebrewをインストールし、
ver.7.9.0をインストールしました。 (Node.jsのインストールは延々ログが出力されるのと、ちょっと時間がかかったため不安になりましたが、無事一発でインストールできました)

インストールが終わったら、使用するNode.jsとして、インストールしたver.7.9.0を指定しておきます。

nodebrew use v7.9.0

【参考】

その他

後述しますが、Riderを使うためにはMono ver.4.4以上が必要なためインストールするのですが、
Unity 3Dをインストールする前に入れておくと、古いパッケージをインストールするのを防げるかもしれません。

Unity 3Dのインストール

Ubuntu用のUnity 3Dの.debファイルのリンクはここにまとめられているようです。

(2017年4月現在の)最新版はなぜか下から2番目にある5.6.0f3だと思いますので、
これをダウンロードし、下記のコマンドでインストールします。

sudo gdebi ダウンロードしてきた.debファイル

エラーが無くインストールが終われば完了です。

WindowsMac用と同じように、ログインやライセンスの選択を行います。

起動後もWindowsMac用と同じように使用可能ですが、何故かScene上でカメラを回転させる操作が、
左クリックではなく右クリックなのはちょっと違和感が。。。

Riderをインストールする

RiderはJetBrainsのサイトからEAP版をダウンロードして任意の場所に展開します。

Linux版はインストーラーが付属しているのではなく、binフォルダ内にあるrider.shというファイルをターミナルで実行することでRiderが起動します。

あとは以前と同じようにプラグインをインストールしました。

よ〜し、じゃあ試しに何か作ってみるかぁ〜、と思ったところ。。。

エラーでRiderが起動しませんでしたorz

Riderでプロジェクトを作成した場合も、下記のようなエラーが出てソリューションファイルの読み込みができませんでした。

Solution 'New Unity Project' load failed: MsBuild not found on this machine

Monoのインストール

エラーでググったところ、どうやらバージョン4.4以上のMonoがインストールされていないのが原因とのこと。

ただ、そのまま「apt install mono-runtime」としてしまうとそれより古いバージョンがインストールされてしまうため、
公式を参考にリポジトリを追加してインストールします。

インストールするのは、「mono-runtime」「mono-devel」「mono-complete」の3つです。

sudo apt install mono-runtime mono-devel mono-complete

【参考】

これで勝ったな、ガハハ

おわりに

Monoのあたりでちょっと手間取ったものの、割とすんなりインストールできてよかったです。
これでいよいよどのOSを選んでも不便なく開発できるようになってきましたね(๑•̀ㅂ•́)و✧

ちなみにタイトルにまでしたUbuntuのUnity上でUnity 3Dを動かす、というのは、
何故かデスクトップ環境をUnity8にしたところ、ウインドウが謎の点滅を起こしたためわずか数分で終了しましたorz

まぁGnome3上では快適ですので。。。^^;