我有一个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;
}
Web安全配置和代码工作正常。我使用基于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配置,而是由于某种原因,我的控制器正在吐出一些应用程序/八位字节流内容类型而不是某些应用程序/ 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);
}
}
web init类:
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
}
答案 0 :(得分:2)
我之前有过疑惑,但现在你发布了所有内容,这就是最新消息。假设您在RestTemplate
方法中使用的getGreetingMessage()
对象与@Bean
方法中声明的对象相同,则问题从此处开始
this.mockServer = MockRestServiceServer.createServer(restTemplate);
此调用将覆盖ClientHttpRequestFactory
对象在内部使用模拟的默认RestTemplate
对象。在您的getGreetingMessage()
方法中,此次调用
ResponseEntity<GreetingResource> responseEntity = restTemplate.getForEntity("/admin/greeting", GreetingResource.class, httpHeaders);
实际上并没有通过网络。 RestTemplate
使用模拟ClientHttpRequestFactory
创建虚假ClientHttpRequest
,其产生的假ClientHttpResponse
没有Content-Type
标头。如果RestTemplate
查看ClientHttpResponse
以确定其Content-Type
但未找到,则默认为application/octet-stream
。
因此,您的控制器未设置内容类型,因为您的控制器永远不会被命中。 RestTemplate
正在为您的响应使用默认内容类型,因为它是模拟的,实际上并不包含一个。
来自您的评论:
我想知道我是否理解模拟服务器正在测试的内容。我明白 它将用于验收测试场景。是应该打 控制器在哪?
MockRestServiceServer
状态的javadoc:
客户端REST测试的主要入口点。用于测试 涉及直接或间接(通过客户代码) 使用
RestTemplate
。提供了一种设置细粒度的方法 对将要通过的请求的期望RestTemplate
以及定义发送回删除的响应的方法 需要一个实际运行的服务器。
换句话说,就好像您的应用程序服务器不存在一样。所以你可以抛出你想要的任何期望(和实际的返回值)并测试从客户端发生的任何事情。所以你没有测试你的服务器,你正在测试你的客户端。
您确定不是在寻找MockMvc
,而是
服务器端Spring MVC测试支持的主要入口点。
您可以设置在集成环境中实际使用@Controller
bean。您实际上并没有发送HTTP请求,但MockMvc
正在模拟它们的发送方式以及服务器的响应方式。
答案 1 :(得分:1)
这是MockHttpServletRequest
中的错误,我会尝试描述它。
跟踪器https://jira.springsource.org/browse/SPR-11308#comment-97327中的问题
已在4.0.1版中修复
当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());
}
}
我们对作品request.getContentType()
感兴趣。请求是MockHttpServletRequest
。让我们看一下方法getContentType():
public String getContentType() {
return this.contentType;
}
它只返回this.contentType
的值。 它不会从标题中返回值!并且this.contentType
始终为NULL。然后,contentType
方法中的matchMediaType
始终为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);
}
我知道这不是很好的解决方案,但它运作正常。