第7章 Jakarta Enterprise Beans インターセプター

7.1. カスタムインターセプター

JBoss EAP では、カスタム Jakarta Enterprise Beans インターセプターを開発および管理できます。

以下のタイプのインターセプターを作成できます。

  • クライアントインターセプター

    クライアントインターセプターは、JBoss EAP がクライアントとして動作するときに実行されます。

  • サーバーインターセプター

    サーバーインターセプターは、JBoss EAP がサーバーとして動作するときに実行されます。これらのインターセプターは、サーバーにグローバルに設定できます。

  • コンテナーインターセプター

    コンテナーインターセプターは、JBoss EAP がサーバーとして動作するときに実行されます。これらのインターセプターは Jakarta Enterprise Beans コンテナーで設定されます。

カスタムインターセプタークラスをモジュールに追加し、$JBOSS_HOME/modules ディレクトリーに格納する必要があります。

7.1.1. インターセプターチェーン

カスタムインターセプターは、インターセプターチェーンの特定ポイントで実行されます。

Jakarta Enterprise Beans に設定されたコンテナーインターセプターは、セキュリティーインターセプターやトランザクション管理インターセプターなど、Wildfly が提供するインターセプターの前に実行されます。そのため、コンテナーインターセプターは、Wildfly インターセプターまたはグローバルインターセプターを起動する前にコンテキストデータを処理または設定できます。

サーバーインターセプターおよびクライアントインターセプターは、Wildfly 固有のインターセプターの後に実行されます。

7.1.2. カスタムクライアントインターセプター

カスタムクライアントインターセプターは、org.jboss.ejb.client.EJBClientInterceptor インターフェイスを実装します。

Org.jboss.ejb.client.EJBClientInvocationContext インターフェイスも含める必要があります。

以下のコードは、クライアントインターセプターの例を示しています。

クライアントインターセプターコードの例

package org.foo;
import org.jboss.ejb.client.EJBClientInterceptor;
import org.jboss.ejb.client.EJBClientInvocationContext;
public class FooInterceptor implements EJBClientInterceptor {
    @Override
    public void handleInvocation(EJBClientInvocationContext context) throws Exception {
        context.sendRequest();
    }
    @Override
    public Object handleInvocationResult(EJBClientInvocationContext context) throws Exception {
        return context.getResult();
    }
}

7.1.3. カスタムサーバーインターセプター

サーバーインターセプターは、@javax.annotation.AroundInvoke アノテーションまたは javax.interceptor.AroundTimeout アノテーションを使用して、Bean での呼び出し中に呼び出されるメソッドをマークします。

以下のコードは、サーバーインターセプターの例を示しています。

サーバーインターセプターコードの例

package org.testsuite.ejb.serverinterceptor;
import javax.annotation.PostConstruct;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
public class TestServerInterceptor {
    @AroundInvoke
    public Object aroundInvoke(final InvocationContext invocationContext) throws Exception {

        return invocationContext.proceed();
    }
}

7.1.4. カスタムコンテナーインターセプター

コンテナーインターセプターは、@javax.annotation.AroundInvoke アノテーションまたは javax.interceptor.AroundTimeout アノテーションを使用して、Bean での呼び出し中に呼び出されるメソッドをマークします。

Jakarta Enterprise Beans 3.2 仕様で定義されている標準の JakartaEE インターセプターは、コンテナーがセキュリティーコンテキストの伝搬、トランザクション管理、およびその他コンテナーによる呼び出し処理の完了後に実行されることが予想されます。

以下のコードは、呼び出しのために iAmAround メソッドをマークするインターセプタークラスを示しています。

コンテナーインターセプターコードの例

public class ClassLevelContainerInterceptor {
    @AroundInvoke
    private Object iAmAround(final InvocationContext invocationContext) throws Exception {
        return this.getClass().getName() + " " + invocationContext.proceed();
    }
}

