Issue
(premise: the question is at the very end)
I made a new project using Spring Boot 2.7.4, that uses Spring Web.
I have to use a custom library for Json objects, so I made a custom HttpMessageConverter
for the class representing these Json objects.
I add that converter in the Spring configuration class, but when I run tests, the RestTemplate
s I use seem to not be using it for some reason.
The error I get is this:
org.springframework.web.client.UnknownContentTypeException: Could not extract response: no suitable HttpMessageConverter found for response type [interface com.example.MyJsonClass] and content type [application/json;charset=UTF-8]
at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:126)
at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:1037)
at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:1020)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:778)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:711)
at org.springframework.web.client.RestTemplate.postForEntity(RestTemplate.java:468)
at org.springframework.boot.test.web.client.TestRestTemplate.postForEntity(TestRestTemplate.java:440)
at com.example.Tests.test(Tests.java:61)
Before showing you the code, I'll mention that I found a way to make it work but I don't understand why it's needed. I'll post it after the code.
Here's the code:
Configuration:
@SpringBootApplication
public class MyApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
// adding the custom converter here
messageConverters.add(new MyCustomHttpMessageConverter());
}
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
}
Custom converter (can't disclose the implementation...):
public class MyCustomHttpMessageConverter implements HttpMessageConverter<MyJsonClass> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
// implementation...
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
// implementation...
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return List.of(MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8);
}
@Override
public MyJsonClass read(Class<? extends MyJsonClass> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
// implementation...
}
@Override
public void write(MyJsonClass t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
// implementation...
}
}
Controller:
@Controller
@RequestMapping("/")
public class MyController {
@Autowired
private RestTemplate restTemplate;
@PostMapping(path = "/test", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<MyJsonClass> test(@RequestBody MyJsonClass body) {
System.out.println("test endpoint called.");
// makes a remote call to an external service
ResponseEntity<MyJsonClass> response = restTemplate.postForEntity(
"http://localhost:8080/test",// this is normally a remote endpoint, I actually set it to localhost in the test configuration properties
body, MyJsonClass.class);
return ResponseEntity.ok(response.getBody());
}
}
Test class:
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class Tests {
@Autowired
private TestRestTemplate testRestTemplate;
@Autowired
private RestTemplate restTemplate;
private MockRestServiceServer mockServer;
private ObjectMapper mapper = new ObjectMapper();
@BeforeEach
public void init() {
mockServer = MockRestServiceServer.createServer(restTemplate);
}
@Test
public void test() throws Exception {
MyJsonClass testPayload = new MyJsonClass("testProperty", "testValue");// just an example
// I need to fake a request to a remote server
mockServer.expect(ExpectedCount.once(),
requestTo(new URI("http:localhost:8080/test")))
.andExpect(method(HttpMethod.POST))
.andExpect(content().string("{\"testProperty\":\"testValue\"}"))
.andRespond(withStatus(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(mapper.writeValueAsString(testPayload.clone().set("testProperty2", "testValue2"))));
ResponseEntity<MyJsonClass> response = testRestTemplate.postForEntity("/test", testPayload, MyJsonClass.class);
// assertions here...
}
}
The way I made it work, was by explicitly adding the converter to the 2 RestTemplate
s. But I thought it wasn't needed since I added it in the configureMessageConverters
method of the configuration class.
The creation of the restTemplate
bean changed to this:
@Bean
RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(new MyCustomHttpMessageConverter());
return restTemplate;
}
And in the test class, in the @BeforeEach
I added this line for the TestRestTemplate
:
testRestTemplate.getRestTemplate().getMessageConverters().add(new MyCustomHttpMessageConverter());
So I guess the final question is: why do I have to add the converter explicitly to every RestTemplate
if I've added it in the configuration through the implementation of the WebMvcConfigurer
class? (Note: I've also tried using the extendMessageConverters
method instead, same result).
Or alternatively: why are the RestTemplate
s not using the converter I've added in the configuration?
Solution
First of all, you create RestTemplate manually, without using Spring bean injection. That way, Spring is not able to autowire any configuration.
You should use RestTemplateBuilder instead to use Spring injection.
@Bean
RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.build();
}
Then, as you are using Spring boot, you don't have to register converters via WebMvcConfigurer but you can simply register a bean with your HttpMethodConverter
@Configuration
public class SpringConfiguration{
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.build();
}
@Bean
public HttpMessageConverter<MyJsonClass> myCustomHttpMessageConverter() {
return new MyCustomHttpMessageConverter();
}
}
That way, Spring should first create a bean with your HttpMessageConverter and then it should automatically inject it in RestTemplateBuilder.
Answered By - CaptainAye
Answer Checked By - Candace Johnson (JavaFixing Volunteer)