系统间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接口调用来说,针对每个接口编写一个联调测试类是很有必要的,开户初期用于联调,后期接口参数有改动时,能够很方便进行测试。
总结起来上面这段代码存在的问题有:
- 代码可阅读性差,不根据上下文的代码逻辑,可能都没法确定调用的是哪个接口。
 - 代码可维护性查,后期如果要更改接口参数,到处都要修改,工作量大而且容易出错,不容易进行单元测试。
 
优化方案
很明显,要优化这段代码,就是要把这段代码抽取到同一个地方去,把面向对象中“抽象”和“封装”的思想运用起来。上面的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类都取有意义的名字,这样一眼就能看出这个类是哪个接口相关的。
系统间HTTP调用代码封装的更多相关文章
- 内部系统间调用client包的封装方法
		
1.何为client 公司内部的系统调用,如果采用http直接调用,会非常不方便,而且不规范,接口升级或者变动,系统间的改动也是相当麻烦.所以在系统间采用client的互相调用,调用简单,如下: 获取 ...
 - WebService与RMI(远程调用方式实现系统间通信)
		
前言 本文是<分布式java应用基础与实践>读书笔记:另外参考了此博客,感觉讲的挺好的,尤其是其中如下内容: 另外,消息方式实现系统间通信本文不涉及.RMI则只采用spring RMI框架 ...
 - 系统间通信(10)——RPC的基本概念
		
1.概述 经过了详细的信息格式.网络IO模型的讲解,并且通过JAVA RMI的讲解进行了预热.从这篇文章开始我们将进入这个系列博文的另一个重点知识体系的讲解:RPC.在后续的几篇文章中,我们首先讲解R ...
 - 系统间通信(9)——通信管理与RMI 下篇
		
接上文<架构设计:系统间通信(8)--通信管理与RMI 上篇>.之前说过,JDK中的RMI框架在JDK1.1.JDK1.2.JDK1.5.JDK1.6+几个版本中做了较大的调整.以下我们讨 ...
 - 系统间通信——RPC架构设计
		
架构设计:系统间通信(10)——RPC的基本概念 1.概述经过了详细的信息格式.网络IO模型的讲解,并且通过JAVA RMI的讲解进行了预热.从这篇文章开始我们将进入这个系列博文的另一个重点知识体系的 ...
 - 解析大型.NET ERP系统 高质量.NET代码设计模式
		
1 缓存 Cache 系统中大量的用到缓存设计模式,对系统登入之后不变的数据进行缓存,不从数据库中直接读取.耗费一些内存,相比从SQL Server中再次读取数据要划算得多.缓存的基本设计模式参考下面 ...
 - 系统间通信(8)——通信管理与RMI 上篇
		
1.概述 在概述了数据描述格式的基本知识.IO通信模型的基本知识后.我们终于可以进入这个系列博文的重点:系统间通信管理.在这个章节我将通过对RMI的详细介绍,引出一个重要的系统间通信的管理规范RPC, ...
 - 系统间通信(5)——IO通信模型和JAVA实践 下篇
		
7.异步IO 上面两篇文章中,我们分别讲解了阻塞式同步IO.非阻塞式同步IO.多路复用IO 这三种IO模型,以及JAVA对于这三种IO模型的支持.重点说明了IO模型是由操作系统提供支持,且这三种IO模 ...
 - 系统间通信(3)——IO通信模型和JAVA实践 上篇
		
来源:http://blog.csdn.net/yinwenjie 1.全文提要 系统间通信本来是一个很大的概念,我们首先重通信模型开始讲解.在理解了四种通信模型的工作特点和区别后,对于我们后文介绍搭 ...
 
随机推荐
- git如何新建分支
			
1) 切换到基础分支,如主干 git checkout master 2)创建并切换到新分支 git checkout -b panda git branch可以看到已经在panda分支上 3)更新分 ...
 - JavaScript控制浏览器全屏及各种浏览器全屏模式的方法、属性和事件
			
实现全屏 个人版:function isFullScreen() { var fullscreenElement = document.fullscreenElement || document.we ...
 - PHP 数据库连接
			
$db = new MySQLi("localhost","root","123","php0307"); !mysql ...
 - git push时出现大文件的处理方法
			
最近在提交项目时出现报错 文件限制只能100M,但是里面有个文件202M,超过了码云的限制. 所以顺手就把这个文件删除了 然后发现还是同样的报错,反复检查目录还是不行,找了资料说,需要git rm 命 ...
 - 乐字节Java之file、IO流基础知识和操作步骤
			
嗨喽,小乐又来了,今天要给大家送上的技术文章是Java重点知识-IO流. 先来看看IO流的思维导图吧. 一. File 在Java中,Everything is Object!所以在文件中,也不例外! ...
 - Linux(Ubuntu)下的OpenGl的环境安装,  在qt程序中使用opengl库
			
OpenGl的环境安装 以下参考自:https://blog.csdn.net/wasaiheihei/article/details/52085397 1. 建立基本编译环境 首先不可或缺的,就是编 ...
 - webpack config to use plugin and webpack-dev-server
			
Demo3操作手册 本Demo演示如何配合各种plugin进行偏复杂的使用 准备环境 初始化环境, cd到demo1目录之后, 执行如下命令: npm init -y npm install webp ...
 - js实现限制容器中字符个数
			
html: <div class="ellipsis"> 夜空中最亮的星/请指引我走出去/夜空中最亮的星 是否知道/那曾与我同心的身影 如今在哪里/夜空中最亮的星 是否 ...
 - C++ 根据两点式方法求直线并求两条直线的交点
			
Line.h #pragma once //Microsoft Visual Studio 2015 Enterprise //根据两点式方法求直线,并求两条直线的交点 #include"B ...
 - 题解 CF1216B 【Shooting】
			
题目大意:给你n个数,让你找到一种排列方式,使得$\sum\limits_{i=1}^{n}a[i]*(b[i]-1)$($b$为$a$的一种排列)最小 应该可以一眼看出是贪心,因为大的放前面先射击一 ...