Retrofit2 условие дескриптора, когда код состояния 200, но структура json отличается от класса datamodel
я использую Retrofit2 и RxJava2CallAdapterFactory
.
API, который я потребляю, возвращает код состояния всегда как 200, а для строки JSON успеха и ответа структура json совершенно другая. Поскольку код состояния всегда равен 200, метод onResponse () вызывается всегда. Следовательно, я не могу извлечь ошибки msgs из json в состоянии ошибки.
Решение 1:
я использую ScalarsConverterFactory
получить строку ответа и вручную использовать Gson для разбора ответ.
как получить ответ в виде строки с помощью дооснащения без использования GSON или любой другой библиотеки в android
проблема с этим решением: я планирую использовать RxJava2CallAdapterFactory для того, чтобы метод retrofit возвращал класс DataModel.
мне нужно найти лучшее решение для этой проблемы, таким образом, я могу продолжать возвращать классы модели данных из метода дооснащения и каким-то образом я определяю условие ошибки из ответа (определите ответ json не соответствует модели данных), а затем проанализируйте ошибку json в модели данных.
Клиент Retrofit
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 parser не возвращает экземпляр нулевого объекта для меня, чтобы понять, что существует несоответствие структуры json и datamodel.
RestAdapter устарел в дооснащении 2
Я ищу лучший подход для решения этой проблемы, желательно избегать разбора вручную json и использовать большинство преимуществ адаптеров retrofit и RX.
редактировать
код ответа 200, следовательно,
response.isSuccessful() == true
response.body() != null
тоже true as Gson никогда не создает экземпляр null или выдает исключение, если есть несоответствие структуры json-
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
решение
решение основано на ответе anstaendig
- я создал базовый универсальный класс для дальнейшего ответа.
- поскольку у меня есть несколько API и моделей данных, я должен создать десерилизаторы для каждого
БАЗОВЫЙ 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;
}
}
БАЗОВЫЙ ДЕСЕРИАЛИЗАТОР
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);
}
}
ПОЛЬЗОВАТЕЛЬСКИЙ ОТВЕТ BEAN
public class LoginResponse extends BaseApiBean<LoginBean> {
public LoginResponse(LoginBean responseBean, ErrorBean errorBean) {
super(responseBean, errorBean);
}
}
клиент
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();
}
});
}
5 ответов
таким образом, у вас есть два разных успешных ответа (код состояния 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 просто неправильный, потому что коды состояния существуют по какой-то причине. Я все еще надеюсь, что это помогает.
EDIT: что вы можете сделать внутри класса, где вы делаете вызов этой модернизации (если вы уже преобразованы из Call<LoginBeanResponse>
to 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())
}})
}
кажется, что 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();
}
}
});
вот еще одна попытка. Общая идея: создайте пользовательский Converter.Factory
на основе GsonConverterFactory
и обычай Converter<ResponseBody, T>
преобразователя на основе GsonRequestBodyConverter
для разбора всего тела 2 раза: первый раз как ошибка и второй раз как фактический ожидаемый тип ответа. Таким образом, мы можем анализировать ошибки в одном месте и по-прежнему сохранять дружественный внешний API. Это на самом деле похоже на ответ @anstaendig, но с гораздо меньшим шаблоном: нет необходимости в дополнительном классе bean-оболочки для каждого ответа и других подобных материал.
первый класс ServerError
это модель для вашей "ошибки 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
. The GsonBodyWithErrorConverter
is делаем главный трюк:
- сначала он читает весь ответ в строку. Это необходимо для обеспечения буферизации тела ответа, чтобы мы могли безопасно перечитывать его 2 раза. Если ваш ответ на самом деле может содержать двоичный файл, вы должны прочитать и буферизировать ответ как двоичный и, к сожалению,
retrofit2.Utils.buffer
не публичный метод, но вы можете создать подобное себе. Я просто читаю тело как строку, поскольку оно должно работать в простых случаях. - создать
jsonErrorReader
от буферизованное тело и попробуйте прочитать тело какServerError
. Если мы можем это сделать, у нас есть ошибка, поэтому бросьте наш customServerErrorException
. Если мы не можем прочитать его в этом формате-просто игнорируйте исключение, поскольку это, вероятно, просто обычный успешный ответ - фактически попробуйте прочитать буферизованное тело (второй раз) как запрошенный тип и вернуть его.
обратите внимание, что если ваш фактический формат ошибки не JSON, вы все равно можете делать все то же самое. Вам просто нужно изменить синтаксический анализ ошибок логика внутри GsonBodyWithErrorConverter.convert
ко всему, что вам нужно.
Итак, в вашем коде вы можете использовать его следующим образом
.addConverterFactory(new GsonBodyWithErrorConverterFactory()) // use custom factory
//.addConverterFactory(GsonConverterFactory.create()) //old, remove
примечание: Я не пробовал этот код, поэтому могут быть ошибки, но надеюсь вы поняли.
вы можете просто сделать это, делать это
try
{
String error = response.errorBody().string();
error = error.replace("\"", "");
Toast.makeText(getContext(), error, Toast.LENGTH_LONG).show();
}
catch (IOException e)
{
e.printStackTrace();
}
одним из возможных решений является сбой 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();
Это должно вызвать ошибку, если шаг unmarshalling находит неизвестное свойство, в этом случае структуру ответа на ошибку.