spring boot ・ doma を使用した際の eclipse (gradle) の環境設定

spring boot で作成した プログラムで、データベースにアクセスする際の O/Rマッパーに doma を選択した際に、依存管理ツールに doma-spring-boot-stater を指定します。

いざ、eclipse で開発しようとした際、doma-n.n.n.jar を使用した環境設定は、公式(※1)を含めて多数存在します。しかし、doma-spring-boot-stater に関しての詳細な設定方法等が見つからなかったので、調査した内容をここに記載します。

■使用環境

windows 10

eclipse 4.15.0
gradle 6.3

doma-spring-boot-starter 1.4.0
doma-processor 2.35.0

■ファクトリーパスの設定

公式ページの説明では、Maven Central Repository より doma を入手し、ファクトリーパスに doma-n.n.n.jar を設定するように記載されています。(※1)

doma-spring-boot-stater の場合、gradle に記載する依存の設定は以下の通りになります。
※必要な箇所のみ抜粋

dependencies {
  implementation "org.seasar.doma.boot:doma-spring-boot-starter:1.4.0"
  annotationProcessor "org.seasar.doma:doma-processor:2.35.0"
}

上記を反映すると、doma-n.n.n.jar が取り込まれません。
代わりに、doma-core-2.35.0.jar が取り込まれます。

gradle の dependencies に設定した doma-processor と共に、doma-core-2.35.0.jar をファクトリーパスに設定する必要があります。
この時、下記画像の通りの順番も関係ありますので、ご注意ください。

※例)[外部jarの追加]より.gradle 配下のファイルを直接指定

■DOMA4019 エラー対応

gradle をデフォルトで使用した際 eclipse 上で[ DOMA4019 ]エラーが発生し、ビルドが失敗します。

javaのビルドパスで、「ソース・フォルダーごとに出力フォルダーの指定を可能にする」にチェックが入っている為です。
こちらのチェックを外すと、エラーが解消されます。

以上、eclipse での躓き箇所と 解決方法でした。

最後に、上記を含めた gradle の設定を公開します。

■gradle の設定

//  doma の gradle設定について、下記を参照してください。
//      https://doma.readthedocs.io/en/2.20.0/build/(日本語訳ページ)

// テンポラリディレクトリのパスを定義する
ext.domaResourcesDir = "${buildDir}/tmp/doma-resources"

  // ---- debug用 -------------------------------------
  //  println "processResources.destinationDir is ${processResources.destinationDir}"
  //  println "ext.domaResourcesDir            is ${ext.domaResourcesDir}"
  //  println "compileJava.destinationDir      is ${compileJava.destinationDir}"

// domaが注釈処理で参照するリソースをテンポラリディレクトリに抽出
task extractDomaResources(type: Copy, dependsOn: processResources)  {
  from processResources.destinationDir
  include 'doma.compile.config'
  include 'META-INF/**/*.sql'
  include 'META-INF/**/*.script'
  into domaResourcesDir
}

// テンポラリディレクトリ内のリソースをcompileJavaタスクの出力先ディレクトリにコピーする
task copyDomaResources(type: Copy, dependsOn: extractDomaResources)  {
  from domaResourcesDir
  into compileJava.destinationDir
}

compileJava {
  // 上述のタスクに依存させる
  dependsOn copyDomaResources
  // テンポラリディレクトリをcompileJavaタスクの入力ディレクトリに設定する
  inputs.dir domaResourcesDir
  options.encoding = 'UTF-8'
}

repositories {
  mavenCentral()
  mavenLocal()
  maven {url 'https://oss.sonatype.org/content/repositories/snapshots/'}
}

dependencies {
  implementation "org.seasar.doma.boot:doma-spring-boot-starter:1.4.0"
  annotationProcessor "org.seasar.doma:doma-processor:2.35.0"
}

