Workaround Spring Boot unability to make Jersey and Spring MVC co-exist
In the current code if the Jersey component is active (with the jersey
profile) then all Spring Web MVC endpoint are not available anymore, if both are
using the same root /
.
Run tests in com.github.bric3.jerseywebmvc.JerseyWebmvcApplicationTests
.
jersey
profile-
GET /jaxrs
⇒404 Not Found
but Jersey is not active -
GET /actuator/status
⇒200 OK
-
GET /favicon.ico
⇒200 OK
-
GET /doc/
⇒200 OK
-
GET /doc/index.html
⇒200 OK
-
GET /rest/
⇒200 OK
jersey
profile-
GET /jaxrs
⇒200 OK
-
GET /actuator/status
⇒404 Not Found
-
GET /favicon.ico
⇒404 Not Found
-
GET /doc/
⇒404 Not Found
-
GET /doc/index.html
⇒404 Not Found
-
GET /rest/
⇒404 Not Found
While having a two technology to expose endpoints may seem brittle, this is quite useful if :
-
One want to expose a few static resources like documentation alongside JAXRS / Jersey endpoints.
-
One want to have actuator endpoints available from the root path.
-
One want to migrate one endpoint at a time from - to Spring WebMVC.
The following workaround covers part that Spring Boot does not.
Workarounds
Servlet filter mitigation
Using the filter from this issue. And configuring this filter with the right prefixes work.
jersey
and filter-mitigation
profiles-
GET /jaxrs
⇒200 OK
-
GET /actuator/status
⇒200 OK
-
GET /favicon.ico
⇒200 OK
-
GET /doc/
⇒404 Not Found
it doesn’t work because when Spring MVC forwards the request it aks the container, however the request is forwarded to the Jersey servlet. -
GET /doc/index.html
⇒200 OK
-
GET /rest/
⇒200 OK
The idea is to have a filter that can forward request to the
correct servlet. Using the standard JEE servletContext
it is possible to ask
for a RequestDispatcher
based the servlet name that should receive the
request. For that this code needs the dispatcherServlet
registration name
which happens to be a public constant of
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME
.
Then if the request URI path starts with the configured prefixes
the request will be forwarded to the Spring dispatcherServlet
.
By doing so this bypasses any subsequent servlet filters.
However some Spring WebMVC mechanisms don’t work, like forward
because they ask the container a RequestDispatcher
based on the
request, the JEE container will return a dispatcher based on url-patterns, and
Jersey servlet is configured with /*
so it receives all request by default.
-
[plus circle] Quite simple to configure
-
[plus circle] Quite simple to maintain
-
[plus circle] Barely rely on Spring MVC
-
[minus circle] May prevents some servlet filter to execute
-
[minus circle] Does not support all Spring Web MVC features like forwarding
See SpringMvcPrefixEnforcerFilter
mitigation code.
Spring Web MVC configuration hack mitigation
Is it possible to configure Spring Boot to do the right thing ? Turns out, yes it’s possible.
jersey
and filter-mitigation
profiles-
GET /jaxrs
⇒200 OK
-
GET /actuator/status
⇒200 OK
-
GET /favicon.ico
⇒200 OK
-
GET /doc/
⇒200 OK
-
GET /doc/index.html
⇒200 OK
-
GET /rest/
⇒200 OK
So the Spring’s DispatcherServlet
servlet url-mapping is hardwired by
Spring Boot to spring.mvc.servlet.path
, which in our case is /
.
And the jersey servlet url-mapping is /*
which means that the Jersey servlet
is configured to receive all request.
The idea is that JEE servlets can have multiple url-mappings, which means it is
should be possible to tell the container to forward HTTP request that follow
the configured url-mappings patterns to the DispatcherServlet
.
The auto-configuration is located in DispatcherServletAutoConfiguration
,
and more precisely spring boot allows to override some of the beans and
especially the dispatcherServlet
dedicated ServletRegistrationBean
(which
is the Spring way to tell the container to register the given servlet).
However it happens that Spring Boot have a specialized version of the
ServletRegistrationBean
named
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletRegistrationBean
who implements
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath
.
Spring Boot uses the bean with this interface to (auto) configure certain part
of their web framework.
Extending DispatcherServletRegistrationBean
to configure url-mappings is not
gonna work, because setUrlMappings
and
addUrlMappings
throws unsupported
operation. The only options are
-
Either to implements another specialized
ServletRegistrationBean
that allows to configure url-mappings for thedispatcherServlet
and of course implementsDispatcherServletPath
. -
Or register two beans, the
ServletRegistrationBean
and theDispatcherServletPath
.
However doing so is not enough, the Spring Web MVC infrastructure needs to be
told how to resolve URIs, this should work with
WebMvcConfigurationSupport
and the companion bean
WebMvcConfigurer.configurePathMatch
. The
PathMatchConfigurer
is supposed to tell if a request
URI path matches a WebMVC resource via another essential sub-component, the
UrlPathHelper
.
The method of interest is UrlPathHelper.getLookupPathForRequest
is by default (alwaysUseFullPath
is false
) configured
to look for sub-path of the url-mappings
(UrlPathHelper.getPathWithinServletMapping
), and as
such the returned lookup path is stripped form the first part, hence
Spring Web MVC cannot match any of these resources against incomplete URLs.
We need to configure this UrlPathHelper
to return the full path via
WebMvcConfigurer.configurePathMatch
.
Unfortunately this configurer only affects the configuration of (the reactive)
RequestMappingHandlerMapping
and a few
other types, but Spring Web MVC has many other HandlerMapping
types.
To workaround this, the AbstractHandlerMapping
are
post-processed to set the UrlPathHelper
with the needed configuration.
And finally this was again not enough, for some mapping like
WebMvcEndpointHandlerMapping
because this parent’s
type uses a private static final configuration non customizable
RequestMappingInfo.BuilderConfiguration builderConfig
with defaults helpers only, its urlPathHelper
is null
, this triggers the
creation of a`PatternsRequestCondition` with
a new instance of UrlPathHelper
that has the default configuration.
In order to bypass this behavior it is necessary to use reflection before
WebMvcEndpointHandlerMapping
bean post initialization and set the
RequestMappingInfo.BuilderConfiguration
with the UrlPathHelper
with the
needed configuration.
-
[plus circle] Seems to properly configure Spring MVC
-
[plus circle] And as such more robust
-
[plus circle] Integrates well with Servlets
-
[minus circle] Difficult to understand
-
[minus circle] Difficult to maintain
-
[minus circle] May break upon Spring MVC code changes
See SpringWebMvcHackConfiguration
mitigation code.