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

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

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

ターミナルからリクエストを送信

curl コマンドを用いて GET POST PUT DELETE を送信

$ curl -X GET REQUEST URL
$ curl -X POST REQUEST URL
$ curl -X DELETE REQUEST URL
$ curl -X PUT REQUEST URL

….あえて書くほどでも無い気もする

【PHP】Slimフレームワーク導入時の404エラー

実装

公式の通りにindex.phpを作成

// index.php
<?php
require_once "vendor/autoload.php";

$app = new \Slim\Slim();
$app->get('/hello/:name', function ($name) {
    echo "Hello, $name";
});

$app->run();

ユーザーディレクトリ配下で開発していたため以下にアクセス http://apache_server.localhost/application/hello/name → 404 エラー

原因

Slim側で設定されている.htaccessファイルが DocumentRootを参照しているため

# vendor/slim/slim/.htaccess

RewriteEngine On

# Some hosts may require you to use the `RewriteBase` directive.
# If you need to use the `RewriteBase` directive, it should be the
# absolute physical path to the directory that contains this htaccess file.
#
#RewriteBase / ← これ

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]

今回はApacheの設定を弄ってユーザーディレクトリでの開発をしているので Baseを変更する必要があるため、index.php と同一ディレクトリに .htaccess ファイルを作成

RewriteEngine On
RewriteBase /application/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]

もう一度アクセス http://apache_server.localhost/application/hello/name

問題なく表示されれば成功

【PHP】composer 導入手順

導入

インストール

$ brew install homebrew/php/composer

依存ファイルの作成
composerに読み込ませるモジュールは当ファイルに追記していく

#composer.json

{
  "require": {
       "読み込むモジュール名"
  }
}

読み込み

$ composer install

更新

$ composer update

オートロード

所謂 require 地獄を解消してくれる機能

├── index.php
├── composer.json
├── composer.phar
├── controllers
├── test
    └── Sample.php

読み込み対象のクラスを作成
namespaceの背定義は必須のため注意

// Sample.php
<?php

namespace Test;

class Sample {
  public function test(){
    echo 'Success';
  }
}

composer.json を編集
ここは test ディレクトリ配下のクラスを Test\クラス名 で呼び出す様に定義。

{
  "require": {
  },
  "autoload": {
    "psr-4": {
      "Test\\"      : "test/"
    }
  }
}

index.php でSampleを呼び出す。

//index.php
require_once "vendor/autoload.php";
use Test\Sample;

$sample = new Sample();
$sample->test();

以下のコマンドを実行

$ php composer.phar dump-autoload

これでディレクトリにvenderディレクトリが生成される。

├── vendor
│   ├── autoload.php
│   └── composer
│       ├── ClassLoader.php
│       ├── LICENSE
│       ├── autoload_classmap.php
│       ├── autoload_namespaces.php
│       ├── autoload_psr4.php
│       ├── autoload_real.php
│       ├── autoload_static.php
│       └── installed.json

Mac での LocalApacheサーバー構築

基本操作

サーバー稼働

$ apachectl start

サーバー再起動

$ apachectl restart

サーバー停止

$ apachectl stop

ブラウザで http://localhost/ にアクセスし表示されれば成功

ユーザーディレクトリの有効化

デフォルト設定だと独自ディレクトリが読み込まれないため設定を有効化

#/etc/apache2/httpd.conf
LoadModule userdir_module libexec/apache2/mod_userdir.so
...
...
Include /private/etc/apache2/extra/httpd-userdir.conf
...
...
Include /private/etc/apache2/extra/httpd-vhosts.conf

ユーザー別設定ファイル読み込みを有効化

# /etc/apache2/extra/httpd-userdir.conf
Include /private/etc/apache2/users/*.conf

ユーザー設定ファイルを作成

# /etc/apache2/users/ユーザー名.conf
<Directory "/Users/ユーザー名/sample/">
    AllowOverride All
    Options Indexes FollowSymLinks Multiviews
    Require all granted
</Directory>

ホスト情報の設定

# /etc/hosts

##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1   localhost
255.255.255.255 broadcasthost
::1             localhost

#追記
127.0.0.1 apache_server.localhost

バーチェルホストを設定

# /etc/apache2/extra/httpd-vhosts.conf

<VirtualHost *:80>
    DocumentRoot "/Users/ohs30359/sample"
    ServerName apache_server.localhost

    <Directory "/Users/ohs30359/ApacheServerDir">
        Options Includes ExecCGI FollowSymLinks
        AllowOverride All
        order deny,allow
        allow from All
    </Directory>
</VirtualHost>

これで apache_server.localhost で指定ディレクトリへのアクセスが可能となる。
今までの設定を反映するためにサーバーを再起動。

$ sudo /usr/sbin/apachectl restart

サンプルディレクトリを作成しアクセス出来るかを確認

$ mkdir ~/sample
$ vim ~/sample/sample.html

公開する html は適当に…

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title></title>
    <script>
    </script>
  </head>
  <body>
    <h1>SAMPLE Directory Load !!</h1>
  </body>
</html>

http://apache_server.localhost/

PHP設定

利用を定義 ※既にサーバーが稼働している場合は再起動が必須

# vim /etc/apache2/httpd.conf
#LoadModule php5_module libexec/apache2/libphp5.so

Ubuntu16.04 日本語入力設定

1.im-configで入力切替

$ im-config -n fcitx

2.画面右上のキーボードアイコン で「再起動」を選択
3.同じくキーボードアイコンから「設定」を選択
4.設定画面にて下記順に並び替え
・キーボード-日本語
・Mozc

【Larabel】migration reset時に外部キー制約エラーが発生

php artisan migrate:refresh

=>
[PDOException]
  SQLSTATE[23000]: Integrity constraint violation: 1217 Cannot delete or update a parent row: a foreign ke
  y constraint fails


外部キー制約が原因でドロップできないエラー。
対策としては一時的に外部キー制約を無効にする。

public function down(){
    DB::statement('SET FOREIGN_KEY_CHECKS = 0');
    Schema::drop('books');
    DB::statement('SET FOREIGN_KEY_CHECKS = 1');
}