💻

Integration as a Script ― Apache CamelとJBangによるインテグレーションスクリプティング

30 min read

Javaは長らくプロジェクトの立ち上げから最初のコーディングまでが重く、スクリプト系のプログラマから批判されてきた。それも最近のJShellやJBangの登場で変わってきた。静的型付け言語としての構文の重たさを除けば、ほぼスクリプト言語の感覚でプログラミングできる。

ちょっとしたタスクをスクリプティングでやるにも、これまではBashやPythonのようなスクリプト言語が主流だったが、今ではJavaも選択肢の1つになる。Javaのメリットは、(比較的重たい処理での)実行時の速さと何より非常に豊富なライブラリのエコシステムだ。

エコシステムという点では、Javaで最もライブラリが成熟した分野がいくつもある。その1つがインテグレーション(システム間の連携)で、Apache Camelはその代表的なライブラリ/フレームワークになる。

Apache CamelとJBangを組み合わせると、インテグレーションという比較的複雑なタスクをスクリプティングで簡単に実現できる。一時期流行ったIFTTTのようなiPaaSを、オンラインでなく手元のスクリプトで実行できる、とイメージしてもらうといい。

前置きが長くなったが、この記事ではApache CamelとJBangによるインテグレーションスクリプティングの方法を紹介したい。

概要

  • Apache CamelとJBangの紹介
  • CamelとJBangでインテグレーションスクリプトを書く
  • 実践的なTIPSとよくあるトラブルの解決方法
  • 関連プロジェクト:Camel K, Kamelet, ICamel (Jupyterカーネル)

Apache CamelとJBang

Apache Camel

https://camel.apache.org/

Apache Camelについては既に色々なところで紹介記事があるので、特徴だけ列挙する。

世界中の企業でシステム間連携に使われているフレームワークで、一度フレームワークの使い方に慣れれば、DBやWebサービスを含む世の中のほとんどのシステムと接続できるようになる。

JBang

https://www.jbang.dev/

JBangはJavaコードをjbangというCLIから直接実行できるようにするツール。こんな形でJavaを直接実行できるし、jbang initで生成したスクリプト雛型を使えば二行目のように直接スクリプトとしても実行できる。

$ jbang script.java
$ ./script.java

依存ライブラリをどうやって使うかというと、このようにスクリプト冒頭にMavenのGAV形式(groupId:artifactId:version)で指定すると実行時にJARをダウンロードしてきてくれる。

script.java
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.apache.camel:camel-core:3.11.0
//DEPS org.apache.camel:camel-main:3.11.0
//DEPS org.apache.camel:camel-stream:3.11.0
//DEPS org.slf4j:slf4j-nop:1.7.31

Javaの他にJShellスクリプトもサポートしているので、そちらを使えばますますJava離れしたスクリプトを書ける。インテグレーションスクリプティングとしては、この記事ではJShellスクリプトの方をお勧めしていく。

さらにJBangはGraalVMベースのネイティブコンパイルにも対応しているので、単なる書き捨てのスクリプトだけでなく本格的な運用ツールやIoTの開発などにも応用できる。

JBangについては別記事でも紹介したので、こちらもどうぞ。

JBangのIDEサポート

スクリプティングなのでターミナルからVimなどでササッと編集して実行、みたいな使われ方が主に想定されるが、せっかくJavaで書いているんだからIDEのコード補完を使ってガシガシとコーディングしてみたくなるかもしれない。

JBangが単なる実行ツールでなく開発環境としてスゴいのは、そうしたユースケースにも対応していること。jbang edit <file>コマンドを使うと、JBangのキャッシュディレクトリに依存ライブラリの設定を含んだプロジェクト一式が展開され、お好みのIDEにインポートして普通に開発できるようになる。

試しにtreeでどんなプロジェクトが生成されるか見てみると、こんな感じ。

$ tree `jbang edit script.java`
/home/tasato/.jbang/cache/projects/script.java_jbang_ad73df3f7d9a1903191e637d42bcd3e33de98f04006cf998dd9ef6441094b571/script
├── bin
│   ├── script$1.class
│   └── script.class
├── build.gradle
├── README.md
└── src
    └── script.java -> /home/tasato/projects/samples/jbang/script.java

2 directories, 5 files

生成されたプロジェクトのソースsrc/script.javaは、元のスクリプトへのシンボリックリンクになっていて、IDEで編集した内容はそのまま元のスクリプトへ反映される。

