编译时获取泛型类型名称用于注解,解决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




