Edited at

実践編:Camel, Tomcat, MySQL によるWeb APIアプリなものを高速開発

More than 3 years have passed since last update.

アプリをAPI化して各アプリを疎結合していく作りが多くなっていくこの世の中、

Apache CamelからDBにつなぐ方法、Tomcat等のコンテナで動作させる方法などなどが分かるとAPI的な実用なものがサクッと作れたりする所を今回説明。

(CamelからJettyを扱えるのでTomcat使わなくても動くけど)


今回の目標

IDを指定してhttpで問い合わせるとDBテーブルの内容をそのままJSONデータで返すアプリを作ること

// tomcatで受信したデータを取得 http://localhost:8080/appname/list?id=123

from("servlet:///list")

// アクセスログを出力(ログイン処理ではない〜)
.to("log:in")

// SQLで情報を取得
.to("sql:SELECT * FROM myitems WHERE user_id = :#id")

// SQLの結果(List<Map>)をjsonに変換
// 出力内容を絞るなら、SELECT文で*を使わないで
.marshal().json(JsonLibrary.Gson)

// ユーザー返却データをログに出力(ログアウト処理ではない〜)
.to("log:out");

// 終端にたどりついたデータがservletの戻り値になる!

全文は

https://github.com/d7kuro/HelloMySQL/blob/master/src/main/java/hello/route/HelloRoute.java

で開発終了。以上。

やりたいことをちゃらっと並べるだけがCamel風。

ただし、ただし、

これを動作させる為の設定がドキュメントからだと分かりにくいので、その辺を中心に解説。

といっても、「設定はコピペで使いまわせればそれでよい」という人はgithubに設定サンプル載せるので、説明は適宜読む程度でもよい。


設定も含めたソースコード

https://github.com/d7kuro/HelloMySQL


テスト用テーブル構造に関して

テーブル構造は一切書いてないけど、自由に作っていいです。

テーブル名がmyitemsと、user_idという列(型は自由)があれば他は自由に作ってください。


Apache Cmaelとは

前回の記事に書いた

Apache Camel (Java)を使うと開発が楽になる7つの理由


Camelが分かってくれば

こんな感じで2行で「ファイルの中身」を文字列で取り出せたり

exchange.getIn().setBody(new File("test.xml"));

String data = exchange.getIn().getBody(String.class);

こんな感じでxml読み出したり

exchange.getIn().setBody(new File("test.xml"));

Document document = exchange.getIn().getBody(Document.class);

こんな感じでJavaで型変換をあまり意識しない作りができるのが一つ特徴。

こんな感じでhttpsアクセスした結果をいろんな所に保存できたり。

.to("https://www.google.co.jp")

// アクセス結果を一旦文字列にしておく
.convertBodyTo(String.class)

// 保存ファイル名を決めておく
.setHeader(Exchange.FILE_NAME).simple("access-${date:now:yyyyMMdd}.txt")

// ログに出力
.to("log://accesslog/?level=INFO&showBody=true&multiline=true")

// ファイルに出力
.to("file://dir/")

// Elasticsearchに入れとく
.to("elasticsearch://cluster?operation=index")

// sftpでファイルアップする
.to("sftp://username@hostname:1212/dir")

// httpで内容をpostする
.to("http://host:port/path")

// smtpでメールを飛ばす
.to("smtp://username@host?To=admin@local.localdomain&Suject=access-log")

// ircで飛ばす
.to("irc://nickname@host?channels=#channel1,#channel2,#channel3")

// 標準出力に出しとく
.to("stream://out")

// facebookに投稿しとく
.to("facebook://postFeed/inBody=postUpdate")

// zipファイルで保存 'access-20140101.txt.zip' しとく
.marshal().zipFile()
.to("file://zipdir/")

これ一応言っとくけど、一個のプログラムになっていて各行がばらばらなサンプルではないので。

普通にプログラムするのが面倒になってしまう --> 人を堕落させるフレームワーク。

コンポーネントプログラムってこんなもんだと言われたらそうだけど。

そんな色々してくれるフレームワークなのに超軽量。

そういえばfacebookって試したことないな。

今回はDBとTomcat(servlet)の設定が課題。


今回の構成

Spring Framework

 色々な初期設定だけを任せる。Springの知識は不要。

Tomcat

 動作させるサーバー。なくても単体でも動くから使っても使わなくてもどっちでもいいけど、今回のサンプルはこれ。

 

