スマートデバイスアプリ開発のあれやこれや

Spring BootアプリをTomcatにデプロイする

 niwaka.hateblo.jp

 前回のSpringBoot記事から暫く空きました...。
気がつけばバージョンも1.3.0.Releaseまで到達しており
そのうち時間を取って知識を更新しなくてはと思っています。

 とはいえ、今回はまた別の話です。
SpringBootは組み込みTomcatの機能を備えているので
単体実行(APサーバ不要で)可能な.jarファイルが作成可能です。

 「こんなアプリ作ったんだけどどう?」みたいな感じで
誰かに簡単にアプリを試してもらうには便利な機能ですが
時たま「やっぱり.warファイル作りたい」というシーンがあります。

 今回はSpringBootアプリを.warファイルにまとめて
Tomcatにデプロイする方法を紹介します。

1. pom.xmlファイルを修正する

 .warファイル作成にあたって、追加で必要な
ライブラリがあるため、pom.xmlファイルを修正していきます。

  <modelVersion>4.0.0</modelVersion>
  <groupId>jp.co.cross_xross.web</groupId>
  <artifactId>SpringBootSample</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <!-- 変更1. パッケージを「war」に設定  -->
  <packaging>war</packaging>

  <!-- 中略 -->
    <!-- 変更2. jarの依存関係を追加する -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-tomcat</artifactId>
      <scope>provided</scope>
    </dependency>

2. SpringBootServletInitializerのサブクラスを作成する

 そのままでは、.warファイルを作成しても
web.xmlファイルが存在しないことから正常に起動できません。
そのため、Servlet起動時の設定情報を付与する必要があります。

 mainメソッドを持ったクラスに以下の変更を加えます。

  • SpringBootServletInitializerを継承する
  • configureメソッドをオーバーライドする

 

public class SampleController extends SpringBootServletInitializer {
// 中略

