エンジニアだけどブログを始めてみた

年末に向けて記事の整理します

【SpringBoot】 RestAPI チュートリアル

業務で急遽SpringBootを使うようになったので簡単に実装方法を纏めるため、
今回は簡単な書籍管理用のRestApiを作成してみます。

事前準備

Eclipse プラグインの導入

EclipseMarketPlaceで以下のプラグインを追加しましょう。

Spring Tool Suite marketplace.eclipse.org

Gradle IDE marketplace.eclipse.org

MySQLでテーブル作成
アプリケーションで利用するデータベースを作成します。
今回はspringというdatabaseを作成していきます。

$ CREATE DATABASE spring
$ USE spring

データベースの作成が出来たらBookテーブルを作成します。

$ CREATE TABLE `Book` (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `title` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) 

プロジェクト作成

適当なディレクトリで build.gradle ファイルを作成します。
今回は /Users/${user}/eclipse-workspace/SpringRestAPI 配下で作成します。

buildscript {
  ext {
    springBootVersion = '1.5.8.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
    
group = 'product'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
  mavenCentral()
}


dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  
  testCompile('org.springframework.boot:spring-boot-starter-test')
    
  compile "org.springframework.boot:spring-boot-starter-data-jpa"
  compile 'mysql:mysql-connector-java'
  
  compile 'io.springfox:springfox-swagger2:2.2.2'
  compile 'io.springfox:springfox-swagger-ui:2.2.2'
  
  compile 'com.google.code.gson:gson'
}

作成した gradle ファイルを実行して Eclipse用プロジェクトを作成します。

$ mkdir -p src/main/java/app src/test/java/app
$ mkdir -p src/main/resources/application.yml

$ gradle eclipse

作成したプロジェクトをEclipseで読み込みます。
一般 > 既存のプロジェクトをワークスペースへ > 作成したプロジェクト
   ※ 今回なら /Users/${user}/eclipse-workspace/SpringRestAPI です。

Applicationクラスの作成

今回はテンプレートから作成していないので、自前でApplicationクラスを実装する必要があります。
src/main/java/app/ 配下に Applicationクラスを実装します。

package app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringRestAPIApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringRestAPIApplication.class, args);
    }
}

外部設定の管理

SpringBootでは 外部設定値を application.yml で管理しています。
※ .yml以外にも .xml や .properties でも管理できます。

src/main/resources/application.yml に以下の設定を追記して下さい。

spring:
  profiles.active: unit
  datasource:
    url: jdbc:mysql://localhost:3306/spring?autoReconnect=true&useSSL=false
    username: root
    password:
    driverClassName: com.mysql.jdbc.Driver
server:
  port: 9000

ここでは MySQLへの接続設定を定義しています。
port指定に関しては 何故か自分の環境ではMySQLとポート(8080)がコンフリクトしていたので
回避策として 9000 番を利用するように指定しています。

また SSLに関しては MySQLSSL接続するちおエラーになるためSSLの利用を無効化しています。
MySQL側の設定でも回避できるのですが、ここではSpring側の設定で回避します。

Modelの作成

書籍を管理するためのBookModelを作成します。 Modelクラスはアノテーションにより様々な制約を設けることが出来ます。
ただし、RailsのActiveModelの様に それ自体がDBへのアクセスを行うことは出来ません。

package app.models;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Book {
    @Id
    @GeneratedValue
    private Long id;
    
    @Column
    private String title;
    
    
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
}

今回はValidationなどは付与せずに必要最低限の設定のみ行っています。
詳細な機能についてはSpring Data JPAのリファレンスを参照することをお勧めします。

Repository の作成

前述した通りModelはそれ単独ではDBアクセスすることは出来ません。
JpaRepositoryを継承したinterfaceを実装することでCRUDメソッドが提供されます。

package app.repositories;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import app.models.Book;

@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
}

今回はinterfaceの継承のみですが、アノテーションでクエリを指定したりと色々と便利です。

Controller

続いて実際にリクエストを処理するControllerを作成します。 先程実装したModel, Repositoryを利用して CRUD を実装してみます。 ※DIコンテナの話は非常に長くなるので ここでは触れません。