例えばVS Codeでscript.javaを編集したい場合はこうする。

$ code `jbang edit script.java`

インテグレーションスクリプティング

さて、ここから本題。まずは、JBang上でCamelを動かす基本的な方法について。

Javaで書く

Javaで書くとクラス定義やmainメソッドを定義する必要があるので少し重たい。しかし、上記のIDEサポートが効くのはこちらなので、IDEを使いたいときは頑張ってこちらを使う。

なお、Javaでは本来クラス名は大文字始まりにするのが慣習だが、JBangでは敢えてスクリプトであることを強調するために小文字クラス名を使うことが多い。

https://github.com/tadayosi/jbang-sandbox/blob/main/camel/camel.java

camel.java
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.apache.camel:camel-bom:3.11.0@pom
//DEPS org.apache.camel:camel-core
//DEPS org.apache.camel:camel-main
//DEPS org.apache.camel:camel-stream
//DEPS org.slf4j:slf4j-nop:1.7.31

// Camel imports
import org.apache.camel.*;
import org.apache.camel.builder.*;
import org.apache.camel.main.*;
import org.apache.camel.spi.*;
import static org.apache.camel.builder.PredicateBuilder.*;

import static java.lang.System.*;

class camel {

    public static void main(String... args) throws Exception {
        out.println("Running Camel route...");

        Main main = new Main();
        main.configure().addRoutesBuilder(new RouteBuilder() {
            public void configure() throws Exception {
                from("timer:hello?period=3000")
                    .setBody().constant("Hello Camel!")
                    .to("stream:out");
            }
        });
        main.run();
    }
}

実行してみる。

$ jbang camel.java 
[jbang] Resolving dependencies...
[jbang] Loading MavenCoordinate [org.apache.camel:camel-bom:pom:3.11.0]
[jbang]     Resolving org.apache.camel:camel-core...Done
[jbang]     Resolving org.apache.camel:camel-main...Done
[jbang]     Resolving org.apache.camel:camel-stream...Done
[jbang]     Resolving org.slf4j:slf4j-nop:1.7.31...Done
[jbang] Dependencies resolved
[jbang] Building jar...
Running Camel route...
Hello Camel!
Hello Camel!
Hello Camel!

初回はこのように依存JARのダウンロードが走るが、二回目以降はスクリプトを修正しなければコンパイル結果がキャッシュされるので実行結果だけが表示される。

JShellで書く

JBangでは、ファイル拡張子を.jshにするとJShellで解釈されるスクリプトを書ける。この場合、クラスやmainメソッドの定義を省略でき、もっとスクリプトぽくなる。
(一行単位ならセミコロン;も省略できるが、メソッド内では省略できなかったりややこしいので自分は敢えて省かない。)

https://github.com/tadayosi/jbang-sandbox/blob/main/camel/camel.jsh

camel.jsh
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.apache.camel:camel-bom:3.11.0@pom
//DEPS org.apache.camel:camel-core
//DEPS org.apache.camel:camel-main
//DEPS org.apache.camel:camel-stream
//DEPS org.slf4j:slf4j-nop:1.7.31

// Camel imports
import org.apache.camel.*;
import org.apache.camel.builder.*;
import org.apache.camel.main.*;
import org.apache.camel.spi.*;
import static org.apache.camel.builder.PredicateBuilder.*;

import static java.lang.System.*;

out.println("Running Camel route...");

Main main = new Main();
main.configure().addRoutesBuilder(new RouteBuilder() {
    public void configure() throws Exception {
        from("timer:hello?period=3000")
            .setBody().constant("Hello Camel!")
            .to("stream:out");
    }
});
main.run();

今のところ、これ以上の省略化はできない。最低限、RouteBuilder#configure()メソッド内でCamelルートを定義するのと、Mainオブジェクトのrun()メソッドを呼び出すところは必須。

実行してみる(こちらは2回目以降の実行結果)。

$ jbang camel.jsh
Running Camel route...
Hello Camel!
Hello Camel!
Hello Camel!

インテグレーションスクリプティングの例

実際にApache CamelとJBangを使ってどんなことが出来るのか紹介したい。

  • Twitterのログをファイルに保存する
  • 外部Webサーバから取得したJSONを加工して表示する
  • 簡易RESTサーバを立ち上げてリクエストを処理する
  • Kameletを使う

Twitterのログをファイルに保存する

