【问题标题】:How to correctly use CDI in @Asynchronous method?如何在@Asynchronous 方法中正确使用 CDI?
【发布时间】:2017-08-07 11:22:54
【问题描述】:

我有一个 JSF 2 应用程序(在 JBoss AS 7.1 之上运行),当用户单击页面中的按钮时,它必须启动一个长进程。这个想法是有一个不阻塞的交互,因此用户可以等待并查看结果,或者只是关闭页面并稍后再回来查看它的进展情况或结果,如果该过程已经结束。

流程本身被编码在以下(简化的)类中:

@Stateless
@LocalBean
@ApplicationScoped
public class MyProcessManager {
    @Inject
    private ProcessHelper processHelper;

    @Asynchronous
    public void start(final ProcessParameters parameters) {
        // the process...
    }
}

这样的类被标记为@ApplicationScoped,因为所有正在运行的进程(对于所有用户)都由它保存。所以,当按钮被点击时,backing bean设置一些参数并调用异步方法start()

在进程尝试使用processHelper 之前一切正常,该进程会运行许多 Hibernate 查询以继续进程的持久性部分。当processHelper 的第一个方法被调用时,我得到以下异常:

WELD-001303: No active contexts for scope type javax.enterprise.context.RequestScoped

作为附加信息,此类方法内部的断点永远不会被命中。

发生了什么以及如何解决?

【问题讨论】:

    标签: jsf asynchronous ejb cdi


    【解决方案1】:

    异常表明ProcessHelper@RequestScoped

    @Asynchronous 被调用时,会产生一个全新的独立线程,它由 HTTP servlet 容器控制。因此,在该线程的上下文中,任何地方都没有 HTTP 请求或 HTTP 会话。你只能用@ApplicationScoped,不能用@RequestScoped,更不用说@SessionScoped

    至于ProcessManager 本身,@Stateless @ApplicationScoped 的组合没有意义。你很可能实际上想要一个@javax.ejb.Singleton。额外的好处是它是有状态的,因此您可以将流程结果作为实例变量保存在那里。

    您提到ProcessHelper 反过来运行一些数据库查询。这意味着它应该在事务中运行。在这种情况下,您应该使它成为一个完全有价值的 EJB,而不是一个 CDI 托管 bean。因此,也将ProcessHelper 设为@Stateless,或者将所有数据库交互作业移至ProcessManager EJB。这也是可能的。

    所以,总而言之,应该这样做:

    <h:form>
        <h:commandButton value="Start" action="#{processBacking.start}" />
    </h:form>
    <p>
        Result (manually refresh page to check): #{processBacking.result}
    </p>
    

    @Named
    @RequestScoped
    public class ProcessBacking {
    
        @Inject
        private ProcessManager processManager;
    
        public void start() {
            // ...
            processManager.start(parameters);
        }
    
        public ProcessResult getResult() {
            return processManager.getResult();
        }
    
        // ...
    }
    

    @Singleton
    @ConcurrencyManagement(ConcurrencyManagementType.BEAN)
    public class ProcessManager {
    
        private ProcessResult result;
    
        @Inject
        private ProcessHelper helper;
    
        @Asynchronous
        @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
        public void start(ProcessParameters parameters) {
            ProcessResult result = runSomeLongRunningNonTransactionalProcess(parameters);
            this.result = helper.persist(result);
        }
    
        public ProcessResult getResult() {
            return result;
        }
    
    }
    

    @Stateless
    public class ProcessHelper {
    
        @PersistenceContext
        private EntityManager entityManager;
    
        @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
        public ProcessResult persist(ProcessResult result) {
            entityManager.persist(result);
            return result;
        }
    
    }
    

    请注意@Singleton 默认情况下是读/写锁定的。因此,在 start() 完成之前,您不能调用 getResult()。因此ConcurrencyManagementType.BEAN,这意味着它已解锁,因此本质上调用者本身负责并发管理。这使您可以在进程仍在运行时不断刷新页面。

    另见:

    【讨论】:

      【解决方案2】:

      我已经为此寻找解决方案,但找不到明确的答案。但它可能对你有用。

      显然,CDI 不会使用异步方法传播其范围(实际上它不会将上下文传播到任何其他线程),因此当您尝试 @Inject 资源时会收到错误消息。我找不到有关此行为的适当文档,但我遇到了至少几个问题,其中的问题与您的问题非常相似:

      Java injection inside @Asynchronous bean

      Communicating between cdi session contexts — with database, will proper cdi context be invoked?

      如果我们假设这实际上是 CDI 的当前行为,那么解决此问题的最佳选择是摆脱 @Inject 并创建 ProcessHelper 的新实例,或者可能摆脱 @ 987654326@ 注释并使用ManagedExecutorService,就像我在上面链接的第一个问题中所建议的那样。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-11-20
        相关资源
        最近更新 更多