MySQL

 今回使うDB。今回はSQLを使ったプログラミング。

Maven

 ライブラリ管理で。色々なライブラリをダウンロードするのは面倒なので。


ウォーミングアップ : Springを使ってテストでCamelが動く所まで

初期設定って面倒だけど、一度分かればほぼ使い回しなので。


まずはライブラリが無いと話にならない

今回はMavenプロジェクトでライブラリを集める。

webアプリ(war)を作る

1.Maven Projectで新規作成

2.packagingという入力枠でwarを指定。

詳しくは Eclipse+Maven という便利な開発環境をインストールからプロジェクト作成まで

pom.xml にライブラリを指定する。

今回は下記でいこう!


pom.xml

<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>hello</groupId>
<artifactId>HelloMySQL</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>

<properties>
<camel-ver>2.13.2</camel-ver>
<slf4j-ver>1.6.6</slf4j-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-spring</artifactId>
<version>${camel-ver}</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-test</artifactId>
<version>${camel-ver}</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-test-spring</artifactId>
<version>${camel-ver}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j-ver}</version>
</dependency>
</dependencies>

</project>


これでSpringのライブラリも入る。


SpringでCamelの初期設定


  • Camelを動かす設定

  • Camelのルートを自動的に見つけてくれる設定

  • コーディングでルートを登録処理しなくてよい --> @Component をクラスの先頭に付けるだけ。


src/main/resources/spring.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
"
>
<import resource="spring-camel.xml" />
</beans>



src/main/resources/spring-camel.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:camel="http://camel.apache.org/schema/spring"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd
"
>

<!-- ルートの自動登録(サブディレクトリも検索される) @Componentが対象 -->
<context:component-scan base-package="hello.route" />

<camel:camelContext id="HelloMySQL">
<camel:contextScan />
</camel:camelContext>

</beans>


これでも面倒だと思っちゃうけど、まぁ最初だけの辛抱。

CamelContextのidは重複すると動かなくなるので注意。


Camelを使って実装

3秒に1回のログ出力というサーバー動いてますよ的な実装。


src/main/java/hello/route/HelloRoute.java

package hello.route;

import org.apache.camel.builder.RouteBuilder;
import org.springframework.stereotype.Component;

@Component
public class HelloRoute extends RouteBuilder {

@Override
public void configure() throws Exception {
from("timer:start?period=3s")
.to("log:end");
}
}


@Componentを付ける事で、ルート登録してくれるようになる。

複数のルートを登録可能。

便利。便利だけど、@Component付け忘れてルートに登録されず動かないオチもある。


試験しよう

実装したら試験。

試験するためにわざわざJenkins先生まではしない。


src/test/java/TestRoute.java

package hello;

import org.apache.camel.test.spring.CamelSpringTestSupport;
import org.junit.Test;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestRoute extends CamelSpringTestSupport{

@Override
protected AbstractApplicationContext createApplicationContext() {
return new ClassPathXmlApplicationContext("spring.xml");
}

@Test
public void 3秒間1回ログを出力すること() throws Exception {
Thread.sleep(7000);
}

}


コードを書いたら、Eclipseから試験コードを右クリックしてRun as > JUnit Testを実行★

確認コードがないので残念ながら目視試験w

モック使った自動確認は別の機会に。

流れているデータを途中で捕まえて中身を確認する検問みたいな事もできる! --> 試験自動化へ!


Tomcat上で動くように設定

web.xmlからspring.xmlを読ませるようにするだけ。

内容は簡単だけど書き方が難しいので、完全にコピペコード。


src/main/webapp/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>

<web-app>

<!-- spring.xml -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/classes/spring.xml</param-value>
</context-param>

