it-swarm.com.ru

Retrofit2 Обработка условия, когда код состояния 200, но структура json отличается от класса модели данных

Я использую Retrofit2 и RxJava2CallAdapterFactory.

API, который я использую, всегда возвращает код состояния, равный 200, и для строки JSON успеха и ответа структура JSON совершенно иная. Поскольку код состояния всегда равен 200, метод onResponse () вызывается всегда. Следовательно, я не могу извлечь сообщения об ошибках из JSON в состоянии ошибки.

Решение 1:

Я использую ScalarsConverterFactory для получения строки ответа и вручную использую Gson для анализа ответа . Как получить ответ в виде строки, используя модификацию без использования GSON или любой другой библиотеки в Android

Проблема с этим решением: я планирую использовать RxJava2CallAdapterFactory, чтобы метод модернизации возвращал класс DataModel.

Мне нужно найти лучшее решение для этой проблемы, так как я могу продолжать возвращать классы модели данных из метода Retrofit и каким-то образом определить условие ошибки по ответу (определить, что JSON ответа не соответствует модели данных), а затем проанализировать ошибку JSON в модель данных.

Модифицированный клиент

 public static Retrofit getClient(String url) {
        if (apiClient == null) {

            HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();

            interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
            OkHttpClient httpClient = new OkHttpClient.Builder().addInterceptor(interceptor).build();
            apiClient = new Retrofit.Builder()
                    .baseUrl(url)
                    /*addCallAdapterFactory for RX Recyclerviews*/
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    /* add ScalarsConverterFactory to get json string as response */
//                    .addConverterFactory(ScalarsConverterFactory.create())
                    .addConverterFactory(GsonConverterFactory.create())
//                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .client(httpClient)
                    .build();
        }
        return apiClient;
    }

Метод

public static void getLoginAPIResponse(String username, String password, String sourceId, String uuid, final HttpCallback httpCallback) {
        baseUrl = AppPreference.getParam(UiUtils.getContext(), SPConstants.BASE_URL, "").toString();
        ApiInterface apiService =
                ApiClient.getClient(baseUrl).create(ApiInterface.class);

        Call<LoginBean> call = apiService.getLoginResponse(queryParams);
        call.enqueue(new Callback<LoginBean>() {

            @Override
            public void onResponse(Call<LoginBean> call, Response<LoginBean> response) {

                if (response.body().isObjectNull()) {
                    httpCallback.resultCallback(APIConstants.API_LOGIN, HttpCallback.REQUEST_TYPE_GET,
                            HttpCallback.RETURN_TYPE_FAILURE, 0, null);
                    return;
                }
                httpCallback.resultCallback(APIConstants.API_LOGIN, HttpCallback.REQUEST_TYPE_GET,
                        HttpCallback.RETURN_TYPE_SUCCESS, response.code(), response.body());
            }





        @Override
        public void onFailure(Call<LoginBean> call, Throwable t) {
            // Log error here since request failed
            httpCallback.resultCallback(APIConstants.API_APP_VERIFICATION, HttpCallback.REQUEST_TYPE_GET,
                    HttpCallback.RETURN_TYPE_FAILURE, 0, t);
            t.printStackTrace();
        }
    });
}

Интерфейс

@GET("App/login")
Call<LoginBean> getLoginResponse(@QueryMap Map<String, String> queryMap);

PS: API пока не может измениться, так как некоторые другие приложения его используют.

  • Парсер Gson не возвращает экземпляр нулевого объекта, чтобы я понял, что существует несоответствие структуры json и модели данных.

  • RestAdapter устарел в Retrofit 2 

Я ищу лучший подход для решения этой проблемы, желательно избегать анализа json вручную и максимально использовать адаптеры RX для модернизации и модернизации.

ПРАВКА 

Код ответа 200 отсюда 

  1. response.isSuccessful() == true 

  2. response.body() != null также имеет значение true, так как Gson никогда не создает пустой экземпляр или выдает какое-либо исключение, если есть несоответствие структуры JSON 

  3. response.errorBody() == null всегда, когда ответ отправляется с сервера как поток ввода.

    if (response.isSuccessful() && response.body() != null) {
        //control always here as status code 200 for error condition also
    }else if(response.errorBody()!=null){
        //control never reaches here
    }
    