とりあえずインテグレーションで何かするときの定番はTwitterなので、Twitterからツィートを検索してそれをファイルに書き出してみる。

やっていることは以下の通り。

  • Twitterでハッシュタグ #ApacheCamel のついたツィートを最新5件(デフォルト値)取得する
  • そのツィートを1件づつ out/twitter.log ファイルに追記する

https://github.com/tadayosi/jbang-sandbox/blob/main/camel/twitter-to-file.jsh

twitter-to-file.jsh
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.apache.camel:camel-bom:3.11.0@pom
//DEPS org.apache.camel:camel-core
//DEPS org.apache.camel:camel-main
//DEPS org.apache.camel:camel-stream
//DEPS org.apache.camel:camel-twitter
//DEPS org.slf4j:slf4j-nop:1.7.31

// Camel imports
import org.apache.camel.*;
import org.apache.camel.builder.*;
import org.apache.camel.main.*;
import org.apache.camel.spi.*;
import static org.apache.camel.builder.PredicateBuilder.*;

import static java.lang.System.*;

setProperty("camel.main.durationMaxMessages", "1");

var consumerKey = getenv("TWITTER_CONSUMER_KEY");
var consumerSecret = getenv("TWITTER_CONSUMER_SECRET");
var accessToken = getenv("TWITTER_ACCESS_TOKEN");
var accessTokenSecret = getenv("TWITTER_ACCESS_TOKEN_SECRET");

var keyword = "#ApacheCamel";

Main main = new Main();
main.configure().addRoutesBuilder(new RouteBuilder() {
    public void configure() throws Exception {
        from("timer:twitter-to-file?repeatCount=1")
            .toF("twitter-search:%s?consumerKey=%s&consumerSecret=%s&accessToken=%s&accessTokenSecret=%s",
                keyword,
                consumerKey, consumerSecret, accessToken, accessTokenSecret)
            .split(body())
                .transform().simple("${body.text}")
                .to("stream:out")
                .setHeader(Exchange.FILE_NAME, constant("twitter.log"))
                .to("file:out?fileExist=Append&appendChars=\\n");
    }
});
main.run();

ポイントはTwitterへ接続するための認証情報を環境変数から取ってきているところ。インテグレーションではDBやWebサービスへの接続にコネクションや認証の情報が必須になるので、直書きするよりこのように環境変数に外だししておくとセキュリティ的にも良いしスクリプトの再利用性も高まる。

環境変数の管理にはdirenvを使うといい。

実行結果はこんな感じになる。

$ jbang twitter-to-file.jsh
...
out/twitter.log
RT @tadayosi: Want to run Kamelets with JBang?  Yes, you can!
https://t.co/J362Ib9qYn
#ApacheCamel #JBang https://t.co/06xxbADgSx
RT @tadayosi: Want to run Kamelets with JBang?  Yes, you can!
https://t.co/J362Ib9qYn
#ApacheCamel #JBang https://t.co/06xxbADgSx
Want to run Kamelets with JBang?  Yes, you can!
https://t.co/J362Ib9qYn
#ApacheCamel #JBang https://t.co/06xxbADgSx
Rayvens: a new project powered by Camel K https://t.co/3iAoOvKAGQ #ApacheCamel
Caused by: org.apache.camel.NoTypeConversionAvailableException: No type converter available to convert from type: java.lang.String to the https://t.co/MhhUa7SoAv 
#apachecamel #Camel #java @ApacheCamel

これだけだとなんてことはないが、Camelでは他にも様々なSNSと接続できるので、FacebookやLinkedInから情報を取得してみたり、取得した情報をさらに後工程でデータ処理する、みたいな使い方に発展できる。

外部Webサーバから取得したJSONを加工して表示する

今どき、外部WebサーバがJSONを返すのは一般的だ。そこから何か有意義な情報を抜き出して使いたい、ということはよくある。

デモとしてお天気サイトwttr.inから今日の天気をJSONで取得して、それを絵文字に変えて表示してみよう。

やっていることは以下の通り。

https://github.com/tadayosi/jbang-sandbox/blob/main/camel/json-to-sysout.jsh

json-to-sysout.jsh
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.apache.camel:camel-bom:3.11.0@pom
//DEPS org.apache.camel:camel-core
//DEPS org.apache.camel:camel-main
//DEPS org.apache.camel:camel-stream
//DEPS org.apache.camel:camel-http
//DEPS org.apache.camel:camel-jsonpath
//DEPS org.slf4j:slf4j-nop:1.7.31

