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

Token認証対応のAPNs ProviderをJavaで実装する

 久しぶりのiOSネタです。iOSバイス相手のPUSH通知を実現するApple Push Notification Services(APNs)ですが、JWT認証の方を使ったことがなかったので今回はそちらに挑戦してみようと思います。

おさらい

 APNsサーバは、Providerとの交信方法を2種類提供しています。

  • バイナリProvider API
  • APNs Provider API

 「バイナリ Provider API」は、iOS黎明期からサポートされてきた昔ながらの交信方法です。APNsサーバとProviderサーバ間でソケット通信を行い、所定フォーマットのバイナリデータで通知を依頼します。自前で実装する場合、ペイロード(依頼内容)のバイナリ変換が面倒くさい印象があります。AppleでもこれからProviderサーバを実装するのであれば、バイナリProvider APIは非推奨……と言っています。その内、サポート切られるのでは?

 「APNs Provider API」は、WWDC2015で発表された新しい交信方法です。HTTPのGET/POSTメソッドでAPNsサーバに対して通知依頼を出すことが出来るので、非常に簡便にプッシュ通知を実現できるようになりました。また、通知失敗時もHTTPリクエストのレスポンスで詳細な原因が返却されますので、再通知やデバッグが捗る点も魅力的ですね。

 交信方法やエラーハンドリングの簡便さ以外に「APNs Provider API」の大きな特徴が、APNsサーバとの認証方式です。

APNsサーバとの認証方式

 「APNs Provider API」は以下の2種類の認証方式をサポートしています。

  • 証明書認証
  • JWT認証

 「証明書認証」は、Member Centerから発行したAPNs用SSL証明書を使った認証です。証明書の有効期限が1年で失効されるため、定期的に証明書を再生成して差し替えないと通知が送れなくなるというデメリットがあります。また、証明書はアプリ単位に作成するので管理も煩雑になりがちです。受託開発案件をやっている人は、結構面倒くさいのではないでしょうか?保守料を取れるという見方もありますが。ちなみに、バイナリProvider APIは、証明書認証だけしかサポートしていません。

 「JWT認証」は、Member Centerから発行したAPNs Auth Keyを使った認証です。APNs Auth Key自身には有効期限が存在せず、このAPNs Auth Keyを使ってAPNsサーバに認証トークンを要求する点が大きな特徴であり、これらの認証トークン取得手続きは全てProviderサーバのプログラムで行う必要があります。その代わり、1度処理を実装してしまえばAPNs Auth Keyを手動で失効させない限り、ずっとプッシュ通知を送り続けられる環境が実現できます。証明書認証と異なり、通知先のアプリに依存する要素が少ない点も嬉しいです。

JavaでProviderサーバ(JWT認証)を実装する

 本題です。フル自前実装でも良いのですが、OSSを活用しましょう。

github.com

(1) pom.xmlの編集

 Mavenに登録されているので、Providerサーバ用プロジェクトのpom.xmlに依存関係を追記します。

<dependency>
    <groupId>com.relayrides</groupId>
    <artifactId>pushy</artifactId>
    <version>0.9.3</version>
</dependency>

 あと、ポイントとしてはログ出力用のライブラリも追加した方が無難です。通知に失敗したときの詳細挙動がログから追えるからです。Java標準のエラー出力だと原因調査が難しいので、ぜひ設定しましょう。

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

(2) 通知処理の実装

 あとは簡単です。

try {
        // 通知クライアントを作成
    final ApnsClient apnsClient = new ApnsClientBuilder().build();
    apnsClient.registerSigningKey(new File("[APNs Auth Keyが置かれた場所までのファイルパス]"), 
                                "[TeamId]", 
                                "[APNs Auth Keyの認証コード]", 
                                "[通知先アプリのAppID]");
        // ペイロードを作成
    ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
    payloadBuilder.setAlertBody("hoge");
    String payload = payloadBuilder.buildWithDefaultMaximumLength();
        // 開発用APNsサーバに接続
    Future<Void> connectFuture = apnsClient.connect(ApnsClient.DEVELOPMENT_APNS_HOST);
        // 接続完了まで同期で待つ
    connectFuture.get();
        // 通知オブジェクトを作成
    SimpleApnsPushNotification pn = new SimpleApnsPushNotification("[デバイスToken]", "[通知先アプリのAppID]", payload);
        // 通知実行
    apnsClient.sendNotification(pn);
        // APNsサーバから切断
    apnsClient.disconnect();
} catch (SSLException e) {
    e.printStackTrace();
} catch (InvalidKeyException e) {
    e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    e.printStackTrace();
}