eclipse {
  classpath {
    // [DOMA4019] 対応
    //   ソース毎の出力先指定を解除
    file.whenMerged {
      entries.each { entry ->
        if (entry.kind == 'src' && entry.hasProperty('output')) {
          entry.output = null
        }
      }
    }
  }
}

eclipse.jdt.file {
    // [Javaコンパイラー]->[注釈処理] の設定
    def  eclipseAptPrefsFile = '.settings/org.eclipse.jdt.apt.core.prefs'
    file(eclipseAptPrefsFile).write """\
      |eclipse.preferences.version=1
      |org.eclipse.jdt.apt.aptEnabled=true
      |org.eclipse.jdt.apt.genSrcDir=.apt_generated
      |org.eclipse.jdt.apt.genTestSrcDir=.apt_generated_tests
      |org.eclipse.jdt.apt.reconcileEnabled=true
      |""".stripMargin()


    // [Javaコンパイラー]->[注釈処理]->[ファクトリーパス] の設定
    def f = file(".factorypath")
    def w = new FileWriter(f)
    def jar = ""

    def xml = new groovy.xml.MarkupBuilder(w)
    xml.setDoubleQuotes(true)
    xml."factorypath"() {
      // doma-core.jar の読込み
      jar = configurations.annotationProcessor.find { File file -> file.name.matches('doma-core[^//]*') }
      'factorypathentry'(kind: 'EXTJAR', id: jar, enabled: true, runInBatchMode: false)

      // doma-processorjar の読込み
      jar = configurations.annotationProcessor.find { File file -> file.name.matches('doma-processor[^//]*') }
      'factorypathentry'(kind: 'EXTJAR', id: jar, enabled: true, runInBatchMode: false)
    }
    w.close()
}

上記を設定し、gradle eclipse もしくは、eclipse 上で [Gradle] -> [Gradle プロジェクトのリフレッシュ]を実行すると、本記事のファクトリーパスの設定・DOMA4019 を自動対応します。

■参考情報
※1:参考にした、doma 公式の設定ページ。
   https://doma.readthedocs.io/en/2.20.0/build/


ReactNative&ExpoによるPush通知(iOS)の実装

こんにちは。
最近ReactNative&ExpoでPush通知を実装する機会があったので、その内容を記事にさせていただきました。
私個人として、Expoを利用したPush通知機能の開発は

●難易度
  仕組みを理解することで実装自体は比較的簡単
  証明書などの手続きが基本的に不要。

●開発することによる効果
  簡易に通知を実装し、アプリの価値を高めることができる。

という点から、Expoでのアプリ開発をする上では非常にオススメの機能です。

その反面、ExpoでのPush通知の実装においては、

・日本語の記事はいくつかあるものの、サンプルが動かなかった。
・Push通知の仕組みも押さえておきたかったが、実装のコードのみの記事が多かった。
・テスト時の情報があまりなく、push通知のテスト部分で少し苦労した。


そういった経験から今回の記事を作成させていただきました。
私自身、そもそもPush通知の仕組みとは?という知識でしたので、Push通知の仕組みも含めて解説させていただきます。
また、今回はiOSでのテストのみ対象としています。
※基本はandroidでも同様ですが、androidの場合は追加で少し設定が必要です。

対象読者

①Expoを利用してより簡易的にPush通知を実装・テストしたいという方。
②そもそもPush通知とは?という方。

目次

1.環境
2.そもそもPush通知とは
3.Expoの提供するPush通知とは
4.実装①ライブラリインストール
5.実装②Push通知許可と通知用トークンの取得
6.実装③通知を開く・フォアグラウンドでの通知受信時の動作
7.ポイント Expoへのログイン
8.テスト①Expo Goのインストール
9.テスト②Push通知を送る
10.まとめ

1.環境

本記事では下記環境が構築されていることを前提としています。
・Expo(40.0.0)
・その他(上記Expoのバージョンに依存)
・テスト用iPhone (今回はiPhoneSE iOS 14.4を使用)
※本記事はReactNative&Expoの環境構築は省いています。

