Apache Camel - APIコンポーネントフレームワークを使ってみる

f:id:tasato-redhat:20190314152401p:plain

Red Hatの佐藤匡剛です。

APIコンポーネントフレームワーク紹介記事の第2回目です。前回はこのフレームワークが何なのかを説明したので、今回は実際にその使い方を説明します。

プロジェクトの生成

APIコンポーネントフレームワークを使ってカスタムコンポーネントを開発するには、まずMavenアーキタイプcamel-archetype-api-componentでプロジェクトの雛形を生成します。

$ mvn archetype:generate \
    -DarchetypeGroupId=org.apache.camel.archetypes \
    -DarchetypeArtifactId=camel-archetype-api-component \
    -DarchetypeVersion=2.23.1 \
    -DgroupId=com.redhat.samples \
    -DartifactId=camel-hello \
    -Dname=Hello \
    -Dscheme=hello \
    -Dversion=1.0-SNAPSHOT \
    -DinteractiveMode=false

以下のようなMavenマルチプロジェクトが生成されます。nameパラメータの文字列がxxxxxComponentxxxxxConfigurationxxxxxConsumerxxxxxEndpointxxxxxProducerといったクラス名のプレフィックスになります。schemeパラメータの文字列が生成されるコンポーネントのエンドポイントURIのスキーム名("hello://...")になります。

camel-hello/
├── camel-hello-api/
│   ├── pom.xml
│   └── src/main/java/com/redhat/sample/camel/hello/api/
│       ├── HelloFileHello.java
│       └── HelloJavadocHello.java
├── camel-hello-component/
│   ├── pom.xml
│   ├── signatures/
│   │   └── file-sig-api.txt
│   └── src/
│       ├── main/
│       │   ├── java/com/redhat/sample/camel/hello/
│       │   │   ├── HelloComponent.java
│       │   │   ├── HelloConfiguration.java
│       │   │   ├── HelloConsumer.java
│       │   │   ├── HelloEndpoint.java
│       │   │   ├── HelloProducer.java
│       │   │   └── internal/
│       │   │       ├── HelloConstants.java
│       │   │       └── HelloPropertiesHelper.java
│       │   └── resources/META-INF/services/org/apache/camel/component/
│       │       └── hello
│       └── test/
│           ├── java/com/redhat/sample/camel/hello/
│           │   └── AbstractHelloTestSupport.java
│           └── resources/
│               ├── log4j2.properties
│               └── test-options.properties
└── pom.xml

プロジェクトは2つのサブプロジェクトで構成されています。

  • xxxxx-api ― (オプション)3rdパーティのJavaライブラリを直接使えない場合に、カスタムでAPIを用意するためのプロジェクト。REST APIの場合も生成したJavaクライアントをここに置く。必要無い場合は、ルートプロジェクトと一緒に削除してxxxxx-componentプロジェクトだけを使う。
  • xxxxx-component ― 開発するコンポーネントの本体。

camel-hello-component/pom.xmlを見ると、camel-hello-apiがdependencyに定義されています。3rdパーティのライブラリを使う場合は、このAPIプロジェクトを削除して代わりにライブラリのdependencyに置き換えます。

    <dependency>
      <groupId>com.redhat.sample.camel.hello</groupId>
      <artifactId>camel-hello-api</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>
    [...]
    <!-- Component API javadoc in provided scope to read API signatures -->
    <dependency>
      <groupId>com.redhat.sample.camel.hello</groupId>
      <artifactId>camel-hello-api</artifactId>
      <version>1.0-SNAPSHOT</version>
      <classifier>javadoc</classifier>
      <scope>provided</scope>
    </dependency>

例えば、camel-twilioでは以下のようになっています。
https://github.com/apache/camel/blob/camel-2.23.1/components/camel-twilio/pom.xml

    <!-- Twilio Java SDK -->
    <dependency>
      <groupId>com.twilio.sdk</groupId>
      <artifactId>twilio</artifactId>
      <version>${twilio-version}</version>
    </dependency>
    [...]
    <!-- Component API javadoc in provided scope to read API signatures -->
    <dependency>
      <groupId>com.twilio.sdk</groupId>
      <artifactId>twilio</artifactId>
      <version>${twilio-version}</version>
      <type>javadoc</type>
      <scope>provided</scope>
    </dependency>

