I have a project (Spring 5.3.7) that contains a controller and exception handlers using the @ControllerAdvice
and @ExceptionHandler
annotations.
I'm also using Spring Integration DSL to create a HTTP inbound gateway. No error channel is set on the gateway.
My exception handlers are not being called when an exception is thrown in the integration flow (in the same thread).
I actually did not expect it to work automatically, but while debugging the code I noticed that it actually almost all works except for one line.
Line 51-53 of AbstractHandlerMethodExceptionResolver
contains:
else if (hasGlobalExceptionHandlers() && hasHandlerMappings()) {
return super.shouldApplyTo(request, handler);
}
Because there are no handler mappings, my exceptions handlers are never applied to exceptions from the integration flow.
I'm surprised about the check of hasHandlerMappings()
in the if statement. Might this be a bug?
I ask this because the logic in the block above it, and in shouldApplyTo
, suggests that a resolver should always be applied when no handler mappings are available.
Line 183 of AbstractHandlerExceptionResolver
:
return !hasHandlerMappings();
If the code actually is intended to work like this, then I'd like to know how I can set the mappings so that the exception handlers work both for my controllers and spring integration.
I'm currently doing it in a very hacky way by iterating over the ExceptionHandlerExceptionResolver
beans, and doing some casts, and calling setMappedHandlerClasses
.
How are the handler mappings supposed to be set?
Comment From: poutsma
This is not a bug. The @ControllerAdvice
and @ExceptionHandler
annotations support error within the context of Spring Framework. In this context we cannot handle errors from Spring Integration, because adding a dependency on Spring Integration would introduce a cyclic dependency.
That said, it might be possible for Spring Integration to add their own support for the @ControllerAdvice
and @ExceptionHandler
annotations, so that you could also use them in that context. Pinging @artembilan because I am not a Spring Integration expert.
Comment From: robertvanloenhout
Hi @poutsma No dependency on Spring Integration is needed. DispatcherServlet is still used even though I use Spring Integration. The DispatcherServlet does the exception resolving as usual. All my exception resolvers are there. It just thinks it should not apply my global exception handlers, because there are no handler mappings. There are no handler mappings if use a Controller too, but in that case the additional check to see if there are any handler mappings isn't done, because the handler is a HandlerMethod.
I think everything will just work fine if the hasHandlerMappings() is removed from the if statement.
Comment From: poutsma
I think everything will just work fine if the hasHandlerMappings() is removed from the if statement.
I am sure that everything will be fine for your particular use case. I am not sure if that is also true for the other Spring users.
We can take a further look at this, if you could please take the time to provide a complete minimal sample (something that we can unzip or git clone, build, and deploy) that reproduces the problem.
Comment From: robertvanloenhout
I understand. I just didn't want to give the impression I'm asking for a big overhaul so it works nicely with spring integration. I have made a minimal project here: https://github.com/robertvanloenhout/spring-framework-27054 You can use requests.http to do a request handled by the controller, and a request handled by an integration flow. I'd like the call to http://localhost:8080/flow to use MyExceptionHandler and return my custom message.
Comment From: artembilan
I concur with @poutsma : while it is your use-case it doesn't mean that such a behavior must be by default.
See setMappedHandlerClasses
JavaDocs:
/**
* Specify the set of classes that this exception resolver should apply to.
* <p>The exception mappings and the default error view will only apply to handlers of the
* specified types; the specified types may be interfaces or superclasses of handlers as well.
* <p>If no handlers or handler classes are set, the exception mappings and the default error
* view will apply to all handlers. This means that a specified default error view will be used
* as a fallback for all exceptions; any further HandlerExceptionResolvers in the chain will be
* ignored in this case.
*/
public void setMappedHandlerClasses(Class<?>... mappedHandlerClasses) {
So, we may consider to customize it from the target project perspective.
See Spring Boot's WebMvcRegistrations
. Perhaps its getExceptionHandlerExceptionResolver()
with supplied HttpRequestHandlingEndpointSupport.class
should help you to solve your requirements:
@Bean
WebMvcRegistrations integrationMvcRegistration() {
return new WebMvcRegistrations() {
@Override
public ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() {
ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver = new ExceptionHandlerExceptionResolver();
exceptionHandlerExceptionResolver.setMappedHandlerClasses(HttpRequestHandlingEndpointSupport.class);
return exceptionHandlerExceptionResolver;
}
};
}
NOTE: Spring Boot 2.5
comes with Spring Integration 5.5
: no need to override it to something else manually.
Comment From: robertvanloenhout
Thanks @artembilan for giving a way for me to set the mapped handler classes.
Sorry that I still don't really understand. The javadoc you pasted states:
"If no handlers or handler classes are set, the exception mappings and the default error view will apply to all handlers."
So, why is this true for HandlerMethod
handlers, but not for HttpRequestHandlingEndpointSupport
or other handlers?
I don't see an explanation for that in the javadocs.
I have added the WebMvcRegistrations
bean. Setting the mappedHandlerClasses
to HttpRequestHandlingEndpointSupport.class
lets my exception handler be applied in the Spring Integration context. However then it no longer works in the MyController
context.
The only way I can make it work in both context is by doing
resolver.setMappedHandlerClasses(Object.class);
At least this solves it for my use case.
You have noted that Spring Boot 2.5
comes with Spring Integration 5.5
. Unfortunately spring-boot-starter-integration does not come with spring-integration-http
Therefore I have added this extra dependency. I forgot to update the spring-integration-http version, when updating the spring boot version in my real project.
It's something I find easy to forget when dealing with many pom files. Thanks for noticing this.
Comment From: poutsma
The underlying cause is that even though both Spring MVC and Spring Integration have web support, the way they work internally is quite different. For one thing, they use different kinds of handlers. In annotation-based MVC the handler is a HandlerMethod
. In Spring Integration, the handler is the HttpRequestHandlingMessagingGateway
which extends from HttpRequestHandlingEndpointSupport
.
As you can see in AbstractHandlerMethodExceptionResolver:: shouldApplyTo
, the path for HandlerMethod
instances is different than other types, which need to be mapped explicitly through the mappedClasses
property. The underlying assumption here is that annotation-based handler methods do not require explicit configuration, but other handlers do. As to why we can't make an exception for HttpRequestHandlingEndpointSupport
as well, we are running into my original explanation: we cannot introduce circular dependencies.
As an alternative to @artembilan's suggestion to register a custom resolver, you can also choose to customize the default resolvers by implementing a WebMvcConfigurer
. And instead of setting mappedHandlerClasses
an Object class, you can also set it to an array of classes. Adding this configuration class to your project did the job for me:
@Configuration
class WebConfig implements WebMvcConfigurer {
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
for (HandlerExceptionResolver resolver : resolvers) {
if (resolver instanceof ExceptionHandlerExceptionResolver exceptionResolver) {
exceptionResolver.setMappedHandlerClasses(HttpRequestHandlingMessagingGateway.class, MyController.class);
}
}
}
}
Comment From: artembilan
Unfortunately
spring-boot-starter-integration
does not come withspring-integration-http
It must not. We can't assume that target project is always going to use web. Well, from the big height Spring Integration is about Messaging not Web.
I forgot to update the
spring-integration-http
version
You must not do that. Spring Boot manages a BOM dependency for Spring Integration and all the modules you are going to declare in your project are going to inherit the version from dependency management controlled by Spring Boot. Typically we don't recommend to override version for deps which are controlled by Spring Boot: only when there is a critical bug to fix quickly on the end-user side.
Now re. error handling: I think @poutsma did the proper point. The default global @ExceptionHandler
is really for method handlers like @RequestMapping
which is a declarative annotation based configuration. Since there is no end-user method signature when we declare Spring Integration channel adapter, but rather some generic low-level API, it is better to control error handling also programmatic way. You may consider to use an Http.inboundControllerGateway()
instead which is very close to the @Controller
infrastructure and comes with viewName
and errorCode
to handle exceptions some desired way.