設定と接続プロバイダーの定義

パラメーターの分散

重要な側面の 1 つは、設定オブジェクトに含める​べき​パラメーターと、設定プロバイダーに含める​べき​パラメーターを決定することです。

接続プロバイダーは、外部システムとの接続を確立して設定するために使用するパラメーターのみを含める​必要があります​。他の目的で使用するすべてのパラメーターは、設定オブジェクトに含める​必要があります​。

設定の POJO パラメーター

設定プロバイダーまたは接続プロバイダーが @ParameterGroup アノテーションを使用せずにパラメーター種別として POJO を使用する場合、POJO は以下の条件を満足する​必要があります​。

  • デフォルトのコンストラクターを持つ

  • 各項目で @Parameter のアノテーションを付加した getter メソッドが公開されている

  • equals() メソッドと hashCode() メソッドが、すべての @Parameter 項目を使用する実装に置き換えられている

ライフサイクルフェーズでは接続を確立しない

ConnectionProvider は、対応するインターフェース (Initialisable、Startable、Stoppable、Disposable) のいずれかを実装することで、アプリケーションのライフサイクルに接続できます。

接続は、initialisable() メソッドや start() メソッドで確立しては​ならず​、stop() メソッドや dispose() メソッドで切断しては​なりません​。したがって、プロバイダーのコンストラクターとライフサイクルメソッドは、ConnectionException をスローしては​なりません​。

接続は、connect() メソッドでのみ確立する​必要があり​、disconnect(T) メソッドでのみ切断する​必要があります​。

SSL 接続の処理

コネクタで TLS/SSL セキュア接続を確立する必要がある場合は、この記事で説明するように TlsContextFactory パラメーターを使用して SSLContext を取得する​必要があります​。

ConnectionProvider で TlsContextFactory パラメーターを使用する場合は、Initialisable インターフェースも実装して、初期化中にこのようなコンテキストで initialise() メソッドを呼び出す​必要があります​。

public class MyConnectionProvider implements ConnectionProvider<Connection>, Initialisable {

        @Parameter
        private TlsContextFactory tlsContext;

        @Override
        public void initialise() throws InitialisationException {
                ((Initialisable) tlsContextFactory).initialise();
        }
}

接続管理戦略の定義

接続管理戦略​を定義または設計する場合は、以下の意思決定プロセスを遵守する​必要があります​。

  • 最も効率的な方法は、CachedConnectionProvider を使用することです。

    ただし、この場合は接続オブジェクトがスレッドセーフである​必要があり​、同期の競合が顕著に発生しては​なりません​。

  • CachedConnectionProvider が使用できない場合には、特に接続の確立に多くのリソースが使用される場合には、代わりに PoolingConnectionProvider を使用する​べきです​。

  • これらの方法がどちらも使用できない場合には、プレーンな ConnectionProvider を検討す​べきです​。

OAuth 保護接続の処理

SDK では、OAuth で保護されたシステムに対するコネクタの開発をサポートしています。 この記事で説明されているように​、SDK は OAuth トークンの取得と更新を自動的に処理します。OAuth トークンは、AuthorizationCodeState オブジェクト (認証コード許可種別を使用している場合) または ClientCredentialsState オブジェクト (クライアントログイン情報許可種別を使用している場合) によって提供されます。

以下のセクションでは、OAuth の使用に関するその他の考慮事項を説明します。

トークンの有効期限の処理

SDK は、スレッドセーフな方法でアクセストークンを更新する機能を提供しますが、トークンの期限切れ通知の方法はサービスプロバイダーによって異なるため、トークンが実際に期限切れになったことを検出するための標準的な手段はありません。そのため、期限切れの検出は各コネクタの責任となります。

OAuth 認証を使用する場合は、すべてのコネクタが、トークンの期限が切れたことを検出して、そのことを AccessTokenExpired 例外で Mule に通知する​必要があります​。

ただし、コネクタは次の条件を見極める​必要がある​ため、この処理は容易ではありません。

  • アクセストークンが期限切れになって更新が必要である

  • アクセストークンは有効だが特定のアクションを実行するための権限がない

  • アクセストークンがリモートで取り消された

各要求での状態オブジェクトの常時クエリ

コネクタは、AuthorizationCodeState オブジェクトと ClientCredentialsState オブジェクトを通して、Mule が管理するアクセストークンにアクセスします。正確に言えば、両方のクラスで共有される getAccessToken() メソッドがアクセスを処理します。

Mule の高度な同時性により、これらのトークンはいつでも更新または承認拒否できます。

そのため、getAccessToken() メソッドは、送信されるすべての要求の状態オブジェクトで呼び出す​必要があります​。アクセストークンをキャッシュしたり、内部変数に格納したりしては​なりません​。

この考慮事項は、キャッシュ処理を行う可能性のあるサードパーティクライアントライブラリを使用する場合には特に重要です。

接続の作成でのアクセストークンの不使用