2.そもそもiOSのPush通知とは

上記の図のように

①端末側でユーザからPush通知の許可を取得。
②APNsからPush通知先の端末を特定するトークンを発行。
③②で発行されたトークン情報をサーバに登録。
④③で登録したトークン情報をキーに通知したい情報をApple社のPush通知用サーバに送信。
⑤Apple社のPush通知用サーバから各ユーザの端末にPush通知を送信。

上記のような仕組みとなり、トークンの発行や通知の送信などはApple社のPush通知用サーバを介する必要があります。
※通常はFirebaseなどのツールを利用することが多いかと思います。
また、iOSのPush通知を利用する場合には証明書の発行などの手続きを行う必要があります。

3.Expo(expo-notifications)を利用したPush通知の仕組み

上記の図のように

①端末側でユーザからPush通知の許可を取得
②Expoを介してApple社のPush通知用サーバから端末を特定するトークンを発行。
③②で発行されたトークン情報をサーバに登録
④③で登録したトークン情報をキーに通知したい情報をExpoのサーバを介してApple社のPush通知用サーバに送信。
⑤Apple社のPush通知用サーバから各ユーザの端末にPush通知を送信

基本的にはやはりApple社のサーバを介す必要があるのですが、そのあたりのやり取り(トークンの取得や通知情報の送信)をExpoが代行して行ってくれるため、
「とりあえずPush通知を実装したい」という場合は非常に簡易的にPush通知を実装することができます。
また、何より証明書発行などの手続きが必要ないということがとても大きなメリットかと思います。

4.実装①ライブラリインストール

今回必要となるライブラリは以下の通りです。

expo-notifications
Push通知を実装するためのコアとなるライブラリ

expo install expo-notifications

expo-constants
Push通知は実機でのみ確認可能となるため、実機かどうかの判定を入れています。

expo install expo-constants

5.実装②Push通知許可と通知用トークンの取得

registerForPushNotificationsAsync
①このアプリからのPush通知の許可を取得
②初回起動時は許可ダイアログを出してユーザからPush通知の許可を取得
③通知用トークンの取得
※今回はサーバなどには送信せず、画面にトークンを表示します。

App.js

import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';
import React, { useState, useEffect, useRef } from 'react';
import { Text, View, Button } from 'react-native';

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: false,
    shouldSetBadge: false,
  }),
});

export default function App() {
  const [expoPushToken,setExpoPushToken] = useState(null);

  return (
    <View
      style={{
        flex: 1,
        alignItems: 'center',
        justifyContent: 'space-around',
      }}>
      <Text>push通知のトークン: {expoPushToken}</Text>
      <Button
        title="push通知用のトークンを取得"
        onPress={async () => {
          const pushToken = await registerForPushNotificationsAsync()
          setExpoPushToken(pushToken);
        }}
      />
  </View>
  );
}


async function registerForPushNotificationsAsync() {
  let token;
  if (Constants.isDevice) {
    //①このアプリからのPush通知の許可を取得
    const { status: existingStatus } = await Notifications.getPermissionsAsync();
    let finalStatus = existingStatus;
    if (existingStatus !== 'granted') {
       //②初回起動時は許可ダイアログを出してユーザからPush通知の許可を取得
      const { status } = await Notifications.requestPermissionsAsync();
      finalStatus = status;
    }
    if (finalStatus !== 'granted') {
      //許可がない場合
      alert('Failed to get push token for push notification!');
      return;
    }
    //③通知用トークンの取得
    token = (await Notifications.getExpoPushTokenAsync()).data;
    console.log(token);
  } else {
    //実機以外の場合
    alert('Must use physical device for Push Notifications');
  }
  return token;
}

6.実装③通知を開く・フォアグラウンドでの通知受信時の動作

④ユーザが通知をフォアグラウンドで開いた場合のリスナー
今回は「フォアグラウンドで通知を受信しました」と表示することにします。
⑤ユーザが通知を開いた場合のリスナー
今回は「通知を開きました」と表示することにします。

