使用HttpClient实现请求转发并获取响应的实践

使用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. 如何区分请求的具体方法?
    3. 各种形式的传参要怎么传递?
  • 逐个来看
    • 第一个问题,入参:

      入参的话,一定要确定:

      ​ 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请求的接口:

image-20231024104958365

入参:

{
    "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请求:

    入参:image-20231024110001607

{
	"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"
}
  • 再来一个删除请求:

    入参:image-20231024110256795

{
	"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"
}
  • 然后我们再来一个纯数组入参的数据

    入参:image-20231024112004984

    {
    	"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和生产不一样,所以应该也没事。所以,这次的文章就写到这里,如果大家有其他更好的想法,或者我这个写法可能会有漏洞的地方,欢迎大家留言指正,一起进步,一起飞。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值