Quarkus @ServerExceptionMapper:解包异常并转发至后续异常映射器
我有若干会抛出WebApplicationException的HTTP端点,这些异常已通过全局异常映射器处理:
@ServerExceptionMapper public Response unwrapWebApplicationException(WebApplicationException exception) { return Response .status(exception.getResponse().getStatus()) .entity("Bla bla something happened, technical details: " + exception.getResponse().getEntity()) .build(); }
但部分场景下,WebApplicationException会被ArcUndeclaredThrowableException等异常包裹,示例代码如下:
@Path("/hello") public class GreetingResource { @GET @Produces(MediaType.TEXT_PLAIN) public String hello() { WebApplicationException webApplicationException = new WebApplicationException(Response.Status.CONFLICT); throw new ArcUndeclaredThrowableException(webApplicationException); } }
我的目标是编写一个异常映射器,解包ArcUndeclaredThrowableException后,将内部异常交给已存在的后续异常映射器处理。我尝试用jakarta.ws.rs.ext.Providers#getExceptionMapper实现逻辑,编写了工具方法:
/** * Attempts to unwrap the exception's cause (shallowly, not recursively) and invoke that exception's exception mapper instead. * If the exception has no cause, it is thrown as-is. If it has a cause but the cause has no exception mapper, the cause exception is thrown as-is. */ private static <T extends Throwable> Response unwrapCause(T exception, Providers providers) throws Throwable { var cause = exception.getCause(); if (cause == null) { throw exception; } @SuppressWarnings("unchecked") Class<Throwable> exceptionClass = (Class<Throwable>) cause.getClass(); ExceptionMapper<Throwable> exceptionMapper = providers.getExceptionMapper(exceptionClass); if (exceptionMapper == null) { throw cause; } return exceptionMapper.toResponse(cause); }
但获取Providers实例时遇到问题:
- 将
Providers作为异常映射器方法参数时,Quarkus构建报错:
Caused by: java.lang.RuntimeException: Parameter 'providers' of method 'unwrapArcUndeclaredThrowableExceptions of class 'org.acme.ExceptionMappers' is not allowed at org.jboss.resteasy.reactive.server.processor.generation.exceptionmappers.ServerExceptionMapperGenerator.getTargetMethodParamsInfo(ServerExceptionMapperGenerator.java:607) at org.jboss.resteasy.reactive.server.processor.generation.exceptionmappers.ServerExceptionMapperGenerator.generateRRResponse(ServerExceptionMapperGenerator.java:478) at org.jboss.resteasy.reactive.server.processor.generation.exceptionmappers.ServerExceptionMapperGenerator.generateGlobalMapper(ServerExceptionMapperGenerator.java:313) at io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveScanningProcessor.handleCustomAnnotatedMethods(ResteasyReactiveScanningProcessor.java:410) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at io.quarkus.deployment.ExtensionLoader$3.execute(ExtensionLoader.java:849) at io.quarkus.builder.BuildContext.run(BuildContext.java:256) at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18) at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513) at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538) at java.base/java.lang.Thread.run(Thread.java:1583) at org.jboss.threads.JBossThread.run(JBossThread.java:501)
- 使用
@Context注入Providers时,运行时报错:
java.lang.IllegalStateException: This should never be called at org.acme.ExceptionMappers$GeneratedExceptionHandlerFor$WebApplicationException$OfMethod$unwrapWebApplicationException.toResponse(Unknown Source) at org.acme.ExceptionMappers$GeneratedExceptionHandlerFor$WebApplicationException$OfMethod$unwrapWebApplicationException.toResponse(Unknown Source) at org.acme.ExceptionMappers.unwrapCause(ExceptionMappers.java:49) at org.acme.ExceptionMappers.unwrapArcUndeclaredThrowableExceptions(ExceptionMappers.java:32) at org.acme.ExceptionMappers$GeneratedExceptionHandlerFor$ArcUndeclaredThrowableException$OfMethod$unwrapArcUndeclaredThrowableExceptions.toResponse(Unknown Source) at org.acme.ExceptionMappers$GeneratedExceptionHandlerFor$ArcUndeclaredThrowableException$OfMethod$unwrapArcUndeclaredThrowableExceptions.toResponse(Unknown Source) at org.jboss.resteasy.reactive.server.core.RuntimeExceptionMapper.mapException(RuntimeExceptionMapper.java:98) at org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext.mapExceptionIfPresent(ResteasyReactiveRequestContext.java:346) at org.jboss.resteasy.reactive.server.handlers.ExceptionHandler.handle(ExceptionHandler.java:15) at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:150) at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147) at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:599) at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513) at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538) at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29) at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29) at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) at java.base/java.lang.Thread.run(Thread.java:1583)
在Quarkus的Resteasy Reactive环境中,直接操作Providers会触发兼容性问题,最可靠的方案是抛出解包后的内部异常,让框架自动匹配已有的异常映射器。
基础实现
针对ArcUndeclaredThrowableException编写专用映射器:
@ServerExceptionMapper public Response unwrapArcUndeclaredThrowableException(ArcUndeclaredThrowableException exception) throws Throwable { // 直接抛出内部异常,框架会自动触发WebApplicationException的映射器处理 throw exception.getCause(); }
原理说明
Resteasy Reactive的异常处理流程支持链式匹配:当当前映射器抛出新异常时,框架会重新启动异常匹配逻辑,自动找到最适合的映射器(也就是你已有的WebApplicationException全局映射器)。这种方式完全避开了Providers的注入和调用问题,逻辑简洁且符合框架设计。
扩展处理(可选)
如果需要处理多层嵌套的异常,或者要确保只传递有对应映射器的异常,可以实现递归解包逻辑:
@ServerExceptionMapper public Response unwrapWrappedExceptions(Throwable exception) throws Throwable { Throwable current = exception; // 递归解包,直到找到WebApplicationException或无嵌套异常 while (current.getCause() != null && !(current instanceof WebApplicationException)) { current = current.getCause(); } // 若最终找到WebApplicationException则抛出,否则抛出原异常 throw current instanceof WebApplicationException ? current : exception; }
注意:这个通用映射器的优先级要低于特定异常的映射器(比如你的WebApplicationException映射器),避免提前拦截其他异常。
内容的提问来源于stack exchange,提问作者Felk

