Spring Boot と Angular2 をタイプセーフに繋ぐ

フロントエンドの開発は、TypeScript や flow によりタイプセーフに行えるようになってきています。
そうなるとバックエンドとフロントエンドの通信もタイプセーフにしたくなってくるはずです。
Swagger を使えばそれが実現できそうです。
Swagger により Angular2 のクライアントのコードを自動生成できるのです。
作成してみた Example Code を github に置きました。

github.com

バックエンド

バックエンドには、Spring Boot を使いました。
Swagger を使うための Springfox というものがありまして、Spring MVC は、Swagger との相性がとても良いです。

Java

まずはバックエンドのコードです。

/backend/src/main/java/app/Application.java

package app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import static springfox.documentation.builders.PathSelectors.regex;

@SpringBootApplication
@EnableSwagger2 // (1)
@RestController
public class Application {

    @GetMapping("/rest/add")
    public Response add(@RequestParam Integer arg1, @RequestParam Integer arg2) {
        return new Response(arg1 + arg2);
    }

    @Bean
    public Docket documentation() {
        return new Docket(DocumentationType.SWAGGER_2)
            .select()
            .apis(RequestHandlerSelectors.any())
            .paths(regex("^/rest/.*$")) // (2)
            .build();
    }

    public static class Response {
        public final Integer result;

