web-dev-qa-db-ja.com

ジェネリックパラメーターを使用したジェネリックメソッドでの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のバグですか?

UPDATE 1@Sotiriosのおかげで、この概念を理解しました。残念ながら、私はここに新しく登録されたので、彼の答えにコメントできないので、ここに書いてください。 MapキーでClassキー(彼の答えの最後に@Sotiriosによって提案された)で私の問題を解決するために提案されたアプローチを実装する方法を明確に理解しているかどうかはわかりません。例を挙げてくれませんか?

50

いいえ、それはバグではありません。これは、ParameterizedTypeReferenceハックの仕組みの結果です。

その実装を見ると、 Class#getGenericSuperclass() を使用しています

このクラスによって表されるエンティティ(クラス、インターフェース、プリミティブ型、またはvoid)の直接のスーパークラスを表すTypeを返します。

スーパークラスがパラメーター化された型の場合、返されるTypeオブジェクトは、ソースコードで使用される実際の型パラメーターを正確に反映する必要があります。

したがって、使用する場合

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

ResponseWrapper<MyClass>に対してTypeを正確に返します。

使用する場合

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

ResponseWrapper<T>に対してTypeを正確に返します。これがソースコードでの表示方法だからです。

Springが実際にTオブジェクトであるTypeVariableを検出すると、使用するタイプがわからないため、デフォルトが使用されます。

ParameterizedTypeReferenceを提案された方法で使用することはできません。あらゆるタイプを受け入れるという意味で汎用的にすることはできません。そのクラスの事前定義されたMapにマップされたClassキーを持つParameterizedTypeReferenceを書くことを検討してください。

ParameterizedTypeReferenceをサブクラス化し、getTypeメソッドをオーバーライドして、適切に作成されたParameterizedTypeIonSpinの提案どおり を返すことができます。

46

以下のコードが示すように、動作します。

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();
    }

}
14
김원겸

Sotiriosが説明するように、ParameterizedTypeReferenceは使用できませんが、ParameterizedTypeReferenceはオブジェクトマッパーにTypeを提供するためにのみ使用され、型消去が発生すると削除されるクラスがあるので、独自の ParameterizedType を作成できます=そしてそれをRestTemplateに渡すと、オブジェクトマッパーが必要なオブジェクトを再構築できるようになります。

最初に、ParameterizedTypeインターフェイスを実装する必要があります。GoogleGsonプロジェクトで実装を見つけることができます here 。プロジェクトに実装を追加したら、次のように抽象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>>());

すべてのタイプ情報が存在する場合、オブジェクトマッパーはResponseWrapper<MyClass>オブジェクトを適切に構築します

9
IonSpin

実際には、これを行うことができますが、追加のコードが必要です。

Guava と同等の ParameterizedTypeReference があり、これは TypeToken と呼ばれます。

Guavaのクラスは、Springの同等クラスよりもはるかに強力です。必要に応じてTypeTokenを作成できます。例えば:

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>>() {});
8

ListResultEntityに org.springframework.core.ResolvableType を使用しています:

    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;
}

これは春を利用するだけで、もちろん返される型についての知識が必要です(しかし、クラスをvarargsとして提供する限り、Wrapper >>のようなものでも動作するはずです)

5
Justin

これを行う別の方法があります... RestTemplateのメッセージコンバーターをStringにスワップすると、生のJSONを受信できるようになります。生のJSONを使用して、Jackson Object Mapperを使用してGenericコレクションにマップできます。方法は次のとおりです。

メッセージコンバーターを交換します。

    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);
        }
        ...
2
Dilettante44

汎用restTemplate呼び出しの独自の実装:

private <REQ, RES> RES queryRemoteService(String url, HttpMethod method, REQ req, Class reqClass) {
    RES result = null;
    try {
        long startMillis = System.currentTimeMillis();

        // Set the Content-Type header
        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.setContentType(new MediaType("application","json"));            

        // Set the request entity
        HttpEntity<REQ> requestEntity = new HttpEntity<>(req, requestHeaders);

        // Create a new RestTemplate instance
        RestTemplate restTemplate = new RestTemplate();

        // Add the Jackson and String message converters
        restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter());

        // Make the HTTP POST request, marshaling the request to JSON, and the response to a String
        ResponseEntity<RES> responseEntity = restTemplate.exchange(url, method, requestEntity, reqClass);
        result = responseEntity.getBody();
        long stopMillis = System.currentTimeMillis() - startMillis;

        Log.d(TAG, method + ":" + url + " took " + stopMillis + " ms");
    } catch (Exception e) {
         Log.e(TAG, e.getMessage());
    }
    return result;
}

コンテキストを追加するために、これでRESTfulサービスを使用しています。したがって、すべての要求と応答は次のように小さなPOJOにラップされます。

public class ValidateRequest {
  User user;
  User checkedUser;
  Vehicle vehicle;
}

そして

public class UserResponse {
  User user;
  RequestResult requestResult;
}

これを呼び出すメソッドは次のとおりです。

public User checkUser(User user, String checkedUserName) {
    String url = urlBuilder()
            .add(USER)
            .add(USER_CHECK)
            .build();

    ValidateRequest request = new ValidateRequest();
    request.setUser(user);
    request.setCheckedUser(new User(checkedUserName));

    UserResponse response = queryRemoteService(url, HttpMethod.POST, request, UserResponse.class);
    return response.getUser();
}

はい、リストdto-sもあります。

1
AlexV

注:この回答は、Sotirios Delimanolisの回答とコメントを参照/追加しています。

Sotiriosのコメントに示されているように、Map<Class, ParameterizedTypeReference<ResponseWrapper<?>>>で動作するように試みましたが、例なしではできませんでした。

最後に、ParameterizedTypeReferenceからワイルドカードとパラメーター化を削除し、代わりにrawタイプを使用しました。

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);

そして、これは最終的に機能しました。

誰かがパラメータ化の例を持っているなら、私はそれを見てとても感謝します。

0
Ginkobonsai