Правка 2

РЕШЕНИЕ

Решение основано на ответе на вопрос 

  • Я создал базовый родовой класс для дальнейшего ответа. 
  • Так как у меня есть несколько моделей API и данных, я должен создать для каждого 

BASE API BEAN 

public class BaseApiBean<T> {

    @Nullable
    private T responseBean;

    @Nullable
    private ErrorBean errorBean;

    public BaseApiBean(T responseBean, ErrorBean errorBean) {
        this.responseBean = responseBean;
        this.errorBean = errorBean;
    }

    public T getResponseBean() {
        return responseBean;
    }

    public void setResponseBean(T responseBean) {
        this.responseBean = responseBean;
    }

    public ErrorBean getErrorBean() {
        return errorBean;
    }

    public void setErrorBean(ErrorBean errorBean) {
        this.errorBean = errorBean;
    }
}

BASE DESERIALIZER 

  public abstract class BaseDeserializer implements JsonDeserializer<BaseApiBean> {


        @Override
        public BaseApiBean deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
                throws JsonParseException {
            // Get JsonObject
            final JsonObject jsonObject = json.getAsJsonObject();
            if (jsonObject.has("result")) {
                   /* {"result":"404"}*/
                ErrorBean errorMessage = new Gson().fromJson(jsonObject, ErrorBean.class);
                return getResponseBean(errorMessage);
            } else {

                return getResponseBean(jsonObject);
            }
        }

        public abstract BaseApiBean getResponseBean(ErrorBean errorBean);
        public abstract BaseApiBean getResponseBean(JsonObject jsonObject);
    }

Пользовательский десериализатор для каждого API 

public class LoginDeserializer extends BaseDeserializer {


    @Override
    public BaseApiBean getResponseBean(ErrorBean errorBean) {
        return new LoginResponse(null, errorBean);
    }

    @Override
    public BaseApiBean getResponseBean(JsonObject jsonObject) {

        LoginBean loginBean = (new Gson().fromJson(jsonObject, LoginBean.class));
        return new LoginResponse(loginBean, null);
    }
}

ТАМОЖЕННЫЙ ОТВЕТНЫЙ БИН

public class LoginResponse extends BaseApiBean<LoginBean> {
    public LoginResponse(LoginBean responseBean, ErrorBean errorBean) {
        super(responseBean, errorBean);
    }

}

CLIENT

public class ApiClient {
    private static Retrofit apiClient = null;
    private static Retrofit apiClientForFeedBack = null;
    private static LoginDeserializer loginDeserializer = new LoginDeserializer();
    private static AppVerificationDeserializer appVerificationDeserializer = new AppVerificationDeserializer();


    public static Retrofit getClient(String url) {
        if (apiClient == null) {

            GsonBuilder gsonBuilder=new GsonBuilder();
            gsonBuilder.registerTypeAdapter(LoginResponse.class,
                    loginDeserializer);
            gsonBuilder.registerTypeAdapter(AppVerificationResponse.class,
                    appVerificationDeserializer);
            Gson gson= gsonBuilder.create();


            HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();

            interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
            OkHttpClient httpClient = new OkHttpClient.Builder().addInterceptor(interceptor)
                    .retryOnConnectionFailure(true)
                    .connectTimeout(15, TimeUnit.SECONDS)
                    .build();

            apiClient = new Retrofit.Builder()
                    .baseUrl(url)
                    /*addCallAdapterFactory for RX Recyclerviews*/
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    /* add ScalarsConverterFactory to get json string as response */
//                    .addConverterFactory(ScalarsConverterFactory.create())
//                    .addConverterFactory(GsonConverterFactory.create())
                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .client(httpClient)
                    .build();
        }
        return apiClient;
    }

РУЧНОЙ ОТВЕТ 