        public Response(final Integer result) {
            this.result = result;
        }
    }

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

リクエストパラメータで受け取った二つの整数を足してレスポンスするという単純なコードです。
Spring Boot 側は次のコマンドで起動します。フロントエンドのビルドも走るのでちょっと時間がかかります。

$ cd backend
$ ./gradlew bootRun

リクエスト・レスポンスは、次のような感じになります。

$ curl -s 'http://localhost:8080/rest/add?arg1=1&arg2=2'
{"result":3}

(1)で EnableSwagger2 アノテーションを付けることにより、バックエンドの仕様を公開するようになります。
(2)で 対象となる URLパターンを限定しています。
次のような感じで JSON で仕様が確認できるようになります。

$ curl -s 'http://localhost:8080/v2/api-docs'
{"swagger":"2.0","info":{"description":"Api Documentation","version":"1.0","title":"Api Documentation","termsOfService":"urn:tos","contact":{},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0"}},"host":"localhost","basePath":"/","tags":[{"name":"application","description":"Application"}],"paths":{"/rest/add":{"get":{"tags":["application"],"summary":"add","operationId":"addUsingGET","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"arg1","in":"query","description":"arg1","required":true,"type":"integer","format":"int32"},{"name":"arg2","in":"query","description":"arg2","required":true,"type":"integer","format":"int32"}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/Response"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}}},"definitions":{"Response":{"type":"object","properties":{"result":{"type":"integer","format":"int32"}}}}}

build script

ビルドには Gradle を使っています。

/backend/build.gradle

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

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

jar {
    baseName = 'app'
    version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile 'io.springfox:springfox-swagger2:2.6.1'
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile 'io.springfox:springfox-staticdocs:2.6.1'
}

def SWAGGER_JSON_FILE = 'publications/swagger.json';
def SWAGGER_CLIENT_DIR = '../frontend/src/swagger';

task generateSwaggerSpec(type: Test, dependsOn: testClasses) {
    inputs.files fileTree('src/main/java')
    outputs.file file(SWAGGER_JSON_FILE)
    filter {
        includeTestsMatching "app.SwaggerSpecGenerator"
    }
}

configurations {
    swaggercodegen
}

dependencies {
    swaggercodegen 'io.swagger:swagger-codegen-cli:2.2.1'
}

task cleanSwaggerCodegen(type: Delete) {
    delete fileTree(SWAGGER_CLIENT_DIR).include('*/*')
}

task swaggerCodegen(type: JavaExec) {
    inputs.file file(SWAGGER_JSON_FILE)
    outputs.dir file(SWAGGER_CLIENT_DIR)
    classpath = configurations.swaggercodegen
    main = 'io.swagger.codegen.SwaggerCodegen'
        args('generate',
        '-l', 'typescript-angular2',
        '-i', SWAGGER_JSON_FILE,
        '-o', SWAGGER_CLIENT_DIR)
}

task compileFrontend(type:Exec) {
    inputs.files fileTree('../frontend/src')
    outputs.dir file('build/resources/main/static')
    workingDir '../frontend'
    ant.condition(property: "isWindows", value: true) { os(family: "windows") }
    commandLine(ant.properties.isWindows ?
        ['cmd', '/c', 'ng', 'build', '--aot', '--target=production'] :
        ['ng', 'build', '--aot', '--target=production'])
}

swaggerCodegen.dependsOn generateSwaggerSpec
compileFrontend.dependsOn swaggerCodegen
jar.dependsOn compileFrontend
bootRun.dependsOn compileFrontend

追加した処理は、以下です。

  • mock mvc を使ったテストで バックエンドの仕様を表す swagger.json の生成
  • swagger.json から Angular2 のクライアントコードの生成
  • gradle から ng コマンドを叩いて フロントエンドのビルドを開始する

Angular2 用のクライアントコードを生成するコマンドは、次になります。

$ ./gradlew swaggerCodegen

次の位置にファイルが生成されます。
/frontend/src/swagger/.

フロントエンド

フロントエンドは、Angular2 を使います。

TypeScript

/frontend/src/app/app.component.ts

import { Component, Input } from '@angular/core';
import {ApplicationApi} from '../swagger/api/ApplicationApi'
import {Http} from '@angular/http';
import { environment } from '../environments/environment';

@Component({
  selector: 'app-root',
  template: `
  <h1>
    <input type="number" [(ngModel)]="arg1" /> +
    <input type="number" [(ngModel)]="arg2" />
    <button (click)="add()">=</button>
    {{result}}
  </h1>
  `,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  arg1: number;
  arg2: number;
  result: number;
  constructor(private http: Http) {
  }
  add() {
    if (this.arg1 || this.arg2) {
      new ApplicationApi(this.http, window.location.origin) // (1)
        .addUsingGET(this.arg1, this.arg2)
        .subscribe(data=>this.result = data.result);
    }
  }
}

Swagger により生成された ApplicationApi を利用しています。
通信部分のコードで補完がきくし、エラーチェックしてくれます。
そうそう、俺が実現したかったのこれ!
(1) で basePath として window.location.origion を指定してるのが微妙(省略したらそれになって欲しい)。
とても単純な例なので複雑なパターンに対応できるのか分かっていません。。
まー、Swagger の場合、コード生成する部分を簡単に置き換えられるようにはなっているので少し安心ですが。

フロントエンド開発用のサーバ起動は次になります。

$ cd frontend
$ npm start

画面は以下。

f:id:t1000leaf:20170305205542p:plain

本題に関しましては、以上です。
以下、おまけ。

おまけ

プロジェクトの雛形作成

プロジェクトのルートに backend, frontend というディレクトリを作って完全にバックエンドのファイルとフロントエンドのファイルを分けて管理するようにしてみました。
Spring Boot 側の雛形は、 Spring Initializr で、Angular側は、Angular-CLI で作成しました。

バックエンド と フロントエンドで使う IDE を分ける

今回、バックエンドは eclipse(STS)、フロントエンドは Visual Studio Code でコーディングしました。
本当は、 eclipse だけで済ますことができれば良いのですが、フロントエンド開発に eclipse は弱そうです。
ルートの backend ディレクトリは、 eclipse の プロジェクトとして読み込み、 frontend ディレクトリは VS Code のプロジェクトとして読み込みます。

フロンエンド開発用サーバへのリクエストをバックエンドのサーバに proxy する

Angular-CLI で作成したプロジェクトの場合、proxy.conf.json に proxy の情報を書きます。

/frontend/proxy.conf.json

{
  "/rest": {
    "target": "http://localhost:8080",
    "secure": false
  }
}

そして、 package.json の scripts.start 部分も書き換えます。

{
  // 省略
  "scripts": {
    "start": "node_modules/.bin/ng serve --proxy-config proxy.conf.json"
  },
  // 省略
}

backend の .gitignore に .bin を追加する

Spring Initializr で作成した .gitignore に .bin がない。
追加しないと eclipseコンパイルしたファイルが commit される。

frontend のビルドでファイルの出力先を変更する

frontend でビルドしたファイルを backend 側に出力し、 jar の中に含めるようにしています。
その設定が、package.json の scripts.build の -op です。

{
  // 省略
  "scripts": {
    "build": "node_modules/.bin/ng build --aot -t production -op ../backend/build/resources/main/static"
  },
  // 省略

Angular2 は、実案件でそろそろ使って良い?

個人的には Angular-CLIVisual Studio Code Exetnsion の正式リリースを待ってから実案件で使うのが良いかと思っているところです。

Angular2 で HTML テンプレートは、どこに書く?

別HTMLに書くか、typescript の Decorator に書くか選択できます。
コンパイル速度や IDE のコード解析を考えると、Decorator に書いた方が良いかもしれません。

Angular2 と React どっちが良い ?

今のところ、 実績のある React かなーと。
React も swagger で fetch API のクライアントを生成すればタイプセーフな通信もできそうですし、JSX, flow, typescript によるタイプセーフなテンプレートも良い感じですし。
Angular2 はデフォルトのコンパイルだとテンプレート部分は、単なる文字列であって、AOTやらでどこまで静的解析してくれるのかよく分かっていません。
React は、ライブラリの組み合わせに悩みそうな感じがあり、フルスタックフレームワーク的な Angular の方はその点悩むこと少なそうと思います。
flow より TypeScript を推したい自分としては、TypeScript 製の Angular2 に期待しているところです。

以上

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

Angular2によるモダンWeb開発 [ 末次 章 ]
価格:3024円(税込、送料無料) (2017/2/26時点)


[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

はじめてのSpring Boot改訂版 [ 槇俊明 ]
価格:2700円(税込、送料無料) (2017/2/26時点)