第 7 章 Jakarta Enterprise Beans Interceptors
7.1. 自定义 Interceptors
JBoss EAP 允许您开发和管理自定义 Jakarta 企业 Beans 拦截器。
您可以创建以下拦截器类型:
客户端拦截器
当 JBoss EAP 充当客户端时,运行客户端拦截器。
服务器拦截器
当 JBoss EAP 充当服务器时,运行服务器拦截器。这些拦截器为服务器全局配置。
容器拦截器
当 JBoss EAP 充当服务器时,容器拦截器会运行。这些拦截器在 Jakarta Enterprise Beans 容器中配置。
自定义拦截器类应添加到模块中,并存储在 $JBOSS_HOME/modules 目录中。
7.1.1. Interceptor Chain
自定义拦截器在拦截器链中的特定点执行。
为 Jakarta Enterprise Beans 配置的容器拦截器在 Wildfly 提供的拦截器(如安全拦截器或事务管理拦截器)之前执行。因此,容器拦截器可以在调用 Wildfly 拦截器或全局拦截器前处理或配置上下文数据。
服务器和客户端拦截器会在 Wildfly 特定拦截器后执行。
7.1.2. 自定义客户端 Interceptors
自定义客户端拦截器实施 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. 自定义服务器 Interceptors
服务器拦截器使用 @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 规范的定义,标准 Jakarta EE 拦截器应该在容器完成了安全性上下文传播、交易管理和其他容器提供的调用处理后运行。
以下代码演示了一个拦截器类,用于标记调用的 iAmAround 方法。
容器拦截器代码示例
public class ClassLevelContainerInterceptor {
@AroundInvoke
private Object iAmAround(final InvocationContext invocationContext) throws Exception {
return this.getClass().getName() + " " + invocationContext.proceed();
}
}
容器拦截器和 Jakarta EE Interceptor API 之间的差异
虽然容器拦截器被建模为类似于 Jakarta EE 拦截器,但 API 的语义存在一些差别。例如,容器拦截器调用 javax.interceptor.InvocationContext.getTarget() 方法是非法的,因为这些拦截器会在 Jakarta Enterprise Beans 组件设置或实例化前被调用。
7.1.5. 配置容器拦截器
容器拦截器使用标准的 Jakarta EE 拦截器库。
因此,它们将 ejb-jar.xml 文件中允许的相同的 XSD 元素用于 ejb-jar 部署描述符的 3.2 版本。
由于它们基于标准的 Jakarta EE 拦截器库,因此只能使用部署描述符来配置容器拦截器。按照设计,应用不需要任何特定于 JBoss EAP 的注释或其他库依赖关系。
配置容器拦截器:
-
在 Jakarta Enterprise Beans 部署的
META-INF/'目录中创建一个jboss-ejb3.xml文件。 在描述符文件中配置容器拦截器元素。
-
使用
urn:container-interceptors:1.0命名空间来指定容器拦截器元素的配置。 -
使用
<container-interceptors>元素来指定容器拦截器。 使用
<interceptor-binding>元素将容器拦截器绑定到 Jakarta Enterprise Beans。可以使用以下任一方式绑定拦截器:- 使用通配符(*)将拦截器绑定到部署中的所有 Jakarta Enterprise Beans。
- 使用特定的 Jakarta Enterprise Beans 名称,在个人 Bean 级别绑定拦截器。
在 Jakarta 企业 Bean 的具体方法级别上绑定拦截器。
注意这些元素使用 Jakarta Enterprise Beans 3.2 XSD 配置,其方式与 Jakarta EE 拦截器相同。
-
使用
以下示例描述符文件演示了配置选项。
容器 Interceptor 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。使用以下命令,将 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. 更改安全性上下文身份
您可以向经过身份验证的用户授予权限,以不同用户身份切换身份和对现有连接执行请求,而不是打开多个客户端连接。
默认情况下,当您远程调用部署到应用服务器的 Jakarta Enterprise Beans 时,与服务器的连接将进行身份验证,并且使用连接的任何后续请求都使用原始身份验证的身份执行。对于客户端到服务器调用和服务器对服务器调用也是如此。如果您需要使用来自同一客户端的不同身份,通常您必须打开与服务器的多个连接,以便每个连接都作为不同的身份进行身份验证。相反,您可以允许经过身份验证的用户更改身份。
更改经过身份验证的用户的身份:
在拦截器代码中实施身份更改。
客户端拦截器
拦截器必须通过上下文数据映射传递请求的身份,该映射可通过对
EJBClientInvocationContext.getContext.getContext()的调用来获取。以下示例代码演示了可切换身份的客户端拦截器。客户端拦截器代码示例
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);; } } }
-
应用可以通过编程方式或使用服务加载器机制将客户端拦截器插入到
EJBClientContext拦截器链中。有关配置客户端拦截器的步骤,请参阅 在应用程序中使用客户端拦截器。 创建 Jakarta 身份验证登录模块。
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#addReturnedContext(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 机制 规定。
- 此文件应当包含单独的行,用于 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();
}