-
Notifications
You must be signed in to change notification settings - Fork 38.9k
Description
Spring Boot version: 3.2.3, 3.2.4 (Spring Core 6.1.4)
Java version: Temurin 17 (17.0.10)
Reproduced on Windows and Github Actions with Ubuntu 22.04.
Minimal example: https://github.com/RHarryH/spring-webmvc-github-issue
Description:
I have observed weird issue of WebMvcTest failure with code 405 instead of expected 200 because Spring does not resolve controller method based on the request url. 405 error happens when there are two endpoints with the same request url but different HTTP method. When request urls are different 404 error is thrown.
This happens only when specific hierarchy of controllers is used and when WebMvcTest is run after SpringBootTest (achieved by changing test class execution order in junit-platform.properties.
The hierarchy of the controllers is as follows:
Controllerinterface defining endpoints and annotating them with@XMappingannotationsAbstractControllerimplementingdeletemethod. Please not it is a package-private abstract classActualControllerimplementing remaining methods
The presence of AbstractController is the main cause of the issue. Working workaround is making it public.
When debugging tests SpringBootTest logs contains:
2024-04-07T12:01:20.781+02:00 DEBUG 33568 --- [ main] _.s.web.servlet.HandlerMapping.Mappings :
c.a.i.ActualController:
{POST [/v1/a]}: add(Body,BindingResult)
{POST [/v1/a/{id}]}: update(UUID,Body,BindingResult)
{DELETE [/v1/a/{id}]}: delete(UUID)
while WebMvcTest logs miss DELETE method:
2024-04-07T12:01:22.203+02:00 DEBUG 33568 --- [ main] _.s.web.servlet.HandlerMapping.Mappings :
c.a.i.ActualController:
{POST [/v1/a]}: add(Body,BindingResult)
{POST [/v1/a/{id}]}: update(UUID,Body,BindingResult)
I have tracked down the rootcause to the org.springframework.core.MethodIntrocpector class and selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup) method (
spring-framework/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java
Line 75 in 9bd6aef
| if (result != null) { |
Line 74 correctly inspects the method. The problem is in line 77. When SpringBootTest tests are run the fields looks like below:
method = {Method@7492} "public void com.avispa.issue.AbstractController.delete(java.util.UUID)"
specificMethod = {Method@7493} "public void com.avispa.issue.ActualController.delete(java.util.UUID)"
result = {RequestMappingInfo@7494} "{DELETE [/v1/a/{id}]}"
bridgedMethod = {Method@7492} "public void com.avispa.issue.AbstractController.delete(java.util.UUID)"
But then whenWebMvcTest tests are run it looks like below:
method = {Method@9155} "public void com.avispa.issue.AbstractController.delete(java.util.UUID)"
specificMethod = {Method@9156} "public void com.avispa.issue.ActualController.delete(java.util.UUID)"
result = {RequestMappingInfo@9157} "{DELETE [/v1/a/{id}]}"
bridgedMethod = {Method@7492} "public void com.avispa.issue.AbstractController.delete(java.util.UUID)"
As you can see in second case method and bridgedMethod represents the same method but are in fact different instances of Method class. And because the comparison in line 77 is done by reference, it failes and does not add found DELETE method to the mappings registry.
When SpringBootTest tests are disabled, the problem does not exist.