目的
RESTってなんだかお堅いイメージ?があるかもしれませんが、実のところシンプルで使いやすいインタフェースだと思います。
実際、WebAPIではよく使われていますね。利用することは多いけど作るのはなんだか大変そう。。でも実は、アプリケーションフレームワークを使用するとこういうデザインパターン系はとても楽に造れたりします。
そこで、ここではSpring Boot を使用して簡単にRESTful APIを作成してみたいと思います。インタフェースを作るだけだと使いどころが分かりにくいので、Androidアプリから利用する部分もちょっとだけ載せます。
そもそもRESTって
RESTって技術書なんかでは難しく解説していたりするけど、私の理解ではHTTPサーバーによってリソースを出し入れするインタフェースを提供することだと思っています。
そうするとWebAPIはみんなREST?ってことになりますが、ステートレスでシンプルなリソースであることがRESTの特長。つまりSQLみたいにHTTPのリクエストを使用するってことですね。
RESTに出し入れするデータのフォーマットはXMLだったりCSVだったりって場合もあるけど、本来のRESTはJSONで出し入れするもの。そんなわけでJSONを使った正しいRESTインタフェースのことをRESTfulと言うらしい。(詳しくはWikipediaに載ってます^^;)
スミマセン。RESTfulの正しい定義はよくわからないので、私的解釈で行きます。
今回は「手軽にRESTful APIを」というテーマなのでご容赦ください。この投稿の初版ではGETとPOSTだけを実装しましたが、RESTと呼ぶには最低でもCRUD(Create/Refer/Update/Delete)をサポートすることが求められるようなので、、
CRUD | メソッド | 処理(SQL) |
---|---|---|
Create | POST | レコードを生成する(INSERT) |
Refer | GET | レコードを取得する(SELECT) |
Update | PUT | レコードを更新する(UPDATE) |
Delete | DELETE | レコードを削除する(DELETE) |
と解釈しました。
作るもの
先日、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#z
serverTimezoneを設定せずにはまることが結構ありますのでご注意を!
アプリケーションの作成
いよいよ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 java.util.Optional;
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;
}
// PUTリクエストによってloggerテーブルのデータをUPDATEする(PUTの本文ではJSON配列ではなく1件のみとする)
@RequestMapping(value = "/logger", method = RequestMethod.PUT)
public Logger loggerPut(@RequestBody Logger logger){
// 指定されたデータのIDを指定してレコードを取り出す。
Optional<Logger> target = loggerRepository.findById(logger.getId());
if(target.isEmpty()) {
// 指定されたIDが見つからない場合は何もせず終了
return null;
} else {
// 見つかったら、与えられたデータで更新する
loggerRepository.save(logger);
// 更新した結果のデータを返す
return target.get();
}
}
// DELETEリクエストによってloggerテーブルのデータをDELETEする
@RequestMapping(value = "/logger", method = RequestMethod.DELETE)
public Logger loggerDelete(@RequestBody Logger logger) {
// 指定されたデータのIDを指定してレコードを取り出す
Optional<Logger> target = loggerRepository.findById(logger.getId());
if(target.isEmpty()) {
// 指定されたIDが見つからない場合は何もせず終了
return null;
} else {
// データをDELETEする
loggerRepository.deleteById(target.get().getId());
// 削除したデータを返す。
return target.get();
}
}
}
実行する
以上でRESTアプリケーションの作成は完了です。
それではアプリケーションを起動してみましょう。Package ExplorerからLoggerRestApplication.javaを探して右クリック→「Run as」→「Spring Boot App」を選択します。
Javaアプリにしては少しお洒落なロゴが表示されて、アプリケーションが起動します。Spring startar Webにはtomcatが内蔵されているので、HTTPクライアントでアクセスすることができます。
RESTアプリケーションはHTTPプロトコルを使用してJSONをやり取りするインタフェースですから、Webブラウザは必要としません。(使ってもいいですが)
一般には、curlコマンドを使用して試験することになるでしょう。例として、東京スカイツリー近辺のGPSデータをPOSTし、その後GETしてみた例を示します。
まずはPOSTですが、curlコマンドは以下の通りです。-Xでメソッド、-HでHTTPヘッダ、-dで本文データを指定します。最後の引数はURLです。
本文データの中にダブルクォートが必要になるので、¥でエスケープします。
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
結果は以下の通り、登録したデータが帰ってきます。auto_incrementによってidが生成されていることが判ります。
[{"id":2,"longitude":139.811602,"latitude":35.710429,"altitude":50.5,"gpstime":"2020-05-24 11:24:00"}]
POSTを2回ほどやってから、GETしてみました。
curl -X GET http://localhost:8080/logger
結果は以下のように、JSONの配列で返ってきます。
[{"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"}]
PUTで更新してみます。
curl -X PUT -H "Content-Type:application/json" -d "{\"id\":1,\"longitude\":100,\"latitude\":50,\"altitude\":20,\"gpstime\":\"2020-05-30 22:00:00\"}" http://localhost:8080/logger
結果は以下のように、更新後のデータが返ってきます。
{"id":1,"longitude":100.0,"latitude":50.0,"altitude":20.0,"gpstime":"2020-05-30 22:00:00"}
DELETEしてみます。これもJSONで指定しますが、パラメータは"id"だけでOKです。
curl -X DELETE -H "Content-Type:application/json" -d "{\"id\":1}" http://localhost:8080/logger
結果は以下のように、削除前のデータが返ってきます。
{"id":1,"longitude":100.0,"latitude":50.0,"altitude":20.0,"gpstime":"2020-05-30 22:00: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.jar
Android側
これはオマケです。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の機能ですが、これはこれで非常に便利なので、是非使ってみてください。