一次Jackson和泛型的优化和抽象
起因
最近在对接供应商的api,由此衍生出了这次优化记录
分层:contrller-业务service-gysApiService
以下所有的优化都基于gysApiService层的代码:
供应商的接口返回的json
{
"status":200,
"msg":"成功",
"data":null,
"stackTrace":null
}
data数据返回,分两种:返回单个对象(query方法),返回list(queryList方法)。
返回单个对象
public A query(String code){
}
返回list
public List<B> queryList(String taxNo){
}
背景介绍完毕
04-24之前的grus中的JSON版本,只有序列化对象和list的方法
版本一
/**
*统一返回
*/
public class gysApiCommonResp {
Integer status;
String code;
String msg;
JsonNode data;
}
public A query(String code) {
String paramJson = "{\"code\":\"" + code + "\"}";
GysApiCommonResp resp = commonHttpPost(paramJson);
if (resp.getStatus().equals(200) && resp.getCode().equals("T0000")) {
return Strings.isNullOrEmpty(resp.getData()) ? null : JSON.parse(resp.getData(), A.class);
}
return null;
}
public List<B> queryList(String taxNo) {
String paramJson = "{\"taxNo\":\"" + taxNo + "\"}";
GysApiCommonResp resp = commonHttpPost(paramJson);
if (resp.getStatus().equals(200) && resp.getCode().equals("T0000")) {
return JSON.parse(JSON.toJSONString(resp.getData()), new TypeReference<>() {
});
}
return Collections.emptyList();
}
/**
*统一请求的方法
*/
private GysApiCommonResp commonHttpPost(String paramJson) {
Map<String, String> param = new HashMap<>(4);
param.put("jsonData", paramJson);
String resultJson = HttpClientHelper.postForm(URL, param);
JsonNode jsonNode = JSON.parse(resultJson);
int resultStatus = JSON.of(jsonNode.get("status")).orElse(JSON.parse("{}")).asInt();
String resultCode = JSON.of(jsonNode.get("code")).orElse(JSON.parse("{}")).asText();
String msg = JSON.of(jsonNode.get("msg")).orElse(JSON.parse("{}")).asText();
JsonNode data = JSON.of(jsonNode.get("data")).orElse(JSON.parse("{}")).asText();
GysApiCommonResp resp = new GysApiCommonResp();
resp.setCode(resultCode);
resp.setMsg(msg);
resp.setData(data);
resp.setStatus(resultStatus);
return resp;
}
能实现业务的代码,很难看;
缺点:每个业务方法需要自己判断诸如500等非业务的异常情况,代码重复,这是能实现业务代码,仅此而已。
版本二
正在写的时候,被长浩瞄到了,说,HttpClientHelper的request传入handler,可以统一解决非业务的异常和错误,于是向我展示了其他系统对接第三方的代码,例如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rzKlMmi2-1646879647030)(C:\Users\Administrator.360XT-20170812G\AppData\Roaming\Typora\typora-user-images\image-20200424171950047.png)]
第一个红框里的信息,就是一些非业务情况的异常统一判断。
第二个红框里的信息,是指统一返回的对象里,不再用JsonNode,而是根据泛型,返回对应的对象。
修改如下,增加了BaseResponseHandler:
public class BaseResponseHandler<T> implements ResponseHandler<GysApiResp<T>> {
private final Class<T> clazz;
public BaseResponseHandler(Class<T> clazz) {
this.clazz = clazz;
}
@Override
public GysApiResp<T> apply(Response response) {
if (!response.isSuccessful()) {
throw new BusinessRuntimeException(1009101023, "调用供应商接口失败,网络不通:" + response.message());
}
ResponseBody body = response.body();
JsonNode resultNode;
try {
String result = null;
if (body != null) {
result = body.string();
}
resultNode = JSON.parse(result);
} catch (Exception e) {
LOGGER.error("读取供应商返回结果失败:" + e);
throw new BusinessRuntimeException(1009101024, "读取供应商返回结果失败");
}
GysApiResp resp = new GysApiResp();
Integer status = JSON.of(resultNode.get("status")).orElse(JSON.parse("0")).asInt();
String msg = JSON.of(resultNode.get("msg")).orElse(JSON.parse("{}")).asText();
boolean array = JSON.of(resultNode.get("data")).orElse(JSON.parse("{}")).isArray();
if (array) {
List<T> list = (List<T>) JSON.toJavaObject(resultNode.get("data"), new TypeReference<T>() {
});
resp.setDataList(list);
} else {
resp.setData(JSON.toJavaObject(resultNode.get("data"), clazz));
}
String code = JSON.of(resultNode.get("code")).orElse(JSON.parse("{}")).asText();
resp.setCode(code);
resp.setMsg(msg);
resp.setStatus(status);
//对方系统的异常
//业务异常
return resp;
}
}
调用
//返回对象
GysApiResp<A> resp = HttpClientHelper.request(request, new BaseResponseHandler<>(A.class));
//返回list
GysApiResp<List> resp = HttpClientHelper.request(request, new BaseResponseHandler<>(B.class);
重点是
boolean array = JSON.of(resultNode.get("data")).orElse(JSON.parse("{}")).isArray();
if (array) {
List<T> list = (List<T>) JSON.toJavaObject(resultNode.get("data"), new TypeReference<T>() {
});
resp.setDataList(list);
} else {
resp.setData(JSON.toJavaObject(resultNode.get("data"), clazz));
}
这里遇到了一个问题,如果是List的话,反序列化的时候,如果直接输出为json没有问题,但是如果要进行遍历操作,就会遇到问题。因为这里的反序列化出来的list里的对象,实际是:LinkdedHashMap,而不是具体的类,此时作为构造函数的B.class没有发挥作用。于是遇到返回list的接口,就暂时用版本一的方法解决了。
缺点:调用方法不统一,维护太麻烦,list和对象分开判断。为了改而改,没有彻底搞明白改的意义。
版本三
后来空了之后问了智伟,然后这段代码就起飞了~
遇到嵌套list的对象,反序列化的时候,有问题。但是Jackson太强大,除了toJavaObject之外,还有objectMapper.readValue(result, javaType)的方法,这里有个javaType,是具体嵌套的类。有了这个嵌套类,理论上,可以有无数层嵌套,只要属性设置正确,都可以通过这个方法反序列化出来,比如List<Set<Map<String,List<A>>>>等。
有了这个方法之后,助跑阶段的代码:
public class BaseResponseHandler<T> implements ResponseHandler<GysApiResp<T>> {
private final ObjectMapper objectMapper = new ObjectMapper();
private final JavaType javaType;
private final JavaType javaTypeList;
//返回list时的构造方法,此时outer为GysApiResp.class,middle为List.class,clazz为内层的B.class
public BaseResponseHandler(Class<?> outer, Class<?> middle, Class<T> clazz) {
//GysApiResp<List>
javaTypeList = objectMapper.getTypeFactory().constructParametricType(outer, middle, clazz);
javaType = null;
}
//返回单个对象时的构造方法,clazz为GysApiResp.class,inner为内层的A.class
public BaseResponseHandler(Class<?> clazz, Class<T> inner) {
this.javaTypeList = null;
//GysApiResp<A>
javaType = objectMapper.getTypeFactory().constructParametricType(clazz, inner);
}
//反序列化
if (请求的放回结果是lsit) {
resultObj = objectMapper.readValue(result, javaTypeList);
} else {
resultObj = objectMapper.readValue(result, javaType);
}
调用:
//返回对象
GysApiResp<A> resp = HttpClientHelper.request(request, new BaseResponseHandler<>(GysApiResp.class,A.class));
//返回list
GysApiResp<List> resp = HttpClientHelper.request(request, new BaseResponseHandler<>(GysApiResp.class,List.class, B.class)
此时也能实现功能,但是在queryList返回list的时候,泛型并不能识别最内层的对象,idea会提示类型问题。
缺点:需要判断list和对象,不简洁。属性类型定义重复。没有仔细去想明白javaType和嵌套的真实含义。
版本四
版本三中,其实定义一个javaType就够了。因为javaType就是javaType,一个是,两个是,三个还是。同一个类型,定义一次就好了,毕竟是嵌套,嵌套就好了,有点像1024那个游戏一样。在智伟的指导下,真的要起飞了:
public class BaseResponseHandler<T> implements ResponseHandler<GysApiResp<T>> {
private final ObjectMapper objectMapper = new ObjectMapper();
private final JavaType javaType;//只需要定义一个。
//对于返回个对象的构造,A.class
public static <T> BaseResponseHandler<T> fromClass(Class<T> inner) {
JavaType javaType = objectMapper.getTypeFactory().constructType(inner);
return new BaseResponseHandler<>(javaType);
}
//对于返回list的构造,嵌套就完了。将内层的B.class和List.class嵌套在一起,就是相当于A.class
//这里的E是内层的B.class, List<E>就是T
public static <E> BaseResponseHandler<List<E>> fromListClass(Class<E> inner) {
JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, inner);
return new BaseResponseHandler<>(javaType);
}
//构造器私有化
private BaseResponseHandler(JavaType javaType) {
this.javaType = objectMapper.getTypeFactory().constructParametricType(GysApiResp.class, javaType);
}
//反序列化
resultObj = objectMapper.readValue(result, javaType);
调用:
GysApiResp<A> resp = HttpClientHelper.request(request, BaseResponseHandler.fromClass(A.class));
GysApiResp<A> resp = HttpClientHelper.request(request,BaseResponseHandler.fromListClass(List.class,A.class));
对于GysApiResp.class,是不变的,所以相对于版本三中,没必要定义Class<?> outer,省了一个构造参数。
静态方法构造对象,对外不直接暴露构造器。
对于嵌套,List<B>嵌套就是A,这样版本三中的javaTypeList就需要了,只需要定义一个javaType类型的属性就好了
此时因为最内层的类型也已经确定了,此时idea不在提示返回list的类型问题。
版本五
此时已经起飞了,为了更平稳地飞行,进行了如下修改:
由于每个接口都直接调用了HttpClientHelper.request这样的方法,重复。
并且BaseResponseHandler这个类由于其他的原因,也需要其他的属性,该属性在gysApiService中,并且该类只供gysApiService只用,故将该类挪到gysApiService中,作为它的内部类使用。
所以定义了
private <T> GysApiResp<T> doRequest(Request request, BaseResponseHandler<T> baseResponseHandler) {
return HttpClientHelper.request(request, baseResponseHandler);
}
BaseResponseHandler类的静态构造方法改gysApiService的泛型方法为:
private <T> BaseResponseHandler<T> fromClass(Class<T> inner) {
JavaType javaType = objectMapper.getTypeFactory().constructType(inner);
return new BaseResponseHandler<>(javaType);
}
private <E> BaseResponseHandler<List<E>> fromListClass(Class<E> inner) {
JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, inner);
return new BaseResponseHandler<>(javaType);
}
调用:
GysApiResp<A> resp = this.doRequest(request, this.fromClass(A.class));
GysApiResp<A> resp = this.doRequest(request,this.fromListClass(List.class,A.class));
版本六
还得再飞高一点:构建Request中,有很多公共的部分,不同的部分,就在于每个方法的参数不同。所以,可以继续抽象出来:
private <T> GysApiResp<T> doRequest(String paramJson, String queryRestAppKey, String queryRestSecret,BaseResponseHandler<T> baseResponseHandler) {
Request request = this.buildPostRequest(paramJson, queryRestAppKey, queryRestSecret);
return HttpClientHelper.request(request, baseResponseHandler);
}
private Request buildPostRequest(String paramJson, String appKey, String secret) {
Map<String, String> formParameters = new HashMap<>(4);
formParameters.put("appKey", appKey);
formParameters.put("token", Sha256Util.sha256_HMAC(paramJson, secret));
formParameters.put("jsonData", paramJson);
okhttp3.FormBody.Builder formBody = new okhttp3.FormBody.Builder(StandardCharsets.UTF_8);
if (formParameters.size() > 0) {
formParameters.forEach((key, value) -> {
if (!Objects.isNull(key) && !Objects.isNull(value)) {
formBody.add(key, value);
}
});
}
return (new Request.Builder()).post(formBody.build()).url(gysApiProperties.getHlzsApiUrl()).build();
}
调用方:
public List<B> queryList() {
GysApiResp<List<B>> result = this.doRequest(paramJson, appKey, secret, fromListClass(B.class));
return result.getData();
}
public A query(){
GysApiResp<A> result = this.doRequest(paramJson, appKey, secret, fromClass(A.class));
retrun result.getData();
}
这样抽象,相对于版本五,业务方法对于http的相关实现并不关注,以后换的话,不涉及业务代码的改动。
感悟
写业务代码是我们的无法避免的工作,如何找点乐子,能让写代码变成一件特别有成就感并且的事,并且乐此不疲呢?我觉得无非就是这个过程了。
再卖一句:纸上得来终觉浅,绝知此事要躬行。