ここでjavadocタイプのdependencyも登場していますが、なぜ必要かはこれから説明します。

ひとまずはcamel-hello-apiを使ってcamel-helloコンポーネントを完成させます。

APIとエンドポイントURIのマッピング

次にやることは、APIとCamelエンドポイントとのマッピングを設定することです。第1回に説明したように、APIコンポーネントのエンドポイントURIは基本的に以下の形式になります。

scheme://apiName/methodName?option1=value1&...&optionN=valueN

自動生成されたcamel-hello-apiのAPIクラスHelloFileHelloを見ると、以下の通りです。

public class HelloFileHello {

    public String sayHi() {
        return "Hello!";
    }

    public String greetMe(String name) {
        return "Hello " + name;
    }

    public String greetUs(String name1, String name2) {
            return "Hello " + name1 + ", " + name2;
    }
}

これを、以下のようなエンドポイントURIにマッピングします。

hello://hello-file/sayHi
hello://hello-file/greetMe
hello://hello-file/greetUs

メソッド引数のマッピング

APIメソッドの引数は、次のようにURIのオプションにハードコードして渡すこともできます。

hello://hello-file/greetUs?name1=Llama&name2=Alpaca

しかし、たいていの場合は動的に値を渡したいと思うので、Camelメッセージのヘッダで渡します。

from("direct:sayHello")
    .setHeader("CamelHello.name1", constant("Llama"))
    .setHeader("CamelHello.name2", constant("Alpaca"))
    .to("hello://hello-file/greetUs");

ヘッダ名はCamelXxxxx. + <引数名>です。Xxxxxには、プロジェクト生成時にnameパラメータで指定した文字列が入ります。

メッセージボディを直接引数に渡したい場合は、特別なURIオプションinBodyを使います。inBodyの値には、ボディをどの引数に渡すかを引数名で指定します。

    .to("hello://hello-file/greetMe?inBody=name");

これでgreetMeメソッドの引数nameにメッセージボディが直接渡されます。

javadoc dependencyが必要な理由

第1回の記事で、APIコンポーネントフレームワークの要点はコード生成とリフレクションだと説明しました。このAPIメソッドからエンドポイントURIへのマッピングでリフレクションが使われるので、開発者がメソッド毎にいちいちエンドポイントの処理を手書きする必要がありません。しかし、メソッドの引数名は、Javaバイトコードの仕様上リフレクションで取得することができません。Javaの.classファイルには引数名の情報が残っていないからです。

そこで、dependencyのJARファイルとは別にAPIのシグネチャを取得する方法が必要です。APIコンポーネントフレームワークは2つの方法をサポートしています。

  1. Javadocから取得する
  2. テキストファイルに書き出したシグネチャを読み込む

そのため、1の方法を用いる場合にjavadocのdependencyが必要になるのです。

APIコンポーネントのマッピング設定

camel-hello-component/pom.xmlを開いて、Mavenプラグインにマッピング設定を記述します。すでに様々なオプションがコメントアウトされた状態で出力されているので、これを眺めるだけでもどんなことができるのか、ある程度理解できると思います。

