LoginSignup
49
50

More than 3 years have passed since last update.

Spring Boot で簡単RESTful APIを作成する

Last updated at Posted at 2020-05-24

目的

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するようにします。

Spring-REST-4.png

開発環境

今回使用した環境は以下の通りです。

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も選択できるけどやったことない。今度やってみますね。

Spring-REST-1.png

Dependenciesを設定します。Spring Boot VersionはSNAPSHOTじゃないものを選びましょう。(SNAPSHOTは開発版)パッケージは、RESTのために必要なのはSpring Webだけです。
今回はMariaDBのデータを出し入れするのでSpring Data JPAとMySQL Driverも入れておきます。

Spring-REST-2.png

「Finish」をクリックするとプロジェクトフォルダが作成されて初回のビルドが実行されます。終わるまで少し待ちましょう。1~2分ぐらいかな。最初は、依存するライブラリをローカルリポジトリにダウンロードする必要があるのでインターネット環境によっては結構時間がかかるかもしれません。

Spring-REST-3.png

会社などでプロキシー設定が必要な場合は「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クライアントでアクセスすることができます。

Spring-REST-5.png

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フォルダができているはずなので探してください。

Spring-REST-6.png

この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の機能ですが、これはこれで非常に便利なので、是非使ってみてください。

49
50
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
49
50