Quarkus中通用ExceptionMapper<Throwable>无法处理部分异常的咨询
已实现的通用ExceptionMapper
@Provider @JBossLog public class RestExceptionMapper implements ExceptionMapper<Throwable> { @Context HttpServerRequest request; @Context SecurityContext securityContext; @Context UriInfo uriInfo; @Override public Response toResponse(Throwable exception) { final Throwable rootCause = ExceptionUtils.getRootCause(exception); log.errorf("General exception: %s", rootCause.getMessage(), rootCause); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } }
已实现的REST资源类
@Path("/process-settings") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class ProcessSettingResource { @POST @RunOnVirtualThread public RestResponse<Object> createProcessSetting( @Valid CreateProcessSettingDto processSettingDto, @Context UriInfo uriInfo ) { URI location = uriInfo.getAbsolutePathBuilder() .path(String.valueOf(1L)) .build(); if (processSettingDto.getProcessInfo().getProcessName().equals("FAIL")) { // ResteasyReactiveViolationException 不会被 RestExceptionMapper 处理 throw new ResteasyReactiveViolationException("Process Name is invalid", new HashSet<>()); // RuntimeException 会被 RestExceptionMapper 处理 // throw new RuntimeException("Process Name is invalid"); // IllegalArgumentException 会被 RestExceptionMapper 处理 // throw new IllegalArgumentException("Process Name is invalid"); } return ResponseBuilder.create(Status.CREATED).location(location).build(); } }
疑问
为何ResteasyReactiveViolationException或其父类ConstraintViolationException无法被该通用ExceptionMapper处理,而RuntimeException、IllegalArgumentException却可以?这是Bug还是特性?规则出处在哪里?
更新内容
Quarkus的JAX-RS组件开发者已指向相关Issue及PR,但暂未了解方案细节。调试时发现Quarkus Resteasy预定义了针对以下异常的ExceptionMapper:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException io.quarkus.security.AuthenticationRedirectException com.fasterxml.jackson.databind.exc.MismatchedInputException io.quarkus.security.UnauthorizedException jakarta.validation.ValidationException io.quarkus.security.ForbiddenException io.quarkus.security.AuthenticationFailedException io.quarkus.security.AuthenticationCompletionException
根据规范,上述异常或其子类会被预定义Mapper优先处理。目前已通过包装异常解决问题,也可实现特定异常的Mapper,希望Quarkus/Resteasy社区给出官方处理方案。
解答
这是Quarkus Resteasy的特性,而非Bug,核心原因源于JAX-RS规范的ExceptionMapper优先级规则,以及Quarkus的内置实现:
JAX-RS规范的优先级逻辑
JAX-RS容器在选择ExceptionMapper时,会优先匹配最具体的类型。也就是说,针对某个异常的直接父类的Mapper,优先级高于通用的ThrowableMapper。比如,针对ValidationException的Mapper会比你的ExceptionMapper<Throwable>优先级更高。内置Mapper的拦截作用
ResteasyReactiveViolationException的继承链是:ResteasyReactiveViolationException→ConstraintViolationException→ValidationException。而Quarkus内置了针对jakarta.validation.ValidationException的ExceptionMapper,所以当抛出ResteasyReactiveViolationException时,这个内置的、更具体的Mapper会被选中处理,你的通用Mapper就不会被触发。RuntimeException/IllegalArgumentException可被处理的原因
这两个异常没有对应的Quarkus内置Mapper(没有比Throwable更具体的内置Mapper匹配它们),所以你的通用ExceptionMapper<Throwable>会被选中处理这些异常。
可行处理方案
- 自定义特定异常Mapper:
实现针对ResteasyReactiveViolationException或ConstraintViolationException的Mapper,由于它比内置的ValidationExceptionMapper更具体,会被优先调用:@Provider public class ResteasyReactiveViolationMapper implements ExceptionMapper<ResteasyReactiveViolationException> { @Override public Response toResponse(ResteasyReactiveViolationException exception) { // 自定义响应逻辑,比如返回400和错误信息 return Response.status(Status.BAD_REQUEST) .entity(Collections.singletonMap("error", exception.getMessage())) .build(); } } - 包装异常:
像你目前的做法一样,将ResteasyReactiveViolationException包装到自定义RuntimeException中,绕过内置Mapper的拦截。 - 禁用内置Validation Mapper:
通过Quarkus配置项quarkus.resteasy-reactive.validation.disable-validation-exception-mapper=true关闭内置的Validation异常处理Mapper,这样你的通用Mapper就能处理这类异常。
内容的提问来源于stack exchange,提问作者joe_specimen