BigCalendarが表示されないときの覚書

 最近,仕事の都合でReact.jsを触っています。規模が大きいアプリを作る際にコンポーネント指向の恩恵を感じており,(コンポーネント単位で)品質を維持しながら開発を進められる点が魅力的ですね。Reduxなどの状態管理ライブラリも触りたいのですが,今関わっているプロジェクトを何とか終えなければ......というところで,詰まった点を覚え書きしておきます。

1. Big Calendarがちゃんと表示できない!

 React.jsの便利なUIライブラリに「Big Calendar」という部品があります。その名の通り,カレンダー(≒スケジューラ)を表示するためのOSSライブラリで,以下のようなカレンダーを簡単に表示できるようになります。


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


github.com

 ただ,初めて使った際は↓のような感じで正しく表示されず,対応に困りました。CSSのリンクを外すと日付は表示されるので,原因はその辺りにありそうだな......と思いながら数刻が過ぎ。


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

2. サイズ指定が要る

 解決しました。公式ページに以下の記述があります。

The default styles use height: 100% which means your container must set an explicit height (feel free to adjust the styles to suit your specific needs).

 コンテナに高さ指定が要るということですね。

      <div className="Main">
      <BigCalendar events={this.state.events}
                   culture='ja-Ja'
                   defaultDate={new Date(2017, 1, 2)}
                   className="Cal"
      />
      </div>

 

.Main .Cal {
  height: 600px;
}


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


出た......!

GradleでWebアプリ

今更ながらJavaのビルドツール「Gradle」を触ってみました。
Mavenに比べるとビルドスクリプトが手軽に書けるのが良いですね!
(pom.xmlの構文エラーとの戦いを経験している特に)


今回はEclipseのGradleプロジェクト形式で作成したプロジェクトを
Tomcat上で動かすところまでの手順を整理してみます。
手元の環境はこんな感じです。

1. Gradle(STS)プロジェクトを作る

予めEclipseにGradle(STS)プラグインをインストールしておきましょう。
ヘルプのEclipse Marketplaceから入手可能です!

Java Quick start」でシンプルにプロジェクトを作成します。


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

2. Servlet-APIを入手する

今回は単純なJavaServletで動作を確認します。
build.gradeを編集し、Servlet-APIを依存ライブラリに追加します。

dependencies {
    compile group: 'commons-collections', name: 'commons-collections', version: '3.2'
    compile group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
    testCompile group: 'junit', name: 'junit', version: '4.+'
}


依存ライブラリを追加したらプロジェクトを選択して
「Refresh Dependencies」を実行すれば依存ライブラリが入手可能です。

3. プロジェクトをWTPに変換する

GradleプロジェクトのままではTomcatにデプロイできないので
WTPプロジェクトに変換したいと思います。

apply plugin: 'war'
apply plugin: 'eclipse-wtp'


「war」プラグインと「eclipse-wtpプラグインを追加で導入します。
上記の通りbuild.gradeファイルの先頭に記述を追加します。

導入後に

gradle eclipse


を実行すればWTPプロジェクトに変換されます。

4. Servletを実装する

ここは普通にServletを実装します。

@WebServlet(name="MyServlet", urlPatterns={"/test"})
public class MyServlet extends HttpServlet {
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		resp.setContentType("text/html");
		PrintWriter writer = resp.getWriter();
		writer.append("<html><body><h1>Hoge</h1></body></html>");
		writer.flush();
	}
}

5. 実行する from Eclipse

実行→Run on Serverで起動します。
http://localhost:8080/test で画面が表示されます。

6. WARファイルを作成する

gradle buildコマンドを実行すればwarファイルも作成可能です。

MultipeerConnectivityで実現する近距離無線通信:序

 ふと思い立って,掲題のMultipeerConnectivity.frameworkを触ってみました。
