一次Jackson和泛型的优化和抽象

一次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的相关实现并不关注,以后换的话,不涉及业务代码的改动。

感悟

写业务代码是我们的无法避免的工作,如何找点乐子,能让写代码变成一件特别有成就感并且的事,并且乐此不疲呢?我觉得无非就是这个过程了。

再卖一句:纸上得来终觉浅,绝知此事要躬行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值