チーム開発用APIスタブの紹介

マイクロサービスが進む中、APIのスタブの需要は非常に高くなっています。
それに伴い様々なAPI スタブが公開されており、
昔よりも遥かに簡単にスタブを用意することができる環境になっています。

しかしながら、意外と実際に利用して開発を進めていくと
以下の問題に ぶつかることが多くなりました。

  • レスポンスの変更に逐一コードの変更が必要になる
  • 例外系のレスポンス
  • JSON, XML などのContentTypeの変更

さらに個人での開発であれば諦めて 逐次変更を加えることも出来ますが
チーム開発だと スタブをデプロイする必要があり
変更の度にデプロイが発生し非常に辛い思いをするケースが多発します。

その経験から 効率的な開発のために
エンドポイントの切替なしに レスポンス管理できる機能を持ったスタブを今回実装しました。

github.com

利用方法に関してはReadmeに任せるとして
主な機能として以下の機能を提供しています

  • プロジェクトごとのAPI管理
  • エンドポイントごとに複数のレスポンスを登録可能
  • リクエスト情報を確認可能

もし、自分と同様の問題を抱えている場合は
導入を検討してみてもいいかもしれません。

( そして要望が出た際に issueを上げてもらえると嬉しいです )

【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のテストを書くのが非常に辛いので割愛しています。

【Rails4】kaminariをAPIで利用する

ページネーション用Gemで有名なkaminari。
ただ、使い方を調べてもAPIでの利用方法が見つからなかったので記載。

前提
サンプルとしてTodoをJSONで渡すAPIを実装

下記Gem導入済み
・kaminari
・active_model_serializers

modelの出力フォーマットをactive_model_serializersを利用して設定

./app/serializers/todo_serializer.rb

class TodoSerializer < ActiveModel::Serializer
  attributes :id, :body, :memo, :complete, :user_id
end


kaminariの共通設定ファイルを定義

config/initializers/kaminari_config.rb

Kaminari.configure do |config|
  config.default_per_page = 7    #最大項目数
  # config.max_per_page = nil       #最大数/page
  # config.window = 4               
  # config.outer_window = 0
  # config.left = 0
  # config.right = 0
  # config.page_method_name = :page
  # config.param_name = :page
end
Contact GitHub API Training Shop Blog About


kaminariをAPIに適用させる

config/initializers/active_model_serializer.rb

ActiveModel::Serializer.config.adapter = :json_api


controllerを実装する

class TodosController < ApplicationController

  def index
    #deviceを利用していますが、ここは各自読み替え
    todos = current_user.todos.page(params[:page] ||= 1)
    
    #meta 以下が今回の設定を適用させる
    render json: todos, meta: pagination_dict(todos)
  end

#以下省略

出力確認

{
"data":[{"id":"11","type":"todos","attributes":{"body":"APIを実装する","memo":"","complete":false,"user-id":9}},  {"id":"12","type":"todos","attributes":{"body":"はてなに投稿する","memo":"","complete":false,"user-id":9}},{"id":"13","type":"todos","attributes":{"body":"記事を投稿する","memo":"","complete":false,"user-id":9}}],
"links":{},
"meta":{"current-page":1,"next-page":null,"prev-page":null,"total-pages":1,"total-count":3}
}

meta 及び links がnodeに存在すれば完了。

【Rails4】before_actionに引数を渡す

以下の様にbefore_actionに引数を渡した状態で定義してもエラーとなる。

before_action :test('hogehoge')

これは以下の様に修正することで解決できる。

before_action -> {
  test('hogehoge')
}


フィルターを設定したい場合は以下の様に追記。

before_action -> {
  test('hogehoge')
},only: [:index, :show]


少しハマったのでメモ。

【Rails4】Devise::MissingWarden エラーへの対処

RepecでControllerのテストを実行した際に発生。
以下エラー文

Devise::MissingWarden:
 Devise could not find the `Warden::Proxy` instance on your request environment.
Make sure that your application is loading Devise and Warden as expected and
 that the `Warden::Manager` middleware is present in your middleware stack.


原因
Device gem によるプロキシエラー。

対応

f you're using RSpec, you can put the following inside a file named spec/support/devise.rb or in your spec/spec_helper.rb (or spec/rails_helper.rb if you are using rspec-rails):

RSpec.configure do |config|
  config.include Devise::Test::ControllerHelpers, type: :controller
  config.include Devise::Test::ControllerHelpers, type: :view
end

とのことなので以下の様に設定を追加

spec/rails_helper.rb

