【问题标题】:Spring: Returning empty HTTP Responses with ResponseEntity<Void> doesn't workSpring:使用 ResponseEntity<Void> 返回空 HTTP 响应不起作用
【发布时间】:2014-12-20 10:07:20
【问题描述】:

我们正在使用 Spring (4.1.1.) 实现 REST API。对于某些 HTTP 请求,我们希望返回一个没有正文的头部作为响应。但是,使用ResponseEntity&lt;Void&gt; 似乎不起作用。当使用MockMvc 测试调用时,将返回 406(不可接受)。使用不带参数值的ResponseEntity&lt;String&gt; (new ResponseEntity&lt;String&gt;( HttpStatus.NOT_FOUND )) 可以正常工作。

方法:

@RequestMapping( method = RequestMethod.HEAD, value = Constants.KEY )
public ResponseEntity<Void> taxonomyPackageExists( @PathVariable final String key ) {

    LOG.debug( "taxonomyPackageExists queried with key: {0}", key ); //$NON-NLS-1$

    final TaxonomyKey taxonomyKey = TaxonomyKey.fromString( key );

    LOG.debug( "Taxonomy key created: {0}", taxonomyKey ); //$NON-NLS-1$

    if ( this.xbrlInstanceValidator.taxonomyPackageExists( taxonomyKey ) ) {

        LOG.debug( "Taxonomy package with key: {0} exists.", taxonomyKey ); //$NON-NLS-1$

        return new ResponseEntity<Void>( HttpStatus.OK );

    } else {

        LOG.debug( "Taxonomy package with key: {0} does NOT exist.", taxonomyKey ); //$NON-NLS-1$

        return new ResponseEntity<Void>( HttpStatus.NOT_FOUND );
    }

}

测试用例(TestNG):

public class TaxonomyQueryControllerTest {

private XbrlInstanceValidator   xbrlInstanceValidatorMock;
private TaxonomyQueryController underTest;
private MockMvc                 mockMvc;

@BeforeMethod
public void setUp() {
    this.xbrlInstanceValidatorMock = createMock( XbrlInstanceValidator.class );
    this.underTest = new TaxonomyQueryController( this.xbrlInstanceValidatorMock );
    this.mockMvc = MockMvcBuilders.standaloneSetup( this.underTest ).build();
}

@Test
public void taxonomyPackageDoesNotExist() throws Exception {
    // record
    expect( this.xbrlInstanceValidatorMock.taxonomyPackageExists( anyObject( TaxonomyKey.class ) ) ).andStubReturn(
            false );

    // replay
    replay( this.xbrlInstanceValidatorMock );

    // do the test
    final String taxonomyKey = RestDataFixture.taxonomyKeyString;

    this.mockMvc.perform( head( "/taxonomypackages/{key}", taxonomyKey ).accept( //$NON-NLS-1$
            MediaType.APPLICATION_XML ) ).andExpect( status().isNotFound() );

}

}

此堆栈跟踪失败:

FAILED: taxonomyPackageDoesNotExist
java.lang.AssertionError: Status expected:<404> but was:<406>
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:60)
at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:89)
at org.springframework.test.web.servlet.result.StatusResultMatchers$10.match(StatusResultMatchers.java:652)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:153)
at de.zeb.control.application.xbrlstandalonevalidator.restservice.TaxonomyQueryControllerTest.taxonomyPackageDoesNotExist(TaxonomyQueryControllerTest.java:54)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:84)
at org.testng.internal.Invoker.invokeMethod(Invoker.java:714)
at org.testng.internal.Invoker.invokeTestMethod(Invoker.java:901)
at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1231)
at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:127)
at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:111)
at org.testng.TestRunner.privateRun(TestRunner.java:767)
at org.testng.TestRunner.run(TestRunner.java:617)
at org.testng.SuiteRunner.runTest(SuiteRunner.java:334)
at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:329)
at org.testng.SuiteRunner.privateRun(SuiteRunner.java:291)
at org.testng.SuiteRunner.run(SuiteRunner.java:240)
at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:52)
at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:86)
at org.testng.TestNG.runSuitesSequentially(TestNG.java:1224)
at org.testng.TestNG.runSuitesLocally(TestNG.java:1149)
at org.testng.TestNG.run(TestNG.java:1057)
at org.testng.remote.RemoteTestNG.run(RemoteTestNG.java:111)
at org.testng.remote.RemoteTestNG.initAndRun(RemoteTestNG.java:204)
at org.testng.remote.RemoteTestNG.main(RemoteTestNG.java:175)

【问题讨论】:

标签: java spring rest spring-mvc


【解决方案1】:

注意:问题中提到的版本 4.1.1.RELEASE 也是如此。

Spring MVC 通过HttpEntityMethodProcessor 处理ResponseEntity 返回值。

ResponseEntity 值没有设置正文时,就像您的 sn-p 中的情况一样,HttpEntityMethodProcessor 尝试从 ResponseEntity 返回类型的参数化中确定响应正文的内容类型在@RequestMapping 处理方法的签名中。

所以