    public static void main(String args[]) {
	SpringApplication.run(SampleController.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(SampleController.class);
    }

3. ビルドする

 あとは簡単。「mvn package」を実行すれば
targetディレクトリ配下に.warファイルが作成されます。
Tomcatのwebappディレクトリに.warファイルに置いて起動確認しましょう。


 ちなみにURLは「http://ホスト名/アプリ名/*」という形式になります。
単体実行可能な.jarファイルと形式が変わるので注意しましょう......。

4. トラブルシューティング的な話

 「.warファイルをデプロイしたんだけど、404エラーが...」
という人のための話を補足でちょっと書いておこうと思います。


 Tomcatが動いているJavaのバージョンと、Mavenビルド時に
設定したJavaのバージョンが異なると404エラーになることがあります。
(自分はMaven: 1.6。Tomcat:1.8だったのでハマりました......)


 Mavenで使うJavaのバージョン指定は以下の方法で出来ます。

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
  </properties>  


 Tomcatは、JAVA_HOMEの設定を使うので確認しましょう。

RxSwiftでUITableViewのバインド処理

niwaka.hateblo.jp

 ↑の記事で、UITableViewとのバインド時はセルが再利用されるので
Disposableをdispose関数で無効にする(キリッ)と言ってましたが

 もっと良い方法がありました......。

1. RxSwiftはUITableViewのバインド機能を提供している

 そうなんです。自前でちまちまバインド解除とか不要だったんです。
UITableViewやUICollectionView向けに簡単バインド機能を提供しているのです。

 どのくらい便利かというと......。

  • UITableViewDelegateの実装が不要
  • UITableViewDataSourceの実装が不要


 上記のプロトコルの実装=UITableViewの利用というイメージが
あったので、初めてこの機能を使ったときは半信半疑でした。
でも、RxSwiftを使うと動くのです......。

2. UITableViewとViewModelのバインドを定義する

 UITableViewに表示したいデータを保持するViewModelを定義します。

class ViewModel {
    /**
     仮面ライダーの名前を保持する配列
     */
    var riders = Variable<[String]>([])

    init() {
        riders.value.append("仮面ライダー龍騎")
        riders.value.append("仮面ライダーカブト")
        riders.value.append("仮面ライダーキバ")
        riders.value.append("仮面ライダーアギト")
        riders.value.append("仮面ライダーW")
        riders.value.append("仮面ライダーディケイド")
        riders.value.append("仮面ライダーオーズ")
        riders.value.append("仮面ライダー電王")
        riders.value.append("仮面ライダーウィザード")
    }
}


 そして、バインド対象のUITableViewを保持するController。

import UIKit
import RxSwift
import RxCocoa
import RxBlocking

class ViewController: UIViewController {

    /**
     TableView
     */
    @IBOutlet weak var list: UITableView!
    
    /**
     ViewModel
     */
    let model = ViewModel()
    
    /**
     Bag
     */
    let bag   = DisposeBag()
    
    /**
     Storyboardバインド後処理
     */
    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewAndModel()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    /**
     データバインド処理
     */
    private func bindViewAndModel() {
        // UITableViewとViewModelのバインド
        model.riders.bindTo(list.rx_itemsWithCellIdentifier("CellIdentifier")) { _, riderName, cell -> Void in
            cell.textLabel?.text = riderName
        }.addDisposableTo(bag)
    }

}

3. 実行結果

 見事バインドされております。

f:id:cross-xross:20151117212630p:plain

4. 補足情報

 UITableViewのセル押下時のイベントハンドリング用の関数や
セル削除の関数もRxSwiftが提供しているので基本的に前述のProtocolの
実装は不要......ですが、複雑なテーブル作成時にはRxSwiftではサポートが
難しい場合もあるような気がします。


 個人的には、SectionHeader対応とかが必要になってきたら
結局はUITableViewDataSourceの実装とかが必要なんだろうな、と。


 ただ、単純なテーブル作成であればすごく便利な機能だと思います。

RxSwiftでデータバインド -補足編-

 このエントリは、Qiitaに投稿した記事の補足です。

qiita.com

 上記の記事では、RxSwiftを使った双方向データバインドの
実現方法をまとめていますが、データバインドの解除方法についても
少し整理して書いておきたいと思います。

1. データバインドを解除しないと困るケース

 自分が遭遇した困るケースは、UITableViewとのデータバインド......。
UITableViewの中で扱われるUITableViewCellのインスタンスは使い回しされます。
例えば、7行表示のテーブルでは、Cellのインスタンスは7つだけ作成され
画面スクロール時も当該インスタンスのプロパティを更新する動きになります。


 そのため、CellとViewModelのインスタンス(Array)をバインドすると
ViewModelとControllerの1対1のバインド関係が崩壊してしまい
ViewModelを更新したタイミングで意図しないCellが書き換えられたりします。
1行目のCellとバインドされたViewModelを更新すると1行目のCellを流用した
○行目のCellがなぜか更新されてしまう......なんて感じですね。


 対策として、非表示になった行については
データバインドを解除してやることでViewModelとControllerのバインド
関係の整合性を保ちます。この方法を採ることで、データバインドされている
Cellは画面に表示されている部分だけ......という状況を作っています。

2. データバインド解除する方法

 簡単です。
データバインドの処理を書いたときのことを思い出します。

/**
 データバインド設定
 */
private func bindViewAndModel() {
    textField.rx_text.subscribeNext { [unowned self] text in
        self.viewModel.text = text
    }.addDisposableTo(bag)
}   


 上記コードのsubscribeNext関数はDisposable型インスタンスを返却します。
(返却されたインスタンスを即座にDisposableBagに突っ込んでますが......)


 このDisposable型インスタンスとは何か?という点がポイントになります。
Disposableは”バインドしたよ!”という保証書だと理解すると簡単です。
RxSwiftではDisposable(保証書)が有効である限りデータバインドを行います。
逆にいうとDisposableを失効させればバインドを解除できます。
Disposableクラスは、dispose関数を提供しているので実行しましょう。

/**
 データバインド設定
 */
private func bindViewAndModel() {
    let disposable = textField.rx_text.subscribeNext { [unowned self] text in
        self.viewModel.text = text
    }
    // データバインド解除
    disposable.dispose()
}   


 ね?簡単でしょう。

3. DisposeBagとは何か?

 Disposableが何か分かると、DisposeBagの役目も分かります。

class ViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!

    let viewModel = ViewModel()
    let bag       = DisposeBag()
    // 中略
}


 Disposableインスタンスを保持するのがDisposeBagです。
その役目は即ち


 ”deinitのタイミングで保持している全Disposableを失効させる


ことです。
上記のコードで言うと
画面破棄のタイミングで自動的にデータバインドが解除されます。

 明示的にデータバインド解除を行わない限りは
DisposeBagを活用するのがベストプラクティスになると思います。
(解除を忘れると怖いですし......)

SwiftBondでデータバインドを実現する

 Modelを実行した後、画面更新処理をせこせこ実装するのが面倒...。
”更新処理を意識せずにiOSアプリが書けんものか”と思って色々調べてみると
SwiftBondというOSSライブラリを発見しました。
 github.com

