Подтвердить что ты не робот

Использование Spring RestTemplate в общем методе с общим параметром

Чтобы использовать общие типы с Spring RestTemplate, нам нужно использовать ParameterizedTypeReference (Невозможно получить общий ResponseEntity <T> , где T - общий класс" SomeClass <SomeGenericType> ; ")

Предположим, что у меня есть некоторый класс

public class MyClass {
    int users[];

    public int[] getUsers() { return users; }
    public void setUsers(int[] users) {this.users = users;}
}

И некоторый класс оболочки

public class ResponseWrapper <T> {
    T response;

    public T getResponse () { return response; }
    public void setResponse(T response) {this.response = response;}
}

Итак, если я пытаюсь сделать что-то вроде этого, все в порядке.

public ResponseWrapper<MyClass> makeRequest(URI uri) {
    ResponseEntity<ResponseWrapper<MyClass>> response = template.exchange(
        uri,
        HttpMethod.POST,
        null,
        new ParameterizedTypeReference<ResponseWrapper<MyClass>>() {});
    return response;
}

Но когда я пытаюсь создать общий вариант вышеупомянутого метода...

public <T> ResponseWrapper<T> makeRequest(URI uri, Class<T> clazz) {
   ResponseEntity<ResponseWrapper<T>> response = template.exchange(
        uri,
        HttpMethod.POST,
        null,
        new ParameterizedTypeReference<ResponseWrapper<T>>() {});
    return response;
}

... и вызывая этот метод так...

makeRequest(uri, MyClass.class)

... вместо получения объекта ResponseEntity<ResponseWrapper<MyClass>> я получаю объект ResponseEntity<ResponseWrapper<LinkedHashSet>>.

Как я могу решить эту проблему? Это ошибка RestTemplate?

ОБНОВЛЕНИЕ 1 Благодаря @Sotirios я понимаю концепцию. К сожалению, я недавно зарегистрирован здесь, поэтому я не могу прокомментировать его ответ, поэтому напишу его здесь. Я не уверен, что я четко понимаю, как реализовать предложенный подход для решения моей проблемы с помощью Map с помощью клавиши Class (предложенный @Sotirios в конце его ответа). Кто-нибудь попытается привести пример?

4b9b3361

Ответ 1

Нет, это не ошибка. Это результат того, как работает ParameterizedTypeReference hack.

Если вы посмотрите на его реализацию, он использует Class#getGenericSuperclass(), в котором говорится

Возвращает тип, представляющий прямой суперкласс объекта (класс, интерфейс, примитивный тип или пустота), представленный этим классом.

Если суперкласс является параметризованным типом, возвращается объект Typeдолжны точно отражать фактические параметры типа, используемые в источнике код.

Итак, если вы используете

new ParameterizedTypeReference<ResponseWrapper<MyClass>>() {}

он точно вернет a Type для ResponseWrapper<MyClass>.

Если вы используете

new ParameterizedTypeReference<ResponseWrapper<T>>() {}

он будет точно возвращать Type для ResponseWrapper<T>, потому что именно так он появляется в исходном коде.

Когда Spring видит T, который фактически является объектом TypeVariable, он не знает тип, который будет использоваться, поэтому он использует свой по умолчанию.

Вы не можете использовать ParameterizedTypeReference способ, которым вы предлагаете, что делает его общим в смысле принятия любого типа. Рассмотрим запись Map с ключом Class, сопоставленным с предопределенным ParameterizedTypeReference для этого класса.

Вы можете подклассифицировать ParameterizedTypeReference и переопределить его метод getType, чтобы вернуть созданное соответственно ParameterizedType, как было предложено IonSpin.

Ответ 2

Как показывает следующий код, он работает.

public <T> ResponseWrapper<T> makeRequest(URI uri, final Class<T> clazz) {
   ResponseEntity<ResponseWrapper<T>> response = template.exchange(
        uri,
        HttpMethod.POST,
        null,
        new ParameterizedTypeReference<ResponseWrapper<T>>() {
            public Type getType() {
                return new MyParameterizedTypeImpl((ParameterizedType) super.getType(), new Type[] {clazz});
        }
    });
    return response;
}