<!-- generate Component source and test source -->
<plugin>
  <groupId>org.apache.camel</groupId>
  <artifactId>camel-api-component-maven-plugin</artifactId>
  <executions>
    <execution>
      <id>generate-test-component-classes</id>
      <goals>
        <goal>fromApis</goal>
      </goals>
      <configuration>
        <apis>
          <api>
            <apiName>hello-file</apiName>
            <proxyClass>com.redhat.sample.camel.hello.api.HelloFileHello</proxyClass>
            <fromSignatureFile>signatures/file-sig-api.txt</fromSignatureFile>
            <!-- Use substitutions to manipulate parameter names and avoid name clashes
            <substitutions>
              <substitution>
                <method>^(.+)$</method>
                <argName>^(.+)$</argName>
                <argType>java.lang.String</argType>
                <replacement>$1Param</replacement>
                <replaceWithType>false</replaceWithType>
              </substitution>
            </substitutions>
            -->
            <!-- Exclude automatically generated endpoint options by name
            <excludeConfigNames>name-pattern<excludeConfigNames>
            -->
            <!-- Exclude automatically generated endpoint options by type
            <excludeConfigTypes>type-pattern<excludeConfigTypes>
            -->
            <!-- Add custom endpoint options to generated EndpointConfiguration class for this API
            <extraOptions>
              <extraOption>
                <type>java.util.List&lt;String&gt;</type>
                <name>customOption</name>
              </extraOption>
            </extraOptions>
            -->
            <!-- Use method aliases in endpoint URIs, e.g. support 'widget' as alias for getWidget or setWidget
            <aliases>
              <alias>
                <methodPattern>[gs]et(.+)</methodPattern>
                <methodAlias>$1</methodAlias>
              </alias>
            </aliases>
            -->
            <!-- for some methods, null can be a valid input
            <nullableOptions>
              <nullableOption>option-name</nullableOption>
            </nullableOptions>
            -->
          </api>
          <api>
            <apiName>hello-javadoc</apiName>
            <proxyClass>com.redhat.sample.camel.hello.api.HelloJavadocHello</proxyClass>
            <fromJavadoc>
              <!-- Use exclude patterns to limit what gets exposed in component endpoint
              <excludePackages>package-name-patterns</excludePackages>
              <excludeClasses>class-name-patterns</excludeClasses>
              <includeMethods>method-name-patterns</includeMethods>
              <excludeMethods>method-name-patterns</excludeMethods>
              <includeStaticMethods>use 'true' to include static methods, false by default<includeStaticMethods>
              -->
            </fromJavadoc>
          </api>
        </apis>
        <!-- Specify global values for all APIs here, these are overridden at API level
        <substitutions/>
        <excludeConfigNames/>
        <excludeConfigTypes/>
        <extraOptions/>
        <fromJavadoc/>
        <aliases/>
        <nullableOptions/>
        -->
      </configuration>
    </execution>
  </executions>
</plugin>

ひとまずポイントはここです。

          <api>
            <apiName>hello-file</apiName>
            <proxyClass>com.redhat.sample.camel.hello.api.HelloFileHello</proxyClass>
            <fromSignatureFile>signatures/file-sig-api.txt</fromSignatureFile>
            [...]
          </api>

<apiName>でエンドポイントURIのapiNameに当たる部分を定義します。<proxyClass>でそのエンドポイントの下敷きとなるAPIクラスを指定します。先ほどAPIのシグネチャを読み込む方法が2通りあると言いましたが、<fromSignatureFile>はファイルベースの方法です。

signatures/file-sig-api.txtを見るとこのようにシグネチャが定義されています。

public String sayHi();
public String greetMe(String name);
public String greetUs(String name1, String name2);

シグネチャファイルでなくJavadocを使う場合は<fromJavadoc>を指定します。この場合、わざわざ自分でシグネチャを書く必要がないので、通常はこちらの方法がお勧めです。3rdパーティライブラリにJavadocがない、など特殊な場合にのみシグネチャファイルを使います。

          <api>
            <apiName>hello-javadoc</apiName>
            <proxyClass>com.redhat.sample.camel.hello.api.HelloJavadocHello</proxyClass>
            <fromJavadoc>
              [...]
            </fromJavadoc>
          </api>

とりあえず設定はこのままでも動くので、このまま進めます。

ConfigurationとEndpointの修正

ソースコードで最低限修正が必要なのは2箇所です。まずはcamel-hello-component/src/main/java/com/redhat/sample/camel/hello/HelloConfiguration.javaを開きます。

@UriParams
public class HelloConfiguration {

    // TODO add component configuration properties
}

