Несколько конвертеров с Retrofit 2

У меня есть служба REST HATEOAS (HAL), и мне удалось поговорить с ней с помощью приведенного ниже кода (используя halarious в качестве механизма преобразования), но когда я пытаюсь объединить конвертеры (stallone а также stallone2), приложение всегда будет выбирать первый конвертер вместо того, который подходит для типа ответа, что, конечно, приводит к ошибке.

Как можно избежать повторяющихся модификаций, которые отличаются только мелким шрифтом?

public interface Stallone {
   @GET("/discovery")
   Call<DiscoveryResponse> discover();
   @POST()
   Call<LoginResponse> login(@Url String url, @Body LoginRequest secret);
}
   public static void main(String... args) throws IOException {
      // Initialize a converter for each supported (return) type
      final Stallone stallone = new Retrofit.Builder()
         .baseUrl(BASE)
         .addConverterFactory(HALConverterFactory.create(DiscoveryResponse.class))
         .build().create(Stallone.class);
      final Stallone stallone2 = new Retrofit.Builder()
         .baseUrl(BASE)
         .addConverterFactory(HALConverterFactory.create(LoginResponse.class))
         .build().create(Stallone.class);

      // Follow the HAL links
      Response<DiscoveryResponse> response = stallone.discover().execute();
      System.out.println(response.code() + " " + response.message());
      Assert.assertNotNull(response.body());
      String loginPath = response.body().getLogin();
      Assert.assertEquals(loginPath, "/login");

      // Follow another link
      if (loginPath.startsWith("/"))
         loginPath = loginPath.substring(1);
      Response<LoginResponse> response2 =
         stallone2.login(loginPath,
                        new LoginRequest(AUTH0TOKEN, null)).execute();
      System.out.println(response2.code() + " " + response2.message());
      Assert.assertNotNull(response2.body());

      String setupPath = response2.body().getSetup();
      Assert.assertEquals(setupPath, "/setup");

      System.out.println("All OK!");
   }
public final class HALConverterFactory extends Converter.Factory {

   private final Gson gson;

   public static HALConverterFactory create(Class<?> type) {
      return new HALConverterFactory(type);
   }

   private HALConverterFactory(Class<?> type) {
      if (!HalResource.class.isAssignableFrom(type))
         throw new NullPointerException("Type should be a subclass of HalResource");
      GsonBuilder builder = new GsonBuilder();
      builder.registerTypeAdapter(HalResource.class, new HalSerializer());
      builder.registerTypeAdapter(HalResource.class, new HalDeserializer(type));
      builder.setExclusionStrategies(new HalExclusionStrategy());
      this.gson = builder.create();
   }

   @Override
   public Converter<ResponseBody, ?> fromResponseBody(Type type, Annotation[] annotations) {
      return new HALResponseBodyConverter<>(gson);
   }

   @Override public Converter<?, RequestBody> toRequestBody(Type type, Annotation[] annotations) {
      return new GsonRequestBodyConverter<>(gson, type);
   }
}
final class HALResponseBodyConverter<T extends HalResource>
   implements Converter<ResponseBody, T> {
   private final Gson gson;

   HALResponseBodyConverter(Gson gson) {
      this.gson = gson;
   }

   @Override public T convert(ResponseBody value) throws IOException {
      BufferedSource source = value.source();
      try {
         String s = source.readString(Charset.forName("UTF-8"));
         return (T) gson.fromJson(s, HalResource.class);
      } catch (Exception e) {
         throw new RuntimeException(e);
      } finally {
         closeQuietly(source);
      }
   }

   private static void closeQuietly(Closeable closeable) {
      if (closeable == null) return;
      try {
         closeable.close();
      } catch (IOException ignored) {
      }
   }
}

Опять же, проблема в том, что когда вы пытаетесь сократить вышеуказанное, вот так:

  final Stallone stallone = new Retrofit.Builder()
     .baseUrl(BASE)
.addConverterFactory(HALConverterFactory.create(DiscoveryResponse.class))
     .addConverterFactory(HALConverterFactory.create(LoginResponse.class))
     .build().create(Stallone.class);

вы получите исключение в Response<LoginResponse> response2 = ... линия:

Исключение в потоке "main" java.lang.ClassCastException: com.example.retrofit.DiscoveryResponse не может быть приведено к com.example.retrofit.LoginResponse.

4 ответа

Решение

Вам нужно вернуть null от Converter.Factory если тип не совпадает. Держать Class<?> вокруг в поле, чтобы сравнить его с.