ConnectionProvider#connect​ メソッドで接続オブジェクトを作成する場合には、有効なアクセストークンの存在に依存しないようにする必要があります。トークンを使用して 1 回限りの要求を実行する接続で、接続の作成に依存しないようにしてください。接続が最初に使用されるときまで要求を遅延させる必要があります。

このプラクティスに準拠するために、​org.mule.runtime.api.util.LazyValue​ クラスを使用することができます。値を最初に使用するときに値が取得され、以降は最初にキャッシュされた値が使用されます。

次の例は、​org.mule.runtime.api.util.LazyValue​ クラスの使い方を示しています。

@AuthorizationCode(accessTokenUrl = MyConnectionProvider.ACCESS_TOKEN_URL,
    authorizationUrl = MyConnectionProvider.AUTH_URL,
    defaultScopes = MyConnectionProvider.DEFAULT_SCOPE)
public class MyConnectionProvider implements ConnectionProvider<Connection> {

  public static final String ACCESS_TOKEN_URL = "accessTokenUrl";
  public static final String AUTH_URL = "authUrl";
  public static final String DEFAULT_SCOPE = "defaultScope";

  private AuthorizationCodeState state;

  @Override
  public Connection connect() throws ConnectionException {
    return new Connection(new LazyValue(getUserId(state.getAccessToken())));
  }


  private String getUserId(String accessToken){
    ...
  }

  ...

}

このように、​LazyValue​ を最初に使用するときに情報が取得され、以降はすでに解決済みの値が使用されます。

接続オブジェクトは内部クライアントを公開しては (内部クライアントであっては) ならない

よくあるコネクタのアンチパターンは、ConnectionProvider が外部システムへのアクセスに使用するクライアントや実装を公開してしまう接続オブジェクトを生成することです。次に例を示します。

public class HttpConnection {

        private HttpClient client;

        public HttpClient getClient() {
                return client;
        }
}

操作では、このオブジェクトを次の疑似コードに示すように使用します。

public void createCustomer(@Connection HttpConnection connection, @Content InputStream content) {

        connection.getClient().send(HttpRequest.builder()
                .path(connection.getPath() + "/customer")
                .method("POST")
                .entity(content)
                .addHeader("Accept", "application/json")
            .build()
        );
}

このコードにはいくつかの問題があります。最も明白な問題は、接続を使用する操作が接続オブジェクトの実装と強力に結合しているという点です。クライアント実装の変更が必要になった場合は、その接続を使用しているすべてのコンポーネントが影響を受けます。

別の問題は、接続オブジェクトをテストできないという点です。クライアントオブジェクトは公開されるため、接続オブジェクトのモックバージョンとやり取りするテストは複雑になりすぎてしまいます。

そして最大の問題は、接続オブジェクトを使用するすべてのコンポーネントの間で機能的な結合が発生し、サポート対象のすべての接続プロバイダーで機能が微妙に異なってしまうという点です。

たとえば、上の例が基本認証と OAuth 認証の両方のメカニズムをサポートするコネクタの一部であるとします。

基本認証接続で正しくないログイン情報が使用された場合には、要求の結果として HTTP 401 状況コードが返され、操作は失敗する​べき​です。

しかし、OAuth で保護された接続でこの状況コードが返された場合には、コネクタはトークンが期限切れかどうかを判断するためのロジックを実行する必要があるため、AccessTokenExpiredException がスローされる​べき​です。

接続種別やコンポーネントがさらに増えると、問題はますます悪化します。これは一例に過ぎません。HTTP クライアントだけではなく、他の接続種別でも同じ問題が発生します。

この問題を防止するためには、接続オブジェクトは内部の通信メカニズムとセキュリティスキーマをカプセル化する​必要があり​、パターンは次のようになります。

public void createCustomer(@Connection HttpConnection connection, @Content InputStream content) {

        connection.createCustomer(content);
}

このアプローチなら、各 ConnectionProvider 実装がそれぞれに適した HttpConnection オブジェクト実装を提供できるため、問題の根本原因そのものが解消し、他のコンポーネントに影響することなく内部の接続メカニズムを変更できるようになります。

代替手段: コマンドパターンの活用

接続オブジェクトに、コンシュームするエンドポイントごとに 1 つのメソッドを与えるのではなく、より汎用的なコマンド設計パターンを実装することができます。

public void createCustomer(@Connection HttpConnection connection, @Content InputStream content) {

        connection.request(HttpRequest.builder()
                .path(connection.getPath() + "/customer")
                .method("POST")
                .entity(content)
        );
}

このアプローチでは、接続オブジェクトは HttpRequestBuilder オブジェクトを受け取る 1 つの汎用要求メソッドのみを持ちます。ビルダーオブジェクトが build() コマンドを受け取ることはありません。接続オブジェクトの実装に応じて、追加のヘッダーを追加したり、要求を異なる方法で実行したり、それらに応じて応答を処理したりできます。

モジュールの API の一部としては公開しない

Configuration、ConnectionProvider、Connection の各クラスは、モジュールの API の一部としてエクスポートしては​なりません​。