为啥我的控制器发送内容类型“application/octet-stream”?
Posted
技术标签:
【中文标题】为啥我的控制器发送内容类型“application/octet-stream”?【英文标题】:Why is my controller sending the content type "application/octet-stream"?为什么我的控制器发送内容类型“application/octet-stream”? 【发布时间】:2013-11-16 12:20:24 【问题描述】:我有一个 REST 控制器:
@RequestMapping(value = "greeting", method = RequestMethod.GET, produces = "application/json; charset=utf-8")
@Transactional(readOnly = true)
@ResponseBody
public HttpEntity<GreetingResource> greetingResource(@RequestParam(value = "message", required = false, defaultValue = "World") String message)
GreetingResource greetingResource = new GreetingResource(String.format(TEMPLATE, message));
greetingResource.add(linkTo(methodOn(AdminController.class).greetingResource(message)).withSelfRel());
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add("Content-Type", "application/json; charset=utf-8");
return new ResponseEntity<GreetingResource>(greetingResource, responseHeaders, HttpStatus.OK);
如您所见,我正在努力指定控制器返回的内容类型。
通过 REST 客户端访问:
public String getGreetingMessage()
String message;
try
HttpHeaders httpHeaders = Common.createAuthenticationHeaders("stephane" + ":" + "mypassword");
ResponseEntity<GreetingResource> responseEntity = restTemplate.getForEntity("/admin/greeting", GreetingResource.class, httpHeaders);
GreetingResource greetingResource = responseEntity.getBody();
message = greetingResource.getMessage();
catch (HttpMessageNotReadableException e)
message = "The GET request FAILED with the message being not readable: " + e.getMessage();
catch (HttpStatusCodeException e)
message = "The GET request FAILED with the HttpStatusCode: " + e.getStatusCode() + "|" + e.getStatusText();
catch (RuntimeException e)
message = "The GET request FAILED " + ExceptionUtils.getFullStackTrace(e);
return message;
http 标头由实用程序创建:
static public HttpHeaders createAuthenticationHeaders(String usernamePassword)
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
byte[] encodedAuthorisation = Base64.encode(usernamePassword.getBytes());
headers.add("Authorization", "Basic " + new String(encodedAuthorisation));
return headers;
网络安全配置和代码运行良好。我使用基于 mockMvc 的集成测试来确保这一点。
唯一失败的测试是基于 REST 模板的测试:
@Test
public void testGreeting() throws Exception
mockServer.expect(requestTo("/admin/greeting")).andExpect(method(HttpMethod.GET)).andRespond(withStatus(HttpStatus.OK));
String message = adminRestClient.getGreetingMessage();
mockServer.verify();
assertThat(message, allOf(containsString("Hello"), containsString("World")));
Maven 构建控制台输出中给出的异常是:
java.lang.AssertionError:
Expected: (a string containing "Hello" and a string containing "World")
got: "The GET request FAILED org.springframework.web.client.RestClientException : Could not extract response: no suitable HttpMessageConverter found for response type [class com.thalasoft.learnintouch.rest.resource.GreetingR esource] and content type [application/octet-stream]\n\tat org.springframework.web.client.HttpMessageConverte rExtractor.extractData(HttpMessageConverterExtract or.java:107)
我在 Java 1.6 版本上使用的是 Spring Framework 3.2.2.RELEASE 版本和 Spring Security 3.1.4.RELEASE 版本。
起初,我有一个简单的 REST 模板:
@Bean
public RestTemplate restTemplate()
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
我现在已经添加了,希望对你有帮助:
private static final Charset UTF8 = Charset.forName("UTF-8");
@Bean
public RestTemplate restTemplate()
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("application", "json", UTF8)));
messageConverters.add(mappingJackson2HttpMessageConverter);
Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
jaxb2Marshaller.setClassesToBeBound(new Class[]
GreetingResource.class
);
MarshallingHttpMessageConverter marshallingHttpMessageConverter = new MarshallingHttpMessageConverter(jaxb2Marshaller, jaxb2Marshaller);
messageConverters.add(marshallingHttpMessageConverter);
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(new FormHttpMessageConverter());
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("text", "plain", UTF8)));
messageConverters.add(stringHttpMessageConverter);
messageConverters.add(new BufferedImageHttpMessageConverter());
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
RestTemplate restTemplate = new RestTemplate();
restTemplate.setMessageConverters(messageConverters);
return restTemplate;
但它并没有改变任何东西,异常保持不变。
我的理解是,不是 REST 模板需要任何特定的 JSON 配置,而是由于某种原因,我的控制器正在吐出一些 application/octet-stream 内容类型而不是一些 application/json 内容输入。
有什么线索吗?
一些附加信息...
web 测试配置中的 admin rest 客户端 bean:
@Configuration
public class WebTestConfiguration
@Bean
public AdminRestClient adminRestClient()
return new AdminRestClient();
@Bean
public RestTemplate restTemplate()
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("application", "json", UTF8)));
messageConverters.add(mappingJackson2HttpMessageConverter);
Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
jaxb2Marshaller.setClassesToBeBound(new Class[]
Greeting.class
);
MarshallingHttpMessageConverter marshallingHttpMessageConverter = new MarshallingHttpMessageConverter(jaxb2Marshaller, jaxb2Marshaller);
messageConverters.add(marshallingHttpMessageConverter);
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(new FormHttpMessageConverter());
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("text", "plain", UTF8)));
messageConverters.add(stringHttpMessageConverter);
messageConverters.add(new BufferedImageHttpMessageConverter());
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
RestTemplate restTemplate = new RestTemplate();
restTemplate.setMessageConverters(messageConverters);
return restTemplate;
基础测试类:
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration( classes = ApplicationConfiguration.class, WebSecurityConfig.class, WebConfiguration.class, WebTestConfiguration.class )
@Transactional
public abstract class AbstractControllerTest
@Autowired
private WebApplicationContext webApplicationContext;
@Autowired
private FilterChainProxy springSecurityFilterChain;
@Autowired
protected RestTemplate restTemplate;
protected MockRestServiceServer mockServer;
@Before
public void setup()
this.mockServer = MockRestServiceServer.createServer(restTemplate);
网络初始化类:
public class WebInit implements WebApplicationInitializer
private static Logger logger = LoggerFactory.getLogger(WebInit.class);
@Override
public void onStartup(ServletContext servletContext) throws ServletException
registerListener(servletContext);
registerDispatcherServlet(servletContext);
registerJspServlet(servletContext);
createSecurityFilter(servletContext);
private void registerListener(ServletContext servletContext)
// Create the root application context
AnnotationConfigWebApplicationContext appContext = createContext(ApplicationConfiguration.class, WebSecurityConfig.class);
// Set the application display name
appContext.setDisplayName("LearnInTouch");
// Create the Spring Container shared by all servlets and filters
servletContext.addListener(new ContextLoaderListener(appContext));
private void registerDispatcherServlet(ServletContext servletContext)
AnnotationConfigWebApplicationContext webApplicationContext = createContext(WebConfiguration.class);
ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(webApplicationContext));
dispatcher.setLoadOnStartup(1);
Set<String> mappingConflicts = dispatcher.addMapping("/");
if (!mappingConflicts.isEmpty())
for (String mappingConflict : mappingConflicts)
logger.error("Mapping conflict: " + mappingConflict);
throw new IllegalStateException(
"The servlet cannot be mapped to '/'");
private void registerJspServlet(ServletContext servletContext)
private AnnotationConfigWebApplicationContext createContext(final Class... modules)
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(modules);
return appContext;
private void createSecurityFilter(ServletContext servletContext)
FilterRegistration.Dynamic springSecurityFilterChain = servletContext.addFilter("springSecurityFilterChain", DelegatingFilterProxy.class);
springSecurityFilterChain.addMappingForUrlPatterns(null, false, "/*");
网页配置:
@Configuration
@EnableWebMvc
@EnableEntityLinks
@ComponentScan(basePackages = "com.thalasoft.learnintouch.rest.controller")
public class WebConfiguration extends WebMvcConfigurerAdapter
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)
PageableArgumentResolver resolver = new PageableArgumentResolver();
resolver.setFallbackPageable(new PageRequest(1, 10));
resolvers.add(new ServletWebArgumentResolverAdapter(resolver));
super.addArgumentResolvers(resolvers);
应用配置暂时为空:
@Configuration
@Import( ApplicationContext.class )
public class ApplicationConfiguration extends WebMvcConfigurerAdapter
// Declare "application" scope beans here, that is, beans that are not only used by the web context
【问题讨论】:
mockServer
是 MockMvc
实例?
是的,它是在一个基类中实例化的:this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).addFilters(this.springSecurityFilterChain).build();
我想看看在你的testGreeting
方法中使用的每个对象的初始化。我认为请求不会按照您的预期进行。
@SotiriosDelimanolis 这是基础测试类:code
@Autowired private WebApplicationContext webApplicationContext; @Autowired private FilterChainProxy springSecurityFilterChain; @Autowired protected RestTemplate restTemplate;受保护的 MockRestServiceServer 模拟服务器; @Before public void setup() this.mockServer = MockRestServiceServer.createServer(restTemplate); code
这就是你要的吗?谢谢。
是的。但不要将其发布在 cmets 中,您可以编辑您的问题并将其添加到那里。同时添加您的上下文配置。
【参考方案1】:
我之前有过怀疑,但现在您已经发布了所有内容,这就是最新情况。假设您在getGreetingMessage()
方法中使用的RestTemplate
对象与在@Bean
方法中声明的对象相同,那么问题就从这里开始
this.mockServer = MockRestServiceServer.createServer(restTemplate);
此调用会覆盖 RestTemplate
对象在内部使用模拟的默认 ClientHttpRequestFactory
对象。在您的 getGreetingMessage()
方法中,此调用
ResponseEntity<GreetingResource> responseEntity = restTemplate.getForEntity("/admin/greeting", GreetingResource.class, httpHeaders);
实际上并不通过网络。 RestTemplate
使用模拟的ClientHttpRequestFactory
创建一个假的ClientHttpRequest
,它产生一个没有Content-Type
标头的假ClientHttpResponse
。当RestTemplate
查看ClientHttpResponse
以确定其Content-Type
并没有找到时,它默认假定application/octet-stream
。
因此,您的控制器没有设置内容类型,因为您的控制器永远不会被击中。 RestTemplate
正在为您的响应使用默认内容类型,因为它是模拟的并且实际上不包含一个。
来自您的 cmets:
我想知道我是否了解模拟服务器正在测试什么。我明白 它将用于验收测试场景。是不是应该打 控制器?
MockRestServiceServer
的 javadoc 声明:
客户端 REST 测试的主要入口点。 用于测试 涉及直接或间接(通过客户端代码) 使用
RestTemplate
。提供一种设置细粒度的方法 对将通过RestTemplate
以及一种定义发送回删除的响应的方法 需要一个实际运行的服务器。
换句话说,就好像您的应用程序服务器不存在一样。因此,您可以抛出您想要的任何期望(和实际返回值)并测试客户端发生的任何事情。所以你不是在测试你的服务器,你是在测试你的客户端。
你确定你不是在寻找MockMvc
,这是
服务器端 Spring MVC 测试支持的主要入口点。
您可以将其设置为在集成环境中实际使用您的 @Controller
bean。您实际上并未发送 HTTP 请求,但 MockMvc
正在模拟它们将如何发送以及您的服务器将如何响应。
【讨论】:
谢谢。你很准。我用一个模拟 mvc 做了几乎同样的事情来进行集成测试,我可以用我的上下文来提供它: this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).addFilters(this.springSecurityFilterChain).build();所以我想如果可以的话,我需要对模拟服务器做同样的事情。 我想知道我是否了解模拟服务器正在测试什么。我知道它将用于验收测试场景。它应该完全击中控制器吗? 我无法在测试基类@Before 方法中定义其余模板,因为它由 AdminRestClient 类使用。所以我必须在 web 测试配置中定义其余模板。我注意到我可以为其余模板提供应用程序上下文,如下所示: this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).addFilters(this.springSecurityFilterChain).build(); RestTemplate restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(this.mockMvc));但这并没有改变异常,错误消息保持不变。 @StephaneEybert 我试图在编辑我的答案时澄清。看看吧。 谢谢哥们。现在你说清楚了。我认为模拟服务器也是一种测试服务器的方法,就像模拟 mvc 一样。我想我现在不会使用模拟服务器。我已经在使用模拟 mvc,它工作得非常好。感谢您的澄清!【参考方案2】:这是MockHttpServletRequest
中的错误,我将尝试描述它。
跟踪器中的问题https://jira.springsource.org/browse/SPR-11308#comment-97327
已在 4.0.1 版本中修复
错误
当DispatcherServlet
寻找使用某些请求条件来调用它的方法时。其中之一是ConsumesRequestCondition
。以下是一段代码:
@Override
protected boolean matchMediaType(HttpServletRequest request) throws HttpMediaTypeNotSupportedException
try
MediaType contentType = StringUtils.hasLength(request.getContentType()) ?
MediaType.parseMediaType(request.getContentType()) :
MediaType.APPLICATION_OCTET_STREAM;
return getMediaType().includes(contentType);
catch (IllegalArgumentException ex)
throw new HttpMediaTypeNotSupportedException(
"Can't parse Content-Type [" + request.getContentType() + "]: " + ex.getMessage());
我们对request.getContentType()
感兴趣。有请求是MockHttpServletRequest
。让我们看看getContentType()方法:
public String getContentType()
return this.contentType;
它只是返回值this.contentType
。 它不会从标头返回值! this.contentType
始终为 NULL。那么contentType
in matchMediaType
方法中的contentType
将始终为MediaType.APPLICATION_OCTET_STREAM
。
解决方案
我尝试了很多方法,但只有一种有效。
在您的测试目录中创建包org.springframework.test.web.client
。
创建org.springframework.test.web.client.MockMvcClientHttpRequestFactory
的副本,但重命名它。例如重命名为FixedMockMvcClientHttpRequestFactory
。
寻线:
MvcResult mvcResult = MockMvcClientHttpRequestFactory.this.mockMvc.perform(requestBuilder).andReturn();
用代码替换它:
MvcResult mvcResult = FixedMockMvcClientHttpRequestFactory.this.mockMvc.perform(new RequestBuilder()
@Override
public MockHttpServletRequest buildRequest(ServletContext servletContext)
MockHttpServletRequest request = requestBuilder.buildRequest(servletContext);
request.setContentType(request.getHeader("Content-Type"));
return request;
).andReturn();
并注册您的 ClientHttpReque
@Bean
public ClientHttpRequestFactory clientHttpRequestFactory(MockMvc mockMvc)
return new FixedMockMvcClientHttpRequestFactory(mockMvc);
我知道这不是很好的解决方案,但效果很好。
【讨论】:
以上是关于为啥我的控制器发送内容类型“application/octet-stream”?的主要内容,如果未能解决你的问题,请参考以下文章
Zend_Http_Client - 为啥我的 POST 请求发送错误的内容类型标头?
为啥 ring 的资源响应以 application/octet-stream 内容类型响应?
Spring boot 控制器调用不支持内容类型“application/json;charset=UTF-8”
为啥用ajax发送post请求时,需要设置请求头类型为application/x-www-form-urlencoded
为啥上传到 S3 的文件的内容类型为 application/octet-stream,除非我将文件命名为 .html?