public class MyParameterizedTypeImpl implements ParameterizedType {
    private ParameterizedType delegate;
    private Type[] actualTypeArguments;

    MyParameterizedTypeImpl(ParameterizedType delegate, Type[] actualTypeArguments) {
        this.delegate = delegate;
        this.actualTypeArguments = actualTypeArguments;
    }

    @Override
    public Type[] getActualTypeArguments() {
        return actualTypeArguments;
    }

    @Override
    public Type getRawType() {
        return delegate.getRawType();
    }

    @Override
    public Type getOwnerType() {
        return delegate.getOwnerType();
    }

}

Ответ 3

Как объясняет Сотириос, вы не можете использовать ParameterizedTypeReference, но ParameterizedTypeReference используется только для предоставления Type объекту mapper, а поскольку у вас есть класс, который удаляется при стирании типа, вы можете создать свой собственный ParameterizedType и передать это RestTemplate, чтобы объект mapper смог восстановить требуемый объект.

Сначала вам нужно реализовать интерфейс ParameterizedType, вы можете найти реализацию в проекте Google Gson здесь. Когда вы добавите реализацию в свой проект, вы можете расширить абстрактный ParameterizedTypeReference следующим образом:

class FakeParameterizedTypeReference<T> extends ParameterizedTypeReference<T> {

@Override
public Type getType() {
    Type [] responseWrapperActualTypes = {MyClass.class};
    ParameterizedType responseWrapperType = new ParameterizedTypeImpl(
        ResponseWrapper.class,
        responseWrapperActualTypes,
        null
        );
    return responseWrapperType;
    }
}

И затем вы можете передать это своей функции обмена:

template.exchange(
    uri,
    HttpMethod.POST,
    null,
    new FakeParameterizedTypeReference<ResponseWrapper<T>>());

Со всей информацией о типе, данный объект mapper будет правильно построить ваш объект ResponseWrapper<MyClass>

Ответ 4

На самом деле, вы можете сделать это, но с дополнительным кодом.

Существует Guava- эквивалент ParameterizedTypeReference, и он называется TypeToken.

Класс Guava намного мощнее, чем Spring. Вы можете составить TypeTokens, как вы хотите. Например:

static <K, V> TypeToken<Map<K, V>> mapToken(TypeToken<K> keyToken, TypeToken<V> valueToken) {
  return new TypeToken<Map<K, V>>() {}
    .where(new TypeParameter<K>() {}, keyToken)
    .where(new TypeParameter<V>() {}, valueToken);
}

Если вы вызываете mapToken(TypeToken.of(String.class), TypeToken.of(BigInteger.class)); вы создадите TypeToken<Map<String, BigInteger>> !

Единственным недостатком здесь является то, что многие Spring API требуют ParameterizedTypeReference а не TypeToken. Но мы можем создать реализацию ParameterizedTypeReference которая является адаптером для самого TypeToken.

import com.google.common.reflect.TypeToken;
import org.springframework.core.ParameterizedTypeReference;

import java.lang.reflect.Type;

public class ParameterizedTypeReferenceBuilder {

    public static <T> ParameterizedTypeReference<T> fromTypeToken(TypeToken<T> typeToken) {
        return new TypeTokenParameterizedTypeReference<>(typeToken);
    }

    private static class TypeTokenParameterizedTypeReference<T> extends ParameterizedTypeReference<T> {

        private final Type type;

        private TypeTokenParameterizedTypeReference(TypeToken<T> typeToken) {
            this.type = typeToken.getType();
        }

        @Override
        public Type getType() {
            return type;
        }

        @Override
        public boolean equals(Object obj) {
            return (this == obj || (obj instanceof ParameterizedTypeReference &&
                    this.type.equals(((ParameterizedTypeReference<?>) obj).getType())));
        }

        @Override
        public int hashCode() {
            return this.type.hashCode();
        }

        @Override
        public String toString() {
            return "ParameterizedTypeReference<" + this.type + ">";
        }

    }

}