コンテナーインターセプターと Jakarta EE インターセプター API の違い

コンテナーインターセプターは Jakarta EE インターセプターと同様にモデル化されますが、API のセマンティクスにはいくつかの違いがあります。たとえば、これらのインターセプターは Jakarta Enterprise Beans コンポーネントの設定またはインスタンス化よりもだいぶ前に呼び出されるため、コンテナーインターセプターが javax.interceptor.InvocationContext.getTarget() メソッドを呼び出すことは違反になります。

7.1.5. コンテナーインターセプターの設定

コンテナーインターセプターは標準の Jakarta EE インターセプターライブラリーを使用します。

そのため、ejb-jar デプロイメント記述子の 3.2 バージョンに対して ejb-jar.xml ファイルで許可されるのと同じ XSD 要素を使用します。

それらは標準の Jakarta EE インターセプターライブラリーをベースとしているため、コンテナーインターセプターはデプロイメント記述子を使用してのみ設定できます。設計アプリケーションには、JBoss EAP 固有のアノテーションやその他のライブラリーの依存関係は必要ありません。

コンテナーインターセプターを設定するには、以下を実行します。

  1. Jakarta Enterprise Beans デプロイメントの META-INF/` ディレクトリーに jboss-ejb3.xml ファイルを作成します。
  2. 記述子ファイルにコンテナーインターセプター要素を設定します。

    1. urn:container-interceptors:1.0 ネームスペースを使用して、コンテナーインターセプター要素の設定を指定します。
    2. <container-interceptors> 要素を使用してコンテナーインターセプターを指定します。
    3. >interceptor-binding> 要素を使用してコンテナーインターセプターを Jakarta Enterprise Beans にバインドします。インターセプターは、以下のいずれかの方法でバインドできます。

      • ワイルドカード (*) を使用して、インターセプターをデプロイメントのすべての Jakarta Enterprise Beans にバインドします。
      • 特定の Jakarta Enterprise Beans 名を使用して、個々の Bean レベルでインターセプターをバインドします。
      • Jakarta Enterprise Beans の特定のメソッドレベルでインターセプターをバインドします。

        注記

        これらの要素は、Jakarta EE インターセプターと同じ方法で Jakarta Enterprise Beans 3.2 XSD を使用して設定されます。

以下の記述子ファイル例は設定オプションを示しています。

コンテナーインターセプター jboss-ejb3.xml ファイルの例

<jboss xmlns="http://www.jboss.com/xml/ns/javaee"
       xmlns:jee="http://java.sun.com/xml/ns/javaee"
       xmlns:ci ="urn:container-interceptors:1.0">
    <jee:assembly-descriptor>
        <ci:container-interceptors>
            <!-- Default interceptor -->
            <jee:interceptor-binding>
                <ejb-name>*</ejb-name>
                <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.ContainerInterceptorOne</interceptor-class>
            </jee:interceptor-binding>
            <!-- Class level container-interceptor -->
            <jee:interceptor-binding>
                <ejb-name>AnotherFlowTrackingBean</ejb-name>
                <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.ClassLevelContainerInterceptor</interceptor-class>
            </jee:interceptor-binding>
            <!-- Method specific container-interceptor -->
            <jee:interceptor-binding>
                <ejb-name>AnotherFlowTrackingBean</ejb-name>
                <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.MethodSpecificContainerInterceptor</interceptor-class>
                <method>
                    <method-name>echoWithMethodSpecificContainerInterceptor</method-name>
                </method>
            </jee:interceptor-binding>
            <!-- container interceptors in a specific order -->
            <jee:interceptor-binding>
                <ejb-name>AnotherFlowTrackingBean</ejb-name>
                <interceptor-order>
                    <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.ClassLevelContainerInterceptor</interceptor-class>
                    <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.MethodSpecificContainerInterceptor</interceptor-class>
                    <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.ContainerInterceptorOne</interceptor-class>
                </interceptor-order>
                <method>
                    <method-name>echoInSpecificOrderOfContainerInterceptors</method-name>
                </method>
            </jee:interceptor-binding>
        </ci:container-interceptors>
    </jee:assembly-descriptor>
</jboss>

allow-ejb-name-regex 属性を使用すると、インターセプターバインディングで正規表現を使用できるようになり、指定の正規表現に一致するすべての Bean にインターセプターをマッピングできます。以下の管理 CLI コマンドを使用して、ejb3 サブシステムの allow-ejb-name-regex 属性を true に有効化します。

/subsystem=ejb3:write-attribute(name=allow-ejb-name-regex,value=true)

urn:container-interceptors:1.0 名前空間のスキーマは http://www.jboss.org/schema/jbossas/jboss-ejb-container-interceptors_1_0.xsd で利用できます。

7.1.6. サーバーおよびクライアントインターセプターの設定

サーバーインターセプターおよびクライアントインターセプターは、使用される設定ファイルの JBoss EAP 設定にグローバルに追加されます。

サーバーインターセプターは、ejb3 サブシステム設定の <server-interceptors> 要素に追加されます。クライアントインターセプターは、ejb3 サブシステム設定の <client-interceptors> 要素に追加されます。

以下の例は、サーバーインターセプターの追加を示しています。

/subsystem=ejb3:list-add(name=server-interceptors,value={module=org.abccorp:tracing-interceptors:1.0,class=org.abccorp.TracingInterceptor})

以下の例は、クライアントインターセプターの追加を示しています。

/subsystem=ejb3:list-add(name=client-interceptors,value={module=org.abccorp:clientInterceptor:1.0,class=org.abccorp.clientInterceptor})

サーバーインターセプターまたはクライアントインターセプターが追加されるか、インターセプターの設定が変更されるたびに、サーバーをリロードする必要があります。

7.1.7. SCC (Security Context Identity) の変更

複数のクライアント接続を開く代わりに、認証されたユーザーにアイデンティティーを切り替え、別のユーザーとして既存の接続で要求を実行するパーミッションを付与することができます。

デフォルトでは、アプリケーションサーバーにデプロイされた Jakarta Enterprise Beans へのリモート呼び出しを行うと、サーバーへの接続は認証され、接続を使用する後続のリクエストは元の認証済みアイデンティティーを使用して実行されます。これは、クライアントとサーバー間の呼び出しの両方に適用されます。同じクライアントから異なるアイデンティティーを使用する必要がある場合は、通常は、異なるアイデンティティーとして認証されるように、サーバーへの接続を複数開く必要があります。代わりに、認証されたユーザーがアイデンティティーを変更できるようにすることもできます。

認証されたユーザーのアイデンティティーを変更するには、以下を実行します。

  1. インターセプターコードでアイデンティティーの変更を実装します。

    • クライアントインターセプター

      インターセプターは、EJBClientInvocationContext.getContextData() への呼び出しを使用して取得できるコンテキストデータマップを介して要求されたアイデンティティーを渡す必要があります。以下のサンプルコードは、アイデンティティーを切り替えるクライアントインターセプターを示しています。

      クライアントインターセプターコードの例

      public class ClientSecurityInterceptor implements EJBClientInterceptor {
      
          public void handleInvocation(EJBClientInvocationContext context) throws Exception {
              Principal currentPrincipal = SecurityActions.securityContextGetPrincipal();
      
              if (currentPrincipal != null) {
                  Map<String, Object> contextData = context.getContextData();
                  contextData.put(ServerSecurityInterceptor.DELEGATED_USER_KEY, currentPrincipal.getName());
              }
              context.sendRequest();
          }
      
          public Object handleInvocationResult(EJBClientInvocationContext context) throws Exception {
              return context.getResult();
          }
      }

    • コンテナーおよびサーバーインターセプター

      これらのインターセプターはアイデンティティーが含まれる InvocationContext を受け取り、新しいアイデンティティーに切り替える要求を行います。以下のコードは、コンテナーインターセプターのブリッジした例を示しています。

      コンテナーインターセプターコードの例

      public class ServerSecurityInterceptor {
      
          private static final Logger logger = Logger.getLogger(ServerSecurityInterceptor.class);
      
          static final String DELEGATED_USER_KEY = ServerSecurityInterceptor.class.getName() + ".DelegationUser";
      
          @AroundInvoke
          public Object aroundInvoke(final InvocationContext invocationContext) throws Exception {
              Principal desiredUser = null;
              UserPrincipal connectionUser = null;
      
              Map<String, Object> contextData = invocationContext.getContextData();
              if (contextData.containsKey(DELEGATED_USER_KEY)) {
                  desiredUser = new SimplePrincipal((String) contextData.get(DELEGATED_USER_KEY));
      
                  Collection<Principal> connectionPrincipals = SecurityActions.getConnectionPrincipals();
      
                  if (connectionPrincipals != null) {
                      for (Principal current : connectionPrincipals) {
                          if (current instanceof UserPrincipal) {
                              connectionUser = (UserPrincipal) current;
                              break;
                          }
                      }
      
                  } else {
                      throw new IllegalStateException("Delegation user requested but no user on connection found.");
                  }
              }
      
              ContextStateCache stateCache = null;
              try {
                  if (desiredUser != null && connectionUser != null
                      && (desiredUser.getName().equals(connectionUser.getName()) == false)) {
                      // The final part of this check is to verify that the change does actually indicate a change in user.
                      try {
                          // We have been requested to use an authentication token
                          // so now we attempt the switch.
                          stateCache = SecurityActions.pushIdentity(desiredUser, new OuterUserCredential(connectionUser));
                      } catch (Exception e) {
                          logger.error("Failed to switch security context for user", e);
                          // Don't propagate the exception stacktrace back to the client for security reasons
                          throw new EJBAccessException("Unable to attempt switching of user.");
                      }
                  }
      
                  return invocationContext.proceed();
              } finally {
                  // switch back to original context
                  if (stateCache != null) {
                      SecurityActions.popIdentity(stateCache);;
                  }
              }
          }

  2. アプリケーションは、プログラムを用いて、またはサービ出力ダーメカニズムを使用して、クライアントインターセプターを EJBClientContext インターセプターチェーンに挿入できます。クライアントインターセプターを設定する手順は、Using a Client Interceptor in an Application を参照してください。
  3. Jakarta Authentication ログインモジュールを作成します。

    Jakarta Authentication LoginModule コンポーネントは、ユーザーが要求されたアイデンティティーとして要求を実行できることを確認します。以下の abridged コード例は、ログインおよび検証を実行するメソッドを示しています。

    LoginModule コードの例

        @SuppressWarnings("unchecked")
        @Override
        public boolean login() throws LoginException {
            if (super.login() == true) {
                log.debug("super.login()==true");
                return true;
            }
    
            // Time to see if this is a delegation request.
            NameCallback ncb = new NameCallback("Username:");
            ObjectCallback ocb = new ObjectCallback("Password:");
    
            try {
                callbackHandler.handle(new Callback[] { ncb, ocb });
            } catch (Exception e) {
                if (e instanceof RuntimeException) {
                    throw (RuntimeException) e;
                }
                // If the CallbackHandler can not handle the required callbacks then no chance.
                return false;
            }
    
            String name = ncb.getName();
            Object credential = ocb.getCredential();
    
            if (credential instanceof OuterUserCredential) {
                // This credential type will only be seen for a delegation request, if not seen then the request is not for us.
    
                if (delegationAcceptable(name, (OuterUserCredential) credential)) {
                    identity = new SimplePrincipal(name);
                    if (getUseFirstPass()) {
                        String userName = identity.getName();
                        if (log.isDebugEnabled())
                            log.debug("Storing username '" + userName + "' and empty password");
                        // Add the username and an empty password to the shared state map
                        sharedState.put("javax.security.auth.login.name", identity);
                        sharedState.put("javax.security.auth.login.password", "");
                    }
                    loginOk = true;
                    return true;
                }
            }
            return false; // Attempted login but not successful.
        }
    
        // Make a trust user to decide if the user switch is acceptable.
        protected boolean delegationAcceptable(String requestedUser, OuterUserCredential connectionUser) {
        if (delegationMappings == null) {
            return false;
        }
    
        String[] allowedMappings = loadPropertyValue(connectionUser.getName(), connectionUser.getRealm());
        if (allowedMappings.length == 1 && "*".equals(allowedMappings[0])) {
            // A wild card mapping was found.
            return true;
        }
        for (String current : allowedMappings) {
            if (requestedUser.equals(current)) {
                return true;
            }
        }
        return false;
    }

7.1.8. アプリケーションでのクライアントインターセプターの使用

アプリケーションは、プログラムを用いて、またはサービ出力ダーメカニズムを使用して、あるいは、ClientInterceptors アノテーションを使用して、クライアントインターセプターを EJBClientContext インターセプターチェーンに挿入できます。

注記

EJBClientInterceptor は、org.jboss.ejb.client.EJBClientInvocationContext#addReturnedContextDataKey(String key) を呼び出してサーバー側の呼び出しコンテキストから特定のデータをリクエストすることができます。要求したデータがコンテキストデータマップの提供されるキー以下に存在する場合、クライアントに送信されます。

7.1.8.1. クライアントインターセプターのプログラムでの挿入

インターセプターを登録した EJBClientContext を作成したら、インターセプターを挿入します。

以下のコードは、インターセプターの登録で EJBClientContext を作成する方法を示しています。

EJBClientContext ctxWithInterceptors = EJBClientContext.getCurrent().withAddedInterceptors(clientInterceptor);

EJBClientContext の作成後、インターセプターを挿入するオプションを使用できます。

  • 以下のコードは、Callable 操作を使用して EJBClientContext を適用して実行できます。Callable 操作内で実行される Jakarta Enterprise Beans 呼び出しはクライアント側のインターセプターを適用します。

    ctxWithInterceptors.runCallable(() -> {
        // perform the calls which should use the interceptor
    })
  • または、新規作成された EJBClientContext を新しいデフォルトとしてマークすることもできます。

    EJBClientContext.getContextManager().setThreadDefault(ctxWithInterceptors);

7.1.8.2. サービ出力ダーメカニズムを使用したクライアントインターセプターの挿入

META-INF/services/org.jboss.ejb.client.EJBClientInterceptor ファイルを作成し、クライアントアプリケーションのクラスパスに配置またはパッケージ化します。

ファイルのルールは、Java ServiceLoader Mechanism によって指示されます。

  • このファイルには、Jakarta Enterprise Beans クライアントインターセプター実装の完全修飾クラス名ごとに個別の行が含まれることが予想されます。
  • Jakarta Enterprise Beans クライアントインターセプタークラスはクラスパスで利用できる必要があります。

サービ出力ダーメカニズムを使用して追加された Jakarta Enterprise Beans クライアントインターセプターは、クラスパスにある順序で追加され、クライアントインターセプターチェーンの最後に追加されます。

7.1.8.3. ClientInterceptor アノテーションを使用したクライアントインターセプターの挿入

@org.jboss.ejb.client.annotation.ClientInterceptors アノテーションを使用すると、Jakarta Enterprise Beans インターセプターをリモート呼び出しのクライアント側に配置できます。

import org.jboss.ejb.client.annotation.ClientInterceptors;
@ClientInterceptors({HelloClientInterceptor.class})

public interface HelloBeanRemote {
   public String hello();
}