异步方法中ConcurrentAccessTimeoutException异常处理方案咨询
我来给你分析下问题出在哪,以及几个可行的解决办法:
首先,为什么你在REST接口的try-catch里抓不到ConcurrentAccessTimeoutException?因为@Asynchronous方法的调用是非阻塞的——当你调用mainService.mainTask()时,EJB容器只是把这个任务提交到异步线程池,然后立刻返回,你的trigger()方法也就直接返回了"Triggered"。而那个超时异常,是在容器后续执行异步任务的时候才抛出的,这时候你的trigger()方法早就执行完了,自然抓不到这个异常。
接下来给你几个可行的方案,从根本上解决这个问题:
方案一:在服务层控制任务的唯一性(最推荐)
既然你的任务不需要并行执行,那最好的办法就是在MainService里加一个线程安全的状态标记,确保同一时间只有一个任务在运行,从根源上避免提交重复任务导致的异常。
修改你的MainService:
import java.util.concurrent.atomic.AtomicBoolean; import javax.ejb.Asynchronous; import javax.ejb.Singleton; @Singleton public class MainService { // 用原子类保证线程安全的状态标记 private final AtomicBoolean isTaskRunning = new AtomicBoolean(false); @Asynchronous public void mainTask() { // CAS操作:只有当前状态是false时,才设置为true,避免竞态条件 if (!isTaskRunning.compareAndSet(false, true)) { // 任务已经在运行,直接退出 return; } try { // 这里写你的长时间运行任务逻辑 // ... } finally { // 无论任务成功还是失败,都重置状态标记 isTaskRunning.set(false); } } // 给REST层提供检查任务状态的方法 public boolean isTaskInProgress() { return isTaskRunning.get(); } }
然后修改你的REST接口,在提交任务前先检查状态:
@Path(ServiceRest.URL) public class ServiceRest { @Inject MainService mainService; private static final Logger logger = Logger.getLogger(ServiceRest.class.getName()); @GET public String trigger(){ if (mainService.isTaskInProgress()) { String message = "Already in progress."; logger.error(message); return message; } mainService.mainTask(); return "Triggered"; } }
这个方案的好处是完全避免了重复提交任务,也就不会触发那个超时异常,同时保证了线程安全,没有竞态条件。
方案二:用Future捕获异步任务的异常
如果你想保留容器的异步线程池管理,同时捕获异常避免日志报错,可以让@Asynchronous方法返回Future,这样就能在后续处理异常:
修改MainService:
import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import javax.ejb.Asynchronous; import javax.ejb.Singleton; @Singleton public class MainService { @Asynchronous public Future<Void> mainTask(){ try { // 你的长时间任务逻辑 // ... return CompletableFuture.completedFuture(null); } catch (Exception e) { // 捕获任务执行中的异常,封装到Future里 return CompletableFuture.failedFuture(e); } } }
然后修改REST接口,通过Future的回调处理异常:
@Path(ServiceRest.URL) public class ServiceRest { @Inject MainService mainService; private static final Logger logger = Logger.getLogger(ServiceRest.class.getName()); @GET public String trigger(){ Future<Void> taskFuture = mainService.mainTask(); // 注册回调,异步处理任务的异常 ((CompletableFuture<Void>) taskFuture).whenComplete((result, ex) -> { if (ex != null) { if (ex instanceof javax.ejb.ConcurrentAccessTimeoutException) { logger.error("Task rejected: Already in progress."); } else { logger.error("Task failed unexpectedly", ex); } } }); return "Triggered"; } }
这个方案可以捕获到异步任务执行时的异常,避免日志里出现未处理的堆栈信息,但要注意:如果是线程池满导致的提交失败,有些容器可能在提交任务时就抛出异常,这时候你可以把mainService.mainTask()放在try-catch里捕获。
方案三:调整EJB容器的异步线程池配置
如果只是想缓解线程池满的问题,可以调整应用服务器的异步线程池参数(比如最大线程数、超时时间)。不过这只是治标不治本,因为你的任务不需要并行,就算线程池大了,还是可能出现多个任务同时运行的情况,不符合你的需求。
比如在WildFly里,你可以修改standalone.xml里的async线程池配置;在WebLogic里,调整EJB异步线程池的参数。但这个方案不推荐作为主要解决办法,还是结合方案一或二更好。
总结一下,最适合你的场景是方案一,因为它从根源上保证了任务不会并行执行,完全避免了超时异常的产生,逻辑也更清晰。
内容的提问来源于stack exchange,提问作者Alexandr