// Camel imports
import org.apache.camel.*;
import org.apache.camel.builder.*;
import org.apache.camel.main.*;
import org.apache.camel.spi.*;
import static org.apache.camel.builder.PredicateBuilder.*;

import static java.lang.System.*;

setProperty("camel.main.durationMaxMessages", "1");

var location = "Tokyo";
var message = "Today's weather in " + location + ": ";

Main main = new Main();
main.configure().addRoutesBuilder(new RouteBuilder() {
    public void configure() throws Exception {
        from("timer:json-to-file?repeatCount=1")
            .toF("http://wttr.in/%s?format=j1", location)
            .transform().jsonpath("$.current_condition[0].weatherDesc[0].value")
            .choice()
                .when(or(body().contains("Sunny"), body().contains("Clear")))
                    .transform().constant(message + "☀")
                    .endChoice()
                .when(or(body().contains("Cloudy"), body().contains("cloudy"), body().contains("Overcast")))
                    .transform().constant(message + "☁")
                    .endChoice()
                .when(body().contains("rain"))
                    .transform().constant(message + "☂")
                    .endChoice()
                .otherwise()
                    .transform().simple(message + "${body}")
            .end()
            .to("stream:out");
    }
});
main.run();

今回のポイントは、EIPのCBRを使っている点。この簡単なデモだとオーバーキル気味だが、Camelをスクリプティングに使えるということは、インテグレーションのデザインパターンであるEIPをフル活用できるということ。

https://camel.apache.org/components/latest/eips/enterprise-integration-patterns.html

ちなみに実行結果(執筆時は曇りだった)。

$ jbang json-to-sysout.jsh 
Today's weather in Tokyo: ☁

簡易RESTサーバを立ち上げてリクエストを処理する

アプリの開発や検証時に、ちょっと簡易的にRESTサーバを立ち上げて使いたいな、というときがある。今までだったらJavaScriptやRubyを使って……となっていたが、これも当然Camel + JBangで簡単にできる。

やっていることは以下の通り。

  • 8080ポートでWebサーバを立ち上げる(サーバはUndertow)
  • RESTのGETエンドポイント/hello/{name}/bye/{name}を定義する

https://github.com/tadayosi/jbang-sandbox/blob/main/camel/simple-rest.jsh

simple-rest.jsh
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.apache.camel:camel-bom:3.11.0@pom
//DEPS org.apache.camel:camel-core
//DEPS org.apache.camel:camel-main
//DEPS org.apache.camel:camel-stream
//DEPS org.apache.camel:camel-rest
//DEPS org.apache.camel:camel-undertow
//DEPS org.slf4j:slf4j-simple:1.7.31

// Camel imports
import org.apache.camel.*;
import org.apache.camel.builder.*;
import org.apache.camel.main.*;
import org.apache.camel.spi.*;
import static org.apache.camel.builder.PredicateBuilder.*;

import static java.lang.System.*;

// SLF4J
setProperty("org.slf4j.simpleLogger.logFile", "System.out");
// Undertow logging
setProperty("org.jboss.logging.provider", "slf4j");

Main main = new Main();
main.configure().addRoutesBuilder(new RouteBuilder() {
    public void configure() throws Exception {
        restConfiguration().port(8080);

        rest()
            .get("/hello/{name}")
                .to("direct:hello")
            .get("/bye/{name}")
                .to("direct:bye");

        from("direct:hello")
            .transform().simple("Hello ${header.name}!");
        from("direct:bye")
            .transform().simple("Bye ${header.name}!");
    }
});
main.run();

今まではログ出力は意図的にオフにしていたが、今回はサーバなので敢えてオンにしている。JBangでのログ出力に関するTIPSは後のセクションで解説する。

なお簡易なRESTサーバと言っているが、CamelのREST DSLはREST APIの実装に必要な機能を完全に備えているので、ここから本格的なREST APIサービスを作っていくことも可能。

https://camel.apache.org/manual/latest/rest-dsl.html

実行結果はこの通り。

