- 投稿日:2020-05-24T21:49:51+09:00
Spring Boot で簡単RESTful APIを作成する
目的
RESTってなんだかお堅いイメージ?があるかもしれませんが、実のところシンプルで使いやすいインタフェースだと思います。
実際、WebAPIではよく使われていますね。利用することは多いけど作るのはなんだか大変そう。。でも実は、アプリケーションフレームワークを使用するとこういうデザインパターン系はとても楽に造れたりします。そこで、ここではSpring Boot を使用して簡単にRESTful APIを作成してみたいと思います。インタフェースを作るだけだと使いどころが分かりにくいので、Androidアプリから利用する部分もちょっとだけ載せます。
そもそもRESTって
RESTって技術書なんかでは難しく解説していたりするけど、私の理解ではHTTPサーバーによってリソースを出し入れするインタフェースを提供することだと思っています。
そうするとWebAPIはみんなREST?ってことになりますが、ステートレスでシンプルなリソースであることがRESTの特長。つまりSQLみたいにHTTPのリクエストを使用するってことですね。RESTに出し入れするデータのフォーマットはXMLだったりCSVだったりって場合もあるけど、本来のRESTはJSONで出し入れするもの。そんなわけでJSONを使った正しいRESTインタフェースのことをRESTfulと言うらしい。(詳しくはWikipediaに載ってます^^;)
作るもの
先日、Androidのアプリを作る練習としてGPSロガーを作りました。ログデータはAndroid端末のSQLite3を使用して保存しています。
このデータをPCのMariaDBにバックアップしたいのですが、AndroidからPCのMariaDBに直接コネクションを張ることはできません。そこでSpring Bootを使ってRESTfulインタフェースを作成し、AndroidからはHTTPを使用してバックアップデータをPOSTするようにします。開発環境
今回使用した環境は以下の通りです。
PC
- Windows 10
- MariaDB 10.3
- Oracle Java 1.8u241
- Spring Boot 2.2.6.RELEASE
- Spring Tool Suite 4 Version:4.6.1.RELEASE
Android
- android 8.0
- android studio 3.6.3
準備
RESTアプリケーションを作成する前に、データベースを準備します。
今回は、Androidで作成するアプリでGPSのログを記録し、これを転送する先のデータベースなので、テーブルの内容は緯度・経度・高度・日時という事になります。また、Spring Bootアプリケーションからアクセスするためのユーザーを作成してデータベースへのアクセス権限を与えておきます。(rootでやるのはやめましょうね!)
CREATE DATABASE loggerdb; USE loggerdb; CREATE TABLE logger ( id INT PRIMARY KEY AUTO_INCREMENT, longitude FLOAT NOT NULL, latitude FLOAT NOT NULL, altitude FLOAT NOT NULL, gpstime TIMESTAMP NOT NULL ); CREATE USER rest; GRANT ALL ON loggerdb.* TO 'rest'@'localhost' IDENTIFIED BY 'asD34j#z';プロジェクトの作成
Spring Tool Suiteで新規プロジェクトを作成します。「New」-「New Spring Starter Project」を選択してウィザードを立ち上げます。
プロジェクトの定義はこんな感じ。JavaはOracle Java 8を使ってます。OpenJavaも選択できるけどやったことない。今度やってみますね。Dependenciesを設定します。Spring Boot VersionはSNAPSHOTじゃないものを選びましょう。(SNAPSHOTは開発版)パッケージは、RESTのために必要なのはSpring Webだけです。
今回はMariaDBのデータを出し入れするのでSpring Data JPAとMySQL Driverも入れておきます。「Finish」をクリックするとプロジェクトフォルダが作成されて初回のビルドが実行されます。終わるまで少し待ちましょう。1~2分ぐらいかな。最初は、依存するライブラリをローカルリポジトリにダウンロードする必要があるのでインターネット環境によっては結構時間がかかるかもしれません。
会社などでプロキシー設定が必要な場合は「Window」-「Preferences」を開いて「Maven」-「Settings」あたりで設定します。
データベースへのアクセス設定
ここで、データベースにアクセスするための設定を行います。
Spring Bootでは、アプリケーションを起動する際にデータベースの接続を確認するので、この設定が完了していないとアプリケーションが立ち上がりません。
てことは、データベースが立ち上がってないとWebアプリが起動しない?? 実際デフォルトはそうなんですが、本当にアプリを作るときにはちゃんと例外をハンドリングしましょう。設定ファイルは src/main/resources/application.properties に記載します。初期状態では空っぽなので、以下のように記述してください。
spring.datasource.url=jdbc:mysql://localhost:3306/loggerdb?serverTimezone=JST spring.datasource.username=rest spring.datasource.password=asD34j#zserverTimezoneを設定せずにはまることが結構ありますのでご注意を!
アプリケーションの作成
いよいよRESTアプリケーションを作成します。
やるべきことは三つ。
- データベースアクセスモデルを作成する
- リポジトリインタフェースを作成する
- HTTPリクエストをホストするコントローラーを作成する
Webアプリケーションのお約束であるMVCのうち、M(odel)とC(ontroller)です。
RESTfulアプリケーションでは入出力インタフェースがJSONなのでV(iew)は必要ありません。データベースアクセスモデルの作成
準備の章で作成したloggerテーブルにアクセスするためのアクセスモデルを作成します。
テーブルのカラムに対応するフィールドを備えたBeanクラスを作成します。
ポイントとしては以下の点があります。
- 最初につくられるパッケージ(com.example.rest)のサブパッケージに配置すること
- @Entityアノテーションを付与すること
- Serializableを実装すること
今回のloggerテーブルはidをAUTO_INCREMENTにしているので、データ生成時にidを自動生成できるように@GeneratedValueアノテーションを付与しています。
また、時刻のフォーマットはJSONのデフォルトのままだとちょっと扱いづらいので調整しています。
これはテーブルの仕様に合わせて設定してください。package com.example.rest.bean; import java.io.Serializable; import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; import com.fasterxml.jackson.annotation.JsonFormat; @Entity @Table(name = "logger") public class Logger implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private int id; private double longitude; private double latitude; private double altitude; @Temporal(TemporalType.TIMESTAMP) @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", timezone="Asia/Tokyo") private Date gpstime; public int getId() { return id; } public void setId(int id) { this.id = id; } public double getLongitude() { return longitude; } public void setLongitude(double longitude) { this.longitude = longitude; } public double getLatitude() { return latitude; } public void setLatitude(double latitude) { this.latitude = latitude; } public double getAltitude() { return altitude; } public void setAltitude(double altitude) { this.altitude = altitude; } public Date getGpstime() { return gpstime; } public void setGpstime(Date gpstime) { this.gpstime = gpstime; } }リポジトリインタフェースの作成
生成したモデルクラスのオブジェクトとデータベースを結びつけるインタフェースを作成します。これがとっても簡単!ここがアプリケーションフレームワークを使用するメリットでもあります。
以下のようなインタフェースを作成するだけです。
- JpaRepository<T,ID>を継承するインタフェース
- ジェネリクスのTにはEntityクラスを、IDにはEntityクラスの@Idカラムの型を指定する(ジェネリクスにプリミティブ型であるintは使用できないのでラッパークラスIntegerを使用します)
package com.example.rest.repo; import org.springframework.data.jpa.repository.JpaRepository; import com.example.rest.bean.Logger; public interface LoggerRepository extends JpaRepository<Logger, Integer> { }これだけです。
RESTコントローラーの作成
コントローラーは、@RestControllerアノテーションを付与したクラスです。
GETリクエストハンドラとPOSTリクエストハンドラを作っておきます。前の項で作成したリポジトリインタフェースの型を指定したフィールドを作成し、@Autowiredアノテーションを付与しておくと、Springが必要時に自動的に無名クラスのオブジェクトを生成してくれます。ですからnewしなくてもfindAll()やsaveAll()と言ったメソッドを利用できます。
これがいわゆるDependency Injection (DI) ですね。package com.example.rest.controller; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import com.example.rest.bean.Logger; import com.example.rest.repo.LoggerRepository; @RestController public class LoggerController { // loggerテーブルのリポジトリを注入する @Autowired LoggerRepository loggerRepository; // GETリクエストに対してloggerテーブルの中身を応答する @RequestMapping(value = "/logger", method = RequestMethod.GET) public List<Logger> loggerGet() { // リポジトリからテーブル内の全レコードを取り出す // ※SELECT * FROM loggerが実行される List<Logger> list = loggerRepository.findAll(); // 取り出したListオブジェクトをリターンすると、 // JSON文字列に変換されてレスポンスが送られる return list; } // POSTリクエストによってloggerテーブルにデータをINSERTする @RequestMapping(value = "/logger", method = RequestMethod.POST) public List<Logger> loggerPost( // リクエストボディに渡されたJSONを引数にマッピングする @RequestBody List<Logger> loggerList) { // listをloggerテーブルにINSERTする⇒INSERTしたデータのリストが返ってくる List<Logger> result = loggerRepository.saveAll(loggerList); // JSON文字列に変換されたレスポンスが送られる return result; } }実行する
以上でRESTアプリケーションの作成は完了です。
それではアプリケーションを起動してみましょう。Package ExplorerからLoggerRestApplication.javaを探して右クリック→「Run as」→「Spring Boot App」を選択します。
Javaアプリにしては少しお洒落なロゴが表示されて、アプリケーションが起動します。Spring startar Webにはtomcatが内蔵されているので、HTTPクライアントでアクセスすることができます。RESTアプリケーションはHTTPプロトコルを使用してJSONをやり取りするインタフェースですから、Webブラウザは必要としません。(使ってもいいですが)
一般には、curlコマンドを使用して試験することになるでしょう。例として、東京スカイツリー近辺のGPSデータをPOSTし、その後GETしてみた例を示します。curl -X POST -H "Content-Type:application/json" -d "[{\"longitude\":139.811602, \"latitude\":35.710429, \"altitude\": 50.5, \"gpstime\":\"2020-05-24 11:24:00\"}]" http://localhost:8080/logger [{"id":2,"longitude":139.811602,"latitude":35.710429,"altitude":50.5,"gpstime":"2020-05-24 11:24:00"}]
POSTを2回ほどやってから、、
curl -X GET http://localhost:8080/logger [{"id":1,"longitude":139.812,"latitude":35.7104,"altitude":50.5,"gpstime":"2020-05-24 11:24:00"},{"id":2,"longitude":139.812,"latitude":35.7104,"altitude":50.5,"gpstime":"2020-05-24 11:24:00"}]
こんな感じです。簡単ですよね!
これでRESTfulアプリケーションは完成です。URL (http://localhost:8080/logger) に対してGETリクエストを送ると、JSONデータが得られます。またJSONデータをPOSTすればテーブルにデータをストアすることができるわけです。
実際のアプリを作成するには、もちろんセキュリティ対策等も必要になりますが、Spring Securityの機能により大抵のことはできます。アプリケーション単体での起動
以上、Spring Tool Suiteの環境で実行することができましたが、実際にはjarファイルを作成して実際のサーバーにインストールすることになりますよね。その方法を確認しておきます。
実はここに一つの落とし穴があります。jarファイルの作成方法は、Package Explorerでプロジェクトルートを選択して右クリック→「Run as」→「Maven install」を選択すればよいのですが、ここでERRORが出ることがあります。というか、Spring Tool Suiteのデフォルトの設定では以下のエラーになります。
[ERROR] No compiler is provided in this environment. Perhaps you are running on a JRE rather than a JDK?
このエラーを張り付けて検索してここにたどり着いた方は、よく読んでくださいね~。「JDKじゃなくJREを使ってませんかー」って書いてますよね。というわけで、治します。
まず、「Window」→「Preferences」を開き、「Java」→「Installed JREs」を選択します。すると確かにjre1.8が選択されています。以下の手順でJDKを追加します。
- 「Add」をクリックする
- JREタイプは「Standard VM」を選択する。
- 「JRE home」の欄の「Directory...」をクリックして、jdkをインストールしたフォルダ(C:\Program Files\Java\jdk1.8.0_241)を指定する
- 「Finish」をクリックする。
- 追加された「jdk1.8」を選択して「Apply」をクリックする。
次に、実行環境の設定でJREを選択します。プロジェクトを右クリック→「Run as」→「Run Configurations」を選択してください。
「JRE」タブを開くと、jre1.8が選択されていると思います。これを「Alternate JRE」に変更し、先ほどインストールしたjdk1.8を選択してください。ここまで設定できたら、再度プロジェクトを右クリック→「Run as」→「Maven install」を実行してください。
今度はビルドできると思います。できなかったら、一度「Maven clean」を実行してから「Maven install」を実行してください。SUCCESSになったら、explorerでワークスペースのフォルダを開き、プロジェクトの中にtargetフォルダができているはずなので探してください。
このjarファイルを起動します。
> java -jar LoggerREST-0.0.1-SNAPSHOT.jarAndroid側
これはオマケです。AndroidアプリでGPSから取得した位置情報を、今回作成したRESTful APIを利用してストアするところを載せておきます。
スミマセンがKotlinです。Javaのソースとか位置情報の取得方法とかは検索すると沢山出てきますので、そちらをご覧ください^^;;// RESTインタフェースにJSONデータをPOSTする fun sendJson(items: List<Map<String, String>>){ // 位置情報をJSON(配列)文字列にするためのオブジェクトを準備する var list = JSONArray() // itemには以下のようなデータが入っている // { // latitude => 緯度 // longitude => 経度 // altitude => GPS高度 // gpstime => センサ時刻 // } // for (item in items) { // JSONデータ一件分に対応するオブジェクトを生成する var json = JSONObject() as JSONObject // itemからキーを取り出してjsonオブジェクトに追加していく for (key in item.keys) { json.put(key, item.get(key)) } // JSONのリストに一件分のJSONオブジェクトを登録する list.put(json) } Log.d("JSONArray", list.toString()) // 作成したオブジェクトをJSON文字列に変換する val jsonString = list.toString() // HTTPのリクエストはUIスレッドで発行することはできないので、 // AsyncTaskを継承するクラスを生成しでマルチスレッドで実行する HttpPost().execute(jsonString) } /** * データ転送時のPOSTリクエスト処理 */ inner class HttpPost : AsyncTask<String, String, String>() { // スレッド実行処理 override fun doInBackground(vararg p0: String?): String { // POSTで送るデータは、パラメータで与えたJSON文字列 val data = p0[0] as String // RESTful API のURL(http://192.168.1.1:8080/logger みたいな感じ) val urlstr = getString(R.string.LOGGER) val url = URL(urlstr) // HTTPクライアントの生成 val httpClient = url.openConnection() as HttpURLConnection // HTTPヘッダ情報の設定 httpClient.apply { readTimeout = 10000 connectTimeout = 5000 requestMethod = "POST" instanceFollowRedirects = true doOutput = true doInput = false useCaches = false setRequestProperty("Content-Type", "application/json; charset=UTF-8") } try { // HTTPサーバーに接続する httpClient.connect() // POSTデータを書き込む val os = httpClient.outputStream val bw = os.bufferedWriter(Charsets.UTF_8) bw.write(data) bw.flush() bw.close() // RESTful APIからのレスポンスコード val code = httpClient.responseCode Log.i("HttpPost", "レスポンスコード:" + code) } catch (e: Exception) { e.printStackTrace() } finally { httpClient.disconnect() } return "" } }まとめ
なんだか長くなってしまいましたが、RESTfulインタフェースの部分は実はRestControllerを作るところだけです。
Mapのオブジェクトを作成してreturnすれば、JSON文字列に変換してレスポンスが送信されるので、もう立派なRESTful APIになっています。Entityの作成やJpaRepositoryの部分はRESTではなくSpring Data JPAの機能ですが、これはこれで非常に便利なので、是非使ってみてください。
- 投稿日:2020-05-24T18:55:57+09:00
通知内のボタンから再生状態を操作するとIllegalStateExceptionが発生する際の対処法
Androidのメディアプレイヤーで通知内のボタンから再生・停止操作をしたときにIllegalStateExceptionが発生する事象について、解決までに調べたことをまとめました。
※この記事の内容は、MediaSessionを使用した実装が前提となりますので、ご注意ください。
忙しい人向けの結論
MediaSessionを使用して音楽再生機能を実装する場合、以下のようにServiceをManifestに登録するかと思いますが、actionタグの中身が正しく指定されていないとIllegalStateExceptionが発生します。
AndroidManifest.xml<service android:name=".MyMediaService"> <intent-filter> <action android:name="android.media.browse.MediaBrowserService" /> </intent-filter> </service>
※actionタグの中身が間違っていることに2日も気付かなかったのは内緒だ通知内のボタンから再生状態を操作したい
IllegalStateExceptionが発生する
Activityのボタンからは正常に操作できるのですが、通知内のボタンを押下すると以下のようなエラーが発生します。
java.lang.RuntimeException: Unable to start receiver androidx.media.session.MediaButtonReceiver: java.lang.IllegalStateException: Could not find any Service that handles android.intent.action.MEDIA_BUTTON or implements a media browser service.要は「メディアボタンの操作を受け付けたけど、処理してくれるServiceがないぞ」と言ってるようです。
MediaButtonReceiverの公式ドキュメントによると、以下の2パターンにおいてIllegalStateExceptionが発生するようです。
1. Intent.ACTION_MEDIA_BUTTONを処理できるサービスまたは、MediaBrowserServiceが見つからない場合
2. Intent.ACTION_MEDIA_BUTTONを処理できるサービスが複数見つかった場合今回のエラーは上記の1のパターンに該当しますね。
原因
Activityからは再生・停止操作を受け付けているんだから、通知内のボタンの設定やServiceの起動方法が間違ってる?と思いきや、AndroidManifestの誤記でした。
activityタグやserviceタグのandroid:name属性はエラーチェックしてくれていたので、actionタグも同じと思っていましたが、どうやら違うようです。(そのせいで気付かなかった)
まとめ
actionタグのandroid:name属性は指定を間違えてもエラーチェックしてくれないので気を付けよう!
コード全体はこちらにあります。
参考
ゼロから学ぶメディアプレイヤーの実装(これをベースに作りました)
https://dev.classmethod.jp/articles/how-to-android-media-player/MediaButtonReceiver
https://developer.android.com/reference/androidx/media/session/MediaButtonReceiver?hl=ja
- 投稿日:2020-05-24T12:33:47+09:00
AndroidStudioでKotlinの変数を覚える
ここまでは
空のアクティビティの生成イベントを利用して画面に値を表示する方法がわかったところで次に進みます。
MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val tvMain = findViewById<TextView>(R.id.tvMain) tvMain.setText("Hello Kotlin World!") } }MainActivity.kt を上のように2行追加すると Hello World!の代わりに Hello Kotlin World!が表示されます。
このtvMain.setText("Hello Kotlin World!")
の所を変えて色々試して見ましょう。Kotlinの変数
setTextの引数に直接値を渡さずに変数strを使って渡してみましょう。
その上の行を参考にMainActivity.ktval str = "Hello Kotlin World!" tvMain.setText(str)たったこれだけで簡単に実現できました。でも色々疑問も出てきますね。
Kotlinは「;」セミコロンが要らない
様々な言語では命令文の区切りとして必ず「;」セミコロンを付ける必要がありましたがKotlinでは付けません。付けてもエラーにはなりませんが付けないのが一般的のようです。
※VisualBasicもなかったようなKotlinは変数の定義場所が自由
変数の定義は最初に行う言語が多いですが、Kotlinではどこに書いても構いません。
Kotlinでの命名規則
これを記載している段階でまだ仕様や規約を探しきれていませんが
1.変数は小文字で始まる
2.変数の型は大文字で始まってる
3.定数は全て大文字で命名の区切りは「_」アンダーバーで区切る
とされていてそれ以外はJavaの命名規則に従っているようです。Kotlinでの型宣言
C言語などで 整数型の変数 i を宣言しようとすると
C.cppint i;という書き方になります。これがKotlinだと
Kotlin.kti : Intと宣言と変数の位置が逆になります。Kotlinの宣言には初期化が必要なので
Kotlin.kti : Int = 0のように記述します。
PascalやDelphiのような連続定義は出来ないようです。
Delphi.pasi,j : Int; // KotlinではNGKotlinの型推測
例題では
kotlin.ktval str = "Hello Kotlin World!"のように型を書きませんでした。これは初期化の時に渡した値で型が推測されるためです。
文字列と数値は渡す値で区別しやすいですが、間違って型が推測されるよりは型指定した方が良いように思えます。Kotlinで良く使う変数型
kotlin.ktvar str : String= "漢字対応" // 文字列 var int : Int = 0 // 32bit符号付き数値 var long : Long = 0L // 64bit符号付き数値 var double : Double = 0.0 // 64bit浮動小数点数値StringとIntはよくわかりますが LongとDoubleは初期化する数値の表現に注意が必要です。
Longの場合は数値の後ろに「L」をDouble型の場合は小数点以下が無くても 0.0という表現にしないとエラーになります。
このように初期化する値の表記が異なるので型の指定が無くても推測されるのです。Int型の初期化時の書き方はさらに
kotlin.ktvar dec : Int = 16 var hex : Int = 0x10 var bin : Int = 0b0001000010進、16進、2進で表現した数値を代入することが出来ます。
10進数の10、16進数の 10h、2進数の 0001 0000 はどれも同じ値になります。