<!-- Start Spring -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- from("servlet:///") -->
<servlet>
<servlet-name>CamelServlet</servlet-name>
<display-name>Camel Http Transport Servlet</display-name>
<servlet-class>org.apache.camel.component.servlet.CamelHttpTransportServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>CamelServlet</servlet-name>
<url-pattern>/test/*</url-pattern>
</servlet-mapping>

</web-app>


pomにはtomcatとspringを連携させるためのライブラリを追加


pom.xml

    <properties>

前と同じ
<spring-ver>3.2.8.RELEASE</spring-ver>
</properties>
<dependencies>
前と同じ
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring-ver}</version>
</dependency>
</dependencies>


準備整いました

プロジェクトを右クリックして Run as > Maven Build..をクリック

なんか入力枠がたくさんあるけど入力すべき所は1箇所。

Goalsにpackageを入力。

すかさずRunボタンをクリック。

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.559 sec

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

OK。ついでに先ほど作った試験も動かしてくれちゃっており。

targetというフォルダがプロジェクト直下のディレクトリに作成されていて、

プロジェクト名-0.0.1-SNAPSHOT.warができている。

それをTomcatにデプロイするだけで、ただひたすらログ出力するアプリが動作!

デプロイ時に下記のようなログがでる

text:logs/catalina.out

INFO org.apache.camel.spring.SpringCamelContext - Total 1 routes, of which 1 is started.

デプロイ時にルートを起動せず、自分の好きなタイミングで起動する方法もあり。

ウォーミングアップ終わり


MySQLを使うよ!

ここからが本題!

DB使うと、いよいよ業務アプリ感がでる。


ライブラリの入手

pom.xmlに3つ追加

そして謎の<scope>provided</scope>の記述が。

これはビルド時や試験時にライブラリを使うけどwarに入れないという意味。

tomcatライブラリを入れないのは分かるとして、mysqlライブラリを入れない理由は、mysqlライブラリをwarに入れるとPermGen領域という難しい所がメモリリークするから。

そしてこの領域がメモリリークするというのは気づきにくい。気をつけて。

ということで、mysqlライブラリ(mysql-connector-java-x.x.x.jar)はtomcat/libに入れておいて。


pom.xml

<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>hello</groupId>
<artifactId>HelloMySQL</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>

<properties>
<camel-ver>2.13.2</camel-ver>
<slf4j-ver>1.6.6</slf4j-ver>

<!-- DB -->
<mysql-conn-ver>5.1.31</mysql-conn-ver>
<tomcat-ver>7.0.54</tomcat-ver>
</properties>

<dependencies>

<!-- 省略(前のpomと同じ) -->

<!-- DB -->
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-sql</artifactId>
<version>${camel-ver}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-conn-ver}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
<version>${tomcat-ver}</version>
<scope>provided</scope>
</dependency>

</dependencies>

</project>



DB接続設定するよ

DBって接続設定がめんどい。少なくてもURLなりパスワードなりが必要なのでしょうがないけど・・・


src/main/resources/spring.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
"
>
<import resource="spring-db.xml" />
<import resource="spring-camel.xml" />
</beans>


src/main/resources/spring-db.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ctx="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
>

<!-- TOMCAT JDBC Connection -->
<bean id="poolProperties" class="org.apache.tomcat.jdbc.pool.PoolProperties">
<property name="url" value="jdbc:mysql://localhost:3306/testdb" />
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="username" value="root" />
<property name="password" value="root" />
<property name="validationQuery" value="SELECT 1" />
<property name="maxActive" value="10" />
<property name="initialSize" value="5" />
</bean>
<bean id="ds_mysql" class="org.apache.tomcat.jdbc.pool.DataSource">
<property name="poolProperties" ref="poolProperties" />
</bean>
<!-- spring トランザクションマネージャ -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="ds_mysql"/>
</bean>
</beans>


さらりと書いたけど、コネクションプーリングしてくれる便利な代物。

まだまだ細かい設定したい人は下記のサイトに上記以外のプロパティまだまだあるよ

http://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html

tomcatの付属ライブラリだけど、tomcatにデプロイしなくても単体で使える便利なライブラリ。

続いて、設定したMySQLをデフォルトでCamelから使う設定


spring-camel.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:camel="http://camel.apache.org/schema/spring"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd
"
>

<!-- ルートの自動登録(サブディレクトリも検索される) @Componentが対象 -->
<context:component-scan base-package="hello.route" />

<!-- SQLコンポーネントのdataSourceデフォルト設定は先程のMySqlを使う -->
<bean id="sql" class="org.apache.camel.component.sql.SqlComponent">
<property name="dataSource" ref="ds_mysql" />
</bean>

<camel:camelContext id="HelloMySQL">
<camel:contextScan />
</camel:camelContext>

</beans>


準備終わり。

やっと終わった。

長い戦いだった。

しかもひたすら設定しかしていないような気がする。

DB使ったり、Tomcat使ったりしているのでしょうがないけど。

これからはどんどん実装するのみ。


実装

先ほどの3秒に1回ログ出力するという実装の中に、テーブルにレコードを1行追加を入れてみる。


src/main/java/hello/route/HelloRoute.java

package hello.route;

import org.apache.camel.builder.RouteBuilder;
import org.springframework.stereotype.Component;

@Component
public class HelloRoute extends RouteBuilder {

@Override
public void configure() throws Exception {
from("timer:start?period=3s")
.to("sql:INSERT INTO testtable (value1, value2) VALUES(1,2)")
.to("log:end?showAll=true&multiline=true");
}

}


普通にSQLを書くという簡単な仕事。設定が面倒だったけど、実装は楽。

そしてjunitを起動すると3秒に1レコード追加されている。


トランザクション使ってみる

今の実装だとINSERTした後にエラーが発生してもrollbackされない!!

transacted()を追加してあげるだけで、transacted文以降はエラー時にロールバックされる。


src/main/java/hello/route/HelloRoute.java

package hello.route;

import org.apache.camel.builder.RouteBuilder;
import org.springframework.stereotype.Component;

@Component
public class HelloRoute extends RouteBuilder {

@Override
public void configure() throws Exception {
from("timer:start?period=3")
.transacted()
.to("sql:INSERT INTO testtable (value1, value2) VALUES(1,2)")
.to("log:end?showAll=true&multiline=true");
}

}



プリペアドステートメント

myValueというヘッダの値をSQLに突っ込むサンプル。


src/main/java/hello/route/HelloRoute.java

package hello.route;

import org.apache.camel.builder.RouteBuilder;
import org.springframework.stereotype.Component;

@Component
public class HelloRoute extends RouteBuilder {

@Override
public void configure() throws Exception {
from("timer:start")
.transacted()
.setHeader("myValue").simple("2")
.to("sql:INSERT INTO testtable (value1, value2) VALUES(1, :#myValue)")
.to("log:end?showAll=true&multiline=true");
}

}


ちなみにCamel v.2.14からはもっと複雑なプリペアドステートメントの記法が使えるようになる。


DBテーブルの内容をさらすサービスを作成

やっとゴール

内容はユーザーidを送信するとアイテム一覧のjsonを返すサービス。

http://localhost:8080/HelloMySQL-0.0.1-SNAPSHOT/test/list?id=111


src/main/java/hello/route/HelloRoute.java

package hello.route;

import hello.processor.CheckUserRecord;

import org.apache.camel.Exchange;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.dataformat.JsonLibrary;
import org.springframework.stereotype.Component;

@Component
public class HelloRoute extends RouteBuilder {

@Override
public void configure() throws Exception {
// tomcatでhttpアクセスされた場合
from("servlet:///list")
.to("log:in")
.to("sql:SELECT * FROM myitems WHERE user_id = :#id")
.marshal().json(JsonLibrary.Gson)
// HTTPヘッダで余計な情報を返さない。
// 本当のフィルタ処理はgithubに載せたサンプルが良い
.removeHeaders("*")
.to("log:out");
}

}


これで完成。

removeHeadersというコードがあるが、HTTPで受信したデータをそのままHTTPの返信のヘッダに付けてしまうので、フィルタリングしている。

この実装方法だと他のルートを実装した時にフィルタ実装漏れが生じる可能性があるので、おすすめの実装をgithubにあげている。


ついでにエラー処理も書いた例


src/main/java/hello/route/HelloRoute.java

package hello.route;

import hello.processor.CheckUserRecord;

import org.apache.camel.Exchange;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.dataformat.JsonLibrary;
import org.springframework.stereotype.Component;

@Component
public class HelloRoute extends RouteBuilder {

@Override
public void configure() throws Exception {
// エラー処理
onException(Exception.class)
.handled(true) // こちらでうまい事処理したよフラグ
.to("log:error")
.removeHeaders("*")
.setHeader(Exchange.HTTP_RESPONSE_CODE).constant(500);

// tomcatでhttpアクセスされた場合
from("servlet:///list")
.to("log:in")
.to("sql:SELECT * FROM myitems WHERE user_id = :#id")
.marshal().json(JsonLibrary.Gson)
.removeHeaders("*")
.to("log:out");
}

}


実行中に何かエラーが発生すると500で返す。

デフォルトの例外ハンドラでも500を返すが、スタックトレースまでhttpで返すのでこっちがいいかな。