 コイツを上手く使ってやると
ViewControllerとViewModel間でデータバインドが簡単に実現できます。
(ViewModelは必須ではないですが、Modelの実行結果の変換処理が
 ViewControllerから切り離せるのでViewModelがあった方が良いですね)

1. SwiftBondを導入する

 CocoaPodsを使って簡単インストールです。

use_frameworks!
pod 'Bond', '~> 4.0'

2. ViewModelを作成する

 ViewModelクラスを作成します。
今回はUITextFieldにバインドするString型変数1つを持ったクラスを作ります。

import Foundation
import Bond

class ViewModel {
    /**
     UITextFieldに対応する変数
     */
    var text = Observable<String?>("初期値")
}


 ポイントはString型で初期化するのではなく
SwiftBondが提供するObservableというWrapperクラスを使う所です。
(Observableクラスがデータバインドの機能を下支えしています......!)

3. ViewControllerを作成する

 ViewControllerクラスを作成します。
StoryboardとバインドされたUITextField型変数と先ほど作成した
ViewModel型変数の2つをメンバ変数として持たせておきます。

import UIKit
import Bond

class ViewController: UIViewController {

    /**
     バインド対象のUITextField
     */
    @IBOutlet weak var inputBox: UITextField!
    
    /**
     ViewModel
     */
    let viewModel = ViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

4. ViewControllerとViewModelのBind処理を書き足す

 では、UITextFieldとViewModelをバインドしていきます。
今回はViewController側にバインド定義処理を実装したprivateな関数
「bindViewAndModel」を作りviewDidload関数から呼び出してみます。
(バインド定義が増えるとviewDidloadがゴミゴミするのでその対策)

    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewAndModel()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    /**
     ViewとModelをバインドする
     */
    private func bindViewAndModel() {
        inputBox.bnd_text.bindTo(viewModel.text)
    }


 SwiftBondの提供する「bindTo」関数を使います。
これでUITextFieldの変更が自動でViewModelの変数textに伝搬します。
print関数でtextの中身を出力するような処理を書いて試してみてください。
ちなみに、「bindTo」関数は単方向のデータバインドを実現します。


 双方向のデータバインドを実現したい場合は
「bidirectionalBindTo」関数を使えばOKです。

    /**
     ViewとModelをバインドする
     */
    private func bindViewAndModel() {
        inputBox.bnd_text.bidirectionalBindTo(viewModel.text)
    }

補足. ViewModelの変数へのアクセス

 実際にバインドが成功しているか確認する場合の注意点です。
前述の通り、ViewModelの変数はObservable型として定義しているので
単純に print(viewModel.text) なんてしてもデータは正しく表示されません。
SwiftBondのvalue変数経由でUnwappingした値を取り出す必要があります。

    func pushCheckBtn() {
        print(viewModel.text.value!)
    }


 value変数はOptional型なのでさらにUnwrappすることも忘れずに!

Mockito事始め

 Javaのモックライブラリ Mockito(もきーと?)を使ってみたので
導入方法や簡単な使い方を整理してみます。

f:id:cross-xross:20150919155517p:plain

1. モックライブラリって何?

 モック=模造品、まがい物 という意味です。


 ある程度規模が大きいソフトウェアを開発するケースだと
自分担当のクラスは完成済みでテストしたいんだけど、クラスの中で
利用している別クラスが未完成
...なんてことはよくあると思います。
(未完成でもIFは設計済みなので完成できてしまう……!)


 よくある解決策として、未完成のソースコードをテスト用に書き換えて
とりあえずテストしてしまう……なんてやりがちですが、よくよく考えると
テストのためにテスト対象を書き換えることになり本末転倒です。


 そんなケースで、テストコードの中で未完成クラスを模造品(モック)に
置き換えてやる
ことで、テスト対象を汚すことなく完成済みクラスを
テストすることが出来ます。

2. Mockitoをインストールする

 公式サイトからMockitoのライブラリを入手しましょう。
Mavenをお使いの方であれば、pom.xmlに以下の記述を追加するだけでOKです。

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>1.10.19</version>
</dependency>

3. モックオブジェクトを使いたい状況の仮定

 では、早速Mockitoを使っていきます。
今回は完成済みクラスとして以下のような「SampleLogic」クラスが存在し

public class SampleLogic {
	
	private final String ODD  = "偶数";
	private final String EVEN = "奇数";
	// 未完成クラス
	private TargetClass target;
	
	/**
	 * Constructor
	 */
	public SampleLogic() {
		if (target == null) {
			target = new TargetClass();
		}
	}
	
