使用HttpClient实现请求转发并获取响应的实践
-
背景:
- 产品的app端,甲方要求app端调用接口,都需要经过ecsp(我也不知道是个什么东东),反正就是说,想web端直接调用网关的接口,由网关再转发给对应的服务接口,然后获取该接口的响应。现在加入这个ecsp就不行了,所有的接口必须都经过这个ecsp来请求调用,这个一下子就很麻烦了,想一个一个改也不可能,app已经都上线了,接口太多了,没法改。所以想找到我,让我想想办法,能不能专门出一个接口,把要调用的接口和对应的参数给我(比如还有token……之类的),然后我通过什么什么手段,把这个接口的响应值返回给app,这样的话,app端工作量就很小,只需要用ecsp调用我这一个接口就好了,不用他一个一个改。说干就干。
-
初步思路:
- 一开始,我的想法是用feign或者RestTemplate来做,用feign可以调用注册中心下任意的接口嘛,但是呢,用feign会有一个问题,那就是feign一般都需要定义一个客户端,用来指向某个服务,这种我们服务有十个左右,那要怎么写。因此这个就放弃掉了。后面就是RestTemplate,这个可以调用服务内的接口,然后试了一下,传了带ip的接口地址给我,然后RestTemplate就报错了,说什么找不到主机,搜了好多解决办法,无果。遂放弃。
-
最终方案:
- 最终还是让机灵的我想到了使用apache的HttpClient,这个还是比较好用和方便的,毕竟feign默认用的就是它,嘿嘿。
-
需求解析:
- 入参,响应,都是什么?
- 如何区分请求的具体方法?
- 各种形式的传参要怎么传递?
-
逐个来看
-
第一个问题,入参:
入参的话,一定要确定:
1、请求方法
2、请求的url
3、请求的参数
4、安全性问题,例如token
5、各种形式的传参:例如post请求,有很多种形式的传参:form/data、application/json、基本上最常用的也就是我说的这两种。
根据第一个问题的五个要求,我定义了一个入参类,用来接收这个接口所需的参数(目前看的是第二个版本了,第一个版本存在的问题后面再说)
/** * @author GmDsg * @date 2023/9/22 9:53 * @description 请求入参封装类 */ @Data public class RequestParams { /** * url */ private String url; /** * 请求方法 */ private String method; /** * 参数 */ private ParamReqDTO params; /** * token */ private String token; /** * post请求是否为json形式的body入参 */ private Boolean postIsBody; /** * 是否文件下载,暂时没用 */ private Boolean isDownloadFile; }
然后把ParamReqDTO放出来
/** * @author GmDsg * @date 2023/10/20 9:20 * @description */ @Data public class ParamReqDTO { /** * 纯数组入参 */ private List<Object> arrayData; /** * Json对象入参或form/data入参 */ private Map<String, Object> objectOrFormData; /** * 是否纯数组入参 */ private Boolean paramIsArrayData; }
这个类主要就是来区分post请求是否为纯数组入参,因为之前是没有ParamReqDTO这个类的,我的请求参数直接用Map<String, Object>来接收的。
第一个版本是这样的:
/** * @author GmDsg * @date 2023/9/22 9:53 * @description 请求入参封装类 */ @Data public class RequestParams { /** * url */ private String url; /** * 请求方法 */ private String method; /** * 参数 */ private Map<String, Object> params; /** * token */ private String token; /** * post请求是否为json形式的body入参 */ private Boolean postIsBody; }
没想到这么多,直接用的Map来接收,昨天app端找到我,说直接数组的post请求入参要怎么传,我仔细一想,还是不能够,这种Map对象传不了。
因此有了第一个发的,完整版。
-
第二个问题:响应什么内容,现在我们做的项目都是响应Json数据,所以说,直接响应Object是没毛病的,所以我这个新接口的返回值肯定是Object了,只不过,我还是有一点点年轻了,以为我们这的方法都是R,无论成功还是失败都是R.successful,或者R.failed,让我最后将响应的Json字符串转换为Map集合返回了,也是昨天才遇到,有的人,竟然直接把List集合返回了,因此我之前写的把Json字符串转换们Map,遇到了List,就报错了,哎,不知道要统一返回值干嘛的,竟然有人不用,也是醉了。不过也不是什么大问题,只要把接口返回的Json字符串转换为Object就可以了,不是啥大问题。
那都确定了,开始写对应的逻辑吧,上代码:
定义一个类:ApiService,因为需要校验token有效性,这也是为什么保证这个接口的安全性,要引入Redis,所以把这个类注册到Spring框架中,用的时候引用一下就可以了。
/** * @author GmDsg * @date 2023/9/20 15:06 * @description 请求接口处理 */ @Slf4j @Component public class ApiService { private RedisUtils redisUtils; @Autowired public void setRedisUtils(RedisUtils redisUtils) { this.redisUtils = redisUtils; } /** * 转发请求并获取目标接口的返回值 * * @param params * @return */ public Object forwardRequestAndGetRes(RequestParams params) { CloseableHttpClient httpClient = null; try { // 获取token String token = params.getToken(); // 校验token,如果token不合法会直接抛出异常 redisUtils.getUserInfoByToken(token); httpClient = HttpClientBuilder.create().build(); // 获取要请求的url String url = params.getUrl(); // 获取请求方法(get or post or delete) String method = params.getMethod(); if (HttpMethod.GET.name().equalsIgnoreCase(method)) { // 是get请求 HttpGet get = new HttpGet(url); // 设置请求token get.setHeader("token", token); String getRes = EntityUtils.toString(httpClient.execute(get).getEntity()); return JacksonUtils.parseObject(getRes, Object.class); } else if (HttpMethod.POST.name().equalsIgnoreCase(method)) { ParamReqDTO paramReqDTO = params.getParams(); // 是post请求 HttpPost post = new HttpPost(url); post.setHeader("token", token); if (params.getPostIsBody()) { // 如果是application/json的post请求 StringEntity payload; if (paramReqDTO.getParamIsArrayData()) { List<Object> arrayData = paramReqDTO.getArrayData(); payload = new StringEntity(JSONUtil.toJsonStr(arrayData), ContentType.APPLICATION_JSON); } else { Map<String, Object> objectOrFormData = paramReqDTO.getObjectOrFormData(); payload = new StringEntity(JSONUtil.toJsonStr(objectOrFormData), ContentType.APPLICATION_JSON); } post.setEntity(payload); } else { // 不是application/json,即是from/data形式的 Map<String, Object> objectOrFormData = paramReqDTO.getObjectOrFormData(); BasicNameValuePair pair; List<BasicNameValuePair> data = new ArrayList<>(); for (String key : objectOrFormData.keySet()) { Object value = objectOrFormData.get(key); pair = new BasicNameValuePair(key, String.valueOf(value)); data.add(pair); } UrlEncodedFormEntity formData = new UrlEncodedFormEntity(data); post.setEntity(formData); } String postRes = EntityUtils.toString(httpClient.execute(post).getEntity()); return JacksonUtils.parseObject(postRes, Object.class); } else if (HttpMethod.DELETE.name().equalsIgnoreCase(method)) { // 是delete请求 HttpDelete delete = new HttpDelete(url); delete.setHeader("token", token); String deleteRes = EntityUtils.toString(httpClient.execute(delete).getEntity()); return JacksonUtils.parseObject(deleteRes, Object.class); } else { // 其他请求方法暂不支持,直接返回错误信息 return R.failed("不支持的请求方法, 请检查入参!"); } } catch (Exception e) { log.error("转发请求发生异常: {}", e.getMessage()); e.printStackTrace(); int exCode = 500; if (e instanceof JPMException) { exCode = ((JPMException) e).getCode(); } return R.failed(e.getMessage(), "", exCode); } finally { PoitlIOUtils.closeQuietlyMulti(httpClient); } } }
其实这个没啥好说的,我们项目中只有Get,Post,Delete请求,没有其他的,所以只写了这三个。
get请求和delete请求,不用做什么处理,要拼接的参数都是在url里面,只需要让调用的人把处理的参数给我就完事了。
主要就是post请求麻烦一点,做了两种入参方式,一种是Json形式的body入参,一种是from/data入参,其中对两个形式做对应的入参包装即可。
-
然后我们再写一个接口,把接口暴露出去即可。
/** * @author GmDsg * @date 2023/9/22 15:19 * @description 请求转发前端控制器 */ @RestController @RequestMapping("/forward") @Slf4j public class ApiForwardRequestController { @Autowired private ApiService apiService; /** * 转发请求并获取接口响应结果 * * @param params * @return */ @PostMapping("/forwardRequestAndGetRes") public Object forwardRequestAndGetRes(@RequestBody RequestParams params) { log.info("转发请求并获取接口响应结果: {}", "/forward/forwardRequestAndGetRes"); log.info("转发请求, url: {}, method: {}, params: {}", params.getUrl(), params.getMethod(), params.getParams()); return apiService.forwardRequestAndGetRes(params); } }
这里就是注入一下这个组件,然后再调用apiService的转发请求方法即可。
-
然后我们来调用一下接口看看:
我们用的是apiPost软件,和postman类似,不过是国产的,功能也更强大一点,适用于前后端分离场景的项目。
-
先来一个get请求的接口:
-
入参:
{
"postIsBody": false,
"method": "get",
"params": {
"arrayData": [],
"objectOrFormData": {},
"paramIsArrayData": false
},
"url": "http://192.168.0.193:7998/personnelApplication/researchers-call-application?current=1&size=10",
"token": "ccc16984680f49b0888c32c659fe3bba.15db8145ed89b6bee9f715a6ce064b70"
}
get请求直接把query中拼接的参数完了,给我,就可以。
响应结果:
{
"statusCode": 200,
"message": "查询成功",
"data": {
"records": [
{
"uuid": "a453987b-7bee-4837-a0cb-51e8bc70067f",
"createTime": "2023-10-18 16:52:24",
"createUserName": "高明大帅哥",
"createUserId": "ae9d3d8c11f846129af89501b1128f74",
"updateTime": null,
"updateUserName": null,
"updateUserId": null,
"processStatus": 7,
"enable": null,
"fileIds": null,
"processNumber": "CJPM-RYDPSQ-20231018-050",
"applicationDate": "2023-10-18 16:52:02",
"id": "00f5d2ee-c6b8-4c07-90d0-ef9f0ab834f3",
"name": "曲秋凝",
"ldapAccount": "QUQIUNING",
"approvalEndTime": "2023-10-18 16:52:40",
"childList": null
},
{
"uuid": "4a38df88-7b5e-4d80-a1d4-252dc61ed438",
"createTime": "2023-10-18 16:51:06",
"createUserName": "高明大帅哥",
"createUserId": "ae9d3d8c11f846129af89501b1128f74",
"updateTime": null,
"updateUserName": null,
"updateUserId": null,
"processStatus": 7,
"enable": null,
"fileIds": null,
"processNumber": "CJPM-RYDPSQ-20231018-049",
"applicationDate": "2023-10-18 16:49:04",
"id": "00f5d2ee-c6b8-4c07-90d0-ef9f0ab834f3",
"name": "曲秋凝",
"ldapAccount": "QUQIUNING",
"approvalEndTime": "2023-10-18 16:51:22",
"childList": null
},
{
"uuid": "194a5470-d542-46b0-a6e4-c3a36fa17d90",
"createTime": "2023-10-18 16:47:45",
"createUserName": "高明大帅哥",
"createUserId": "ae9d3d8c11f846129af89501b1128f74",
"updateTime": null,
"updateUserName": null,
"updateUserId": null,
"processStatus": 7,
"enable": null,
"fileIds": null,
"processNumber": "CJPM-RYDPSQ-20231018-048",
"applicationDate": "2023-10-18 16:46:58",
"id": "644b497f33ec4b99b232b8e67f498a91",
"name": "IMing",
"ldapAccount": "gm",
"approvalEndTime": "2023-10-18 16:48:01",
"childList": null
},
{
"uuid": "8810735f-9220-435d-a101-7cbad38ba1cf",
"createTime": "2023-10-18 16:46:40",
"createUserName": "高明大帅哥",
"createUserId": "ae9d3d8c11f846129af89501b1128f74",
"updateTime": null,
"updateUserName": null,
"updateUserId": null,
"processStatus": 7,
"enable": null,
"fileIds": null,
"processNumber": "CJPM-RYDPSQ-20231018-047",
"applicationDate": "2023-10-18 16:45:52",
"id": "644b497f33ec4b99b232b8e67f498a91",
"name": "IMing",
"ldapAccount": "gm",
"approvalEndTime": "2023-10-18 16:46:55",
"childList": null
},
{
"uuid": "d0a07420-885e-4a4a-8f77-65e19ecba917",
"createTime": "2023-10-18 15:39:25",
"createUserName": "高明大帅哥",
"createUserId": "ae9d3d8c11f846129af89501b1128f74",
"updateTime": null,
"updateUserName": null,
"updateUserId": null,
"processStatus": 7,
"enable": null,
"fileIds": null,
"processNumber": "CJPM-RYDPSQ-20231018-046",
"applicationDate": "2023-10-18 15:37:46",
"id": "ae9d3d8c11f846129af89501b1128f74",
"name": "高明大帅哥",
"ldapAccount": "gmdsg",
"approvalEndTime": "2023-10-18 15:39:52",
"childList": null
},
{
"uuid": "1fecd38a-fb84-42f7-bf79-5259f68892b3",
"createTime": "2023-10-18 15:08:58",
"createUserName": "高明大帅哥",
"createUserId": "ae9d3d8c11f846129af89501b1128f74",
"updateTime": null,
"updateUserName": null,
"updateUserId": null,
"processStatus": 7,
"enable": null,
"fileIds": null,
"processNumber": "CJPM-RYDPSQ-20231018-045",
"applicationDate": "2023-10-18 15:08:32",
"id": "15ad0396-7d4e-45dc-ae8a-eda348d2442f",
"name": "彭斌鑫",
"ldapAccount": "PENGBINXIN",
"approvalEndTime": "2023-10-18 15:09:32",
"childList": null
},
{
"uuid": "c963f8f4-5c9b-44f7-8867-f96ff655f514",
"createTime": "2023-10-18 14:19:07",
"createUserName": "高明大帅哥",
"createUserId": "ae9d3d8c11f846129af89501b1128f74",
"updateTime": null,
"updateUserName": null,
"updateUserId": null,
"processStatus": 7,
"enable": null,
"fileIds": null,
"processNumber": "CJPM-RYDPSQ-20231018-044",
"applicationDate": "2023-10-18 14:18:04",
"id": "15ad0396-7d4e-45dc-ae8a-eda348d2442f",
"name": "彭斌鑫",
"ldapAccount": "PENGBINXIN",
"approvalEndTime": "2023-10-18 14:19:26",
"childList": null
},
{
"uuid": "d544382f-f9a5-4904-a0a3-6e2770aef4bf",
"createTime": "2023-10-18 14:17:03",
"createUserName": "高明大帅哥",
"createUserId": "ae9d3d8c11f846129af89501b1128f74",
"updateTime": null,
"updateUserName": null,
"updateUserId": null,
"processStatus": 7,
"enable": null,
"fileIds": null,
"processNumber": "CJPM-RYDPSQ-20231018-043",
"applicationDate": "2023-10-18 14:16:20",
"id": "036A5B09-EE78-40D1-B8C5-32D6F1854E01",
"name": "高鸣",
"ldapAccount": "GAOMING90",
"approvalEndTime": "2023-10-18 14:17:23",
"childList": null
},
{
"uuid": "5ecabb20-402e-4d00-b050-289161fc74cf",
"createTime": "2023-09-14 11:10:41",
"createUserName": "高明大帅哥",
"createUserId": "ae9d3d8c11f846129af89501b1128f74",
"updateTime": null,
"updateUserName": null,
"updateUserId": null,
"processStatus": 7,
"enable": null,
"fileIds": null,
"processNumber": "CJPM-RYDPSQ-20230914-042",
"applicationDate": "2023-09-14 11:09:58",
"id": "01c9c765-4917-4373-8b83-b7844d9ae6b9",
"name": "李嘉仪",
"ldapAccount": "SZDQ-0135",
"approvalEndTime": "2023-09-14 11:10:57",
"childList": null
},
{
"uuid": "5bd40fa1-94c7-4c4f-8199-cf16068af903",
"createTime": "2023-08-30 15:29:43",
"createUserName": "杨声康",
"createUserId": "83f5c10606da4ffbb5d8f0a2ef708526",
"updateTime": null,
"updateUserName": null,
"updateUserId": null,
"processStatus": 0,
"enable": null,
"fileIds": null,
"processNumber": "CJPM-RYDPSQ-20230830-041",
"applicationDate": "2023-08-30 15:29:43",
"id": null,
"name": null,
"ldapAccount": null,
"approvalEndTime": null,
"childList": null
}
],
"total": 621,
"size": 10,
"current": 1,
"orders": [],
"optimizeCountSql": true,
"hitCount": false,
"countId": null,
"maxLimit": null,
"searchCount": true,
"pages": 63
},
"success": true
}
-
然后再来一个post请求:
入参:
{
"postIsBody": true,
"method": "post",
"params": {
"arrayData": [],
"objectOrFormData": {
"pageNum": 1,
"pageSize": 10,
"name": "IMing"
},
"paramIsArrayData": false
},
"url": "http://192.168.0.193:7998/summary/release/queryPersonnelDistributionSummary",
"token": "ccc16984680f49b0888c32c659fe3bba.15db8145ed89b6bee9f715a6ce064b70"
}
method改为post,传参是json形式的body传参,所以postIsBody改为true,params.objectOrFormData把入参的json数据写进去即可。
返回值:
{
"data": [
{
"uuid": null,
"createTime": "2023-10-18 16:47:45",
"createUserName": null,
"createUserId": null,
"parentId": null,
"ldapAccount": "gm",
"id": "644b497f33ec4b99b232b8e67f498a91",
"name": "IMing",
"costCenterDepartment": null,
"projectNumber": "80001017",
"area": "a537f3ed-cbf9-48f9-a01f-650661b14ccc",
"projectId": "5caab158-6247-4866-9251-c805fa5bcd05",
"projectName": "兰香四街",
"positionType": "0",
"position": "JPMsoft_XMRYZW_XMHY",
"costDistributionProportion": 23,
"jobNumber": null
},
{
"uuid": null,
"createTime": "2023-10-18 16:47:45",
"createUserName": null,
"createUserId": null,
"parentId": null,
"ldapAccount": "gm",
"id": "644b497f33ec4b99b232b8e67f498a91",
"name": "IMing",
"costCenterDepartment": null,
"projectNumber": "1000312",
"area": "a537f3ed-cbf9-48f9-a01f-650661b14ccc",
"projectId": "078709b3-227d-46b7-9fb6-f89630978f84",
"projectName": "望海小学",
"positionType": "1",
"position": "JPMsoft_QYRYZW_QYHY",
"costDistributionProportion": 77,
"jobNumber": null
}
],
"message": "查询成功",
"statusCode": 200,
"success": true,
"timestamp": "2023-10-24 11:00:38"
}
-
再来一个删除请求:
入参:
{
"postIsBody": false,
"method": "delete",
"params": {
"arrayData": [],
"objectOrFormData": {},
"paramIsArrayData": false
},
"url": "http://192.168.0.193:7998/sgybqd/construction-samples-list/4a575419-ecfc-445f-a01b-0406f9884a2d", // 与get请求一样,将拼接完参数的url传给我。
"token": "ccc16984680f49b0888c32c659fe3bba.15db8145ed89b6bee9f715a6ce064b70"
}
响应:
{
"data": null,
"message": "删除成功",
"statusCode": 200,
"success": true,
"timestamp": "2023-10-24 11:03:22"
}
-
然后我们再来一个纯数组入参的数据
入参:
{ "postIsBody": true, "method": "POST", "params": { "arrayData": [ "a7a60161-d63b-4534-891e-63af63c4a530" ], "objectOrFormData": {}, "paramIsArrayData": true }, "url": "http://192.168.0.193:8085/apis/zuul/fileServer/fileRecord/getFileByFileRelationIds", "token": "e10e89be3b36429f907abb4db1558d7a.51fe756a6d4fa5482fcba4f9dfe8f222" }
postIsBody为true,params.arrayData传对应的数组,也可以是对象,因为封装的是List,params.paramIsArrayData为true
- 结果:
[ { "uid": "83393136-63f7-438e-a48d-62f1cf9eb640", "name": "14后海二小总包补协一.pdf", "status": "done", "url": "http://192.168.0.193:8085/apis/fileServer/jpm/downloadFile/ded6c1e1-a824-4985-ac5c-9e1b85568a2e", "noHostUrl": "/fileServer/jpm/downloadFile/ded6c1e1-a824-4985-ac5c-9e1b85568a2e", "openUrl": "http://192.168.0.193:8085/apis/fileServer/jpm/downloadAloneOpenFile/83393136-63f7-438e-a48d-62f1cf9eb640", "relationid": "a7a60161-d63b-4534-891e-63af63c4a530", "uploadTime": "2022-12-26T01:53:24.000+00:00", "fileSize": null, "fileSuffix": null, "fileInputName": null, "fileInputCode": null, "fileInputTime": null, "fileInputVersion": null, "adName": null, "adCode": null, "adId": null, "adLink": null } ]
-
总结
到这里基本上就演示完了,值得说的问题就是,因为用的是httpCilent,我们可以在开发环境调用生产环境的接口,只要token对的情况下,不知道这个算不算问题,我想应该没事吧,因为生产的本地的token和生产不一样,所以应该也没事。所以,这次的文章就写到这里,如果大家有其他更好的想法,或者我这个写法可能会有漏洞的地方,欢迎大家留言指正,一起进步,一起飞。