アプリを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に設定サンプル載せるので、説明は適宜読む程度でもよい。
設定も含めたソースコード
テスト用テーブル構造に関して
テーブル構造は一切書いてないけど、自由に作っていいです。
テーブル名が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 にライブラリを指定する。
今回は下記でいこう!
<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 をクラスの先頭に付けるだけ。
<?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>
<?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回のログ出力というサーバー動いてますよ
的な実装。
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先生まではしない。
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を読ませるようにするだけ。
内容は簡単だけど書き方が難しいので、完全にコピペコード。
<?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を連携させるためのライブラリを追加
<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にデプロイするだけで、ただひたすらログ出力するアプリが動作!
デプロイ時に下記のようなログがでる
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に入れておいて。
<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なりパスワードなりが必要なのでしょうがないけど・・・
<?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>
<?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から使う設定
<?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行追加
を入れてみる。
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文以降はエラー時にロールバックされる。
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に突っ込むサンプル。
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を返すサービス。
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にあげている。
ついでにエラー処理も書いた例
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で返すのでこっちがいいかな。