vaguely

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

KANJAVA PARTYに参加してきました

はじめに

6/24に行われたKANJAVA PARTYに参加してきました。

今回は関ジャバとしては初となる複数トラック制の、
参加人数も100名超と、普段より少し規模の大きい勉強会でした。

関ジャバとJava

www.slideshare.net

まずは関ジャバについてのお話から。

簡単な歴史や、関ジャバこと関西ジャバエンジニアの会は、あくまで関西中心のジャバエンジニアのための会であって、
必ずしもジャバだけを勉強する会ではない(のでフロントエンドなどの勉強会も行う)、といったお話をされていました。

私も過去に何度も参加させてもらいながら、歴史については知らずにいたためなるほど〜、と感心。

一点、もとの題名であった「すべらない話」を期待していたのは内緒ですw
(念の為書いておくと、滑ってはいませんでしたw)

Spring Securityにできること・できないこと

qiita.com

IPAが公開している安全なWebサイトの作り方で指摘されている脆弱性の内、
Spring Securityが対策してくれることとそうでないことを、脆弱性対策しているサイトとそうでないサイトのサンプルを用意して、比較しながら説明されていました。

Spring Security対策してくれるところ、そうでないところでも保険的なフォローをしてくれるところといった内容を、
デモを交えて説明してくれたのがとてもわかりやすかったです。

あと実践で使用するにあたっては、そのフレームワークや技術がどのように動いているのか、
ということをしっかりと理解できていなければいけない、と改めて感じました。

どうも自分は表面的なところだけで次に進んでしまいがちなので、
セキュリティはもちろん、それ以外も深く追っていかねばと思いました。

安全なWebサイトの作り方も早いうちに読んでおきたいと思います。

JUnit5の味見

speakerdeck.com

ここも相当迷いましたがJUit5に。

このセッションではJUnit5のユーザーガイドをベースに、
かみくだいた内容から気になるところをピックアップする、という方法で進められていました。

ようやくJUnit4を触りだした私としては、話についていけたとは言い難いのですが、
話されていない内容も含めてガッツリ資料にまとめてくれていると、あとから追えるので大変助かります^^;
(翌日次のバージョンが出る、という悲しいオチもありましたが。。。)

Java8に対応したり、便利になっているということはわかった(気がする)ので、
特にプライベートでは積極的にトライしていきたいと思います。

ストリーム処理ことはじめ 〜Akka StreamとRxJava

speakerdeck.com

Java Stream APIの基礎的な説明から、forではなくストリーム処理が何故良いのか、というお話へ。
さらに、Stream APIの抱える課題点と、Akka Streamがどうそれを解決するか、といった流れで説明されていました。

mapなどの処理の中身をStreamから分離する、というのは処理が複雑になったり、
複数ヶ所で同じ処理を行う時に力を発揮しそうですし、これはC#Linqを使うときなんかにも使えるのでは、と感じました。

処理が進んで値が流れていく様子がスライドで表現されていたのも良かったと思いました。

ちなみにAkka Streamを調べるとScalaだけでなくJavaもあるのですが、
Java版も同じように使えるのでしょうか。

その辺りも試してみたいところです。

Kafkaを使ってイベントを中心にしたアプリケーションを作ってみる。Spring Cloudにのせて

bufferings.hatenablog.com

スライドがなく、マインドマップ?をベースにやってみたこと、
困ったことや考えなどを話されていて、ある意味異色のセッションでしたw

Kafkaについての前知識が全くない状態で聴いていたため、
本当にぼんやりとこんな感じかなぁ。。。?と思っているくらいですが、とにかく楽しいセッションでした。

まぁKafkaについても近い内触ってみる、ということで。

あと今回に限らないのですが、bufferingsさんの資料やデモに、
娘さんが描かれたという絵が載せられていて、うらやましいです。

うちの子がもう少し大きくなって、絵を掻けるようになったら真似したいな〜と思っているのですが、いつになるやらw

関西Java女子部ショートセッション大会

関西Java女子部の方々によるショートセッション × 3 + 1

まずは関西Java女子部の活動の説明から、システムに対して理解が薄い顧客に対して自分たちの仕事をどう理解してもらうか、
リファクタリングのススメ、ハンズオン(講師側)のススメといった内容でした(順番違うかも)。