public ResponseEntity<Void> taxonomyPackageExists( @PathVariable final String key ) {

该类型将是Void。然后HttpEntityMethodProcessor 将遍历所有已注册的HttpMessageConverter 实例,并找到一个可以为Void 类型编写主体的实例。根据您的配置,它可能找到也可能找不到。

如果它确实找到了,它仍然需要确保相应的正文将使用与请求的Accept 标头中提供的类型相匹配的 Content-Type,在您的情况下为 application/xml

如果在所有这些检查之后,不存在这样的HttpMessageConverter,Spring MVC 将决定它不能产生可接受的响应,因此返回 406 Not Acceptable HTTP 响应。

使用ResponseEntity&lt;String&gt;,Spring 将使用String 作为响应体并找到StringHttpMessageConverter 作为处理程序。并且由于StringHttpMessageHandler 可以为任何媒体类型生成内容(在Accept 标头中提供),它将能够处理您的客户请求的application/xml

Spring MVC 已更改为仅在 ResponseEntity 中的主体不是 null 时返回 406。如果您使用的是更新版本的 Spring MVC,您将看不到原始问题中的行为。


iddy85's solution 中,似乎暗示ResponseEntity&lt;?&gt;,body 的类型将被推断为Object。如果您的类路径中有正确的库,即。 Jackson(版本 > 2.5.0)及其 XML 扩展,Spring MVC 将可以访问MappingJackson2XmlHttpMessageConverter,它可以使用它为Object 类型生成application/xml他们的解决方案只在这些条件下有效。否则,它会因为我上面描述的相同原因而失败。

【讨论】:

  • 嗨 Sotirios,感谢您的回复。请确保:您实际上是在说我必须使用 ResponseEntity ,对吗?
  • @MatthiasT 一种选择是将返回类型设置为ResponseEntity&lt;?&gt;,并在构造实际返回值时使用任何内容。您还可以向ResponseEntity 构造函数提供具有适当内容类型的HttpHeaders。虽然当你没有内容时设置内容类型并没有什么意义。
  • 如果你使用了ResponseEntity>,然后设置了一个没有body的responseEntity,那body不还是无效的吗?意思是你有同样的问题?
  • 从我的测试中,设置 ResponseEntity 会在 responseBody 上写一个“null”,这是非常错误的。如果您真的打算有一个空的 responseBody,请返回一个带有空字符串的 ResponseEntity,例如 "return ResponseEntity("", HttpStatus.OK)"
  • @Renatinn 这取决于您注册了哪些HttpMessageConverter 实例。但是,当然,空字符串也可以。
【解决方案2】:

根据Spring 4 MVC ResponseEntity.BodyBuilder and ResponseEntity Enhancements Example可以写成:

....
   return ResponseEntity.ok().build();
....
   return ResponseEntity.noContent().build();

更新:

如果返回值为Optional,有方便的方法,返回ok()notFound()

return ResponseEntity.of(optional)

【讨论】:

    【解决方案3】:

    您也可以不指定类型参数,这看起来更简洁,以及 Spring 在查看 docs 时的意图:

    @RequestMapping(method = RequestMethod.HEAD, value = Constants.KEY )
    public ResponseEntity taxonomyPackageExists( @PathVariable final String key ){
        // ...
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
    

    【讨论】:

      【解决方案4】:

      您的方法实现不明确,请尝试以下操作,稍微编辑您的代码并使用HttpStatus.NO_CONTENT 即 204 No Content 代替HttpStatus.OK

      服务器已完成请求但不需要返回 实体主体,并且可能想要返回更新的元信息。这 响应可能包括新的或更新的元信息,形式为 entity-headers,如果存在应该与 请求的变体。

      T 的任何值都会被 204 忽略,但不会被 404 忽略

        public ResponseEntity<?> taxonomyPackageExists( @PathVariable final String key ) {
                  LOG.debug( "taxonomyPackageExists queried with key: {0}", key ); //$NON-NLS-1$
                  final TaxonomyKey taxonomyKey = TaxonomyKey.fromString( key );
                  LOG.debug( "Taxonomy key created: {0}", taxonomyKey ); //$NON-NLS-1$
      
                  if ( this.xbrlInstanceValidator.taxonomyPackageExists( taxonomyKey ) ) {
                      LOG.debug( "Taxonomy package with key: {0} exists.", taxonomyKey ); //$NON-NLS-1$
                       return new ResponseEntity<T>(HttpStatus.NO_CONTENT);
                  } else {
                     LOG.debug( "Taxonomy package with key: {0} does NOT exist.", taxonomyKey ); //$NON-NLS-1$
                      return new ResponseEntity<T>( HttpStatus.NOT_FOUND );
                  }
      
          }
      

      【讨论】:

      • T 是一个响应体类型,可以是任何东西,String 是一个选项。如果它确实t work share the new code with and the stack-trace. @SotiriosDelimanolis I am curious whats 代码有问题,请尝试?
      • T 是类型变量吗?它在哪里声明?您的建议会有什么不同。
      • 我没有对原始代码进行任何重大更改,而是建议使用 HttpStatus.NO_CONTENT 而不是 HttpStatus.OK 来获得无正文响应
      • 所以请解释一下这将如何解决 OP 的问题。
      【解决方案5】:

      对于 Spring 5.2+,这对我有用:

      @PostMapping("/foo")
      ResponseEntity<Void> foo(@PathVariable UUID fooId) {
          return fooService.findExam(fooId)
                  .map(uri -> ResponseEntity.noContent().<Void>build())
                  .orElse(ResponseEntity.notFound().build());
      }
      

      【讨论】:

        【解决方案6】:

        就个人而言,为了处理空响应,我在集成测试中使用了 MockMvcResponse 对象,如下所示:

        MockMvcResponse response = RestAssuredMockMvc.given()
                        .webAppContextSetup(webApplicationContext)
                        .when()
                        .get("/v1/ticket");
        
            assertThat(response.mockHttpServletResponse().getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value());
        

        在我的控制器中,我在特定情况下返回空响应:

        return ResponseEntity.noContent().build();
        

        【讨论】:

          猜你喜欢
          • 2020-05-24
          • 2020-05-23
          • 1970-01-01
          • 2019-02-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多