package app.controllers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.Errors;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
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 app.models.Book;
import app.repositories.BookRepository;

@RestController
@RequestMapping("/books")
public class BookController {
    
    @Autowired
    private BookRepository book_repository;
    
    @RequestMapping(method = RequestMethod.GET)
    public Object index() {
        return book_repository.findAll();
    }
    
    @RequestMapping(method = RequestMethod.GET, value="{id}")
    public Object show(@PathVariable("id") Long id) {
        return book_repository.findOne(id);
    }

    @RequestMapping(method = RequestMethod.POST)
    public Object create(@Validated @RequestBody Book book, Errors err) {
        
        book_repository.save(book);
        return book;
    }
    
    @RequestMapping(method = RequestMethod.PUT, value="{id}")
    public Object update(@PathVariable("id") Long id,@Validated @RequestBody Book book, Errors err) {
        
        book.setId(id);
        book_repository.save(book);
        return book;
    }

    @RequestMapping(method = RequestMethod.DELETE, value="{id}")
    public Object delete(@PathVariable("id") Long id) {
        book_repository.delete(id); 
        return "200";
    }   
    
}

アプリケーションを実行し、curl などでリクエストを送ってみましょう。

# INDEX
curl -XGET -H "Content-Type:application/json" http://localhost:9000/books

# INSERT
curl -XPOST -H "Content-Type:application/json" http://localhost:9000/books -d '{ "title":"Java" }'

# UPDATE
curl -XPUT -H "Content-Type:application/json" http://localhost:9000/books/1 -d '{ "title":"PHP" }'

# DELETE
curl -XPOST -H "Content-Type:application/json" http://localhost:9000/books/1

実際にDBへのトランザクションが実行されているはずです。

共通エラー処理を実装する

SpringBootには共通エラーハンドリング機能が備わっています。
共通エラーハンドリングを行うとアプリケーションのエラー時の挙動を一元管理できるのでお薦めです。

まず、以下のディレクトリを作成します。

$ mkdir -p src/main/java/app/lib/exceptions/

まず、今回定義するエラークラスを作成します。

全エラークラスの振る舞いを定義するスーパークラスを実装します。
今回は単純にJSON形式でメッセージとHttpStatusを返すだけの機能とします。

package app.lib.exceptions;

import org.springframework.http.HttpStatus;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Expose;

/**
 * HttpStatus エラー抽象クラス
 * @author ynobuhar
 */
public abstract class HttpStatusException extends RuntimeException {
    
    private static final long serialVersionUID = 1L;
    private Gson gson;
    
    @Expose
    private String response;
    @Expose
    private HttpStatus http_status;
    
    /**
     * コンストラクタ
     * @param http_status エラーステータスコード
     * @param message レスポンス値
     */
    public HttpStatusException(HttpStatus http_status, String response) {
            this.http_status = http_status;
            this.response = response;
            
            this.gson =  new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
    }
    
    /**
     * レスポンスを取得
     * @return レスポンス値
     */
    public String getResponse() {
            return gson.toJson(this);
    }
    
    /**
     * HttpStatus取得
     * @return エラーステータス
     */
    public HttpStatus getHttp_status() {
        return http_status;
    }
}

HttpStatusExceptionを継承したエラー400用クラスを実装します。
とりあえずリクエストに不都合があれば このクラスを投げます。

package app.lib.exceptions;

import org.springframework.http.HttpStatus;

/**
 * HttpStatus 400
 * @author ynobuhar
 */
public class BadRequestException extends HttpStatusException{
    
    private static final long serialVersionUID = 1L;
    
    public BadRequestException() {
        super(HttpStatus.BAD_REQUEST, "不正なリクエストです");
        }
}

同じくこちらは500エラークラス。
予期しないエラーが発生した場合はこちらを投げます。

package app.lib.exceptions;

import org.springframework.http.HttpStatus;

/**
 * HttpStatus 500
 * @author ynobuhar
 */
public class InternalServerErrorException extends HttpStatusException{
    
