从未见过如此厚颜无耻之 网络请求客户端

最近趁着公司项目业务需求不是很多,专门花了一段时间对我们团队负责的项目代码做了一个 code review, 其中发现了一个让我非常恼火的事情。我发现就我们一个项目居然出现了好几个负责网络请求的客户端,其中包括Java 原生客户端, Apache HTTPClient, OKHttp, 还有Spring RestTemplate,甚至还有基于Java 原生客户端自行封装的一个 OutGateway。

再加上公司内部还有两个需求,一个需求要求我们能够实现通过不同的网络环境(如公网、专线,乃至VPN)向同一个 HTTP/HTTPS 协议的 API (比如调用高德地图解析地理位置的经纬度)发起请求,并支持动态切换等功能。另外一个需求是需要适配公司内部跨系统通讯的好几种不同的私有网络协议。

图片

一想到在很长一段时间内,我要维护如此多基于不同客户端的网络请求工具所负责的业务,这就让我感到很绝望。于是我掀桌子了!

现有解决方案

先看看现有常用的客户端的简单使用情况吧(以最简单的 GET 请求为例)

1 Java 原生工具

public static String doGet(String httpUrl){
        HttpURLConnection connection = null;
        InputStream is = null;
        BufferedReader br = null;
        StringBuffer result = new StringBuffer();
        try {
            //创建连接
            URL url = new URL(httpUrl);
            connection = (HttpURLConnection) url.openConnection();
            //设置请求方式
            connection.setRequestMethod("GET");
            //设置连接超时时间
            connection.setReadTimeout(15000);
            //开始连接
            connection.connect();
            //获取响应数据
            if (connection.getResponseCode() == 200) {
                //获取返回的数据
                is = connection.getInputStream();
                if (null != is) {
                    br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                    String temp = null;
                    while (null != (temp = br.readLine())) {
                        result.append(temp);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IoUtils.close(br);
            IoUtils.close(is);
            connection.disconnect();
        }
        return result.toString();
    }

2.  Apache HttpClient

    public static String doGet(String url) {
        StringBuffer resultBuffer = null;
        BufferedReader bufferedReader = null;
        CloseableHttpResponse response = null;
        CloseableHttpClient closeableHttpClient = null;
        String result = null;
        try {
            closeableHttpClient = HttpClients.custom().build();
​
            HttpGet httpGet = new HttpGet(url);
            response = closeableHttpClient.execute(httpGet);
            if (Objects.nonNull(response.getEntity())) {
                result = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
            }
        } catch (URISyntaxException e) {
            log.error("url synctax error", e);
        } catch (Exception e) {
            log.error("execute get request error", e);
        } finally {
            close(response);
        }
        return result;
    }

3. OKHttp

  public static String doGet(String url) {
        OKHttpClient client = new OKHttpClient();
        Request request = new Request.Builder().url(url).get().build();
        Response response;
        try {
            response = client.newCall(request).execute();
            return response.body().string();
        } catch (IOException e){
            // do on error
        }
        return "";
    }

4. RestTemplate

    public static String doGet(String url) throws Exception{
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> entity = restTemplate.getForEntity(url,String.class);
        return entity.getBody();
    }

不得不说,OKHttp与RestTemplate 还是比较简洁的,不过感觉还是没有达到我想要的样子。因为当前只是一个简单的GET 请求,甚至都不带有添加Header 的操作,不判断响应状态码等逻辑。

所以,在使用这些工具的时候,绝大多数情况下我们首先想的是自行封装一个工具类,用于实现网络请求,但几遍如此,也存在诸如不同系统环境(研发、测试、灰度、生产等)需要对请求连接进行拼装,对于响应结果的多元化处理需求等情况,这让我将目光转向了封装程度更高的开源软件。

更好解决方案

为了在很长一段时间内,我不用维护那么多套不同的网络请求工具,我开始寻求更好的解决方案。在这其中我把目光放到了 OpenFeign, retrofit 和 Forest之上。

OpenFeign 

首先是 OpenFeign。在Spring-Boot/Spring-Cloud 包打天下的时代,我想绝大部分Java开发者对此都比较熟悉。作为一个声明式的HTTP 客户端,想要发起一次网络请求那是再简单不过了,还拿最简单的GET 请求为例.

@FeignClient(url = "https://www.baidu.com")
public interface GetGet {
    @GetMapping
    String simpleGet();
}

retrofit

/**
 * 源自官方文档:https://square.github.io/retrofit/#api-declaration
 */
public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}
​
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();
​
GitHubService service = retrofit.create(GitHubService.class);
​
Call<List<Repo>> repos = service.listRepos("octocat");

Forest

public interface AmapClient {
​
    @Get("http://ditu.amap.com/service/regeo?longitude={0}&latitude={1}")
    Map getLocation(String longitude, String latitude);
}

只考虑 HTTP/HTTPS 协议的情况下,此三种工具也基本上满足了我们项目的需求,不过依然有一些不足。 

比如,feign 更加适合在 Spring-Cloud 体系下,微服务之间的互相调用,对于三方API 的多网络环境切换支持不友好。对于后两种框架也不太好支持。

而对于非HTTP/HTTPS 的网络通讯协议,翻遍各种开源社区,能够找到的也都是自己实现。而公司内部现有解决方案是直接通过 Socket 进行处理,以xml 和 freemarker 作为模板处理网络请求上下文(参数),每次新增一个接口,都需要写好几个 xml文档和 freemarker文档,如此痛苦的过程,再加上我迫切想要一统公司内部网络通讯客户端江山,于是我做出了一个违背宗门的决定:自己造轮子!

最适合解决方案

Remote

参考Fegin,Forest 等声明式网络请求客户端框架,我将 Remote 也设置成为声明式的,并提供了大量的扩展点。也就是说如果需要对一个新的 API 进行适配,则只需要写一个 Java 接口方法即可(同一协议、域名、端口下的API 可以写到同一个 Java接口当中)。并利用Spring高度的可扩展性,对Remote进行了Spring 集成,于是一个简单的 HTTP Get 请求就变成如下的样子(以获取微信公众平台应用接口访问令牌API为例)

    @Server( supplier = "wechat", namespace = "common", schema = "https", host = "api.weixin.qq.com", port = 443)
    interface HttpGetInterface {
​
        @HttpMapping(method = HttpMethod.GET, uri = "/cgi-bin/token", queries = @HttpQuery(name = "grant_type", value = "client_credential"))
        WeChatAccessTokenRes accessToken(@HttpQuery(name = "appid") String appid, @HttpQuery(name = "secret") String secret);
    }

此案例中,用户可自行实现网络环境仓库接口,即可对供应商为 ‘wechat’ , 业务线为 ‘common’ 的所有 API 进行动态的网络环境管理。

再完成Remote的开发后,终于我们项目组替换掉了N多种不同的网络请求客户端。

以下是一些案例:

私有协议案例:
/**
 *  GGP  是一个基于 TCP/IP 的私有通讯协议,协议内容在此不做展示,协议名称已混淆,勿对号
 */
@Server(supplier = "0315", namespace = "creditcard", schema = "GGP")
public interface CreditCardPlatformRemoting {
​
    /**
     * GGP 协议下的其中一个API
     */
    @GGPMapping(value = "130021", to = "0315", from = "752")
    _130021Res _130021(@GGPHeader(name = GGPHeaderConstants.FROM) String fromChlCode,
                       @GGPBody _130021Req req);
​
}
部分微信公众API 案例(HTTPS)
@Server(
        supplier = WeChatCons.Supplier.WECHAT,
​        namespace = WeChatCons.Namespace.COMMON,
        schema = WeChatCons.Api.DEFAULT_SCHEMA,
        host = WeChatCons.Api.DEFAULT_HOST,
        port = WeChatCons.Api.DEFAULT_PORT
)
public interface WeChatPublicAccountMenuRemoting {
​
    /**
     * 创建公众号菜单
     *
     * @param subjectId {@link String subjectId}
     * @param body      {@link WeChatPublicAccountCreateMenuReq }
     * @return {@link WeChatApiRes}
     */
    @HttpMapping(method = HttpMethod.POST, uri = "/cgi-bin/menu/create")
    WeChatApiRes create(@WeChatAccessTokenParam String subjectId, @JsonBody WeChatPublicAccountCreateMenuReq body);
​
    /**
     * 查询公众号菜单接口
     *
     * @param subjectId {@link String subjectId} 公众号应用关键字,可选值:publicId, subjectId, appId
     * @return {@link WeChatPublicAccountCurrentMenuInfoRes }
     */
    @HttpMapping(method = HttpMethod.GET, uri = "/cgi-bin/get_current_selfmenu_info")
    WeChatPublicAccountCurrentMenuInfoRes currentSelfMenuInfo(@WeChatAccessTokenParam String subjectId);
​
}
微信商户媒体文件上传API 案例
@Server(
        supplier = WeChatCons.Supplier.WECHAT,
        namespace = WeChatCons.Namespace.PAY,
        schema = WeChatCons.Api.DEFAULT_SCHEMA,
        host = WeChatCons.Api.PAY_HOST,
        port = WeChatCons.Api.DEFAULT_PORT
)
public interface WeChatPayMediaRemoting {
​
    @HttpMapping(method = HttpMethod.POST,uri = "/v3/merchant/media/upload", headers = @HttpHeader(name = "Accept",value = "application/json"))
    V3MerchantMediaUploadResponse uploadImage(
            @WeChatPayAuthorization String mch_id,
            @HttpHeader(name = "Authorization") String authorization,
            @FormData @WeChatPayMedia V3MerchantMediaUploadRequest file,
            On on);
​
}

到此,我算是终于舒服了。后面我也将此项目剔除私有协议部分后,准备开源。不过这是后话,

最后,我厚颜无耻地将Remote 称之为最强客户端【手动狗头】.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Asial Jim

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值