Home > Net >  Spring RestTemplate missing custom HttpMessageConverter even though it was added in configuration
Spring RestTemplate missing custom HttpMessageConverter even though it was added in configuration

Time:10-26

(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 RestTemplates 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 RestTemplates. 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 RestTemplates not using the converter I've added in the configuration?

CodePudding user response:

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.

  • Related