An open API service indexing awesome lists of open source software.

https://github.com/bric3/jersey-webmvc

Configuration and code to enable Spring WebMVC and Jersey endpoints to co-exists in Spring Boot 2.1
https://github.com/bric3/jersey-webmvc

jersey2 spring spring-boot-2 spring-mvc

Last synced: about 2 months ago
JSON representation

Configuration and code to enable Spring WebMVC and Jersey endpoints to co-exists in Spring Boot 2.1

Awesome Lists containing this project

README

          

= Workaround Spring Boot unability to make Jersey and Spring MVC co-exist
:icons: font
:url-sb-17523: https://github.com/spring-projects/spring-boot/issues/17523
:servlet-filter-mitigation: https://github.com/bric3/jersey-webmvc/blob/master/src/main/java/com/github/bric3/jerseywebmvc/servlet_filter_mitigation/SpringMvcPrefixEnforcerFilter.java
:spring-config-mitigation: https://github.com/bric3/jersey-webmvc/blob/master/src/main/java/com/github/bric3/jerseywebmvc/spring_configuration_mitigation/SpringWebMvcHackConfiguration.java

:res-get-rd: https://github.com/spring-projects/spring-framework/blob/5.1.x/spring-webmvc/src/main/java/org/springframework/web/servlet/view/InternalResourceView.java#L149-L150
:res-rd-forward: https://github.com/spring-projects/spring-framework/blob/5.0.x/spring-webmvc/src/main/java/org/springframework/web/servlet/view/InternalResourceView.java#L170
:dispatcher-autoconf: https://github.com/spring-projects/spring-boot/blob/2.1.x/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java
:dispatcher-reg-bean: https://github.com/spring-projects/spring-boot/blob/2.1.x/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletRegistrationBean.java
:dispatcher-path: https://github.com/spring-projects/spring-boot/blob/2.1.x/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletPath.java

:webmvc-configurer: https://github.com/spring-projects/spring-framework/blob/5.1.x/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java
:pathmatch-configurer: https://github.com/spring-projects/spring-framework/blob/5.1.x/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java
:webmvc-configurer-support: https://github.com/spring-projects/spring-framework/blob/5.1.x/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
:abstract-handler-mapping: https://github.com/spring-projects/spring-framework/blob/5.1.x/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java
:patterns-condition: https://github.com/spring-projects/spring-framework/blob/5.1.x/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java

:req-mapping-handler-mapping: https://github.com/spring-projects/spring-framework/blob/5.1.x/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java
:url-path-helper: https://github.com/spring-projects/spring-framework/blob/5.1.x/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java
:actuator-webmvc-mapping: https://github.com/spring-projects/spring-boot/blob/2.1.x/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java
:actuator-abstract-webmvc-mapping: https://github.com/spring-projects/spring-boot/blob/2.1.x/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java

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`.

.without `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`

.with `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`

See {url-sb-17523}[spring-projects/spring-boot#17523].

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 {url-sb-17523}[issue]. And configuring this filter
with the right prefixes work.

.with `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 {res-rd-forward}[forward]
because they ask the container a {res-get-rd}[`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.

- icon:plus-circle[] Quite simple to configure
- icon:plus-circle[] Quite simple to maintain
- icon:plus-circle[] Barely rely on Spring MVC
- icon:minus-circle[] May prevents some servlet filter to execute
- icon:minus-circle[] Does not support all Spring Web MVC features
like forwarding

See {servlet-filter-mitigation}[`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.

.with `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 {dispatcher-autoconf}[`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
{dispatcher-reg-bean}[`org.springframework.boot.autoconfigure.web.servlet.DispatcherServletRegistrationBean`]
who implements
{dispatcher-path}[`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 {dispatcher-reg-bean}#L55-L58[`setUrlMappings`] and
{dispatcher-reg-bean}#L60-L63[`addUrlMappings`] throws unsupported
operation. The only options are

* Either to implements another specialized
`ServletRegistrationBean` that allows to configure url-mappings for the
`dispatcherServlet` and of course implements `DispatcherServletPath`.
* Or register two beans, the `ServletRegistrationBean` and the
`DispatcherServletPath`.

However doing so is not enough, the Spring Web MVC infrastructure needs to be
told how to resolve URIs, this should work with
{webmvc-configurer-support}[`WebMvcConfigurationSupport`] and the companion bean
{webmvc-configurer}#L60[`WebMvcConfigurer.configurePathMatch`]. The
{pathmatch-configurer}[`PathMatchConfigurer`] is supposed to tell if a request
URI path matches a WebMVC resource via another essential sub-component, the
{url-path-helper}[`UrlPathHelper`].

The method of interest is {url-path-helper}#L156-L178[`UrlPathHelper.getLookupPathForRequest`]
is by default ({url-path-helper}#L64[`alwaysUseFullPath`] is `false`) configured
to look for sub-path of the url-mappings
({url-path-helper}#L171[`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
{webmvc-configurer}#L60[`WebMvcConfigurer.configurePathMatch`].

Unfortunately this configurer only affects the configuration of (the reactive)
{req-mapping-handler-mapping}[`RequestMappingHandlerMapping`] and a few
other types, but Spring Web MVC has many other `HandlerMapping` types.
To workaround this, the {abstract-handler-mapping}[`AbstractHandlerMapping`] are
_post-processed_ to {abstract-handler-mapping}#L145-L157[set the `UrlPathHelper`]
with the needed configuration.

And finally this was again not enough, for some mapping like
{actuator-webmvc-mapping}[`WebMvcEndpointHandlerMapping`] because this parent's
type uses a private static final configuration non customizable
{actuator-abstract-webmvc-mapping}#L87[`RequestMappingInfo.BuilderConfiguration builderConfig`]
with defaults _helpers_ only, its {actuator-abstract-webmvc-mapping}#L151[`urlPathHelper`
is `null`], {actuator-abstract-webmvc-mapping}#L203-L204[this triggers] the
{patterns-condition}#L108[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.

- icon:plus-circle[] Seems to properly configure Spring MVC
- icon:plus-circle[] And as such more robust
- icon:plus-circle[] Integrates well with Servlets
- icon:minus-circle[] Difficult to understand
- icon:minus-circle[] Difficult to maintain
- icon:minus-circle[] May break upon Spring MVC code changes

See {spring-config-mitigation}[`SpringWebMvcHackConfiguration`] mitigation code.