近距離でのLINE風チャットアプリなんかが簡単に作れそうで楽しいAPIです……!
今回はMultipeerConnectivity.frameworkについて情報を整理してみました。

1. MultipeerConnectivityことはじめ

 MultipeerConnectivity.frameworkは,iOS7から使えるようになった近距離
無線通信を実現するためのフレームワークです。具体的には,Wi-FiとBlutoothを
使って,近くのiOSデバイスと通信するための基本機能をAPIで提供しています。
このフレームワークを使うと↓のようなアプリが作れます。

  • トランシーバーアプリ
  • チャットアプリ
  • 会議資料共有アプリ
  • ゲームの対戦機能

2. 機能のイメージ

 近距離通信するデバイスですが,大きく「ホスト」「クライアント」の2種類
存在しており,それぞれで微妙に役割が異なっています。「ホスト」は,1つの
通信グループ(同じサービス名を持つ通信)で1人だけ存在します。「ホスト」以外の
接続ユーザーは全て「クライアント」です。

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

 そして,クライアントは他のクライアントと直接通信できません。
クライアントが通信できるのは,ホスト1人だけ。そのため,接続ユーザー全員で
状況共有が必要な場合は,ホストが各クライアントに情報をブロードキャスト
する必要があります。


 また,1つの通信グループは最大でも8人までしか同時接続できないので
無尽蔵に大規模なグループは作れない……という点にも注意が必要です。


 MultipeerConnectivity.frameworkとは,言うなれば各接続ユーザに
「ホスト」「クライアント」の役割を与え,ユーザ間での通信を行うための
機能を提供するフレームワークといえます。

3. 各接続ユーザを識別する情報「Peer」

 次にPeerという概念について紹介します。Peerとは,接続ユーザーを
区別するための識別情報です。「ホスト」「クライアント」に関わらず
全てのユーザーがユニークなPeerを保持しています。

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

 MultipeerConnectivityの世界では,Peerを表現するためのクラスとして
MCPeerIDというクラスが用意されています。

4. 交信機能を提供する「Session」

 続いて,ユーザ間のメッセージングを実現するSessionについて説明します。
Peerと同じくSessionも各ユーザが1つ持っています。そして,自分が保持する
Sessionを使って他のユーザと交信(データ送信,データ受信)を行います。

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

 ちなみに,Sessionは内部属性として,前述のPeer,つまり誰がそのSession
の持ち主か?という情報と,サービス名という情報を持っています。サービス
名というのは,通信グループの識別名称と理解してください。Session同士が
交信を行うためには,サービス名が同じである,という前提条件があるので
注意してください。


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


 MultipeerConnectivityの世界では,Sessionを表現するためのクラスとして
MCSessionというクラスが用意されています。

5. 存在を主張する「Advertiser」

 ここまで,ユーザの識別情報を保持する「Peer」と,ユーザ間での交信を
担う「Session」について説明しました。では,実際のユーザ間の接続は
いったいどういう流れで行われるのでしょうか?そこには,「Advertiser」
という要素が絡みます。Advertiserとは,サービスの存在を主張する機能です。


 ユーザ間で交信をするにしても,最初はまずサービスへの接続が必要です。
そのためには,他のユーザに対してホストが,サービスの存在を主張する
必要があります。その存在主張をAdvertiserを通して行うのです。


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


MultipeerConnectivityの世界では,MCAdvertiserAssistantクラスが
Advertiserの機能を提供しています。

6. 次回予告

 今回は概念的な話で近距離通信を説明したので,次回は同じ話を実装レベル
で解説してみたいと思います。

今年個人的に注力したい技術

 あけましておめでとうございます。
今年も月に最低1件の技術ネタ投稿を目標にやっていきたいです。
さて,今年の抱負...ではないですが,今年は未経験の以下の技術について
調査しながら力を付けていきたいです。

 ・node.js
 ・Golang
 ・データベース一般知識

 去年初めて良かったことは継続し,反省すべきことは反省しで
今年も頑張っていきたいと思っています。
今後共よろしくお願いします。

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の実装とかが必要なんだろうな、と。


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