You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

编译时获取泛型类型名称用于注解,解决Gson反序列化问题

我明白你的痛点——REST API返回的列表总是用领域对象的名称作为外层键,但泛型擦除让你没法直接在编译时拿到T的类名来匹配这个键。别担心,有几个实用的办法可以解决这个问题,我一个个给你拆解:

方案一:手动解析JsonObject(最简单直接)

核心思路是显式传入领域对象的Class实例,通过Class.getSimpleName()拿到类名作为JSON的外层键,再手动提取数组进行反序列化。

举个例子,假设你的API返回的JSON长这样:

{
  "Book": [
    {"title": "Java实战", "author": "张三"},
    {"title": "Gson指南", "author": "李四"}
  ]
}

对应的领域类Book

public class Book {
    private String title;
    private String author;
    
    // getter、setter、构造方法省略
}

然后写一个通用的解析工具类:

public class ApiParser {
    private final Gson gson = new Gson();

    public <T> List<T> parseList(String json, Class<T> domainClass) {
        // 先把JSON解析成JsonObject
        JsonObject rootObj = gson.fromJson(json, JsonObject.class);
        // 用领域类的简单名称作为键(比如Book对应"Book")
        String listKey = domainClass.getSimpleName();
        // 提取对应的数组
        JsonArray dataArray = rootObj.getAsJsonArray(listKey);
        // 把数组反序列化成List<T>
        return gson.fromJson(dataArray, new TypeToken<List<T>>() {}.getType());
    }
}

使用的时候非常简单:

String apiResponse = "..."; // 从API获取的JSON字符串
List<Book> books = new ApiParser().parseList(apiResponse, Book.class);

这个方案的优点是代码少、易理解,适合大多数简单场景;缺点是如果API的键名和类名不一致(比如API用"book"而类是Book),就需要额外处理。


方案二:用自定义注解适配不同键名

如果API的键名和类名不匹配,可以给领域类加一个自定义注解,指定对应的JSON键名,然后通过反射读取注解值。

首先定义注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ApiListKey {
    String value(); // 用来指定JSON的外层键名
}

然后给领域类加上注解:

@ApiListKey("book") // API返回的键是"book",而类名是Book
public class Book {
    private String title;
    private String author;
}

修改解析方法,优先读取注解值:

public <T> List<T> parseList(String json, Class<T> domainClass) {
    JsonObject rootObj = gson.fromJson(json, JsonObject.class);
    String listKey;
    
    // 检查类上是否有ApiListKey注解
    ApiListKey keyAnnotation = domainClass.getAnnotation(ApiListKey.class);
    if (keyAnnotation != null) {
        listKey = keyAnnotation.value();
    } else {
        // 默认用类的简单名称
        listKey = domainClass.getSimpleName();
    }
    
    JsonArray dataArray = rootObj.getAsJsonArray(listKey);
    return gson.fromJson(dataArray, new TypeToken<List<T>>() {}.getType());
}

这样不管API键名和类名是否一致,都能灵活适配,扩展性更强。


方案三:自定义TypeAdapterFactory封装响应类(适合复杂场景)

如果你的API响应除了列表,还包含分页信息、状态码等元数据,可以封装一个泛型的响应类,再通过TypeAdapterFactory自动处理键名匹配。

首先定义泛型响应类:

public class ListResponse<T> {
    private List<T> data;
    private int total; // 假设API返回总条数
    private int page; // 当前页码
    
    // getter、setter省略
}

然后写一个自定义的TypeAdapterFactory,用来处理ListResponse<T>类型:

public class ListResponseTypeAdapterFactory implements TypeAdapterFactory {
    @SuppressWarnings("unchecked")
    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
        // 只处理ListResponse类型
        if (!ListResponse.class.isAssignableFrom(typeToken.getRawType())) {
            return null;
        }
        
        // 获取泛型的实际类型(比如ListResponse<Book>中的Book)
        Type genericType = typeToken.getType();
        if (!(genericType instanceof ParameterizedType)) {
            throw new IllegalArgumentException("ListResponse必须指定泛型类型");
        }
        Type domainType = ((ParameterizedType) genericType).getActualTypeArguments()[0];
        Class<?> domainClass = (Class<?>) domainType;
        
        // 获取List<T>的TypeAdapter
        TypeAdapter<List<?>> listAdapter = (TypeAdapter<List<?>>) gson.getAdapter(TypeToken.get(domainType));
        
        // 返回自定义的TypeAdapter
        return (TypeAdapter<T>) new ListResponseAdapter<>(domainClass, listAdapter);
    }
    
    private static class ListResponseAdapter<T> extends TypeAdapter<ListResponse<T>> {
        private final Class<T> domainClass;
        private final TypeAdapter<List<T>> listAdapter;
        private final Gson gson = new Gson();

        public ListResponseAdapter(Class<T> domainClass, TypeAdapter<List<T>> listAdapter) {
            this.domainClass = domainClass;
            this.listAdapter = listAdapter;
        }

        @Override
        public void write(JsonWriter out, ListResponse<T> value) throws IOException {
            // 序列化时的逻辑(如果需要的话)
            out.beginObject();
            String listKey = getListKey();
            out.name(listKey);
            listAdapter.write(out, value.getData());
            out.name("total").value(value.getTotal());
            out.name("page").value(value.getPage());
            out.endObject();
        }

        @Override
        public ListResponse<T> read(JsonReader in) throws IOException {
            JsonObject rootObj = JsonParser.parseReader(in).getAsJsonObject();
            String listKey = getListKey();
            
            // 解析列表数据
            List<T> data = listAdapter.fromJsonTree(rootObj.get(listKey));
            // 解析元数据
            int total = rootObj.get("total").getAsInt();
            int page = rootObj.get("page").getAsInt();
            
            ListResponse<T> response = new ListResponse<>();
            response.setData(data);
            response.setTotal(total);
            response.setPage(page);
            return response;
        }
        
        // 封装键名获取逻辑
        private String getListKey() {
            ApiListKey annotation = domainClass.getAnnotation(ApiListKey.class);
            return annotation != null ? annotation.value() : domainClass.getSimpleName();
        }
    }
}

使用时注册这个Factory:

Gson gson = new GsonBuilder()
        .registerTypeAdapterFactory(new ListResponseTypeAdapterFactory())
        .create();

ListResponse<Book> response = gson.fromJson(apiResponse, new TypeToken<ListResponse<Book>>() {}.getType());
List<Book> books = response.getData();
int totalBooks = response.getTotal();

这个方案适合需要封装完整响应的场景,代码更规整,也能自动适配不同的领域类。


总结一下:

  • 如果只是简单的列表反序列化,方案一+自定义注解足够用,代码最少;
  • 如果需要处理包含元数据的复杂响应,方案三更合适,扩展性更强。

内容的提问来源于stack exchange,提问作者Marco Nembrini

火山引擎 最新活动