Java 为什么我的控制器发送内容类型“应用程序/八位字节流”?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/19792160/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Why is my controller sending the content type "application/octet-stream"?
提问by Stephane
I have a REST controller:
我有一个 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);
}
As you can see, I'm trying hard to specify the content type returned by the controller.
如您所见,我正在努力指定控制器返回的内容类型。
It is accessed with a REST client:
它是通过 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;
}
The http headers are created by a utility:
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;
}
The web security configuration and code work fine. I make sure of this using a mockMvc based integration test which succeeds.
网络安全配置和代码工作正常。我使用成功的基于 mockMvc 的集成测试来确保这一点。
The only test that fails is the one based on the REST template:
唯一失败的测试是基于 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")));
}
The exception given in the Maven build console output is:
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)
I'm using the Spring Framework 3.2.2.RELEASE version and the Spring Security 3.1.4.RELEASE version on the Java 1.6 version.
我在 Java 1.6 版本上使用 Spring Framework 3.2.2.RELEASE 版本和 Spring Security 3.1.4.RELEASE 版本。
At first, I had a bare bone REST template:
起初,我有一个简单的 REST 模板:
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}
I have now added to it, hoping it would help:
我现在已经添加到它,希望它会有所帮助:
private static final Charset UTF8 = Charset.forName("UTF-8");
@Bean
public RestTemplate restTemplate() {
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
MappingHymanson2HttpMessageConverter mappingHymanson2HttpMessageConverter = new MappingHymanson2HttpMessageConverter();
mappingHymanson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("application", "json", UTF8)));
messageConverters.add(mappingHymanson2HttpMessageConverter);
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;
}
But it didn't change anything and the exception remains the same.
但它没有改变任何东西,异常保持不变。
My understanding is that, it is not the REST template that needs any specific JSON configuration, but rather, that, for some reason, my controller is spitting out some application/octet-stream content type instead of some application/json content type.
我的理解是,并不是 REST 模板需要任何特定的 JSON 配置,而是,出于某种原因,我的控制器正在吐出一些应用程序/八位字节流内容类型而不是一些应用程序/json 内容类型。
Any clue?
有什么线索吗?
Some additional information...
一些额外的信息...
The admin rest client bean in the web test configuration:
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<?>>();
MappingHymanson2HttpMessageConverter mappingHymanson2HttpMessageConverter = new MappingHymanson2HttpMessageConverter();
mappingHymanson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("application", "json", UTF8)));
messageConverters.add(mappingHymanson2HttpMessageConverter);
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;
}
}
The base test class:
基础测试类:
@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);
}
}
The web init class:
网络初始化类:
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, "/*");
}
}
The web configuration:
网页配置:
@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);
}
}
The application configuration is empty for now:
应用程序配置暂时为空:
@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
}
采纳答案by Sotirios Delimanolis
I had my doubts before, but now that you've posted everything, here's what's up. Assuming the RestTemplate
object you use in your getGreetingMessage()
method is the same as the one declared in the @Bean
method, the problem starts here
我以前有过怀疑,但现在你已经发布了所有内容,这就是最新情况。假设RestTemplate
您在getGreetingMessage()
方法中使用的对象与@Bean
方法中声明的对象相同,问题就从这里开始
this.mockServer = MockRestServiceServer.createServer(restTemplate);
This call overwrites the default ClientHttpRequestFactory
object that the RestTemplate
object uses internally with a mock. In your getGreetingMessage()
method, this call
此调用使用模拟覆盖对象内部使用的默认ClientHttpRequestFactory
对象RestTemplate
。在您的getGreetingMessage()
方法中,此调用
ResponseEntity<GreetingResource> responseEntity = restTemplate.getForEntity("/admin/greeting", GreetingResource.class, httpHeaders);
doesn't actually go through the network. The RestTemplate
uses the mocked ClientHttpRequestFactory
to create a fake ClientHttpRequest
which produces a fake ClientHttpResponse
which doesn't have a Content-Type
header. When the RestTemplate
looks at the ClientHttpResponse
to determine its Content-Type
and doesn't find one, it assumes application/octet-stream
by default.
实际上并不通过网络。RestTemplate
使用模拟ClientHttpRequestFactory
来创建一个假的ClientHttpRequest
,它产生一个ClientHttpResponse
没有Content-Type
标题的假。当RestTemplate
查看ClientHttpResponse
以确定其Content-Type
并且没有找到时,它application/octet-stream
默认假定。
So, your controller isn't setting the content type because your controller is never hit. The RestTemplate
is using a default content type for your response because it is mocked and doesn't actually contain one.
因此,您的控制器没有设置内容类型,因为您的控制器永远不会被命中。在RestTemplate
因为它是嘲笑和实际上并不包含一个使用默认的内容类型您的答复。
From your comments:
从你的评论:
I wonder if I understand what the mock server is testing. I understand it is to be used in acceptance testing scenario. Is it supposed to hit the controller at all ?
我想知道我是否了解模拟服务器正在测试什么。我知道它是用于验收测试场景。它应该完全击中控制器吗?
The javadoc for MockRestServiceServer
states:
MockRestServiceServer
状态的javadoc :
Main entry point for client-side REST testing.Used for tests that involve direct or indirect (through client code) use of the
RestTemplate
. Provides a way to set up fine-grained expectations on the requests that will be performed through theRestTemplate
and a way to define the responses to send back removing the need for an actual running server.
客户端 REST 测试的主要入口点。用于涉及直接或间接(通过客户端代码)使用
RestTemplate
. 提供一种对将通过 执行的请求设置细粒度期望RestTemplate
的方法,以及一种定义发送回响应的方法,无需实际运行的服务器。
In other words, it's as if your application server didn't exist. So you could throw any expectations (and actual return values) you wanted and test whatever happens from the client side. So you aren't testing your server, you are testing your client.
换句话说,就好像您的应用程序服务器不存在一样。所以你可以抛出你想要的任何期望(和实际返回值)并测试客户端发生的任何事情。所以你不是在测试你的服务器,而是在测试你的客户端。
Are you sure you aren't looking for MockMvc
, which is
你确定你不是在找MockMvc
,这是
Main entry point for server-side Spring MVC test support.
服务器端 Spring MVC 测试支持的主要入口点。
which you can setup to actually use your @Controller
beans in an integration environment. You aren't actually sending HTTP request, but the MockMvc
is simulating how they would be sent and how your server would respond.
您可以将其设置为@Controller
在集成环境中实际使用您的bean。您实际上并不是在发送 HTTP 请求,而是在MockMvc
模拟它们将如何发送以及您的服务器将如何响应。
回答by Avseiytsev Dmitriy
It is bug in MockHttpServletRequest
and I will try to describe it.
Issue in tracker https://jira.springsource.org/browse/SPR-11308#comment-97327Fixed in version 4.0.1
这是错误,MockHttpServletRequest
我将尝试描述它。跟踪器中的问题https://jira.springsource.org/browse/SPR-11308#comment-97327 已在 4.0.1 版中修复
Bug
漏洞
When DispatcherServlet
looking for method to invoke it using some RequestConditions. One of them is ConsumesRequestCondition
. The following is a piece of code:
在DispatcherServlet
寻找使用某些 RequestConditions 调用它的方法时。其中之一是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());
}
}
We are interested in piece request.getContentType()
. There request is MockHttpServletRequest
. Let's look on method getContentType():
我们对一块感兴趣request.getContentType()
。有请求是MockHttpServletRequest
。让我们看看方法 getContentType():
public String getContentType() {
return this.contentType;
}
It just return value of this.contentType
. It does not return a value from the header! And this.contentType
is always NULL. Then contentType
in matchMediaType
methos will be always MediaType.APPLICATION_OCTET_STREAM
.
它只是返回 的值this.contentType
。它不会从标头返回值!并且this.contentType
始终为 NULL。那么contentType
在matchMediaType
方法中将永远是MediaType.APPLICATION_OCTET_STREAM
。
Solution
解决方案
I have tried many ways but have found only one that works.
我尝试了很多方法,但只找到了一种有效的方法。
- Create package
org.springframework.test.web.client
in your test directory. - Create copy of
org.springframework.test.web.client.MockMvcClientHttpRequestFactory
but rename it. For example rename toFixedMockMvcClientHttpRequestFactory
. Find line:
MvcResult mvcResult = MockMvcClientHttpRequestFactory.this.mockMvc.perform(requestBuilder).andReturn();
Replace it with code:
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();
And register your ClientHttpReque
@Bean public ClientHttpRequestFactory clientHttpRequestFactory(MockMvc mockMvc) { return new FixedMockMvcClientHttpRequestFactory(mockMvc); }
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); }
I know that it is not beautiful solution but it works fine.
我知道这不是很好的解决方案,但效果很好。