@Override
public Converter<ResponseBody, ?> fromResponseBody(Type type, Annotation[] annotations) {
  if (!this.type.equals(type)) {
    return null;
  }
  return new HALResponseBodyConverter<>(gson);
}

Это позволит использовать несколько экземпляров, поскольку каждый из них относится только к своему типу.

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

@Override
public Converter<ResponseBody, ?> fromResponseBody(Type type, Annotation[] annotations) {
  if (!HALResponse.class.isAssignableFrom(type)) {
    return null;
  }
  // TODO create converter with `type` now that you know what it is...
}

Вы можете посмотреть на Конвертер проводов в репо, который делает это для полного примера.

package ch.halarious.core;

import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * Custom Hal Deserializer  
 *
 * @author jaren
 */
public class CustomHalDeserializer extends HalDeserializer {

    /**
     * Intialisiert ein HalDeserializer-Objekt
     *
     * @param targetType Typ, den wir eigentlich deserialisieren sollten
     */
    public CustomHalDeserializer(Class<?> targetType) {
        super(targetType);
    }

    class CustomArrayList extends ArrayList implements HalResource{}

    public HalResource deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context, Class<?> targetType) throws JsonParseException {
        // Es handelt sich um ein JSON-Objekt.
        JsonObject jsonObject = json.getAsJsonObject();
        JsonObject embeddedRoot = jsonObject.getAsJsonObject(HalConstants.EMBEDDED_ROOT);

        if(embeddedRoot != null){
            Set<Map.Entry<String, JsonElement>> set = embeddedRoot.entrySet();
            if(set.toArray().length == 1){
                JsonArray ja = embeddedRoot.getAsJsonArray(set.iterator().next().getKey());
                if(ja.isJsonArray()) {
                    CustomArrayList arrayResult = new CustomArrayList();
                    Iterator<JsonElement> i = ja.iterator();
                    while(i.hasNext()){
                        JsonElement je = i.next();
                        arrayResult.add(super.deserialize(je, typeOfT, context, targetType));
                    }
                    return arrayResult;
                }
            }
        }

        return super.deserialize(json, typeOfT, context, targetType);
    }
}

В моем случае мне нужно было сериализовать и десериализовать только один класс в XML. Для всего остального мне нужен был Джсон. Итак, я зарегистрировал свои адаптеры так:

retrofit = new Retrofit.Builder()
                .baseUrl(BuildConfig.BASE_URL)
                .addConverterFactory(EditUserXmlConverterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(createGson()))
                .client(httpClient.build())
                .build();

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

if (!(type instanceof Class)) return null;

в

if (type != NeedToBeXML.class) return null;

Таким образом, только ответы и запросы типа NeedToBeXML конвертируются в XML - и все остальное JSON.

Я сделал почти то же самое, что @jake-wharton сказал в /questions/1524931/neskolko-konverterov-s-retrofit-2/1524950#1524950 но добавил некоторые изменения:

public class GenericConverterFactory<T> extends Converter.Factory {

    private final Class<T> clazz;

    public static GenericConverterFactory create(Class<T> clazz) {
        return new GenericConverterFactory(clazz);
    }

    private GenericConverterFactory(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        if (!isNeededType(type)) {
            return null;
        }

        // some converter that knows how to return your specific type T
        return new GenericConverter(clazz);
    }

    private boolean isNeededType(Type type) {
        if(type instanceof GenericArrayType) {
            // if type is array we should check if it has the same component as our factory clazz
            // if our factory clazz is not array getComponentType will return null
            return ((GenericArrayType) type).getGenericComponentType().equals(clazz.getComponentType());
        } else if(clazz.getComponentType() == null) {
            // if factory clazz is not array and type is not array too
            // type is just a Class<?> and we should check if they are equal
            return clazz.equals(type);
        } else {
            // otherwise our clazz is array and type is not
            return false;
        }
    }
}

Тип исходит из модифицированного интерфейса, например, если у вас есть:

public interface SomeApi{
     @GET("customelement")
     CustomElement[] getCustomElements();
     @GET("customelement/{id}")
     CustomElement getCustomElement(@Path("id") int id);
}

Для метода getCustomElements() тип будет GenericArrayType с GenericComponentType как CustomElement.class и для второго типа метода будет просто CustomElement.class

Не уверен, что это лучшее решение, но для меня это работает. Надеюсь, поможет.

Другие вопросы по тегам