    private static final long serialVersionUID = 1L;
    
    public InternalServerErrorException() {
        super(HttpStatus.INTERNAL_SERVER_ERROR, "予期しないエラーが発生しました");
    }
}

続いて 作成した lib配下に共通エラーハンドリングクラスを作成します。
ResponseEntityExceptionHandlerを継承したクラスは自動的にControllerで発生した例外を受け取ります。

package app.lib;

import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import com.fasterxml.jackson.core.JsonProcessingException;

import app.lib.exceptions.HttpStatusException;
import app.lib.exceptions.InternalServerErrorException;

/**
 * エラーハンドリングクラス
 * @author ynobuhar
 */
@RestControllerAdvice
public class ExceptionHandleService extends ResponseEntityExceptionHandler  {
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handleHttpStatusException(Exception ex, WebRequest request) throws JsonProcessingException {
        
        //ハンドリングしていないExceptionは 500 エラーとして扱う
        HttpStatusException http_status_exception = 
                ( ex instanceof HttpStatusException )?  (HttpStatusException)ex : new InternalServerErrorException();
        
        return handleExceptionInternal(
                http_status_exception, http_status_exception.getResponse(), new HttpHeaders(), http_status_exception.getHttp_status(), request);
    }
}

試しにControllerで強制的に例外を発生させてみます。

@RequestMapping(method = RequestMethod.POST)
    public Object create(@Validated @RequestBody Book book, Errors err) {
        
                //強制エラー
      throw new BadRequestException();
        
        book_repository.save(book);
        return book;
    }

リクエストを投げます

$ curl -XPOST -H "Content-Type:application/json" http://localhost:9000/books -d '{}'
> {"response":"不正なリクエストです","http_status":"BAD_REQUEST"}

例外をcatchできました。

APIドキュメントを自動生成する

基本的にプロダクトでは柔軟な変更が求められます。
よくあるパターンとしては開発当初に作成されたドキュメントが変更に対応されず置き去りになるパターンです。
(システムのバージョンが2.0なのにドキュメントが1.Xで更新が止まっているなど)

この様な問題はドキュメントをコードから自動生成することで ある程度は回避することが可能です。
今回は その方法としてSwaggerを採用してみます。

Swaggerの詳しい使い方については以下のURLを参照して下さい swagger.io

まずは confディレクトリを作ります。
※confディレクトリが必要という訳ではありませんが、構成上ここに置く方が しっくりするというだけです

$ mkdir -p src/main/java/app/conf

本題のSwagger クラスを実装します。

package app.conf;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;

/**
 * Swagger 設定クラス
 *  URL: /swagger-ui.html
 * @author ynobuhar
 */
@Configuration
@EnableSwagger2
public class Swagger {
    @Bean
    public Docket swaggerSpringMvcPlugin() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .paths(paths())
                .build()
                .apiInfo(apiInfo());
    }
    
    /**
     * 仕様書生成対象のURLパスを指定
     * @return
     */
    @SuppressWarnings("unchecked")
    private Predicate<String> paths() {

        return Predicates.and(
                Predicates.or(
                        Predicates.containsPattern("/books/*")
                        ));
    }
    
    /**
     * 仕様書情報の取得
     * @return
     */
    private ApiInfo apiInfo() {
        return new ApiInfo(
                "SpringRestApi Web API",              
                "SpringRestApi API 仕様書",    
                "0.0.1",                              
                "",                                    
                "your name",                             
                "MIT license",                    
                "https://opensource.org/licenses/MIT");                                   
    }
}

Swagger.class を作成したことで 自動的にドキュメントが生成されるようになります。

アプリケーションを実行し、以下のパスにアクセスして確認してみます。
http://localhost:9000/swagger-ui.html

以下の様な画面が出ると思います。
以降、対象のAPIに変更がかかると自動的にドキュメントも更新されるようになります。 f:id:ohs30359:20171217145728p:plain

ここまでが一通りのAPI実装の流れになります。
テストは? と思わるでしょうが 正直 SpringBootのテストを書くのが非常に辛いので割愛しています。