Тогда вы можете использовать это так:

public <T> ResponseWrapper<T> makeRequest(URI uri, Class<T> clazz) {
   ParameterizedTypeReference<ResponseWrapper<T>> responseTypeRef =
           ParameterizedTypeReferenceBuilder.fromTypeToken(
               new TypeToken<ResponseWrapper<T>>() {}
                   .where(new TypeParameter<T>() {}, clazz));
   ResponseEntity<ResponseWrapper<T>> response = template.exchange(
        uri,
        HttpMethod.POST,
        null,
        responseTypeRef);
    return response;
}

И назовите это как:

ResponseWrapper<MyClass> result = makeRequest(uri, MyClass.class);

И тело ответа будет правильно десериализовано как ResponseWrapper<MyClass> !

Вы даже можете использовать более сложные типы, если переписываете свой общий метод запроса (или перегружаете его) следующим образом:

public <T> ResponseWrapper<T> makeRequest(URI uri, TypeToken<T> resultTypeToken) {
   ParameterizedTypeReference<ResponseWrapper<T>> responseTypeRef =
           ParameterizedTypeReferenceBuilder.fromTypeToken(
               new TypeToken<ResponseWrapper<T>>() {}
                   .where(new TypeParameter<T>() {}, resultTypeToken));
   ResponseEntity<ResponseWrapper<T>> response = template.exchange(
        uri,
        HttpMethod.POST,
        null,
        responseTypeRef);
    return response;
}

Таким образом, T может быть сложным типом, например List<MyClass>.

И назовите это как:

ResponseWrapper<List<MyClass>> result = makeRequest(uri, new TypeToken<List<MyClass>>() {});

Ответ 5

У меня есть другой способ сделать это... Предположим, вы заменили свой конвертер сообщений на String для вашего RestTemplate, после чего вы можете получить raw JSON. Используя необработанный JSON, вы можете сопоставить его в свою коллекцию Generic с помощью Mapper объекта Jackson. Вот как:

Смените конвертер сообщений:

    List<HttpMessageConverter<?>> oldConverters = new ArrayList<HttpMessageConverter<?>>();
    oldConverters.addAll(template.getMessageConverters());

    List<HttpMessageConverter<?>> stringConverter = new ArrayList<HttpMessageConverter<?>>();
    stringConverter.add(new StringHttpMessageConverter());

    template.setMessageConverters(stringConverter);

Затем получите ответ JSON следующим образом:

    ResponseEntity<String> response = template.exchange(uri, HttpMethod.GET, null, String.class);

