【问题标题】:How to manage state in EJBClientInterceptor如何在 EJBClientInterceptor 中管理状态
【发布时间】:2020-06-16 21:33:03
【问题描述】:

org.jboss.ejb.client.EJBClientInterceptor中处理状态的正确方法是什么?

在 Wildfly 12 中,我想为我们的通用拦截器链创建一个适配器。 让我们简化问题以创建简单的持续时间日志记录 EJB 客户端拦截器。 不幸的是,EJBClientInterceptor 2-method 的设计很奇怪:

public interface EJBClientInterceptor {

    /**
     * Handle the invocation.  Implementations may short-circuit the invocation by throwing an exception.  This method
     * should process any per-interceptor state and call {@link EJBClientInvocationContext#sendRequest()}.
     *
     * @param context the invocation context
     * @throws Exception if an invocation error occurs
     */
    void handleInvocation(EJBClientInvocationContext context) throws Exception;

    /**
     * Handle the invocation result.  The implementation should generally call {@link EJBClientInvocationContext#getResult()}
     * immediately and perform any post-invocation cleanup task in a finally block.
     *
     * @param context the invocation context
     * @return the invocation result, if any
     * @throws Exception if an invocation error occurred
     */
    Object handleInvocationResult(EJBClientInvocationContext context) throws Exception;
}

问题是由于拦截器分为2个方法,你必须将状态从“之前”部分(handleInvocation)转移到“之后”部分(handleInvocationResult)。

流程概览

    @Override
    public void handleInvocation(EJBClientInvocationContext ejbClientInvocationContext) throws Exception {
        System.out.println("handleInvocation - before sendRequest");
        ejbClientInvocationContext.sendRequest();
        System.out.println("handleInvocation - after sendRequest");
    }

    @Override
    public Object handleInvocationResult(EJBClientInvocationContext ejbClientInvocationContext) throws Exception {
        System.out.println("handleInvocationResult - before getResult");
        try {
            return ejbClientInvocationContext.getResult();
        } finally {
            System.out.println("handleInvocationResult - after getResult");
        }
    }

此输出中的结果

client_1  | 16:50:24,575 INFO  [stdout] (default task-1) handleInvocation - before sendRequest
client_1  | 16:50:24,718 INFO  [stdout] (default task-1) handleInvocation - after sendRequest
server_1  | 16:50:25,737 INFO  [stdout] (default task-2) Doing work at EJB server
client_1  | 16:50:25,771 INFO  [stdout] (default task-1) handleInvocationResult - before getResult
client_1  | 16:50:25,795 INFO  [stdout] (default task-1) handleInvocationResult - after getResult

糟糕的解决方案 #1 - 实例字段

另一个问题是拦截器实例是单例的并且被所有调用重用。所以你不能像这样在拦截器中使用字段

public class DurationLogging1ClientInterceptor implements EJBClientInterceptor {

    private long startTime;

    @Override
    public void handleInvocation(EJBClientInvocationContext ejbClientInvocationContext) throws Exception {
        startTime = System.currentTimeMillis();
        ejbClientInvocationContext.sendRequest();
    }

    @Override
    public Object handleInvocationResult(EJBClientInvocationContext ejbClientInvocationContext) throws Exception {
        try {
            return ejbClientInvocationContext.getResult();
        } finally {
            long duration = System.currentTimeMillis() - startTime;
            System.out.println("Call took " + duration + "ms");
        }
    }
}

糟糕的解决方案 #2 - ThreadLocal

另一种方法是使用ThreadLocal

public class DurationLogging2ClientInterceptor implements EJBClientInterceptor {

    private final ThreadLocal<Long> startTimeTL = new ThreadLocal<>();

    @Override
    public void handleInvocation(EJBClientInvocationContext ejbClientInvocationContext) throws Exception {
        startTimeTL.set(System.currentTimeMillis());
        ejbClientInvocationContext.sendRequest();
    }

    @Override
    public Object handleInvocationResult(EJBClientInvocationContext ejbClientInvocationContext) throws Exception {
        try {
            return ejbClientInvocationContext.getResult();
        } finally {
            long startTime = startTimeTL.get();
            long duration = System.currentTimeMillis() - startTime;
            System.out.println("Call took " + duration + "ms");
        }
    }
}

