高品質なサーバーアプリを作成してみる - Apache Camel


# 概要

* サーバーアプリと言っても、ブラウザでアクセスされるような画面系なアプリではなくWebAPIアプリだったり、画面の裏っかわで動くアプリの事。
* そして、Apache Camel(Java)を使ったプロジェクトだと高品質なものができる！
* 理由は簡単で、試験が簡単にできるから。

# Apache Camelに関して

* 軽量なメッセージ処理フレームワーク（EIPと呼ばれている)。
* MVCが画面系の表なフレームワークで、EIPは裏方のフレームワーク。
* どんな動きかは過去に色々書いたので見てください。
* 下のような絵にすると普通にプログラム書いてもできると思うけど、色々な定期ポーリング処理、バッチ処理、コンポーネント処理、データ変換、エラー処理、リトライ処理、トランザクション処理だったり色々な恩恵を受けられる。簡単に「開始A」とか書いているけどたくさんの恩恵を受けられる。

![スクリーンショット 2015-02-07 14.22.36.png](https://qiita-image-store.s3.amazonaws.com/0/37624/ba4ce8fe-1c3b-7cb4-25f5-9834150fd710.png)

* 意味が違うけど試験
http://www.redhat.com/ja/services/training/ex421-red-hat-certificate-expertise-camel-development-exam




# どんな感じで試験ができるか？

 「環境がそろわないと試験できないよ！」という感じにさせない環境を提供できる

* 担当した機能を作ったら動作を試す感覚で試験コードが書ける。
* 実装したら動作を試す --> 試す為にはちょっとした試験コードを書く --> 書いたコードはせっかくなので残す --> 後々何回でも繰り返し使える　という好循環を生み出していく。
* 手元で動かせる。サーバーに載せなくても動く。デプロイ不要。
* メッセージング処理フレームワーク（ベルトコンベアみたいな感じ）なので、決まった箱に想定入力情報を入れて、期待値が出てくるのを確認するだけ



# アプリ作成

試験するにも、試験対象のアプリがないと話が始まらないので。

HTTPで受信してデータを返信するAPI的なアプリをサクッと作る。

作るアプリ

* http://localhost:5555/?q=3,4,5 とアクセスされると
* 結果はjsonで{"result",12}と戻ってくる

mavenアプリで下記のような感じ


```pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>test</groupId>
    <artifactId>hellotest</artifactId>
    <version>1</version>
    <packaging>war</packaging>

    <properties>
        <camel-ver>2.14.1</camel-ver>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-core</artifactId>
            <version>${camel-ver}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-test</artifactId>
            <version>${camel-ver}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-gson</artifactId>
            <version>${camel-ver}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-jetty</artifactId>
            <version>${camel-ver}</version>
        </dependency>
    </dependencies>
</project>
```

HTTP受信ルート
これでHTTP5555ポートで受信できる。
受信したあとはCalcAddRouteにデータを流して、結果をHTTP送信元に返す。

```java:hellotest.route.HttpRoute
package hellotest.route;

import org.apache.camel.builder.RouteBuilder;

/**
 * HTTP受信
 */
public class HttpRoute extends RouteBuilder {

    @Override
    public void configure() throws Exception {
        from("jetty:http://0.0.0.0:5555/")
        .to("log:in?showAll=true&multiline=true")

        .to(CalcAddRoute.IN)

        .to("log:out?showAll=true&multiline=true");
    }
}
```

カンマ区切りのデータを足し込んで、結果をjsonで返すルート

```java:hellotest.route.CalcAddRoute
package hellotest.route;

import hellotest.process.ArraySumProcess;
import hellotest.process.CsvToArrayProcess;
import hellotest.process.MakeResponseProcess;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.dataformat.JsonLibrary;

/**
 * カンマ区切りのデータを足して、結果をjsonで返す
 */
public class CalcAddRoute extends RouteBuilder {

    public static final String CLASSNAME = CalcAddRoute.class.getSimpleName();
    public static final String IN = "direct:in" + CLASSNAME;

    @Override
    public void configure() throws Exception {
        from(IN).routeId(CLASSNAME)
        .to("log:in" + CLASSNAME)

        .process(new CsvToArrayProcess())
        .process(new ArraySumProcess())
        .process(new MakeResponseProcess())
        .marshal().json(JsonLibrary.Gson)

        .to("log:out" + CLASSNAME);
    }
}
```

HTTPクエリのqのカンマ区切りを配列に変換

```java:hellotest.process.CsvToArrayProcess
package hellotest.process;

import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.Processor;

import java.util.ArrayList;

/**
 * HTTPクエリのqのカンマ区切りを配列に変換
 */
public class CsvToArrayProcess implements Processor {
    @Override
    public void process(Exchange exchange) throws Exception {
        Message in = exchange.getIn();
        String q = in.getHeader("q", String.class);
        String[] list = q.split(",");
        ArrayList<Integer> resultList = new ArrayList<Integer>();

        for (String item: list) {
            try {
                Integer i = Integer.parseInt(item);
                resultList.add(i);
            } catch (Exception e) {
                throw new Exception("数字じゃない");
            }
        }

        in.setBody(resultList);
    }
}
```

配列のIntegerを足し込む処理

```java:hellotest.process.ArraySumProcess
package hellotest.process;

import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.Processor;

import java.util.ArrayList;

/**
 * 配列の数字を足し合わせる
 */
public class ArraySumProcess implements Processor {
    @Override
    public void process(Exchange exchange) throws Exception {
        Message in = exchange.getIn();
        ArrayList<Integer> list = in.getBody(ArrayList.class);
        int result = 0;
        for (Integer item: list) {
            result += item;
        }
        in.setHeader("result", result);
    }
}
```

計算結果をmap形式に変換

```java:hellotest.process.MakeResponseProcess
package hellotest.process;

import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.Processor;

import java.util.HashMap;

/**
 * 結果をMapに入れる処理
 */
public class MakeResponseProcess implements Processor{
    @Override
    public void process(Exchange exchange) throws Exception {
        Message in = exchange.getIn();
        HashMap<String, Object> map = new HashMap<String, Object>();
        map.put("result", in.getHeader("result"));
        in.setBody(map);
    }
}
```

以上で完成

# 試験（Processor）

Processorの試験は簡単
試しにCsvToArrayProcessをテストしてみる

```java:src/test/java/CsvToArrayProcessTest.java
import hellotest.process.CsvToArrayProcess;
import org.apache.camel.Exchange;
import org.apache.camel.test.junit4.CamelTestSupport;
import org.junit.Test;

import java.util.List;

/**
 * CsvToArrayProcessのテスト
 */
public class CsvToArrayProcessTest extends CamelTestSupport {

    @Test
    public void CSVを配列に変換できること() throws Exception {
        Exchange exchange = createExchangeWithBody("");
        exchange.getIn().setHeader("q", "1,2,3");
        CsvToArrayProcess p = new CsvToArrayProcess();
        p.process(exchange);

        List<Integer> list = exchange.getIn().getBody(List.class);
        assertEquals(3, list.size());
        assertEquals(1, (int)list.get(0));
        assertEquals(2, (int)list.get(1));
        assertEquals(3, (int)list.get(2));
    }

    @Test
    public void カンマが入っていない場合でも配列が取得できること() throws Exception {
        Exchange exchange = createExchangeWithBody("");
        exchange.getIn().setHeader("q", "3");
        CsvToArrayProcess p = new CsvToArrayProcess();
        p.process(exchange);

        List<Integer> list = exchange.getIn().getBody(List.class);
        assertEquals(1, list.size());
        assertEquals(3, (int)list.get(0));
    }


    @Test(expected = Exception.class)
    public void 数値以外が含まれた時はエラーになること() throws Exception {
        Exchange exchange = createExchangeWithBody("");
        exchange.getIn().setHeader("q", "1,2,AA");
        CsvToArrayProcess p = new CsvToArrayProcess();
        p.process(exchange);

        List<Integer> list = exchange.getIn().getBody(List.class);
    }

}
```

# 試験(ルートのin/outで確認）

ルートの試験の場合はルートを起動させてやる必要がある。
この場合はcreateRouteBuildersのoverrideしてテストのルートを登録するとOK。
今回はルートにデータを投入して、ルートから出てきたデータを確認する。


```java:src/test/java/CalcAddRouteTest.java
import hellotest.route.CalcAddRoute;
import org.apache.camel.Exchange;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.test.junit4.CamelTestSupport;
import org.junit.Test;

/**
 * CalcAddRouteのテスト
 */
public class CalcAddRouteTest extends CamelTestSupport {

    @Override
    protected RouteBuilder[] createRouteBuilders() throws Exception {
        return new RouteBuilder[]{new CalcAddRoute()};
    }

    @Test
    public void カンマ区切り3つの時でも計算できる事() throws Exception {
        Exchange exchange = createExchangeWithBody("");
        exchange.getIn().setHeader("q", "1,2,3");
        template.send(CalcAddRoute.IN, exchange);
        throwIfError(exchange);

        assertEquals("{\"result\":6}", exchange.getIn().getBody(String.class));
    }

    @Test
    public void カンマ区切り無しでも計算できる事() throws Exception {
        Exchange exchange = createExchangeWithBody("");
        exchange.getIn().setHeader("q", "1");
        template.send(CalcAddRoute.IN, exchange);
        throwIfError(exchange);

        assertEquals("{\"result\":1}", exchange.getIn().getBody(String.class));
    }


    private void throwIfError(Exchange exchange) throws Exception {
        if (exchange.getException() != null)
            throw exchange.getException();
    }

}
```

## ちょっと解説

CamelTestSupportを使うことで
template.sendというメソッドが利用できる。

template.sendでcamelのコンポーネントを使ってデータを送信できる。
今回は`direct:`というコンポーネントを使ってルートに送信している事になる。
送信するデータは第２引数のexchangeとなる。

throwIfErrorメソッドをわざわざ使っている理由
ルート内でエラーが発生した場合は例外が投げられるわけではなく、exchangeのexceptionの変数に入れられるだけ。
これをチェックしないと、実際はルート内でエラーが発生しているかもしれないのに気づかないといった事になってしまう。

# 試験(ルートの途中で確認）

目視で確認するかのごとく、実装にlogを追加する

```java:
package hellotest.route;

import hellotest.process.ArraySumProcess;
import hellotest.process.CsvToArrayProcess;
import hellotest.process.MakeResponseProcess;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.dataformat.JsonLibrary;

/**
 * カンマ区切りのデータを足して、結果をjsonで返す
 */
public class CalcAddRoute extends RouteBuilder {

    public static final String CLASSNAME = CalcAddRoute.class.getSimpleName();
    public static final String IN = "direct:in" + CLASSNAME;

    @Override
    public void configure() throws Exception {
        from(IN).routeId(CLASSNAME)
        .to("log:in" + CLASSNAME)

        .process(new CsvToArrayProcess())
        .process(new ArraySumProcess())

        .to("log:result")

        .process(new MakeResponseProcess())
        .marshal().json(JsonLibrary.Gson)

        .to("log:out" + CLASSNAME);
    }
}
```

試験コード

mockという機能を使う。
mockは試験用の特別なコンポーネントで、流通しているデータを捉える事ができるし、確認機能もある。

mockはルートの中に動的に挿入できるものになっている。
ただ、to()という所だけにmockを設定できるので、試験したい場所にはto("log:xxxx")を設定する。
例えば、to("http://xxxx/")はそのままmockを設定できる。


```java:src/test/java/CalcAddRouteTest.java
import hellotest.route.CalcAddRoute;
import org.apache.camel.Exchange;
import org.apache.camel.builder.AdviceWithRouteBuilder;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.test.junit4.CamelTestSupport;
import org.junit.Test;

/**
 * CalcAddRouteのテスト
 */
public class CalcAddRouteTest extends CamelTestSupport {

    @Override
    protected RouteBuilder[] createRouteBuilders() throws Exception {
        return new RouteBuilder[]{new CalcAddRoute()};
    }

    @Test
    public void カンマ区切り無しでも計算できる事() throws Exception {

        // 指定ルートのto("")のメソッド全てにMockを仕込む
        setMock(CalcAddRoute.CLASSNAME);

        // log:resultのモックを取得
        MockEndpoint mock = getMockEndpoint("mock:log:result");

        // モックの期待値を設定（メッセージが１件流れてくる事）
        mock.expectedMessageCount(1);
        // モックの期待値を設定（resultヘッダに1がセットされている事）
        mock.expectedHeaderReceived("result", 1);

        // 試験データをルートに流す
        Exchange exchange = createExchangeWithBody("");
        exchange.getIn().setHeader("q", "1");
        template.send(CalcAddRoute.IN, exchange);
        throwIfError(exchange);

        // モックが期待値通りになっているか確認
        assertMockEndpointsSatisfied();
    }

    /** 指定のルートに動的Mockを挿入 */
    public void setMock(String routeId) throws Exception {
        final String pattern = "*";

        context.getRouteDefinition(routeId)
                // すべてのポイントで割り込み
        .adviceWith(context, new AdviceWithRouteBuilder() {
            @Override
            public void configure() throws Exception {
                // 通常のMOCK
                mockEndpoints(pattern);
                //mockEndpointsAndSkip(skipPattern);
            }
        });
    }

    private void throwIfError(Exchange exchange) throws Exception {
        if (exchange.getException() != null)
            throw exchange.getException();
    }
}
```

モックにはプログラムを仕込むこともできて、下記のようにsetReporterを使って実現できる。
下記の例だと、log:result以降のルートにはデータを流さないという事ができている。

```java:
   @Test
    public void カンマ区切り無しでも計算できる事() throws Exception {

        // to("")のメソッド全てにMockを仕込む
        setMock(CalcAddRoute.CLASSNAME);

        // log:resultのモックを取得
        MockEndpoint mock = getMockEndpoint("mock:log:result");

        // モックの期待値を設定（メッセージが１件流れてくる事）
        mock.expectedMessageCount(1);
        // モックの期待値を設定（resultヘッダに1がセットされている事）
        mock.expectedHeaderReceived("result", 1);

        mock.setReporter(new Processor() {
            @Override
            public void process(Exchange exchange) throws Exception {
                new StopProcessor().process(exchange);
            }
        });

        Exchange exchange = createExchangeWithBody("");
        exchange.getIn().setHeader("q", "1");
        template.send(CalcAddRoute.IN, exchange);
        throwIfError(exchange);

        // モックが期待値通りになっているか確認
        assertMockEndpointsSatisfied();
    }
```

上の例ではsetReporterに実装できることを伝える為に面倒な書き方をしたが、下記のように書くのと同等。

```java
        mock.setReporter(new StopProcessor());
```

これを追加する事で、ルートの実装が途中までしかできていないけど試験したい！
できていない所までデータを流したくないといった場合にでも試験できるというわけ。


モックの機能は
http://camel.apache.org/mock.html

この辺のテストの話は
http://camel.apache.org/testing.html

Springを使ったCamelのテストは次回にでも