RSpec.configure do |config|
  config.include Devise::Test::ControllerHelpers, type: :controller
  config.include Devise::Test::ControllerHelpers, type: :view
end

【Java】SSL通信を実装する

環境
作業対象ファイル: C:\ssl
JDKのパス: C:\Program Files\Java\jdk1.6.0_45\bin
対象のサーバー証明書名: sample.cer

keyStore と trustStoreとは?
keystore: サーバーに送信するクライアントの証明書を格納するファイル。
trustStore: クライアント側が認証するサーバー側の証明書を格納するファイル。

手順
KeyStoreとTrustStore保存用ディレクトリを作成(C:\ssl)

認証対象のサーバー証明書(sample.cer)を C:\ssl に配置

ターミナルをAdministrator権限で実行し、インストール済みのJDK(C:\Program Files\Java\jdk1.6.0_45\bin)に移動。

以下のコマンドでkeyStoreを新規作成 -storepass の引数がパスワードとなります。

$ keytool -genkey -keystore keystore -storepass hogehoge

※この際、色々と設定を聞かれますが、よしなに...


同じくtrustStoreを新規作成

$ keytool -genkey -keystore truststore -storepass hogehoge


続いて sample.cer を trustStore に以下のコマンドでインポート -alias の引数が管理名として登録されます。

$ keytool -import -trustcacerts -file C:/ssl/sample.cer -keystore "C:/ssl/truststore" -alias sample_ssl


インポートされているかを以下のコマンドで確認

$ keytool -v -list -keystore  "C:/ssl/truststore"


インポートされている証明書の一覧が出てくるので、今回追加した sample_ssl が存在していれば成功。

以下サンプルコード

    /**
     * 認証付SSLContext作成
     * @param key_path Keystoreファイルのpath
     * @param key_password Keystoreファイルのpassword
     * @param trust_path Truststoreファイルのpath
     * @param trust_password Truststoreファイルのpassword
     * @return
     * @throws Exception
     */
    private SSLContext ssl_auth(String key_path, String key_password, String trust_path, String trust_password) throws Exception{
        //KeyStore読み込み
        char[] key_pass_char = key_password.toCharArray();
        KeyStore key_store = KeyStore.getInstance("JKS");
        KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");

        key_store.load(new FileInputStream(key_path), key_pass_char );
        kmf.init(key_store, key_pass_char);

        //TrustStoreの読み込み
        char[] trust_pass_char =TrustPass.toCharArray();;
        KeyStore trust_store = KeyStore.getInstance("JKS");
        TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");

        trust_store.load(new FileInputStream(TrustPath), trust_pass_char);
        tmf.init(trust_store);

        KeyManager[] key_managers = kmf.getKeyManagers();
        TrustManager[] trust_managers = tmf.getTrustManagers();

        SSLContext sslcontext = SSLContext.getInstance("TLS");
        sslcontext.init(key_managers, trust_managers, null);

        return sslcontext;
    }


     URL urlObj = new URL(url);
     HttpsURLConnection http = (HttpsURLConnection) urlObj.openConnection();
     http.setRequestMethod("POST");
     http.setDoOutput(true);
     http.setConnectTimeout(30000); 
     http.setReadTimeout(30000); 
     http.setSSLSocketFactory(ssl_auth("C:/ssl/keystore", "hogehoge", "C:/ssl/trustStore", "hogehoge").getSocketFactory());
     http.connect();

     OutputStream os = http.getOutputStream();
     PrintStream ps = new PrintStream(os);
     ps.print("送信するパラメータ");
     ps.close();

【javascript】ES6で導入されたclassにクラスメソッドを実装

ES6で導入されたclass記法は元々あった擬似クラス構文のシンタックスシュガー。
そのため、ES5時代の手法が そのまま転用できる。

class Person{

  constructor(){
     this.name = '山田太郎'
  }
  
  say(){
    console.log(this.name);
  }
}

今回定義したPersonクラスにクラスメソッドを追加する。

Person.method = function(){
   console.log("クラスメソッド");
}


これだけ。
実際に試してみる。

let tarou = new Person();

//インスタンスからクラスメソッドにアクセスできない
tarou.method()  //Uncaught TypeError

//インスタンスはインスタンスメソッドにアクセスできる
tarou.say()    //山田太郎

//クラスはインスタンスメソッドにアクセスできない
Person.say()    //Uncaught TypeError

//クラスはクラスメソッドにアクセスできる
Person.method()  //クラスメソッド


ES5での擬似クラス定義を理解していれば然程難しくはない。