7장. Jakarta Enterprise Beans Interceptors

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 주석을 사용하여 빈에서 호출되는 메서드를 표시합니다.

다음 코드는 서버 인터셉터의 예를 보여줍니다.

서버 인터셉터 코드 예

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 주석을 사용하여 빈에서 호출되는 메서드를 표시합니다.

Jakarta Enterprise Beans 3.2 사양에 정의된 표준 Jakarta EE 인터셉터는 컨테이너에서 보안 컨텍스트 전파, 트랜잭션 관리 및 호출 처리를 완료한 후 실행될 것으로 예상됩니다.

다음 코드는 호출을 위한 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 Bean에 바인딩합니다. 인터셉터는 다음 방법 중 하나로 바인딩할 수 있습니다.

      • 와일드카드(*)를 사용하여 배포의 모든 Jakarta Enterprise Beans에 인터셉터를 연결합니다.
      • 특정 Jakarta Enterprise Beans 이름을 사용하여 개별 빈 수준에서 인터셉터를 바인딩합니다.
      • Jakarta Enterprise Bean의 특정 메서드 수준에서 인터셉터를 바인딩합니다.

        참고

        이러한 요소는 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 특성을 사용하면 인터셉터 바인딩에서 정규 표현식을 사용하고 지정된 정규 표현식과 일치하는 모든 빈에 인터셉터를 매핑할 수 있습니다. 다음 관리 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. 보안 컨텍스트 ID 변경

여러 클라이언트 연결을 여는 대신 인증된 사용자에게 ID를 전환하고 기존 연결에 대한 요청을 다른 사용자로 실행할 수 있는 권한을 부여할 수 있습니다.

기본적으로 애플리케이션 서버에 배포된 자카르타 엔터프라이즈 빈에 대한 원격 호출을 수행할 때 서버에 대한 연결이 인증되며 해당 연결을 사용하는 후속 요청은 원래 인증된 ID를 사용하여 실행됩니다. 이는 클라이언트-서버와 서버 간 호출에 모두 적용됩니다. 동일한 클라이언트의 다른 ID를 사용해야 하는 경우 일반적으로 서로 다른 ID로 인증되도록 서버에 대한 여러 연결을 열어야 합니다. 대신 인증된 사용자가 ID를 변경할 수 있습니다.

인증된 사용자의 ID를 변경하려면 다음을 수행합니다.

  1. 인터셉터 코드에서 ID 변경 사항을 구현합니다.

    • 클라이언트 인터셉터

      인터셉터는 요청된 ID를 EJBClientInvocationContext.getContextData() 호출을 사용하여 가져올 수 있는 컨텍스트 데이터 맵을 통해 전달해야 합니다. 다음 예제 코드는 ID를 전환하는 클라이언트 인터셉터를 보여줍니다.

      클라이언트 인터셉터 코드 예

      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();
          }
      }

    • 컨테이너 및 서버 인터셉터

      이러한 인터셉터는 ID가 포함된 InvocationContext 를 수신하고 해당 새 ID로 전환하도록 요청합니다. 다음 코드는 컨테이너 인터셉터의 축약 예를 보여줍니다.

      컨테이너 인터셉터 코드 예

      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 인터셉터 체인에 삽입할 수 있습니다. 클라이언트 인터셉터 구성 지침은 애플리케이션에서 클라이언트 인터셉터 사용을 참조하십시오.
  3. Jakarta Authentication 로그인 모듈을 생성합니다.

    Jakarta Authentication LoginModule 구성 요소는 사용자가 요청한 ID로 요청을 실행할 수 있는지 확인합니다. 다음 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 인터셉터 체인에 클라이언트 인터셉터를 프로그래밍 방식으로 삽입할 수 있습니다.

참고

EJBClientInterceptororg.jboss.ejb.client.EJBClientInvocationContext#addReturnedContextDataKey(문자열 키) 를 호출하여 서버 측 호출 컨텍스트에서 특정 데이터를 요청할 수 있습니다. 요청된 데이터가 컨텍스트 데이터 맵에 제공된 키 아래에 있는 경우 클라이언트로 전송됩니다.

7.1.8.1. 프로그래밍 방식으로 클라이언트 인터셉터 삽입

등록된 인터셉터를 사용하여 EJBClientContext 를 생성한 후 인터셉터를 삽입합니다.

다음 코드는 인터셉터 등록을 사용하여 EJBClientContext 를 생성하는 방법을 보여줍니다.

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

EJBClientContext 생성 후 인터셉터를 삽입하는 데 두 가지 옵션을 사용할 수 있습니다.

  • 호출 가능한 작업을 사용하여 EJBClientContext 가 적용된 다음 코드를 실행할 수 있습니다. call able 작업 내에서 수행되는 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 클라이언트 인터셉터 구현의 정규화된 클래스 이름에 대해 별도의 줄이 포함되어야 합니다.
  • class 경로에서 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();
}