我正在编写一个使用Json API的库,当使用Gson作为解析库时,我遇到了设计问题。
如果一切顺利的话,其中一个端点会返回<div class="line_up">
<font face="Times New Roman" size="1">stage1</font>
<span class="circle" style="display:block">
<span class="circle-inner" style="display:block"></span>
</span>
<hr style="height:1px; margin-top:-10px; border:none; color:#000; background-color:#000;width:80px;margin-left:22px">
</div>
<div class="line_up">
<font face="Times New Roman" size="1">stage2</font>
<span class="circle" style="display:block">
<span class="circle-inner" style="display:block"></span>
</span>
<hr style="height:1px; margin-top:-10px; border:none; color:#000; background-color:#000;width:80px;margin-left:22px">
</div>
个对象:
array
但是,API中所有端点的错误架构是json [
{
"name": "John",
"age" : 21
},
{
"name": "Sarah",
"age" : 32
},
]
而不是数组。
object
在POJO中对此进行建模时会出现问题。因为错误模式对于所有API端点都是通用的,所以我决定使用一个抽象的{
"errors": [
{
"code": 1001,
"message": "Something blew up"
}
]
}
类,它只映射错误属性
ApiResponse
现在我想从public abstract class ApiResponse{
@SerializedName("errors")
List<ApiResponseError> errors;
}
public class ApiResponseError {
@SerializedName("code")
public Integer code;
@SerializedName("message")
public String message;
}
继承以获得错误映射&#34;免费&#34;和每个API端点响应的POJO。但是,此响应的顶级json对象是一个数组(如果服务器成功执行请求),所以我无法像我希望的那样创建一个新类来映射它。
我决定继续创建一个扩展ApiResponse
的类:
ApiResponse
并实现了一个自定义反序列化器,以根据顶级对象的类型正确解析json,并将其设置为以下类中的正确字段:
public class ApiResponsePerson extends ApiResponse {
List<Person> persons;
}
然后我将添加到Gson
public class DeserializerApiResponsePerson implements JsonDeserializer<ApiResponsePerson> {
@Override
public ApiResponse deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
ApiResponsePerson response = new ApiResponsePerson();
if (json.isJsonArray()) {
Type personType = new TypeToken<List<Person>>() {}.getType();
response.persons = context.deserialize(json, personType);
return response;
}
if (json.isJsonObject()) {
JsonElement errorJson = json.getAsJsonObject().get("errors");
Type errorsType = new TypeToken<List<ApiResponseError>>() {}.getType();
response.errors = context.deserialize(errorJson, errorsType);
return response;
}
throw new JsonParseException("Unexpected Json for 'ApiResponse'");
}
}
有没有办法对这个POJO建模并让Gson识别这个结构而不必手动处理这种情况? 有没有更好的方法来实现这一目标? 我是否遗漏了反序列化器可能失败或无法按预期工作的任何情况?
由于
答案 0 :(得分:2)
有时API响应不适合像Java这样的静态类型语言。我会说如果你遇到一个问题与一个不太方便的响应格式对齐,你必须编写更多的代码,如果你想让它方便。在大多数情况下,Gson可以在这种情况下提供帮助,但不是免费的。
有没有办法对这个POJO建模并让Gson识别这个结构而不必手动处理这种情况?
没有。 Gson不会混合不同结构的物体,所以你仍然需要告诉它你的意图。
有没有更好的方法来实现这个目标?
我想是的,对于响应的建模和实现解析这些响应的方式。
我是否遗漏了反序列化程序可能失败或无法按预期工作的任何情况?
它的响应格式与所有反序列化器一样敏感,所以一般来说它足够好,但可以改进。
首先,我们考虑您只能有两种情况:常规响应和错误。这是一个典型的案例,可以这样建模:
abstract class ApiResponse<T> {
// A bunch of protected methods, no interface needed as we're considering it's a value type and we don't want to expose any of them
protected abstract boolean isSuccessful();
protected abstract T getData()
throws UnsupportedOperationException;
protected abstract List<ApiResponseError> getErrors()
throws UnsupportedOperationException;
// Since we can cover all two cases ourselves, let them all be here in this class
private ApiResponse() {
}
static <T> ApiResponse<T> success(final T data) {
return new SucceededApiResponse<>(data);
}
static <T> ApiResponse<T> failure(final List<ApiResponseError> errors) {
@SuppressWarnings("unchecked")
final ApiResponse<T> castApiResponse = (ApiResponse<T>) new FailedApiResponse(errors);
return castApiResponse;
}
// Despite those three protected methods can be technically public, let's encapsulate the state
final void accept(final IApiResponseConsumer<? super T> consumer) {
if ( isSuccessful() ) {
consumer.acceptSuccess(getData());
} else {
consumer.acceptFailure(getErrors());
}
}
// And make a couple of return-friendly accept methods
final T acceptOrNull() {
if ( !isSuccessful() ) {
return null;
}
return getData();
}
final T acceptOrNull(final Consumer<? super List<ApiResponseError>> errorsConsumer) {
if ( !isSuccessful() ) {
errorsConsumer.accept(getErrors());
return null;
}
return getData();
}
private static final class SucceededApiResponse<T>
extends ApiResponse<T> {
private final T data;
private SucceededApiResponse(final T data) {
this.data = data;
}
@Override
protected boolean isSuccessful() {
return true;
}
@Override
protected T getData() {
return data;
}
@Override
protected List<ApiResponseError> getErrors()
throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
}
private static final class FailedApiResponse
extends ApiResponse<Void> {
private final List<ApiResponseError> errors;
private FailedApiResponse(final List<ApiResponseError> errors) {
this.errors = errors;
}
@Override
protected boolean isSuccessful() {
return false;
}
@Override
protected List<ApiResponseError> getErrors() {
return errors;
}
@Override
protected Void getData()
throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
}
}
interface IApiResponseConsumer<T> {
void acceptSuccess(T data);
void acceptFailure(List<ApiResponseError> errors);
}
错误的简单映射:
final class ApiResponseError {
// Since incoming DTO are read-only data bags in most-most cases, even getters may be noise here
// Gson can strip off the final modifier easily
// However, primitive values are inlined by javac, so we're cheating javac with Integer.valueOf
final int code = Integer.valueOf(0);
final String message = null;
}
还有一些价值观:
final class Person {
final String name = null;
final int age = Integer.valueOf(0);
}
第二个组件是一个特殊类型的适配器,用于告诉Gson 如何必须反序列化API响应。请注意,与JsonSerializer
和JsonDeserializer
不同,类型适配器以流式方式工作,不需要将整个JSON模型(JsonElement
)存储在内存中,因此可以节省内存并提高性能大型JSON文档。
final class ApiResponseTypeAdapterFactory
implements TypeAdapterFactory {
// No state, so it can be instantiated once
private static final TypeAdapterFactory apiResponseTypeAdapterFactory = new ApiResponseTypeAdapterFactory();
// Type tokens are effective value types and can be instantiated once per parameterization
private static final TypeToken<List<ApiResponseError>> apiResponseErrorsType = new TypeToken<List<ApiResponseError>>() {
};
private ApiResponseTypeAdapterFactory() {
}
static TypeAdapterFactory getApiResponseTypeAdapterFactory() {
return apiResponseTypeAdapterFactory;
}
@Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// Is it ApiResponse, a class we can handle?
if ( ApiResponse.class.isAssignableFrom(typeToken.getRawType()) ) {
// Trying to resolve its parameterization
final Type typeParameter = getTypeParameter0(typeToken.getType());
// And asking Gson for the success and failure type adapters to use downstream parsers
final TypeAdapter<?> successTypeAdapter = gson.getDelegateAdapter(this, TypeToken.get(typeParameter));
final TypeAdapter<List<ApiResponseError>> failureTypeAdapter = gson.getDelegateAdapter(this, apiResponseErrorsType);
@SuppressWarnings("unchecked")
final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) new ApiResponseTypeAdapter<>(successTypeAdapter, failureTypeAdapter);
return castTypeAdapter;
}
return null;
}
private static Type getTypeParameter0(final Type type) {
// Is this type parameterized?
if ( !(type instanceof ParameterizedType) ) {
// No, it's raw
return Object.class;
}
final ParameterizedType parameterizedType = (ParameterizedType) type;
return parameterizedType.getActualTypeArguments()[0];
}
private static final class ApiResponseTypeAdapter<T>
extends TypeAdapter<ApiResponse<T>> {
private final TypeAdapter<T> successTypeAdapter;
private final TypeAdapter<List<ApiResponseError>> failureTypeAdapter;
private ApiResponseTypeAdapter(final TypeAdapter<T> successTypeAdapter, final TypeAdapter<List<ApiResponseError>> failureTypeAdapter) {
this.successTypeAdapter = successTypeAdapter;
this.failureTypeAdapter = failureTypeAdapter;
}
@Override
public void write(final JsonWriter out, final ApiResponse<T> value)
throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
@Override
public ApiResponse<T> read(final JsonReader in)
throws IOException {
final JsonToken token = in.peek();
switch ( token ) {
case BEGIN_ARRAY:
// Is it array? Assuming that the responses come as arrays only
// Otherwise a more complex parsing is required probably replaced with JsonDeserializer for some cases
// So reading the next value (entire array) and wrapping it up in an API response with the success-on state
return success(successTypeAdapter.read(in));
case BEGIN_OBJECT:
// Otherwise it's probably an error object?
in.beginObject();
final String name = in.nextName();
if ( !name.equals("errors") ) {
// Let it fail fast, what if a successful response would be here?
throw new MalformedJsonException("Expected errors` but was " + name);
}
// Constructing a failed response object and terminating the error object
final ApiResponse<T> failure = failure(failureTypeAdapter.read(in));
in.endObject();
return failure;
// A matter of style, but just to show the intention explicitly and make IntelliJ IDEA "switch on enums with missing case" to not report warnings here
case END_ARRAY:
case END_OBJECT:
case NAME:
case STRING:
case NUMBER:
case BOOLEAN:
case NULL:
case END_DOCUMENT:
throw new MalformedJsonException("Unexpected token: " + token);
default:
throw new AssertionError(token);
}
}
}
}
现在,如何将它们放在一起。请注意,响应不会明确地暴露其内部,而是要求消费者接受使其私有实际封装。
public final class Q43113283 {
private Q43113283() {
}
private static final String SUCCESS_JSON = "[{\"name\":\"John\",\"age\":21},{\"name\":\"Sarah\",\"age\":32}]";
private static final String FAILURE_JSON = "{\"errors\":[{\"code\":1001,\"message\":\"Something blew up\"}]}";
private static final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(getApiResponseTypeAdapterFactory())
.create();
// Assuming that the Type instance is immutable under the hood so it might be cached
private static final Type personsApiResponseType = new TypeToken<ApiResponse<List<Person>>>() {
}.getType();
@SuppressWarnings("unchecked")
public static void main(final String... args) {
final ApiResponse<Iterable<Person>> successfulResponse = gson.fromJson(SUCCESS_JSON, personsApiResponseType);
final ApiResponse<Iterable<Person>> failedResponse = gson.fromJson(FAILURE_JSON, personsApiResponseType);
useFullyCallbackApproach(successfulResponse, failedResponse);
useSemiCallbackApproach(successfulResponse, failedResponse);
useNoCallbackApproach(successfulResponse, failedResponse);
}
private static void useFullyCallbackApproach(final ApiResponse<Iterable<Person>>... responses) {
System.out.println("<FULL CALLBACKS>");
final IApiResponseConsumer<Iterable<Person>> handler = new IApiResponseConsumer<Iterable<Person>>() {
@Override
public void acceptSuccess(final Iterable<Person> people) {
dumpPeople(people);
}
@Override
public void acceptFailure(final List<ApiResponseError> errors) {
dumpErrors(errors);
}
};
Stream.of(responses)
.forEach(response -> response.accept(handler));
}
private static void useSemiCallbackApproach(final ApiResponse<Iterable<Person>>... responses) {
System.out.println("<SEMI CALLBACKS>");
Stream.of(responses)
.forEach(response -> {
final Iterable<Person> people = response.acceptOrNull(Q43113283::dumpErrors);
if ( people != null ) {
dumpPeople(people);
}
});
}
private static void useNoCallbackApproach(final ApiResponse<Iterable<Person>>... responses) {
System.out.println("<NO CALLBACKS>");
Stream.of(responses)
.forEach(response -> {
final Iterable<Person> people = response.acceptOrNull();
if ( people != null ) {
dumpPeople(people);
}
});
}
private static void dumpPeople(final Iterable<Person> people) {
for ( final Person person : people ) {
System.out.println(person.name + " (" + person.age + ")");
}
}
private static void dumpErrors(final Iterable<ApiResponseError> errors) {
for ( final ApiResponseError error : errors ) {
System.err.println("ERROR: " + error.code + " " + error.message);
}
}
}
上面的代码将产生:
&lt;完整的回电&gt;
约翰(21)
莎拉(32)
错误:1001东西爆炸了 &lt; SEMI CALLBACKS&gt;
约翰(21)
莎拉(32)
错误:1001东西爆炸了 &lt; NO CALLBACKS&gt;
约翰(21)
莎拉(32)
答案 1 :(得分:1)
在您的无错误情况下,由于顶级元素是数组而不是对象,因此您必须使用自定义反序列化器。你无法摆脱这种情况。 (我假设您无法更改响应格式。)
据我所知,使代码更清晰的最佳尝试是创建一个抽象的顶级反序列化器类并在此处检查error
。如果没有错误,请将解析字段委托给一些抽象方法,该方法将在您为每个类编写的自定义序列化程序中实现。
答案 2 :(得分:0)
此解决方案对于此方案几乎非常有用。但我想定义更一般的响应,是否应该有一个状态来识别请求的成功或失败?所以我更喜欢json格式:
成功:
{
"status": "success",
"results": [
{
"name": "John",
"age" : 21
}
]
}
失败:
{
"status": "failure",
"errors": [
{
"code": 1001,
"message": "Something blew up"
}
]
}