App.js

import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';
import React, { useState, useEffect, useRef } from 'react';
import { Text, View, Button } from 'react-native';

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: false,
    shouldSetBadge: false,
  }),
});

export default function App() {
  const notificationListener = useRef();
  const responseListener = useRef();
  const [pushState,setPushState] = useState(null);
  const [expoPushToken,setExpoPushToken] = useState(null);

  useEffect(() => {
    // ④ユーザが通知をフォアグラウンドで開いた場合のリスナー
    notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
      setPushState("フォアグラウンドで通知を受信しました。");
    });

    // ⑤ユーザが通知を開いた場合のリスナー
    responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
      setPushState("通知を開きました。")
    });
    // userEffectのreturnに登録する関数は、コンポーネントがunmountされるときに実行される。ここで主にcleanup処理を定義する
    return () => {
      Notifications.removeNotificationSubscription(notificationListener);
      Notifications.removeNotificationSubscription(responseListener);
    };
  }, []);
  return (
    <View
      style={{
        flex: 1,
        alignItems: 'center',
        justifyContent: 'space-around',
      }}>
      <Text>push通知のトークン: {expoPushToken}</Text>
      <View style={{ alignItems: 'center', justifyContent: 'center' }}>
        <Text>push通知受信時の動作: {pushState} </Text>
      </View>
      <Button
        title="push通知用のトークンを取得"
        onPress={async () => {
          const pushToken = await registerForPushNotificationsAsync()
          setExpoPushToken(pushToken);
        }}
      />
  </View>
  );
}


async function registerForPushNotificationsAsync() {
  let token;
  if (Constants.isDevice) {
    ////①このアプリからのPush通知の許可を取得
    const { status: existingStatus } = await Notifications.getPermissionsAsync();
    let finalStatus = existingStatus;
    if (existingStatus !== 'granted') {
      //②初回起動時は許可ダイアログを出してユーザからPush通知の許可を取得
      const { status } = await Notifications.requestPermissionsAsync();
      finalStatus = status;
    }
    if (finalStatus !== 'granted') {
      //許可がない場合
      alert('Failed to get push token for push notification!');
      return;
    }
    //③通知用トークンの取得
    token = (await Notifications.getExpoPushTokenAsync()).data;
    console.log(token);
  } else {
    //実機以外の場合
    alert('Must use physical device for Push Notifications');
  }
  return token;
}

7.ポイント Expoへのログイン

expo-notifiationsの機能を利用してテストをするためには、Expoにログインしておく必要があります。
以下のコマンドでログインしておくことでテストが可能になります。
Expoのユーザはこちらで作成可能です。

expo login

8.テスト①Expo Goのインストール

storeから「Expo Goアプリ」をインストールします。

アプリインストール後、[expo start]を実行した際に表示されるQRコードを実機でスキャンします。
これで画面が表示されるはずです。

9.テスト②Push通知を送る

Push通知を送信するためには、Expoの提供するAPIに必要な情報を送信する必要があります。
下記のようなJSONを「https://exp.host/–/api/v2/push/send」に対してPOST送信します。

[
  {
    "to": "取得したpush通知用のトークン",
    "title":"通知のタイトル部分",
    "body": "通知の内容部分"
  }
]

この際、ヘッダー情報は下記のものを設定します。
host: exp.host
accept: application/json
accept-encoding: gzip, deflate
content-type: application/json

例えば以下のようなPOST送信用のツールから情報を送信した場合

通知受信時(フォアグラウンド)

通知を開いた場合

10.まとめ

今回はできるだけ時間をかけずに早くリリースしたいということから「Expo」と「Expoが提供するPush通知用ライブラリ」の組み合わせでPush通知の実装・テストをより簡易的に行いました。
Push通知と聞くと「何やら難しそう」と思っていましたが、Expoを利用することでとても簡単に実装することができます。
是非皆さんも気軽にPush通知を実装してみてください。