zoukankan      html  css  js  c++  java
  • 系统间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类都取有意义的名字,这样一眼就能看出这个类是哪个接口相关的。

  • 相关阅读:
    wamp配置虚拟主机
    php单例模式
    YII缓存操作
    YII的延迟加载
    之字形打印二叉树
    对称的二叉树
    二叉树的下一节点
    删除链表的重复节点
    链表中环的入口
    字符流中第一个不重复的字符
  • 原文地址:https://www.cnblogs.com/bluemilk/p/10285314.html
Copyright © 2011-2022 走看看