関西Java女子部はTwitterなどで情報は流れてくるものの、
一緒に参加してくれる女性の知り合いはおらんしな〜と思っていたので、活動内容が知れてよかったです。

自分たちの仕事をどう理解してもらうか、というところは、他人事でなく自分も取り組まなきゃいけない内容ですが、
相手も勉強会に賛同してくれた、という話は少しうらやましく感じました。

リファクタリングについては、とにかくネーミングの面白さがwww

speakerdeck.com

真面目なところとしては、私もリファクタリングしようとして、
歯止めが効かなくなって延々と作業し続けてしまうことがあるので、
うまくいかなかったら素直に元に戻す、というのは気をつけていきたいと思います。

ハンズオンについては、講師側を務めたことがないのですが、
今後自分が参加するだけでなく、サポートする側(ハンズオンだけでなく)にもまわってみようかなぁ、と思います。

自分も学ばなきゃいけないことがありすぎるので、バランスが難しそうではありますが(; ・`ω・´)

はてなブックマークAndroidアプリへのKotlinの導入について

speakerdeck.com

以前関モバ(だったと思います)で、Kotlinを使いたい、という話を聴いていたので、
最初このセッションのタイトルを見た時に「おぉ!ついに!」と思っておりました。

というところで前半はKotlin導入への経緯と、既存のJavaコードをどのようにKotlinに置き換えていくか、というお話が、 後半はKotlinの代表的な言語機能をピックアップして説明されていました。

前半については、やっぱり情熱って大事だなぁと感じました。
少々リスクや課題があったとしても、やりたい欲と技術があればまぁなんとかなりますよね。

後半については、プロパティがいまひとつよくわかっていなかったので、
そこに触れてもらえたのが助かりました。

Kansai.kt #3も行われるというし、またKotlinで何か作ってみるかなぁ。

ビアバッシュ

あっという間にセッションもおわり、ビアバッシュに。

今回も懲りずにLTに挑戦しました。

speakerdeck.com

本来の予定では、もう少しJavaよりにするつもりだったのですが、
進捗の具合からAngularメインのお話となりました。

時間は問題なかったのですが、特にビアバッシュに行うLTは、
もう少し聴いている人を楽しませる、ということにも気を遣ってみようと思いました。

まぁビールを出すとか、おっさんギャグを言うとかじゃなくて良いのですがw

あとはせっかくなので、もう少しいろんな人に話しかけるようにしたいところ。

あと岡山の勉強会参加したい。

おわりに

今回に限りませんが、やっぱり複数トラックでセッションがあると、
どれを聴くか迷ったり、自分が聴いている方も良いのだけどあっちも聴きたかった!となりますね。

スタッフの皆様、参加された皆様、会場を提供していただいた日本マイクロソフト様、ありがとうございました!

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

はじめに

引き続きWebページを作成すべくあれこれ試しています。

今回は、Angularでページを作っていく上で必要になった知識?も合わせてまとめることにします。

node_moduleをインストールし直す

1台のマシンだけで開発している場合は問題がないのですが、
他のマシンにプロジェクトを移したい場合、node_moduleは持っていってもエラーになってしまいます。
(.gitignoreに含まれてもいます)

そのため、プロジェクトを移したあとで「npm install」を行い、node_moduleをインストールし直す必要があります。

インストールされる内容は、プロジェクト直下にあるpackage.jsonのものです。

なお、この中のアプリのバージョン(下記参照)を更新したい場合は、手動で書き直すのでしょうか…?

package.json

{
  "name": "anime-crud-sample",
  "version": "0.0.0",
  "license": "MIT",
〜省略〜

ビルドデータの出力先の変更

以前書きましたが、
Angularで作成したページを実際サーバー上で表示するときは、「ng build」でビルドしたデータを使うことになります。

通常ではプロジェクト直下に作成される「dist」というフォルダの中にビルドしたデータが出力されるのですが、
これを任意の場所に出力するためには、プロジェクト直下にある.angular-cli.jsonを変更します。

.angular-cli.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "project": {
    "name": "anime-crud-sample"
  },
  "apps": [
    {
      "root": "src",
      "outDir": "ここを変更する"
〜省略〜

CSS Grid Layoutを使う

以前は下記を再現するには、tableを使用して結合などを利用していました。

f:id:mslGt:20170624021504j:plain

しかし、CSS Grid Layoutを使うことでより簡単に再現できるようになります。

さっそく試してみます。

元のHTML

< div class='top-news-item-area'>
    < div class='top-news-item-date'>2017年6月20日< /div>
    < div class='top-news-item-title'>ニュースだよ< /div>
    < div class='top-news-item-article'>中身です< /div>
< /div>

top-news-item-area(クラスを持つdiv)の中に、3つのdivが含まれています。

このHTMLに対して設定していきます。

top-news-item-area

まず、このLayoutが何行何列なのか、またそれぞれの高さ・幅を、
3つのdivの親であるtop-news-item-areaに設定します。

.top-news-item-area{
    display: grid;                  /* Grid layoutを有効にする */
    grid-template-rows: 50px 100px; /* 左から順に1行目、2行目の高さを指定 */
    grid-template-columns: 20% 1fr; /* 左から順に1列目、2列目の幅を指定 */
}
  • 今回は2行2列のため、grid-template-rowsとgrid-template-columnsにはそれぞれ2つずつ値を設定しています。
    行・列数を増やす場合は更に値を追加していきます。
  • grid-template-columnsで指定している「1fr」は、1列目で指定した幅の残りを2列目に全て割り当てるために使用しています。

top-news-item-date・top-news-item-title・top-news-item-article

Layoutの行・列数と高さ・幅が決まれば、あとは子となるdivの配置を決めるだけです。

.top-news-item-date{
    grid-row: 1 / 3;                /* 1行目から3行目まで(つまり2行全て)を指定する */
    grid-column: 1 / 2;             /* 1列目から2列目まで(つまり1列目)を指定する */
    border: 1px solid #000;
}
.top-news-item-title{
    grid-row: 1;                    /* 1行目を指定する */
    grid-column: 2;                 /* 2列目を指定する */
    border: 1px solid #000;
}
.top-news-item-article{
    grid-row: 2 / span 1;           /* 2行目から1行分(つまり2行目のみ)を指定する */
    grid-column: 2;                 /* 2列目を指定する */
    border: 1px solid #000;
}
  • top-news-item-titleのように1行または1列だけを指定する場合は、「/」以降の数字を省略できます。
  • 「/」以降を省略した場合、top-news-item-articleのgrid-rowと同じように、指定の行(または列)から1行(列)分が設定されます。

結果

ここまでのものを表示すると、下記のようになります。

f:id:mslGt:20170624021448j:plain

あとは角丸にしたり、marginなどで幅を調整したりすればOKです。

課題

ChromeFirefoxだけであれば問題なく表示できるのですが、
少なくともAngularでCSS Grid Layoutを使うと、警告出る場合があります。

IEでは、「grid-row: 1 / 3;」のように「grid-row(またはgrid-column)」で「/」以降の数字を指定できないためです。

手元にIEがないため未確認ですが、下記のようにすればIEにも対応できるようです。

.top-news-item-area{
    display: grid;
    display: -ms-grid;
    grid-template-rows: 50px 100px;
    grid-template-columns: 20% 1fr;
    -ms-grid-rows: 50px 100px;
    -ms-grid-columns: 150px 1fr;
}
.top-news-item-date{
    -ms-grid-row: 1;
    -ms-grid-row-span: 2;
    -ms-grid-column: 1;
    -ms-grid-column-span: 1;
    grid-row: 1 / 3;
    grid-column: 1 / 2;
    border: 1px solid #000;
}
.top-news-item-title{
    -ms-grid-row: 1;
    -ms-grid-column: 2;
    grid-row: 1;
    grid-column: 2;
    border: 1px solid #000;
}
.top-news-item-article{
    -ms-grid-row: 2;
    -ms-grid-column: 2;
    grid-row: 2;
    grid-column: 2;
    border: 1px solid #000;   
}

ただし警告は消えません。。。

まぁこれに関する警告をでないようにすれば(たぶんtslint.json)良いのでしょうが、
ちょっと気持ち悪いですね。。。

RxJSでJSONデータ取得

さて、CSS Grid Layoutで作ったフォームに、データを入れてみます。

Spring bootでGETリクエストをした時にJSONを返すようにしておき、
RxJSを使ってそれを受け取ってHTMLに渡す、という処理を行います。

Spring boot

今回は一旦GETリクエストがあった場合に、JSONデータを固定値で生成して返す、という処理をすることにします。

ということで、「spring-boot-starter-web」を含めてSpring bootのプロジェクトを作成し、下記のクラスを追加します。

News.java

public class News {
    public int id;
    public String createdDate;
    public String title;
    public String article;
}
  • JSONデータを返すための値を保持するクラスです。

CrudSampleController.java

import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List;

@RestController public class CrudSampleController { @GetMapping(“/topnewslist”) public List newsList(){ List newsItems = new ArrayList<>(); News newsItem1 = new News(); newsItem1.id = 0; newsItem1.createdDate = “2017.06.22”; newsItem1.title = “うどん一杯目”; newsItem1.article = “きつねうどん”; newsItems.add(newsItem1);

    News newsItem2 = new News();
    newsItem2.id = 1;
    newsItem2.createdDate = "2017.06.23";
    newsItem2.title = "うどん二杯目";
    newsItem2.article = "味噌煮込みうどん";
    newsItems.add(newsItem2);

    return newsItems;
}

}

  • Controllerクラスです。
  • localhost:8080/topnewslistにGETリクエストがあった場合にnewsList()が呼び出され、
    Newsクラスのリストを返します。
    (リストやクラスを戻り値にすると、自動でJSONとして受け取ることができるの便利ですね)

RxJS

フロント側に戻ります。

AngularのプロジェクトにはデフォルトでRxJSが含まれているため、
インストールなどは特にしなくてもそのまま使用できます。

GETリクエストを送るためには、app.module.tsにHTTPModuleを追加する必要があります。

app.module.ts

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

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';
import { MenulistPageComponent } from './menulist-page/menulist-page.component';

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

あとはHTTPとRxJSを使ってJSONデータを取得します。

top-news.ts

export interface TopNews {
    id: number;
    createdDate: string;
    title: string;
    article: string;
}
  • サーバー側から受け取ったJSONデータを格納するためのインターフェースです。

top-news.component.ts

import { Component, OnInit } from '@angular/core';
import { TopNews } from '../top-news';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

@Component({
  selector: 'app-top-news',
  templateUrl: './top-news.component.html',
  styleUrls: ['./top-news.component.css']
})
export class TopNewsComponent implements OnInit {
  newsList: TopNews[];
  
  constructor(private http_: Http) {
    http_.get("/topnewslist")
        .map(response => response.json()) /* response.json()の「()」を忘れないこと */
        .subscribe(gotNews => this.newsList = gotNews);
  }
  ngOnInit() {
  }
}
  • 今回は初回ロード時のみデータを読み込むため、constructorで、DIでインスタンスを受け取ったらそのまま処理を行っています。
  • constructor以外でも「this.http_」でアクセスできるため(なんだか不思議な感じがしますが)、任意のタイミングで実行することも可能です。
  • 「map(response => response.json())」で、response.json()の「()」がついていなくてもエラーにはならないのですが、
    戻り値が変わってしまい、subscribeの時に値が取り出せなくなってしまいます。

注意点

上記コードをng serveを使い、ビルドしない状態で実行するとエラーが発生します。

それは、同一ドメイン以外のURLにアクセスしに行く場合に必要なXMLHttpRequestの設定が抜けており、
ブラウザによってアクセスを禁止されるためです。

ただ今回は、最終的には同じドメインでやり取りをすることになるため、
ng buildでビルドを行い、Spring bootのプロジェクトに組み込んだ状態で確認を行うことにします。

おわりに

今回はほとんど触れられませんでしたが、RxJSももう少し突っ込んで調べてみたいと思います。

参照

Angular

CSS Grid Layout

DI

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 >」とするよう変更しました。

切り替えのたびにページ全体がリロードしてしまうこと、
ng buildでビルド後、Spring bootなどで動かした時にルーティングが効かなくなってしまうためです。

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}

おわりに

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

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

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

参照