 public static void getLoginAPIResponse(String username, String password, String sourceId, String uuid, final HttpCallback httpCallback) {
        baseUrl = AppPreference.getParam(getContext(), SPConstants.MT4_BASE_URL, "").toString();
        ApiInterface apiService =
                ApiClient.getClient(baseUrl).create(ApiInterface.class);
        HashMap<String, String> queryParams = new HashMap<>();
        queryParams.put(APIConstants.KEY_EMAIL, sourceId + username.toLowerCase());
        queryParams.put(APIConstants.KEY_PASSWORD, Utils.encodePwd(password));


        Call<LoginResponse> call = apiService.getLoginResponse(queryParams);
        call.enqueue(new Callback<LoginResponse>() {

            @Override
            public void onResponse(Call<LoginResponse> call, Response<LoginResponse> response) {

                if (response.body().getResponseBean()==null) {
                    httpCallback.resultCallback(APIConstants.API_LOGIN, HttpCallback.REQUEST_TYPE_GET,
                            HttpCallback.RETURN_TYPE_FAILURE, 0,  response.body().getErrorBean());
                    return;
                }
                httpCallback.resultCallback(APIConstants.API_LOGIN, HttpCallback.REQUEST_TYPE_GET,
                        HttpCallback.RETURN_TYPE_SUCCESS, response.code(), response.body().getResponseBean());

            }


            @Override
            public void onFailure(Call<LoginResponse> call, Throwable t) {
                // Log error here since request failed
                httpCallback.resultCallback(APIConstants.API_APP_VERIFICATION, HttpCallback.REQUEST_TYPE_GET,
                        HttpCallback.RETURN_TYPE_FAILURE, 0, t);
                t.printStackTrace();
            }
        });
    }
15
Rachita Nanda

Таким образом, у вас есть два разных успешных (код состояния 200) ответа с одной и той же конечной точки. Один из них - фактическая модель данных, а другой - ошибка (как структура json, подобная этой?

Действительный ответ LoginBean:

{
  "id": 1234,
  "something": "something"
}

Ошибка ответа

{
  "error": "error message"
}

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

class LoginBeanResponse {
  @Nullable private final LoginBean loginBean;
  @Nullable private final ErrorMessage errorMessage;

  LoginBeanResponse(@Nullable LoginBean loginBean, @Nullable ErrorMessage errorMessage) {
    this.loginBean = loginBean;
    this.errorMessage = errorMessage;
  }
  // Add getters and whatever you need
}

Обертка для ошибки:

class ErrorMessage {
  String errorMessage;
  // And whatever else you need
  // ...
}

Тогда вам нужен JsonDeserializer:

public class LoginBeanResponseDeserializer implements JsonDeserializer<LoginBeanResponse> {

  @Override
  public LoginBeanResponse deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {

    // Based on the structure you check if the data is valid or not
    // Example for the above defined structures:

    // Get JsonObject
    final JsonObject jsonObject = json.getAsJsonObject();
    if (jsonObject.has("error") {
      ErrorMessage errorMessage = new Gson().fromJson(jsonObject, ErrorMessage.class);
      return new LoginBeanResponse(null, errorMessage)
    } else {
      LoginBean loginBean = new Gson().fromJson(jsonObject, LoginBean.class):
      return new LoginBeanResponse(loginBean, null);
    }
  }
}

Затем добавьте этот десериализатор в GsonConverterFactory:

GsonBuilder gsonBuilder = new GsonBuilder().registerTypeAdapter(LoginBeanResponse.class, new LoginBeanResponseDeserializer()).create():

apiClient = new Retrofit.Builder()
    .baseUrl(url)
    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    .addConverterFactory(GsonConverterFactory.create(gsonBuilder))
    .client(httpClient)
    .build();

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

Правка: То, что вы можете затем сделать внутри класса, где вы делаете вызов этой модификации (если вы уже конвертировали из Call<LoginBeanResponse> в Single<LoginBeanResponse> с помощью RxJava), фактически возвращает правильную ошибку. Что-то вроде:

Single<LoginBean> getLoginResponse(Map<String, String> queryMap) {
    restApi.getLoginResponse(queryMap)
        .map(loginBeanResponse -> { if(loginBeanResponse.isError()) {
            Single.error(new Throwable(loginBeanResponse.getError().getErrorMessage()))
        } else { 
            Single.just(loginBeanReponse.getLoginBean()) 
        }})
}
11
anstaendig

Вот еще одна попытка. Общая идея: создать пользовательский Converter.Factory на основе GsonConverterFactory и пользовательский Converter<ResponseBody, T> конвертер на основе GsonRequestBodyConverter для анализа всего тела 2 раза: первый раз как ошибка и второй раз как фактический ожидаемый тип ответа. Таким образом, мы можем анализировать ошибки в одном месте и при этом сохранять дружественный внешний API. На самом деле это похоже на ответ @anstaendig, но с гораздо меньшим количеством шаблонов: нет необходимости в дополнительном классе bean-объекта-оболочки для каждого ответа и других подобных вещей.

Первый класс ServerError, который является моделью для вашего "error JSON" и пользовательского исключения ServerErrorException, чтобы вы могли получить все детали

public class ServerError
{ 
    // add here actual format of your error JSON
    public String errorMsg;
}

public class ServerErrorException extends RuntimeException
{
    private final ServerError serverError;

    public ServerErrorException(ServerError serverError)
    {
        super(serverError.errorMsg);
        this.serverError = serverError;
    }

    public ServerError getServerError()
    {
        return serverError;
    }
}

Очевидно, вы должны изменить класс ServerError, чтобы он соответствовал вашему фактическому формату данных.

А вот основной класс GsonBodyWithErrorConverterFactory:

public class GsonBodyWithErrorConverterFactory extends Converter.Factory
{
    private final Gson gson;
    private final GsonConverterFactory delegate;
    private final TypeAdapter<ServerError> errorTypeAdapter;


    public GsonBodyWithErrorConverterFactory()
    {
        this.gson = new Gson();
        this.delegate = GsonConverterFactory.create(gson);
        this.errorTypeAdapter = gson.getAdapter(TypeToken.get(ServerError.class));
    }

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit)
    {
        return new GsonBodyWithErrorConverter<>(gson.getAdapter(TypeToken.get(type)));
    }

    @Override
    public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit)
    {
        return delegate.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit);
    }

