系统间HTTP调用代码封装

痛点

最近接手一个老项目,这个项目几经转手,到我这里时,发现代码的可阅读性实在是很差,对于一个有点代码洁癖的我来说,阅读起来实在是很难受。其中一个痛点,现在就拉出来讲讲。该项目需要与另外一个项目进行业务对接,因此有很多HTTP接口要调用。现在项目发送HTTP请求的代码跟业务代码完全合在一起,看起来就像这样:

public void doSomething() {
    // do some business operation
    ...
    // send http request
    Map<String, String> param = new HashMap<String, String>();
    param.put("idCard",idNo);
    String requestUrl = serverSettings.getRequestPrefix() + serverSettings.getQueryRegisterStatusUrl();
    logger.info("post customerId:{}, param:{},url:{}",customerId,param,requestUrl);
    long startMillis = System.currentTimeMillis();
    String response = httpClientUtil.doPostJSONSign(requestUrl,param);
    long endMillis = System.currentTimeMillis();
    logger.info("queryKaolaStatus response:{},time:{}",response,(endMillis-startMillis)/1000+"秒");
    QueryStatusRto queryStatusRto = JSONObject.parseObject(response, QueryStatusRto.class);
    
    // do other business operation
    ...
}

上面这段代码如果项目中只出现一次,也许还好,项目中到处都是这种代码的时候,就简直让人崩溃。这段代码让本来就复杂的业务代码更难去阅读和维护,也无法进行复用,如果项目中另外一段逻辑中需要调用这个HTTP接口,只能将这段HTTP请求的代码copy过去,造成代码重复。维护过项目的相信都会对重复代码生出感慨,修改重复代码简直就是噩梦,必须小心谨慎,不能遗漏任何一处。

这段代码还有一个问题,就是不方便进行测试联调。对于HTTP接口调用来说,针对每个接口编写一个联调测试类是很有必要的,开户初期用于联调,后期接口参数有改动时,能够很方便进行测试。

总结起来上面这段代码存在的问题有:

  1. 代码可阅读性差,不根据上下文的代码逻辑,可能都没法确定调用的是哪个接口。
  2. 代码可维护性查,后期如果要更改接口参数,到处都要修改,工作量大而且容易出错,不容易进行单元测试。

优化方案

很明显,要优化这段代码,就是要把这段代码抽取到同一个地方去,把面向对象中“抽象”和“封装”的思想运用起来。上面的HttpClientUtil类实际上是在尝试做这个工作,只是封装的不够彻底,把日志仍然遗留在业务代码里面。我们尝试第一个优化版本,把日志从业务代码中移除,放到httpClientUtil.doPostJSONSign()方法中去,具体代码如下:

public void doSomething() {
    // do some business operation
    ...
    // send http request
    Map<String, String> param = new HashMap<String, String>();
    param.put("idCard",idNo);
    String requestUrl = serverSettings.getRequestPrefix() + serverSettings.getQueryRegisterStatusUrl();
    String response = httpClientUtil.doPostJSONSign(requestUrl,param);
    QueryStatusRto queryStatusRto = JSONObject.parseObject(response, QueryStatusRto.class);
    
    // do other business operation
    ...
}

这样代码是不是清晰多了?一下子少了四行代码,如果系统很多地方都有这段代码,一下子就少了很多行代码,节约了阅读代码的时间。当然了,这没什么技术含量,大部分开发都会这么做,写下这段代码的那个同事除外,他对代码的要求很低。这个优化方案给我们减少了大量代码,尽管只抽取了四行日志代码。

不过这个方案还不够面向对象,一方面参数用Map进行存储,参数少还好,参数一旦多起来就不好维护。另一方面,调用HTTP请求这个操作应该要封装起来,requestUrl这个参数(也就是接口url地址)目前是在业务代码中拼接的,这也意味着业务代码中每调用一次就要拼接一次。想办法将这部分代码也抽象出去,于是有了接下来的这个方案。

public void doSomething() {
    // do some business operations
    ...
    // send http request
    QueryStatusReq req = new QueryStatusReq();
    req.setIdCard(idNo);
    QueryStatusRto queryStatusRto = queryStatusProxy.queryStatus(req);
    // do other business operations
    ...
}

怎么样?是不是很简洁,具体做了什么事情也是一目了然。代码变得容易看懂多了,单元测试也好进行,调用queryStatusProxy.queryStatus(req)方法就好了。接口参数有了变化时,全局搜索QueryStatusReq这个类就能找到所有使用的地方,完全不用担心会遗漏,如果是Map就根本没法使用全局搜索。

具体是怎么实现的呢?将Map请求参数改成对象,这很好理解。QueryStatusProxy才是接下来的重点:

@Component
public class QueryStatusProxy {
    @Autowired
    private BaseAPIProxy baseAPIProxy;
    
    public QueryStatusRto queryStatus(QueryStatusReq req) {
        return baseAPIProxy.request("/v1/queryStatus", req, QueryStatusRto.class);
    }
}

这里我们运用了代理模式,BaseAPIProxy代理了真正的HTTP调用,QueryStatusProxy只是填入了一些必要参数,所以我们再继续看BaseAPIProxy的代码:

@Component
public class BaseAPIProxy {
    private static final Logger logger = LoggerFactory.getLogger(BaseAPIProxy.class);
    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private ServerSettings serverSettings;

    public <T> T request(String url, Object reqBody, Class<T> respType) {
        Class api = reqBody.getClass();
        String seq = OrderNoUtil.generateOrderNo();
        try {
            String data = JSONObject.toJSONString(reqBody);
            logger.info("====================>request kaola api:{}, seq:{}, data:{}", api, seq, data);
            String sign = EncryptUtil
                    .encrypt(data, "UTF-8", serverSettings.getsKey(),
                            serverSettings.getIvParameter());
            Map<String, String> params = new HashMap<>();
            params.put("data", sign);
            params.put("platform_id", serverSettings.getPlatformId());
            String request = JSONObject.toJSONString(params);
            HttpHeaders headers = new HttpHeaders();
            MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
            headers.setContentType(type);
            HttpEntity<String> formEntity = new HttpEntity<>(request, headers);
            String requestUrl = serverSettings.getRequestPrefix() + url;
            long begin = System.currentTimeMillis();
            logger.info("====================>request api:{}, seq:{}, request:{}", api, seq, request);
            String responseBody = restTemplate.postForObject(requestUrl, formEntity, String.class);
            long end = System.currentTimeMillis();
            logger.info("====================>request api:{}, seq:{}, use time:{}ms", api, seq, end - begin);
            logger.info("====================>request api:{}, seq:{}, response:{}", api, seq, responseBody);
            return JSONObject.parseObject(responseBody, respType);
        } catch (Exception e) {
            logger.error("request exception api:{}, seq:{}",api, seq, e);
            throw new RuntimeException("request exception");
        }
    }
}

BaseAPIProxy这个类将项目中所有与发送HTTP请求有关的代码都抽取过来了,那些烦人的重复代码再也不会在业务代码中出现了,开发效率都变高了,有木有?

当然了,这么做会导致系统中出现很多Proxy类,建议给每个Proxy类都取有意义的名字,这样一眼就能看出这个类是哪个接口相关的。

原文地址:https://www.cnblogs.com/bluemilk/p/10285314.html