Vert.x 3入門 〜 サービス

[Vert.x](http://vert-x3.github.io/)とは、公式ページより

> JVM上にリアクティブアプリケーションを構築するためのツールキットです

[@timfox](https://twitter.com/timfox)氏が中心となって、[現在2015年6月22日を目標](https://github.com/vert-x3/wiki/wiki/Vert.x-Roadmap#vertx-300-final-release-22-june-2015)にversion 3が開発されています。この投稿含め何回かに分けてVert.x 3の動くサンプルを実装していこうと思います。

# 概要

「サービス」はversion 3から導入された概念です。
サービスは任意の機能を実現するVerticleです。

以下にシンプルなサービスの例を示します、この`hoge-service`はデプロイメントされたときに`hoge`をログ出力するだけのサービスです。

サービスを実現する`HogeServiceVerticle.java`の中身は以下の通りです:

```java
package com.example.hogeservice.services;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.impl.LoggerFactory;

public class HogeServiceVerticle extends AbstractVerticle {
  private final static Logger log = LoggerFactory.getLogger(HogeServiceVerticle.class);

  @Override
  public void start() {
    log.info("hogehoge");
  }
}
```

サービスを利用してもらうために、サービス識別子と呼ばれるJSONファイルを提供する必要があります。`hoge-service`のサービス識別子`com.example.hogeservice.hoge-service.json`の中身はは以下の通りです:

```java
{
  "main": "com.example.hogeservice.services.HogeServiceVerticle",
  "options": {
    "config": {
      "address": "hogeservice.product"
    }
  }
}
```

利用者側は、dependencyに[Vert.x Service Factory](https://github.com/vert-x3/vertx-service-factory)を指定した上で、以下の通り`hoge-service`をデプロイメントすることで利用できます。

```java
vertx.deployVerticle("service:com.example.hogeservice.hoge-service");
```

このように任意の機能をサービスとして実装することで、サービスの利用者と実際にデプロイメントされるサービスVerticleを分離することができます。

## Vert.x Service Proxy

Verticle間で通信するには[EventBus](http://vert-x3.github.io/docs/vertx-core/java/#event_bus)を用います。サービスもVerticleなので、サービスを実装する際には、`EventBus`に対するハンドラの登録や`EventBus`を通した結果の応答等冗長なコードを記述する必要があります。[Vert.x Service Proxy](https://github.com/vert-x3/vertx-service-proxy)を用いることでこれらボイラープレートなコードを減らすことができます。

`Vert.x Service Proxiy`は以下を実現します:

* ボイラープレートなコードの削減
* サービスの利用者向けのプロキシの自動生成―利用者はこのプロキシを用いることで、`EventBus`に直接メッセージを送信する代わりに、統一されたAPIを通じてサービスを利用することができる
* Vert.xのサポートするプログラミング言語向けのサービススタブの自動生成―一度Javaでサービスを実装すれば、JavaScriptやGroovyからもこのサービスを利用することができる

今回は、データベース関連の処理を担うサービスを`Vert.x Service Proxiy`を用いて実装し、これを利用するシンプルなRESTサーバを動かしてみます。

# 前提

Vert.x 3はJava 8が必須となります、あらかじめJava 8をインストールしておいてください。Java 8がインストールできているかは以下コマンドで確認できます:

```bash
$ java -version
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)
```

また、IntelliJを前提に解説しています、Eclipse等他のIDEの場合適宜読み替えてください。

データベースにはMySQLを用いています、PostgreSQL等他のデータベースを利用する場合適宜読み替えてください。
データベースのユーザはroot(パスワードもroot)を用いています、別のものを用いる場合適宜読み替えてください。

# サンプルの構成

Gradleの[Multi-project](http://gradle.org/docs/current/userguide/multi_project_builds.html)を用います、以下のディレクトリ構成を作ります

```
myservice
  build.gradle
  settings.gradle
  servicies/      // データベース関連の処理を担うサービスを格納
  server/         // 上記サービスを利用するサーバを格納
```

# 手順

## データベースの準備

予めサンプルデータを格納するデータベースを作成しておきます、MySQLに接続して以下SQL文を実行します。

```bash
$ msql -u root -p
> create database vertx_example;

> create table products (
  id int not null auto_increment primary key,
  name varchar(20),
  price float,
  weight int
) engine=InnoDB;

> insert into products (name, price, weight) values
("Egg Whisk", 3.88, 150), ("Tea Cosy", 5.99, 100), ("Spatula", 1.00, 80);
```

## ひな形のクローン

ひな形となるプロジェクトをクローンします。

```bash
$ git clone https://github.com/p-baleine/vertx-gradle-template.git my-service
$ cd my-service
$ git remote rm origin # origin削除
```

## IntelliJでインポート

IntelliJを起動してWelcomeダイアログで[Import Project]を選択し、クローンした際にできたディレクトリを選択します。

Import Projectのダイアログで、[Import project from external model]にチェックを入れて、[Gradle]を選択し[Next]をクリックし、次に表示される画面で[Finish]をクリックします。

## Multi-projectの準備

IntelliJでトップ階層の`my-service`を右クリックして、[New]→[File]をクリックし、New Fileダイアログで`settings.gradle`を入力して[OK]をクリックします。`settings.gradle`を以下内容で編集します。

```
include 'server', 'services:productService'
```

`build.gradle`を開いて以下内容で編集します。


```gradle
subprojects {
    apply plugin: 'java'

    repositories {
        mavenCentral()
        maven {
            url = 'http://oss.sonatype.org/content/repositories/snapshots/'
        }
    }
}

project(':server') {
    dependencies {
        compile 'io.vertx:vertx-apex:3.0.0-milestone4'
        compile 'io.vertx:vertx-core:3.0.0-milestone4'
        compile 'io.vertx:vertx-service-factory:3.0.0-milestone4'
        compile project(':services:productService')
    }
}

project(':services:productService') {
    ext {
        generated = file("$projectDir/src/main/generated")
    }
    configurations {
        annotationProcessor
    }
    sourceSets {
        main {
            java {
                srcDirs generated
            }
        }
    }
    dependencies {
        compile 'io.vertx:vertx-auth-service:3.0.0-milestone4'
        compile 'io.vertx:vertx-codegen:3.0.0-milestone4'
        compile 'io.vertx:vertx-core:3.0.0-milestone4'
        compile 'io.vertx:vertx-lang-js:3.0.0-milestone4'
        compile 'io.vertx:vertx-service-proxy:3.0.0-milestone4'
        compile 'com.mchange:c3p0:0.9.5'
        compile 'com.mchange:mchange-commons-java:0.2.9'
        compile 'mysql:mysql-connector-java:5.1.35'
        compile 'org.jdbi:jdbi:2.62'

        annotationProcessor 'io.vertx:vertx-codegen:3.0.0-milestone4'
    }

    task cleanGenerated(type: Delete) {
        delete generated
    }

    compileJava {
        dependsOn cleanGenerated
        doFirst {
            mkdir generated
        }
        options.compilerArgs += [
                "-s", generated,
                "-AoutputDirectory=$projectDir/src/main",
                "-processor", "io.vertx.codegen.CodeGenProcessor"
        ]
        classpath += configurations.annotationProcessor
    }
}
```

IntelliJで[View]→[Tool Windows]→[Gradle]を選択してGradle Projectsビューを表示したら、同期アイコンをクリックして依存関係を読み込みます。

シェルより、`src`ディレクトリは不要なので削除します。

```bash
$ rm -r src
```

`server`及び`services`のためのディレクトリを作ります。

```bash
$ mkdir -p {server,services/productService}/src/main/java/com/example/myservice
$ mkdir services/productService/src/main/java/com/example/myservice/services
```

## ProductService

[Vert.x Service Proxy](https://github.com/vert-x3/vertx-service-proxy)を参考にして、データベース関連の処理を担う`ProductService`を実装します。

### ProductServiceインタフェース

まず利用者側に対するインタフェースとなる`ProductSerice`を作成します。

services/productService/src/main/java/com/example/myservice/services/ProductService.java

```java
package com.example.myservice.services;

import com.example.myservice.services.impl.ProductServiceImpl;
import io.vertx.codegen.annotations.ProxyGen;
import io.vertx.codegen.annotations.ProxyIgnore;
import io.vertx.codegen.annotations.VertxGen;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.serviceproxy.ProxyHelper;

@ProxyGen
@VertxGen
public interface ProductService {

  static ProductService create(Vertx vertx, JsonObject config) {
    return new ProductServiceImpl(vertx, config);
  }

  static ProductService createProxy(Vertx vertx, String address) {
    return ProxyHelper.createProxy(ProductService.class, vertx, address);
  }

  @ProxyIgnore
  public void start();

  @ProxyIgnore
  public void stop();

  void list(Handler<AsyncResult<JsonArray>> resultHandler);
}
```

### ProductServiceインタフェースの実装

実態である`ProductServiceImpl`を実装します。

services/productService/src/main/java/com/example/myservice/services/impl/ProductServiceImpl.java

```java
package com.example.myservice.services.impl;

import com.example.myservice.services.ProductService;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.impl.LoggerFactory;
import org.skife.jdbi.v2.DBI;
import org.skife.jdbi.v2.DefaultMapper;
import org.skife.jdbi.v2.Handle;
import org.skife.jdbi.v2.ResultIterator;

import java.util.Map;

public class ProductServiceImpl implements ProductService {

  private static final Logger log = LoggerFactory.getLogger(ProductService.class);

  private final Vertx vertx;
  private final JsonObject config;

  private ComboPooledDataSource dataSource = null;
  private DBI dbi = null;

  public ProductServiceImpl(Vertx vertx, JsonObject config) {
    this.vertx = vertx;
    this.config = config;
  }

  @Override
  public void start() {
    String url = config.getString("url");

    if (url == null) {
      throw new NullPointerException("url cannot be null");
    }

    ComboPooledDataSource cpds = new ComboPooledDataSource();
    cpds.setJdbcUrl(url);
    dataSource = cpds;
    dbi = new DBI(dataSource);
  }

  @Override
  public void stop() {
    dataSource.close();
  }

  @Override
  public void list(Handler<AsyncResult<JsonArray>> resultHandler) {
    Handle handle = dbi.open();
    ResultIterator<Map<String, Object>> iter = handle.createQuery("select * from products")
        .map(new DefaultMapper())
        .iterator();
    JsonArray results = new JsonArray();
    while (iter.hasNext()) {
      results.add(iter.next());
    }
    resultHandler.handle(Future.succeededFuture(results));
  }
}
```

### ProductServiceVerticle

`ProductService`を起動する`ProductServiceVerticle`を実装します。

services/productService/src/main/java/com/example/myservice/services/ProductServiceVerticle.java

```java
package com.example.myservice.services;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.impl.LoggerFactory;
import io.vertx.serviceproxy.ProxyHelper;

public class ProductServiceVerticle extends AbstractVerticle {

  private static final Logger log = LoggerFactory.getLogger(ProductServiceVerticle.class);

  private ProductService service;

  @Override
  public void start() {
    service = ProductService.create(vertx, config());

    String address = config().getString("address");
    if (address == null) {
      throw new IllegalStateException("address field must be specified in config for service verticle");
    }

    ProxyHelper.registerService(ProductService.class, vertx, service, address);

    service.start();
  }
}
```

### package-info

`services/productService/src/main/java/com/example/myservice/services`に以下内容で`package-info.java`を作成します。

```java
@GenModule(name = "myservice-product", groupPackageName="com.example.myservice")
package com.example.myservice.services;

import io.vertx.codegen.annotations.GenModule;
```

### サービス識別子

`services/productService/src/main/resources`に以下内容でこのサービスの識別子である`com.example.myservice.product-service.json`を作成します。

```json
{
  "main": "com.example.myservice.services.ProductServiceVerticle",
  "options": {
    "config": {
      "address": "myservice.product"
    },
    "worker": true
  }
}
```

## ServerVerticle

利用者側である`ServerVerticle`を実装します。

server/src/main/java/com/example/myservice/ServerVerticle.java

```java
package com.example.myservice;

import com.example.myservice.services.ProductService;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.impl.LoggerFactory;
import io.vertx.ext.apex.Router;
import io.vertx.ext.apex.RoutingContext;
import io.vertx.ext.apex.handler.BodyHandler;

public class ServerVerticle extends AbstractVerticle {

  private static final Logger log = LoggerFactory.getLogger(ServerVerticle.class);

  @Override
  public void start() throws Exception {
    JsonObject config = new JsonObject().put("url", "jdbc:mysql://127.0.0.1:3306/vertx_example?user=root&password=root");
    DeploymentOptions options = new DeploymentOptions().setConfig(config);

    vertx.deployVerticle("service:com.example.myservice.product-service", options, res -> {
      if (res.succeeded()) {
        Router router = Router.router(vertx);

        router.route().handler(BodyHandler.create());
        router.route().handler(routingContext -> {
          routingContext.response().putHeader("Content-Type", "application/json");
          routingContext.next();
        });

        router.get("/products").handler(this::handleListProduct);

        vertx.createHttpServer().requestHandler(router::accept).listen(8080);
      } else {
        throw new RuntimeException(res.cause());
      }
    });
  }

  private void handleListProduct(RoutingContext routingContext) {
    ProductService service = ProductService.createProxy(vertx, "myservice.product");

    service.list(res -> {
      if (res.succeeded()) {
        routingContext.response().end(res.result().encode());
      } else {
        routingContext.fail(res.cause());
      }
    });
  }
}

```

## アプリの起動

IntelliJの[Run]→[Edit Configurations...]を選択します。

Run/Debug Configurationsダイアログで[+]→[Application]を選択します。

以下の通り入力します:

* Name: MyService
* Main classs: io.vertx.core.Starter
* Program arguments: run com.example.myservice.ServerVerticle

また、下部Before launchの[+]をクリックしてRun Gradle Takを選択します。Select Gradle taskダイアログが開いたら、以下の通り入力します:

* Gradle Project: :services:productService
* Tasks: clean build

最終的にRun/Debug Configurationsダイアログは以下の様な内容となります:

![スクリーンショット 2015-05-07 20.11.13.png](https://qiita-image-store.s3.amazonaws.com/0/14712/ddfdb165-bcc7-9048-ff21-8b601a9af017.png)


IntelliJの[Run]→[Run 'MyService']を選択します、IntelliJ上のコンソールで、エラーなく起動することを確認します

## 確認

以下コマンドを実行して、プロダクト一覧が返却されることを確認します:

```bash
$ curl -D - localhost:8080/products
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 123

[{"id":"1","name":"Egg Whisk","price":150},{"id":"2","name":"Tea Cosy","price":100},{"id":"3","name":"Spatula","price":30}]
```

# 解説

サービス識別子`com.example.myservice.product-service.json`にはデプロイメント時の設定を指定することができます。`options`フィールドに記述する内容は`DeploymentOptions`に対応します。基本的にここで定義された値は利用者側から上書きすることはできませんが、`config`フィールドの値のみ利用者側で上書きすることができます。今回の例で行けば、`address`はデフォルト値が`myservice.product`で、これを任意の値で上書きすることは可能ですが、`worker: true`の設定は利用者側から変更することはできません。

`Vert.x Service Proxy`を用いているので、利用者側である`ServerVerticle`では`EventBus`を介してメッセージを送信する代わりに、以下のように`ProducService`の提示するメソッドを通して処理を依頼することができます。

```java
ProductService service = ProductService.createProxy(vertx, "myservice.product");
service.list(res -> {
...
```

また、`Vert.x Service Proxy`がコンパイル時にJavaScript向けインタフェースも自動的に生成してくれるので、以下のようにJavaScript側から`ProductService`を利用することができます。

```js
var Router = require("vertx-apex-js/router");
var BodyHandler = require("vertx-apex-js/body_handler");
var ProductService = require("myservice-product-js/product_service");

var config = { url: "jdbc:mysql://127.0.0.1:3306/vertx_example?user=root&password=root" };

vertx.deployVerticle("service:com.example.myservice.product-service", { config: config }, function(res, err) {
    if (err) {
      throw err;
    }

    var router = Router.router(vertx);

    router.route().handler(BodyHandler.create().handle);

    router.get("/products").handler(handleListProduct);

    vertx.createHttpServer().requestHandler(router.accept).listen(8082);
});

function handleListProduct(routingContext) {
  var proxy = ProductService.createProxy(vertx, "myservice.product");
  proxy.list(function(res, err) {
    if (err) {
      routingContext.fail(err);
    } else {
      routingContext.response().putHeader("Content-Type", "application/json").end(JSON.stringify(res));
    }
  });
}
```

今回の完全なソースは[こちら](https://github.com/p-baleine/step-by-step-vert.x/tree/master/examples/my-service)にあげてあります。

サービスについてまとまったドキュメントはまだあまり見つけられませんが、[こちら](https://groups.google.com/forum/?hl=ja#!searchin/vertx/Service/vertx/vezlmH-H4NI/uBe9iM5Z1U8J)のGoogle Groupsに[@timfox](https://twitter.com/timfox)氏によるサービスの紹介があります。

# 色々なサービス

Vert.x公式はいろいろなサービスを提供しています、以下に一部を抜粋します、より詳細は[公式ページ]()を参照してください。

* [MongoDB client](http://vert-x3.github.io/docs/vertx-mongo-client/java/): MongoDBに非同期にアクセスするためのサービス
* [JDBC client](http://vert-x3.github.io/docs/vertx-jdbc-client/java/): JDBCに非同期にアクセスするためのサービス
* [Vert.x Auth service](http://vert-x3.github.io/docs/vertx-auth-service/java/): 認証機能を提供するサービス

# こちらもどうぞ

* [Vert.x 3入門 〜 HelloWorldVerticle](http://qiita.com/p-baleine@github/items/304912bea4a041ae653b)
* [Vert.x 3入門 〜 Webアプリケーションフレームワーク Apex](http://qiita.com/p-baleine@github/items/d9975dad4e44987b48c8)
* [Vert.x 3入門 〜 JDBC Service](http://qiita.com/p-baleine@github/items/f5ecfcd8387d07418aac)
* [Vert.x 3入門 〜 複数Verticleのデプロイ](http://qiita.com/p-baleine@github/items/b187acf3b792940988ab)