Сопоставление ответа RestTemplate с java-объектом

Я использую RestTemplate получать данные из службы удаленного отдыха, и мой код выглядит так.

ResponseEntity<List<MyObject >> responseEntity = restTemplate.exchange(request, responseType);

Но служба отдыха вернет только текстовое сообщение, в котором говорится, что запись не найдена, если нет результатов, и моя приведенная выше строка кода выдаст исключение. Я мог бы сначала сопоставить результат со строкой, а затем использовать Jackson 2 ObjectMapper для сопоставления с MyObject.

ResponseEntity<String> responseEntity = restTemplate.exchange(request, responseType);
String jsonInput= response.getBody();
List<MyObject> myObjects = objectMapper.readValue(jsonInput, new TypeReference<List<MyObject>>(){});

Но мне такой подход не нравится. Есть ли лучшее решение для этого?


person user9735824    schedule 28.02.2019    source источник
comment
А как насчет настройки собственного обработчика ошибок? spring-resttemplate-overriding-responseerrorhandler   -  person FlorianDe    schedule 11.03.2019
comment
Я тоже пробовал этот подход, но не смог его решить   -  person user9735824    schedule 11.03.2019
comment
Если весь API, к которому вы запрашиваете, обрабатывает их ответы подобным образом, вы также можете попытаться создать и добавить собственный AbstractHttpMessageConverter, как показано здесь resttemplate- jackson-custom-json-deserializing. Но вместо проверки на пустую строку проверьте текстовое сообщение, в котором говорится, что текст не найден.   -  person FlorianDe    schedule 11.03.2019


Ответы (2)


В своих проектах с restTemplate я обычно сохраняю ответ в java.util.Map и создаю метод, который преобразует эту карту в нужный мне объект. Возможно, сохранение ответа в абстрактном объекте, таком как Map, поможет вам с этой проблемой исключения.

Например, я делаю такой запрос:

List<Map> list = null;
List<MyObject> listObjects = new ArrayList<MyObject>();
HttpHeaders headers = new HttpHeaders();
HttpEntity<String> entity = new HttpEntity<>(headers);

ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.GET, entity, Map.class);

if (response != null && response.getStatusCode().value() == 200) {
    list = (List<Map>) response.getBody().get("items"); // this depends on the response
    for (Map item : list) { // we iterate for each one of the items of the list transforming it
        MyObject myObject = transform(item);
        listObjects.add(myObject);
    }
}

Функция transform () - это специально созданный мной метод: MyObject transform(Map item);, который получает объект Map и возвращает объект, который мне нужен. Вы можете сначала проверить, не были ли найдены записи, вместо того, чтобы вызвать преобразование метода.

person Carlos    schedule 14.03.2019

Прежде всего, вы можете написать оболочку для всего API. Аннотируйте его с помощью @Component, и вы можете использовать его где угодно, хотя Springs DI. Взгляните на этот пример проекта, который показывает сгенерированный код для клиента resttemplate с использованием генерации кода swagger.

Как вы сказали, вы безуспешно пытались реализовать настраиваемый обработчик ошибок респондента, я предполагаю, что API возвращает тело ответа "no record found", а код состояния - 200.

Поэтому вы можете создать собственный AbstractHttpMessageConverter, как упоминалось в моем втором ответе. Поскольку вы используете springs resttemplate, который использует objectmapper с jackson, нам не обязательно использовать этот очень общий суперкласс для создания нашего собственного. Мы можем использовать и расширить более подходящий AbstractJackson2HttpMessageConverter класс. Реализация для вашего конкретного варианта использования может выглядеть следующим образом:

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;

public class WeirdAPIJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
    public static final String NO_RECORD_FOUND = "no record found";

    public WeirdAPIJackson2HttpMessageConverter() {
        // Create another constructor if you want to pass an already existing ObjectMapper
        // Currently this HttpMessageConverter is applied for every MediaType, this is application-dependent
        super(new ObjectMapper(), MediaType.ALL);
    }

    @Override
    public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        try (BufferedReader br = new BufferedReader(new InputStreamReader(inputMessage.getBody(), DEFAULT_CHARSET))) {
            String responseBodyStr = br.lines().collect(Collectors.joining(System.lineSeparator()));
            if (NO_RECORD_FOUND.equals(responseBodyStr)) {
                JavaType javaType = super.getJavaType(type, contextClass);
                if(Collection.class.isAssignableFrom(javaType.getRawClass())){
                    return Collections.emptyList();
                } else if( Map.class.isAssignableFrom(javaType.getRawClass())){
                   return Collections.emptyMap();
                }
                return null;
            }
        }
        return super.read(type, contextClass, inputMessage);
    }
}

Пользовательский HttpMessageConverter проверяет тело ответа на наличие вашего конкретного сообщения «запись не найдена». В этом случае мы пытаемся вернуть значение по умолчанию в зависимости от общего типа возвращаемого значения. Atm возвращает пустой список, если тип возвращаемого значения является подтипом Collection, пустым набором для Set и null для всех других типов Class.

Кроме того, я создал RestClientTest с помощью MockRestServiceServer, чтобы продемонстрировать вам, как вы можете использовать свой RestTemplate в вышеупомянутом компоненте-оболочке API и как настроить его для использования нашего настраиваемого AbstractJackson2HttpMessageConverter.

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.client.ExpectedCount;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;

import java.util.List;
import java.util.Optional;

import static org.junit.Assert.*;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {RestTemplateResponseErrorHandlerIntegrationTest.MyObject.class})
@RestClientTest
public class RestTemplateResponseErrorHandlerIntegrationTest {

    static class MyObject {
        // This just refers to your MyObject class which you mentioned in your answer
    }

    private final static String REQUEST_API_URL = "/api/myobjects/";
    private final static String REQUEST_API_URL_SINGLE = "/api/myobjects/1";

    @Autowired
    private MockRestServiceServer server;

    @Autowired
    private RestTemplateBuilder builder;


    @Test
    public void test_custom_converter_on_weird_api_response_list() {
        assertNotNull(this.builder);
        assertNotNull(this.server);

        RestTemplate restTemplate = this.builder
                .messageConverters(new WeirdAPIJackson2HttpMessageConverter())
                .build();

        this.server.expect(ExpectedCount.once(), requestTo(REQUEST_API_URL))
                .andExpect(method(HttpMethod.GET))
                .andRespond(withStatus(HttpStatus.OK).body(WeirdAPIJackson2HttpMessageConverter.NO_RECORD_FOUND));

        this.server.expect(ExpectedCount.once(), requestTo(REQUEST_API_URL_SINGLE))
                .andExpect(method(HttpMethod.GET))
                .andRespond(withStatus(HttpStatus.OK).body(WeirdAPIJackson2HttpMessageConverter.NO_RECORD_FOUND));


        ResponseEntity<List<MyObject>> response = restTemplate.exchange(REQUEST_API_URL,
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<MyObject>>() {
                });

        assertNotNull(response.getBody());
        assertTrue(response.getBody().isEmpty());

        Optional<MyObject> myObject = Optional.ofNullable(restTemplate.getForObject(REQUEST_API_URL_SINGLE, MyObject.class));
        assertFalse(myObject.isPresent());

        this.server.verify();
    }
}
person FlorianDe    schedule 14.03.2019