但我不确定方法handleInvocationResult 是否保证在与handleInvocation 相同的线程中被调用。即使是这样,我也不喜欢ThreadLocal 的用法。

糟糕的解决方案 #3 - 使用 contextData 地图

另一种方式是通过EJBClientInvocationContext参数传递状态,可能使用getContextData()属性:

public class DurationLogging3ClientInterceptor implements EJBClientInterceptor {

    @Override
    public void handleInvocation(EJBClientInvocationContext ejbClientInvocationContext) throws Exception {
        ejbClientInvocationContext.getContextData().put("startTime", System.currentTimeMillis());
        ejbClientInvocationContext.sendRequest();
    }

    @Override
    public Object handleInvocationResult(EJBClientInvocationContext ejbClientInvocationContext) throws Exception {
        try {
            return ejbClientInvocationContext.getResult();
        } finally {
            long startTime = (Long) ejbClientInvocationContext.getContextData().get("startTime");
            long duration = System.currentTimeMillis() - startTime;
            System.out.println("Call took " + duration + "ms");
        }
    }
}

但是这个 contextData 映射被序列化并发送到 EJB 服务器,这是我不希望的。

以前的 Jboss 拦截器设计

在以前的 Jboss 版本中,org.jboss.aop.advice.Interceptor 设计更容易解决所有这些问题:

public interface Interceptor {
    String getName();

    Object invoke(Invocation invocation) throws Throwable;
}

持续时间日志拦截器可以这样写

public class DurationLogging4ClientInterceptor implements Interceptor {

    @Override
    public String getName() {
        return getClass().getName();
    }

    @Override
    public Object invoke(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis();

        try {
            return invocation.invokeNext();
        } finally {
            long duration = System.currentTimeMillis() - startTime;
            System.out.println("Call took " + duration + "ms");
        }
    }
}

Dockerized 演示项目来玩

我在 docker-compose 中创建了一个包含 2 个 Wildfly 实例的演示项目。一个实例充当 ejb 客户端,另一个实例充当 ejb 服务器。有多个带有拦截器的 EJB 调用场景演示。

https://github.com/petr-ujezdsky/w2w

【问题讨论】:

  • 您能否澄清handleInvocationhandleInvocationResult 是否确实会在同一个线程中?我认为他们需要这样才能使包装 EJB 调用的逻辑成功,但还没有找到表明一种或另一种方式的文档。你能确定它们是同一个线程吗?

标签: java wildfly ejb-3.0 interceptor


【解决方案1】:

我认为解决方案 3 还不错,可以通过一些技巧来改进。

如果您不想通过线路发送实际对象,也许您可​​以将实际对象的“哈希”放入上下文数据中(我假设您可以为此目的使用 Objects.hashcode())。

同时,您将使用此唯一哈希键控的对象保存在静态 LRUMap 字段 (https://commons.apache.org/proper/commons-collections/apidocs/org/apache/commons/collections4/map/LRUMap.html) 中。

这样,当您在第二种方法中取回控件时,您可以根据其哈希查找已保存到静态 Map 中的内容。

【讨论】:

  • 我想你可以.. 但是静态存储必须与 所有 服务器调用同步。您还必须确保正确地从映射中删除状态以防止内存泄漏。这个存储也应该存在于每个给定的拦截器中。您还应该创建新对象来描述状态应该具有的类型,而不是那么漂亮:(
  • @PetrÚjezdský关于内存泄漏:LRUMap 负责删除“旧”数据;这是一张最近使用的地图,它会丢弃(因此释放)未使用的对象。
  • @PetrÚjezdský 1. 关于内存泄漏:LRUMap 负责删除“旧”数据;这是最近使用的最后一张地图,它将丢弃(因此释放)未使用的对象。您还可以通过手动删除 handle*result() 来帮助它。 2.关于同步:我想建议在LRUmap中应该使用Objects.hashcode(o)作为key; AFAIK Objects.hashcode() 返回给定对象的内存地址,因此 Map 上不会存在数据竞争(假设保存的对象在服务器调用期间未完成)。我已经编辑了我的答案,Objects.hashcode()...
猜你喜欢
  • 2011-02-08
  • 1970-01-01
  • 2016-08-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-16
  • 1970-01-01
  • 2023-01-17
相关资源
最近更新 更多