ここに、コンポーネントの設定に必要なパラメータを追加します。Webサービスであれば、最低でも接続に認証情報が必要になるはずです。そういった、ユーザがこのコンポーネントを使う際に設定すべきパラメータをここに追加します。とりあえず、今回はダミーのHello APIを呼び出すだけなのでこのままにします。

次に、camel-hello-component/src/main/java/com/redhat/sample/camel/hello/HelloEndpoint.javaを開きます。

@UriEndpoint(firstVersion = "1.0-SNAPSHOT", scheme = "hello", title = "Hello", syntax="hello:name", 
             consumerClass = HelloConsumer.class, label = "custom")
public class HelloEndpoint extends AbstractApiEndpoint<HelloApiName, HelloConfiguration> {
    [...]
    // TODO create and manage API proxy
    private Object apiProxy;
    [...]
    @Override
    protected void afterConfigureProperties() {
        // TODO create API proxy, set connection properties, etc.
        switch (apiName) {
            case HELLO_FILE:
                apiProxy = new HelloFileHello();
                break;
            case HELLO_JAVADOC:
                apiProxy = new HelloJavadocHello();
                break;
            default:
                throw new IllegalArgumentException("Invalid API name " + apiName);
        }
    }

重要なのはafterConfigureProperties()メソッドです。ここで、エンドポイント毎にAPIオブジェクトのインスタンスを生成し、apiProxyフィールドに保持します。APIオブジェクトを生成する際にWebサービスへの接続が必要な場合は、ここで先ほど修正したConfigurationクラスから必要なパラメータを受け取って接続を行います。ここも、今回のサンプルではこのままにします。

ビルドと実行

プロジェクトが完成したら、以下を実行してビルド&インストールをします。

$ mvn clean install

ビルドが成功すると、camel-hello-component/target/にソースコードが生成されているのが見つかります。

camel-hello-component/target/
├── generated-sources/camel-component/
│   └── com/redhat/sample/camel/hello/
│       ├── HelloFileHelloEndpointConfiguration.java
│       ├── HelloJavadocHelloEndpointConfiguration.java
│       └── internal/
│           ├── HelloApiCollection.java
│           ├── HelloApiName.java
│           ├── HelloFileHelloApiMethod.java
│           └── HelloJavadocHelloApiMethod.java
└── generated-test-sources/camel-component/
    └── com/redhat/sample/camel/hello/
        ├── HelloFileHelloIntegrationTest.java
        └── HelloJavadocHelloIntegrationTest.java

generated-sourcesの方は、Mavenの設定でビルド時に一緒にコンパイルされるようになっています。毎回ビルド時に自動生成されるものなので、ソースコードリポジトリにコミットする必要はありません。

generated-test-sourcesについても同様ですが、最終回のテストのところで詳しく説明します。

出来上がったコンポーネントは、標準のCamelコンポーネントと同じようにMavenのdependencyに追加して、Camelルートより実行してください。

    <dependency>
      <groupId>com.redhat.sample.camel.hello</groupId>
      <artifactId>camel-hello</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>

試しに次のような単体テストを実行してみましょう。

public class HelloTest extends CamelTestSupport {

    @Override
    protected RouteBuilder createRouteBuilder() {
        return new RouteBuilder() {
            @Override
            public void configure() throws Exception {
                from("direct:sayHello")
                    .setHeader("CamelHello.name1", constant("Llama"))
                    .setHeader("CamelHello.name2", constant("Alpaca"))
                    .to("hello://hello-file/greetUs")
                    .log("${body}");
            }
        };
    }

    @Test
    public void sayHello() throws Exception {
        template.sendBody("direct:sayHello", null);
    }
}

実行結果はこんな感じになります。

$ mvn test -Dtest=HelloTest
...
[amel-1) thread #1 - CamelHello] route1                         INFO  Hello Llama, Alpaca

おわりに

とりあえず動作するコンポーネントが完成しました。しかし、野生のWebサービスのAPIは千差万別であり、それを上手くコンポーネントに飼いならすには高度なマッピングの設定が必要不可欠です。

次回は、高度なマッピング設定の方法を説明します。

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。