	public String checkNumberType() {
		// 偶数と奇数を判断して文字列を返却する
		int randamNumber = target.generateRandamNumber();
		if (randamNumber % 2 == 0) {
			return ODD;
		} else {
			return EVEN;
		}
	}
}


 その内部で利用している「TargetClass」クラスが未完成という仮定で説明します。

public class TargetClass {
	
	public int generateRandamNumber() {
		//TODO: 乱数を返却する
		return 1;
	}
}


 TargetClassのgenerateRandamNumberメソッドは未完成で
ランダムな値を返すはずが固定値しか返却しないため、このままでは
命令網羅を実現できません。そのため、ランダムな数値を返却する
TargetClassのモック(模造品)を作る必要があります。

4. モック作成の準備

 テストクラス作成の準備をしていきます。
完成品を見た方が早いので晒してしまいます。

public class SampleLogicTest {
    @Mock
    private TargetClass mockObject;
    @InjectMocks
    private SampleLogic testTarget = new SampleLogic();
	
    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
    }
}


 今回のケースでは、未完成のTargetClassのモックを作成したいので
メンバ変数でTargetClassを定義した上で@Mockのアノテーションを付けます。
@Mock付きのメンバ変数がモック対象となる点を覚えておいてください。

    @Mock
    private TargetClass mockObject;

 

 一方、モックを利用する側のクラスについてもアノテーションが必要です。
SampleLogicが利用するので@InjectMockというアノテーションを設定します。
こちらはモックの挿入(インジェクト)先を指定する意味を持ちます。

    @InjectMocks
    private SampleLogic testTarget = new SampleLogic();


 そして、忘れちゃいけない設定があります。
今回はアノテーションベースでMockitoを扱うため@Before付きの
前処理メソッドで初期化処理を実装しておく必要があります。

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
    }

5. モック作成

 実際にモックを作成していきます。
基本的には@Mock付きのmockObjectを弄るだけです。
(インジェクト設定は既に済んでますので)

@Test
public void test() {
	// generateRandamNumberメソッドのモックを作成する
	when(mockObject.generateRandamNumber()).thenReturn(2);
	String result = testTarget.checkNumberType();
	assertEquals("偶数", result);
}


 whenメソッドでモックしたいメソッド(generateRandamNumber)を指定します。
そしてそのまま、thenReturnメソッドの引数で戻り値で返したい値を設定します。
基本的にwhen(どのメソッドをモックする?)/then(どのようにモックする?)という感じで直感的です。


 結果、checkNumberTypeメソッドの内部で実行されるTargetClassは
模造品(モック)で置き換えられているので、generateRandamNumberメソッドも2を返します。
それによって、checkNumberTypeメソッドも偶数を返すようになりテストは成功する......。

6. モックしずらいテストクラス

 とはいえ、Mockitoも万能ではありません。
例えば今まで説明に使っていたテスト対象クラスがこう変わったらどうでしょうか?

	public String checkNumberType() {
		TargetClass target  = new TargetClass();
		int randamNumber = target.generateRandamNumber();
		if (randamNumber % 2 == 0) {
			return ODD;
		} else {
			return EVEN;
		}
	}


 @InjectMockアノテーションモックを差し込めるのは
メンバ変数だけなので、ローカル変数になってしまうとお手上げです。

そのため、設計段階からMockitoでテストすることを意識することが重要です。


 とはいえ、メンバ変数に切り出せない(例:スレッドセーフにならないクラス)場合も
ありえるのでそういう場合は泣く泣く諦めるしかありません......。
Mockitoは便利だけれど、万能ではない、という点を覚えておくと良いと思います。

Apacheのmod_rewriteモジュール

Apachemod_rewriteモジュールを使うことで
URLとそれに紐付くコンテンツの対応関係を書き換えることができます。

分かりやすくいうと、http://sample/test.htmlというページがあった場合に
mod_rewriteを使うと架空のURL http://sample/hoge で同じtest.htmlを表示する
なんてことが可能になるわけです。

消して〜♪ リライトして〜♪

1 mod_rewriteをロードする

Apachehttpd.confファイルを開きます。

LoadModule alias_module libexec/apache2/mod_alias.so
LoadModule rewrite_module libexec/apache2/mod_rewrite.so
#LoadModule php5_module libexec/apache2/libphp5.so


mod_rewrite.soの先頭に付いている#を外します。

2 リライト対象のDirectory設定を変更する

リライトしたい要素をhttpd.confから探します。
今回はApache直下のDocumentsディレクトリ以下を対象にします。