Обработать ответ следующим образом:

     String body = null;
     List<T> result = new ArrayList<T>();
     ObjectMapper mapper = new ObjectMapper();

     if (response.hasBody()) {
        body = items.getBody();
        try {
            result = mapper.readValue(body, mapper.getTypeFactory().constructCollectionType(List.class, clazz));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            template.setMessageConverters(oldConverters);
        }
        ...

Ответ 6

Я использую org.springframework.core.ResolvableType для ListResultEntity:

    ResolvableType resolvableType = ResolvableType.forClassWithGenerics(ListResultEntity.class, itemClass);
    ParameterizedTypeReference<ListResultEntity<T>> typeRef = ParameterizedTypeReference.forType(resolvableType.getType());

Итак, в вашем случае:

public <T> ResponseWrapper<T> makeRequest(URI uri, Class<T> clazz) {
   ResponseEntity<ResponseWrapper<T>> response = template.exchange(
        uri,
        HttpMethod.POST,
        null,
        ParameterizedTypeReference.forType(ResolvableType.forClassWithGenerics(ResponseWrapper.class, clazz)));
    return response;
}

Это использует только Spring и, конечно, требует некоторых знаний о возвращаемых типах (но даже должно работать для таких вещей, как Wrapper >>, если вы предоставляете классы как varargs)

Ответ 7

Примечание. Этот ответ ссылается/добавляет ответ и комментарий Sotirios Delimanolis.

Я попытался заставить его работать с Map<Class, ParameterizedTypeReference<ResponseWrapper<?>>>, как указано в комментарии Sotirios, но не может без примера.

В конце концов, я сбросил подстановочный знак и параметризацию из ParameterizedTypeReference и вместо этого использовал необработанные типы, например

Map<Class<?>, ParameterizedTypeReference> typeReferences = new HashMap<>();
typeReferences.put(MyClass1.class, new ParameterizedTypeReference<ResponseWrapper<MyClass1>>() { });
typeReferences.put(MyClass2.class, new ParameterizedTypeReference<ResponseWrapper<MyClass2>>() { });

...

ParameterizedTypeReference typeRef = typeReferences.get(clazz);

ResponseEntity<ResponseWrapper<T>> response = restTemplate.exchange(
        uri, 
        HttpMethod.GET, 
        null, 
        typeRef);

и это, наконец, сработало.

Если у кого-нибудь есть пример с параметризацией, я был бы очень благодарен за это.

Ответ 8

Поэтому я нашел хорошее решение, которое даже не требует от вас использования ResponseWrapper, поскольку использование ResponseWrapper может привести к проблеме десериализации.

Мой универсальный метод:

 private <T> ResponseEntity<T> performGetRequest(final String url, TypeToken<T> responseType) {
    ParameterizedTypeReference<T> responseTypeRef =
            MyParameterizedTypeImpl.fromTypeToken(
                    new TypeToken<T>(getClass()) {}
                            .where(new TypeParameter<T>() {}, responseType));

    return restTemplate.exchange(
                url,
                HttpMethod.GET,
                null,
                responseTypeRef);
  }

Как вам нужно позвонить:

final ResponseEntity<List<CustomType>> response = performGetRequest("www.sample.com",new TypeToken<List<CustomType>>() {});

и реализация MyParameterizedTypeImpl, как @김원겸, упоминается:

public class MyParameterizedTypeImpl  {

    public static <T> ParameterizedTypeReference<T> fromTypeToken(TypeToken<T> typeToken) {
        return new TypeTokenParameterizedTypeReference<>(typeToken);
    }

    private static class TypeTokenParameterizedTypeReference<T> extends ParameterizedTypeReference<T> {

        private final Type type;

        private TypeTokenParameterizedTypeReference(TypeToken<T> typeToken) {
            this.type = typeToken.getType();
        }

        @Override
        public Type getType() {
            return type;
        }

        @Override
        public boolean equals(Object obj) {
            return (this == obj || (obj instanceof ParameterizedTypeReference &&
                    this.type.equals(((ParameterizedTypeReference<?>) obj).getType())));
        }

        @Override
        public int hashCode() {
            return this.type.hashCode();
        }

        @Override
        public String toString() {
            return "ParameterizedTypeReference<" + this.type + ">";
        }

    }

Таким образом, вы более обобщенно относитесь к типу, вы можете передать его как List<customType>

Ответ 9

Вы могли бы сделать это, как показано ниже, без использования класса-оболочки. Попытайтесь использовать реальную мощность, если дженерики

/**
 * 
 * Method for GET Operations
 * 
 * @param url url to send request
 * @return returned json String
 * @throws Exception exception thrown
 */
public List<T> getJSONString(String url, Class<T[]> clazz) throws Exception {

    logger.debug("getJSONString() : Start");

    List<T> response = null;

    ResponseEntity<T[]> responseEntity = null;

    List<String> hostList = Arrays.asList(propertyFileReader.getRestApiHostList().split("\\s*,\\s*"));

    Iterator<String> hostListIter = hostList.iterator();

    String host = null;

    while (true) {
        try {
            host = hostListIter.next();

            logger.debug("getJSONString() : url={}", (host + url));
            responseEntity = restTemplate.getForEntity(host + url, clazz);
            if (responseEntity != null) {
                response = Arrays.asList(responseEntity.getBody());
                break;
            }
        } catch (RestClientException ex) {
            if (!hostListIter.hasNext()) {
                throw ex;
            }
            logger.debug("getJSONString() : I/O exception {} occurs when processing url={} ", ex.getMessage(),
                    (host + url));
        }
    }

    return response;
}