Movatterモバイル変換


[0]ホーム

URL:


佐藤 匡剛佐藤 匡剛
💻

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

に公開

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.java2 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 importsimportorg.apache.camel.*;importorg.apache.camel.builder.*;importorg.apache.camel.main.*;importorg.apache.camel.spi.*;importstaticorg.apache.camel.builder.PredicateBuilder.*;importstaticjava.lang.System.*;class camel{publicstaticvoidmain(String... args)throwsException{        out.println("Running Camel route...");Main main=newMain();        main.configure().addRoutesBuilder(newRouteBuilder(){publicvoidconfigure()throwsException{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 importsimportorg.apache.camel.*;importorg.apache.camel.builder.*;importorg.apache.camel.main.*;importorg.apache.camel.spi.*;importstaticorg.apache.camel.builder.PredicateBuilder.*;importstaticjava.lang.System.*;out.println("Running Camel route...");Main main=newMain();main.configure().addRoutesBuilder(newRouteBuilder(){publicvoidconfigure()throwsException{from("timer:hello?period=3000").setBody().constant("Hello Camel!").to("stream:out");}});main.run();

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

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

$ jbang camel.jshRunning 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 importsimportorg.apache.camel.*;importorg.apache.camel.builder.*;importorg.apache.camel.main.*;importorg.apache.camel.spi.*;importstaticorg.apache.camel.builder.PredicateBuilder.*;importstaticjava.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=newMain();main.configure().addRoutesBuilder(newRouteBuilder(){publicvoidconfigure()throwsException{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/06xxbADgSxRT @tadayosi: Want to run Kamelets with JBang?  Yes, you can!https://t.co/J362Ib9qYn#ApacheCamel #JBang https://t.co/06xxbADgSxWant to run Kamelets with JBang?  Yes, you can!https://t.co/J362Ib9qYn#ApacheCamel #JBang https://t.co/06xxbADgSxRayvens: a new project powered by Camel K https://t.co/3iAoOvKAGQ #ApacheCamelCaused 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 importsimportorg.apache.camel.*;importorg.apache.camel.builder.*;importorg.apache.camel.main.*;importorg.apache.camel.spi.*;importstaticorg.apache.camel.builder.PredicateBuilder.*;importstaticjava.lang.System.*;setProperty("camel.main.durationMaxMessages","1");var location="Tokyo";var message="Today's weather in "+ location+": ";Main main=newMain();main.configure().addRoutesBuilder(newRouteBuilder(){publicvoidconfigure()throwsException{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 importsimportorg.apache.camel.*;importorg.apache.camel.builder.*;importorg.apache.camel.main.*;importorg.apache.camel.spi.*;importstaticorg.apache.camel.builder.PredicateBuilder.*;importstaticjava.lang.System.*;// SLF4JsetProperty("org.slf4j.simpleLogger.logFile","System.out");// Undertow loggingsetProperty("org.jboss.logging.provider","slf4j");Main main=newMain();main.configure().addRoutesBuilder(newRouteBuilder(){publicvoidconfigure()throwsException{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/CamelHello Camel!$ curl localhost:8080/bye/CamelBye 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 importsimportorg.apache.camel.*;importorg.apache.camel.builder.*;importorg.apache.camel.main.*;importorg.apache.camel.spi.*;importstaticorg.apache.camel.builder.PredicateBuilder.*;importstaticjava.lang.System.*;setProperty("camel.main.durationMaxMessages","1");Main main=newMain();main.addInitialProperty("camel.component.kamelet.location","github:apache:camel-kamelets");main.addInitialProperty("camel.main.lightweight","true");main.configure().addRoutesBuilder(newRouteBuilder(){publicvoidconfigure()throwsException{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, TexasEarthquake with magnitude 3.15 at 26 km E of Honaunau-Napoopoo, HawaiiEarthquake with magnitude 2.8 at 53 km NW of Toyah, TexasEarthquake with magnitude 4.8 at Earthquake with magnitude 4.3 at Fiji regionEarthquake with magnitude 4.5 at 12 km W of Kakunodatemachi, JapanEarthquake with magnitude 4 at 157 km NE of Lospalos, Timor LesteEarthquake 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.31System.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
importorg.apache.camel.*;importorg.apache.camel.builder.*;importorg.apache.camel.main.*;importorg.apache.camel.spi.*;importstaticorg.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/opencamel-importsMain main=newMain();main.configure().addRoutesBuilder(newRouteBuilder(){publicvoidconfigure()throwsException{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共通で必要なインポート宣言が含まれています。このスクリプトは、他の起動スクリプトが提供されない場合に使用します。
PRINTINGJShellメソッドを定義し、PrintStreamのprint、printlnおよびprintfメソッドにリダイレクトされます。
JAVASEjava.seモジュールで定義されるコアJava SE APIをインポートします。パッケージ数によっては、JShellの起動が大幅に遅延します。

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

printing.jsh
///usr/bin/env jbang "$0" "$@" ; exit $?/openPRINTINGprintln("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=newMain();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(newRouteBuilder(){publicvoidconfigure()throwsException{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 allowedJolokia: 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以外のコンポーネントを動的にカーネルにダウンロードしてくる方法が実現できていないので、実用レベルまで持って行けていない。

佐藤 匡剛

オープンソースソフトウェアエンジニア / Apache Camel, Hawtioコミッター / IBM Japan

バッジを贈って著者を応援しよう

バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。

オープンソースソフトウェアエンジニア / Apache Camel, Hawtioコミッター / IBM Japan

目次
  1. 概要
  2. Apache CamelとJBang
    1. Apache Camel
    2. JBang
    3. JBangのIDEサポート
  3. インテグレーションスクリプティング
    1. Javaで書く
    2. JShellで書く
  4. インテグレーションスクリプティングの例
    1. Twitterのログをファイルに保存する
    2. 外部Webサーバから取得したJSONを加工して表示する
    3. 簡易RESTサーバを立ち上げてリクエストを処理する
    4. Kameletを使う
  5. 実践的TIPS
    1. ログの表示/非表示
    2. BOMを使ってバージョン指定を省略する
    3. Camelのインポートをもっと簡潔にする
    4. JShellで標準出力をもっと簡潔にする
    5. ルートを一度だけ実行したい
    6. 外部システムへの接続情報の設定方法
    7. JMXでCamelルートをモニタリングする
    8. スクリプトをKubernetes上で実行したい
    9. Kotlinで書きたい
  6. 関連プロジェクト
    1. Camel K
    2. Kamelet
    3. ICamel

[8]ページ先頭

©2009-2025 Movatter.jp