<Directory "/Library/WebServer/Documents">
  <!-- この部分にリライトの設定を入れる -->
</Directory>


上記のコメント部分に以下の内容を追記します。

<Directory "/Library/WebServer/Documents">
  <!-- この部分にリライトの設定を入れる -->
  RewriteEngine On
  RewriteRule ^rewrite sample.html
</Directory>


”RewriteEngine On”は言わずもがな、リライトします!という宣言です。
"RewriteRule"の部分は「書き換え対象」と「書き換え先」を設定します。
上の例でいうとrewriteというURLが呼ばれたら内部的にはsample.htmlを呼ぶ...という意味です。

ここでは決め打ちで書いていますが、正規表現でも書けます。
(というか正規表現で書くことの方が多いと思います)

3. 忘れずApacheを再起動する

Apacheを再起動しましょー。

sudo apachectl restart

4. 実験する

http://localhost/rewriteをブラウザから呼んでみましょう。
sample.htmlのデータが表示されましたか?

Docker環境構築(Mac篇)

巷で流行ってるContainer型仮想環境 DockerをMacにインストールしてみます。
手元の環境は以下の通りです。

1. はじめに(環境概説)

DockerはContainer型仮想環境構築ツールです。
ホストOSとゲストOSでカーネルなどのリソースを共有することで
Hyper-Vのような完全仮想化環境よりも高速展開可能な仮想環境を実現します。


"カーネルの共有"という点がポイントでして
ホストOSとゲストOSがカーネル共有できるようなパターン......
つまりLinux環境ではないとDockerは使えないという制限があります。


Linux寄りの環境とはいえ、Macも例外ではありません。
そのため、Virtual Box上にLinuxのゲストOSを立て
そのゲストOSにDockerコンテナを管理させることで環境を実現します。
(言ってしまえば3層仮想化っぽい感じです)


3構造になるとパフォーマンス面が心配になりますが
VirtualBox上に立てるゲストOSとしてDocker特化型の
超軽量OSイメージである"boot2docker"を用いることで対策しています。

2. boot2dockerをインストールする

公式サイトからboot2dockerのイメージファイルを入手します。

github.com

インストーラを使うと楽なので、pkgファイル版をダウンロードします。
あとはインストーラーに従って次へ次へでインストールが完了します。
ちなみにVirtual Boxも同時にインストールされるので楽ちんです。

3. boot2dockerを初期化する

ターミナルを起動してboot2dockerを初期化します。
と、その前に念のためにインストール確認...

docker --version
# Docker version 1.7.0, build 0baf609


正常にバージョン情報が表示されてればOKです。


続いて、boot2dockerの初期化。

boot2docker init
#Latest release for github.com/boot2docker/boot2docker is v1.7.0
#Downloading boot2docker ISO image...


boot2dockerの動作に必要なイメージファイルがダウンロードされます。

そしてboot2dockerを起動します。

boot2docker up
# Waiting for VM and Docker daemon to start...
# ..........................ooooooooooooooooooooooooo


Dockerにアクセスするための環境変数の設定を促されますので
.bash_profileに設定を追加しましょう。

export DOCKER_HOST=tcp://XXX.XXX.XXX.XXX:2376
export DOCKER_CERT_PATH=/Users/UserName/.boot2docker/certs/boot2docker-vm
export DOCKER_TLS_VERIFY=1


追加したら,source .bash_profileで設定を反映します。

4. Dockerイメージを入手する

dockerコマンドを使ってContainerの素であるイメージを入手します。
試しに最新版のCentOSを入手してみます。

docker pull centos:latest


ダウンロードに成功すると、docker imagesで以下のように表示されるはず。

REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
centos              latest              7322fbe74aa5        2 weeks ago         172.2 MB

5. Dockerコンテナを起動する

ダウンロードしたイメージからコンテナを起動します。

docker run -t -i centos /bin/bash


コンテナが起動され、SSHでアクセスした状態になります。
この状態で必要なミドルウェアをインストールしてお好みの環境を作りましょう。

余談. アンインストール方法

boot2dockerの削除方法も書いておきます。

アンインストールスクリプトが公式で提供されているので
冒頭で書いたサイトからboot2dockerのzip版 or tar版も入手しておきます。


ターミナルからboot2dockerの仮想マシンを削除します。

boot2docker delete


その後、boot2dockerのzip版 or tar版に含まれるuninstall.shを実行しましょう。

あとは、/Applicationsからboot2dockerとVirtualBoxを削除すればOKです。