$ jbang simple-rest.jsh 
[main] INFO org.apache.camel.component.undertow.DefaultUndertowHost - Starting Undertow server on http://0.0.0.0:8080
[main] INFO io.undertow - starting server: Undertow - 2.2.8.Final
[main] INFO org.xnio - XNIO version 3.8.0.Final
[main] INFO org.xnio.nio - XNIO NIO Implementation Version 3.8.0.Final
[main] INFO org.jboss.threads - JBoss Threads version 3.1.0.Final
[main] INFO org.apache.camel.impl.engine.AbstractCamelContext - Routes startup summary (total:4 started:4)
[main] INFO org.apache.camel.impl.engine.AbstractCamelContext -     Started route1 (direct://hello)
[main] INFO org.apache.camel.impl.engine.AbstractCamelContext -     Started route2 (direct://bye)
[main] INFO org.apache.camel.impl.engine.AbstractCamelContext -     Started route3 (rest://get:/hello/%7Bname%7D)
[main] INFO org.apache.camel.impl.engine.AbstractCamelContext -     Started route4 (rest://get:/bye/%7Bname%7D)
[main] INFO org.apache.camel.impl.engine.AbstractCamelContext - Apache Camel 3.11.0 (camel-1) started in 474ms (build:31ms init:122ms start:321ms)
$ curl localhost:8080/hello/Camel
Hello Camel!
$ curl localhost:8080/bye/Camel
Bye Camel!

Kameletを使う

Kameletは最新のCamel 3.11 LTSからCamel本体で使えるようになった新機能で、汎用的に作られた個々の接続コンポーネントと比較して、より特定の用途に特化してその分設定パラメータを絞って使いやすくした再利用可能なCamelコードのスニペット。

以下のようなカタログが作られていて、その中から使いたいものを選んで、複数のコンポーネントが絡んだ特定のインテグレーションを簡単に自分のルートに取り込める。カタログは今後どんどん拡充されていく予定。

https://camel.apache.org/camel-kamelets/latest/

ここではサンプルとしてEarthquake Source Kameletを使ってみよう。このKameletはUSGSのAPIから世界中の地震情報をJSONで取得してくれるもの。

https://camel.apache.org/camel-kamelets/latest/earthquake-source.html

やっていることは以下の通り。

  • earthquake-source Kameletから地震情報を受け取る
  • 地震情報からマグニチュードと震源地を抜き出して標準出力する

https://github.com/tadayosi/jbang-sandbox/blob/main/camel/kamelet-earthquake.jsh

kamelet-earthquake.jsh
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.apache.camel:camel-bom:3.11.0@pom
//DEPS org.apache.camel:camel-core
//DEPS org.apache.camel:camel-main
//DEPS org.apache.camel:camel-kamelet-main
//DEPS org.apache.camel:camel-stream
//DEPS org.apache.camel:camel-caffeine
//DEPS org.apache.camel:camel-http
//DEPS org.apache.camel:camel-jackson
//DEPS org.apache.camel:camel-jsonpath
//DEPS org.slf4j:slf4j-nop:1.7.31

// Camel imports
import org.apache.camel.*;
import org.apache.camel.builder.*;
import org.apache.camel.main.*;
import org.apache.camel.spi.*;
import static org.apache.camel.builder.PredicateBuilder.*;

import static java.lang.System.*;

setProperty("camel.main.durationMaxMessages", "1");

Main main = new Main();
main.addInitialProperty("camel.component.kamelet.location", "github:apache:camel-kamelets");
main.addInitialProperty("camel.main.lightweight", "true");

main.configure().addRoutesBuilder(new RouteBuilder() {
    public void configure() throws Exception {
        from("kamelet:earthquake-source")
            .unmarshal().json()
            .transform().simple("Earthquake with magnitude ${body[properties][mag]} at ${body[properties][place]}")
            .to("stream:out");
    }
});
main.run();

実行結果は次の通り。

$ jbang kamelet-earthquake.jsh 
Earthquake with magnitude 2.8 at 52 km NW of Toyah, Texas
Earthquake with magnitude 3.15 at 26 km E of Honaunau-Napoopoo, Hawaii
Earthquake with magnitude 2.8 at 53 km NW of Toyah, Texas
Earthquake with magnitude 4.8 at 
Earthquake with magnitude 4.3 at Fiji region
Earthquake with magnitude 4.5 at 12 km W of Kakunodatemachi, Japan
Earthquake with magnitude 4 at 157 km NE of Lospalos, Timor Leste
Earthquake with magnitude 1.21 at 4 km SE of Meno, Oklahoma
...

インテグレーションスクリプティングにとって、Kameletとの親和性が高いのが分かってもらえるのではないか。つまり、Kameletは高次元のインテグレーションタスクを再利用可能にするものなので、もしちょうどやりたいタスクにはまるKameletが見つかれば複雑な処理でも簡単に実現できてしまう。

ただし、現時点ではCamel + JBangにKameletを使う上で問題もある。高次元のタスクをパッケージングしている代わりに、内部でどのCamelコンポーネントを依存として使用しているかがすぐに分からない。したがって、実際にスクリプトにKameletを取り入れるには何度もエラーを確認しながら1つずつ依存を足していかなくてはならない。この辺はいずれ解決していきたい。

実践的TIPS

ここまででインテグレーションスクリプティングで何ができそうかイメージしてもらえたと思うので、実際にスクリプトを書く上で役立ちそうなTIPSをまとめたい。

  • ログの表示/非表示
  • BOMを使ってバージョン指定を省略する
  • Camelのインポートをもっと簡潔にする
  • JShellで標準出力をもっと簡潔にする
  • ルートを一度だけ実行したい
  • 外部システムへの接続情報の設定方法
  • JMXでCamelルートをモニタリングする
  • スクリプトをKubernetes上で実行したい
  • Kotlinで書きたい

ログの表示/非表示

おそらくJBangを使おうとして最初にハマるのはここだと思う。JavaのライブラリはとにかくSLF4Jのようなロギングフレームワークと密結合しているので、ロギングの設定を何もしないで使うと汚いログがぼろぼろと表示される。

スクリプティングでは基本、デバッグ以外ではログは必要ないので、完全にログを切ってしまう。今までのサンプルコードで既にやっていたように、CamelのようにSLF4Jに依存したライブラリであれば、org.slf4j:slf4j-nopでログをオフにできる。

//DEPS org.slf4j:slf4j-nop:1.7.31

逆に、デバッグ時など敢えてログを出力したいときは、ファイルに書き出すのではなく直接標準出力に書き出してしまえばいい。org.slf4j:slf4j-simpleを使った上で、システムプロパティ"org.slf4j.simpleLogger.logFile""System.out"(または"System.err")に指定する。

//DEPS org.slf4j:slf4j-simple:1.7.31

System.setProperty("org.slf4j.simpleLogger.logFile", "System.out");

ログレベルの細かなチューニングも、システムプロパティ経由で設定できる。

BOMを使ってバージョン指定を省略する

既にサンプルコードで使用していたが、JBangの依存定義でもBOMを利用できる。BOMを利用することで、一度のバージョン指定で同じグループに属するライブラリ(厳密にはBOM内で定義されたライブラリ)のバージョンを統一できる。

//DEPS org.apache.camel:camel-bom:3.11.0@pom
//DEPS org.apache.camel:camel-core
//DEPS org.apache.camel:camel-main
//DEPS org.apache.camel:camel-stream

ただし、BOM定義は//DEPSの一番最初に書く必要がある。また、2つ以上のBOMは指定できないので、複数BOMを使いたくてもどちらか一方を妥協してバージョンを個別に直書きしなければいけない。

Camelのインポートをもっと簡潔にする

JShell(.jsh)で書いている場合、スクリプト内でJShellコマンドまで利用できる。とくに/openコマンドを使えば、別ファイルに定義したスニペットを読み込むことも可能。

これを利用して、camel-importsファイルによく使われるインポート定義だけをしておき、スクリプトから/open camel-importsとすることでインポート定義を省略できる。

camel-imports
import org.apache.camel.*;
import org.apache.camel.builder.*;
import org.apache.camel.main.*;
import org.apache.camel.spi.*;

import static org.apache.camel.builder.PredicateBuilder.*;
camel2.jsh
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.apache.camel:camel-bom:3.11.0@pom
//DEPS org.apache.camel:camel-core
//DEPS org.apache.camel:camel-main
//DEPS org.apache.camel:camel-stream
//DEPS org.slf4j:slf4j-nop:1.7.31

/open camel-imports

Main main = new Main();
main.configure().addRoutesBuilder(new RouteBuilder() {
    public void configure() throws Exception {
        from("timer:hello?period=3000")
            .setBody().constant("Hello Camel!")
            .to("stream:out");
    }
});
main.run();

JShellで標準出力をもっと簡潔にする

JShellには定義済スクリプトというのがあって、インポート宣言が省略できるスニペットが用意されている。

https://docs.oracle.com/javase/jp/9/jshell/scripts.htm#GUID-C3A41878-9A9A-4D31-BBDF-909729848A3E

スクリプト名 スクリプトの内容
DEFAULT 共通で必要なインポート宣言が含まれています。このスクリプトは、他の起動スクリプトが提供されない場合に使用します。
PRINTING JShellメソッドを定義し、PrintStreamのprint、printlnおよびprintfメソッドにリダイレクトされます。
JAVASE java.seモジュールで定義されるコアJava SE APIをインポートします。パッケージ数によっては、JShellの起動が大幅に遅延します。

PRINTINGスクリプトを使えば、標準出力だけなら極限まで簡略化できる。

printing.jsh
///usr/bin/env jbang "$0" "$@" ; exit $?

/open PRINTING

println("Hello!");

ルートを一度だけ実行したい

Camelは本来サーバ上で持続的に稼働するルーティングエンジンとして作られているので、スクリプティングにおけるバッチ的な使い方は元々考慮されていない。そのままだとスクリプトを実行するとCamelルートはずっと稼働を続け、メッセージを受け取る限り処理を続ける。

しかし、camel.main.durationMaxMessagesというパラメータがあり、Camelが何回メッセージを処理したらシャットダウンするかを設定できる。システムプロパティでこのパラメータを1に設定すれば、1回のルーティングでCamelをシャットダウンできる。

System.setProperty("camel.main.durationMaxMessages", "1");

なお、ルートをバッチ的に起動させるにはサンプルでも使っているようにcamel-timerコンポーネントを使うのが最も簡単だが、これにもrepeatCountというパラメータがある。repeatCount=1にすればルートは一度しか実行されなくなるが、この場合はCamel自体はシャットダウンしない。

        from("timer:fire-once?repeatCount=1")
            .setBody().constant("Hello Camel!")
            .to("stream:out");

Camelを確実にシャットダウンしスクリプトを終了させたいなら、camel.main.durationMaxMessagesシステムプロパティを使う必要がある。

外部システムへの接続情報の設定方法

インテグレーションの中心は外部システム・サービスとの接続で、接続には通常、接続先URLやアカウント情報など、各種の設定が必須になる。インテグレーションスクリプティングで一番面倒くさいが避けて通れないのがここ。

Camelでは接続情報はエンドポイントURI毎に個別に設定するか、コンポーネントにグローバルに設定するかの2通り。コンポーネントによってはエンドポイント毎の設定ができないこともある。

エンドポイント

Twitterのログをファイルに保存するでやっているのは、エンドポイントに直接設定する方法。その際、from(...)to(...)の代わりにfromF(...)toF(...)を使うとURIをテンプレートで書けて接続情報をパラメータ化できるのでお勧め。

twitter-to-file.jsh
var consumerKey = getenv("TWITTER_CONSUMER_KEY");
var consumerSecret = getenv("TWITTER_CONSUMER_SECRET");
var accessToken = getenv("TWITTER_ACCESS_TOKEN");
var accessTokenSecret = getenv("TWITTER_ACCESS_TOKEN_SECRET");

...

from("timer:twitter-to-file?repeatCount=1")
    .toF("twitter-search:%s?consumerKey=%s&consumerSecret=%s&accessToken=%s&accessTokenSecret=%s",
        keyword,
        consumerKey, consumerSecret, accessToken, accessTokenSecret)

コンポーネント

コンポーネントにグローバルに設定したい場合、通常camel-mainではapplication.propertiesにコンポーネント毎のパラメータを設定できるが、スクリプティングでは少し面倒なのでMain#addProperty(String, String)メソッドを使ってMainランタイムに直接プロパティを設定する。

https://github.com/tadayosi/jbang-sandbox/blob/main/camel/twitter-to-file2.jsh
twitter-to-file2.jsh
var consumerKey = getenv("TWITTER_CONSUMER_KEY");
var consumerSecret = getenv("TWITTER_CONSUMER_SECRET");
var accessToken = getenv("TWITTER_ACCESS_TOKEN");
var accessTokenSecret = getenv("TWITTER_ACCESS_TOKEN_SECRET");

var keyword = "#ApacheCamel";

Main main = new Main();
main.addProperty("camel.component.twitter-search.consumer-key", consumerKey);
main.addProperty("camel.component.twitter-search.consumer-secret", consumerSecret);
main.addProperty("camel.component.twitter-search.access-token", accessToken);
main.addProperty("camel.component.twitter-search.access-token-secret", accessTokenSecret);
main.configure().addRoutesBuilder(new RouteBuilder() {
    public void configure() throws Exception {
        from("timer:twitter-to-file?repeatCount=1")
            .toF("twitter-search:%s", keyword)
            .split(body())
                .transform().simple("${body.text}")
                .to("stream:out")
                .setHeader(Exchange.FILE_NAME, constant("twitter.log"))
                .to("file:out?fileExist=Append&appendChars=\\n");
    }
});
main.run();

コンポーネントに設定すると、ルート中のエンドポイントURIがすっきりする。また同じコンポーネントで複数回エンドポイントを使いたいときもコンポーネントに設定した方がよい。

認証情報の定義の仕方

Twitterのログをファイルに保存するでも触れたが、認証情報はスクリプトに直書きするより環境変数で外から与えた方がよい。セキュリティ的にも良いし、もしDockerなどでコンテナ化したときにも環境変数の方が親和性が高い。

direnvを使うと、特定ディレクトリにcdしたときに自動的に環境変数を有効化できる。

https://github.com/direnv/direnv
スクリプトのあるディレクトリにこんな感じに.envrcを定義しておく。
.envrc
export TWITTER_CONSUMER_KEY=<consumer-key>
export TWITTER_CONSUMER_SECRET=<consumer-secret>
export TWITTER_ACCESS_TOKEN=<access-token>
export TWITTER_ACCESS_TOKEN_SECRET=<access-token-secret>

JMXでCamelルートをモニタリングする

JBangでは、jbang --javaagent=<agent>[=<options>]Javaエージェントもアタッチできる。この機能を使ってJolokia JVM Agentをアタッチすることで、CamelルートをJMXモニタリングできる。

jbang --javaagent=<agent>.javaファイルにしか使えない。

$ jbang --javaagent=org.jolokia:jolokia-jvm:1.6.2:agent script.java 

なおCamelでJMXを有効にするにはcamel-managementを追加しないといけない点に注意。

script.java
//DEPS org.apache.camel:camel-management

実際にCamelルートをモニタリングしてみる。

https://github.com/tadayosi/jbang-sandbox/blob/main/camel/camel-jmx.java

$ jbang --javaagent=org.jolokia:jolokia-jvm:1.6.2:agent camel-jmx.java 
Running Camel route...
I> No access restrictor found, access to any MBean is allowed
Jolokia: Agent started with URL http://127.0.0.1:8778/jolokia/
Hello Camel!
Hello Camel!
Hello Camel!

http://127.0.0.1:8778/jolokia/ にJolokiaエンドポイントが立ち上がる。

CamelのJMXモニタリングに定番のHawtioを使ってみる。

$ curl -LO https://repo1.maven.org/maven2/io/hawt/hawtio-app/2.13.5/hawtio-app-2.13.5.jar
$ java -jar hawtio-app-2.13.5.jar

立ち上がったHawtioコンソール http://localhost:8080/hawtio/ からJolokiaエンドポイントに接続すると、このようにCamelコンテキストをJMX経由で見ることができる。

Hawtio

スクリプトをKubernetes上で実行したい

最初にもちらっと紹介したが、別記事に詳しく書いてあるのでそちらを参照。

Kotlinで書きたい

一応、JBangはKotlinにも対応しているので試してみるといいかも。

関連プロジェクト

最後に、インテグレーションスクリプティングの観点で関連するプロジェクトについて紹介する。

Camel K

https://camel.apache.org/camel-k/latest/

CamelをクラウドネイティブにするためのOperatorを開発するプロジェクト。Camel KのKはKubernetes/KnativeのK。

一応、Camel + JBangスクリプトをKubernetes上で走らせる方法も書いたが、Kubernetes上でインテグレーションをやりたいのであれば最初からCamel Kを使う方がいい。Camel Kは最初から複数のスクリプト言語に対応している。

Kamelet

https://camel.apache.org/camel-kamelets/latest/

特定用途に特化して設定パラメータを絞って使いやすくした再利用可能なCamelコードスニペットのカタログ。インテグレーションをより簡単にできるが、Camel + JBangで使うにはまだ課題もある。

ICamel

https://github.com/tadayosi/icamel

Jupyter NotebookでCamelを実行しようというPoCプロジェクト。まだcamel-core以外のコンポーネントを動的にカーネルにダウンロードしてくる方法が実現できていないので、実用レベルまで持って行けていない。

Discussion

ログインするとコメントできます