    @Override
    public Converter<?, String> stringConverter(Type type, Annotation[] annotations, Retrofit retrofit)
    {
        return delegate.stringConverter(type, annotations, retrofit);
    }


    class GsonBodyWithErrorConverter<T> implements Converter<ResponseBody, T>
    {
        private final TypeAdapter<T> adapter;

        GsonBodyWithErrorConverter(TypeAdapter<T> adapter)
        {
            this.adapter = adapter;
        }

        @Override
        public T convert(ResponseBody value) throws IOException
        {
            // buffer whole response so we can safely read it twice
            String contents = value.string();

            try
            {
                // first parse response as an error
                ServerError serverError = null;
                try
                {
                    JsonReader jsonErrorReader = gson.newJsonReader(new StringReader(contents));
                    serverError = errorTypeAdapter.read(jsonErrorReader);
                }
                catch (Exception e)
                {
                    // ignore and try to read as actually required type
                }
                // checked that error object was parsed and contains some data
                if ((serverError != null) && (serverError.errorMsg != null))
                    throw new ServerErrorException(serverError);

                JsonReader jsonReader = gson.newJsonReader(new StringReader(contents));
                return adapter.read(jsonReader);
            }
            finally
            {
                value.close();
            }
        }
    }
}

Основная идея заключается в том, что фабрика делегирует другие вызовы стандартному GsonConverterFactory, но перехватывает responseBodyConverter для создания пользовательского GsonBodyWithErrorConverter. GsonBodyWithErrorConverter делает основной трюк:

  1. Сначала он читает весь ответ как String. Это необходимо для обеспечения буферизации тела ответа, чтобы мы могли безопасно перечитать его 2 раза. Если ваш ответ на самом деле может содержать некоторый двоичный файл, вы должны прочитать и буферизовать ответ как двоичный, и, к сожалению, retrofit2.Utils.buffer не является публичным методом, но вы можете создать аналогичный метод самостоятельно. Я просто читаю тело как строку, так как оно должно работать в простых случаях.
  2. Создайте jsonErrorReader из буферизованного тела и попытайтесь прочитать тело как ServerError. Если мы можем сделать это, у нас есть ошибка, поэтому бросьте наш пользовательский ServerErrorException. Если мы не можем прочитать его в этом формате - просто проигнорируйте исключение, так как это, вероятно, просто нормальный успешный ответ
  3. На самом деле попробуйте прочитать буферизованное тело (второй раз) как запрошенный тип и вернуть его.

