痛点
最近接手一个老项目,这个项目几经转手,到我这里时,发现代码的可阅读性实在是很差,对于一个有点代码洁癖的我来说,阅读起来实在是很难受。其中一个痛点,现在就拉出来讲讲。该项目需要与另外一个项目进行业务对接,因此有很多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类都取有意义的名字,这样一眼就能看出这个类是哪个接口相关的。