Обратите внимание, что если ваш фактический формат ошибок не является JSON, вы все равно можете делать все то же самое. Вам просто нужно изменить логику синтаксического анализа ошибок внутри GsonBodyWithErrorConverter.convert на любую другую, необходимую вам.

Так что теперь в вашем коде вы можете использовать его следующим образом

.addConverterFactory(new GsonBodyWithErrorConverterFactory()) // use custom factory
//.addConverterFactory(GsonConverterFactory.create()) //old, remove

Примечание: на самом деле я не пробовал этот код, поэтому могут быть ошибки, но я надеюсь, что вы поняли идею.

1
SergGr

Одним из возможных решений является сбой Gson на неизвестных свойствах. Кажется, уже возникла проблема ( https://github.com/google/gson/issues/188 ). Вы можете использовать обходной путь, представленный на странице проблемы. Итак, шаги следующие:

Добавьте обходной путь ValidatorAdapterFactory к базе кода:

public class ValidatorAdapterFactory implements TypeAdapterFactory {

@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
    // If the type adapter is a reflective type adapter, we want to modify the implementation using reflection. The
    // trick is to replace the Map object used to lookup the property name. Instead of returning null if the
    // property is not found, we throw a Json exception to terminate the deserialization.
    TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);

    // Check if the type adapter is a reflective, cause this solution only work for reflection.
    if (delegate instanceof ReflectiveTypeAdapterFactory.Adapter) {

        try {
            // Get reference to the existing boundFields.
            Field f = delegate.getClass().getDeclaredField("boundFields");
            f.setAccessible(true);
            Map boundFields = (Map) f.get(delegate);

            // Then replace it with our implementation throwing exception if the value is null.
            boundFields = new LinkedHashMap(boundFields) {

                @Override
                public Object get(Object key) {

                    Object value = super.get(key);
                    if (value == null) {
                        throw new JsonParseException("invalid property name: " + key);
                    }
                    return value;

                }

            };
            // Finally, Push our custom map back using reflection.
            f.set(delegate, boundFields);

        } catch (Exception e) {
            // Should never happen if the implementation doesn't change.
            throw new IllegalStateException(e);
        }

    }
    return delegate;
    }

}

Создайте объект Gson с этим TypeAdaptorFactory:

Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ValidatorAdapterFactory()).create()

А затем используйте этот экземпляр gson в GsonConverterFactory, как показано ниже:

apiClient = new Retrofit.Builder()
                .baseUrl(url)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(gson)) //Made change here
                .client(httpClient)
                .build();

Это должно выдать ошибку, если шаг демаршаллинга обнаружит неизвестное свойство, в этом случае структуру ответа об ошибке.

1
James

Кажется, что использование Retrofit's по умолчанию Gson упрощает добавление пользовательской десериализации для обработки части документа JSON, которая была проблемой.

Образец кода

@FormUrlEncoded
    @POST(GlobalVariables.LOGIN_URL)
    void Login(@Field("email") String key, @Field("password") String value, Callback<Response> callback);

getService().Login(email, password, new MyCallback<Response>(context, true, null)
{
    @Override
    public void failure(RetrofitError arg0)
     {
        // TODO Auto-generated method stub
        UtilitySingleton.dismissDialog((BaseActivity<?>) context);
        System.out.println(arg0.getResponse());
      }

    @Override
    public void success(Response arg0, Response arg1)
    {
         String result = null;
         StringBuilder sb = null;
         InputStream is = null;
         try
         {
                is = arg1.getBody().in();
                BufferedReader reader = new BufferedReader(new InputStreamReader(is));
                sb = new StringBuilder();
                String line = null;
                while ((line = reader.readLine()) != null)
                {
                    sb.append(line + "\n");
                    result = sb.toString();
                    System.out.println("Result :: " + result);
                }
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }
    });
1
Maheshwar Ligade

Вы можете просто сделать это, делая это 

try
{
String error = response.errorBody().string();
error = error.replace("\"", "");
Toast.makeText(getContext(), error, Toast.LENGTH_LONG).show();
}
catch (IOException e)
{
e.printStackTrace();
}
1
M.Saad Lakhan