java中http调用组件深入详解

目录


一、前言

在java项目中,尤其是在微服务开发中,不同服务之间难免要进行通信,由于微服务属于不同的进程,在这种情况下,不同服务间通过http请求调用是很常见的,但在开发过程中,选择哪种方式调用,选择什么样的sdk,或者说如何更高效的调用http接口等,很多同学并没有深入的研究过,比如在springboot开发中,比可以选择httpclient进行微服务间的接口调用,也可以选择RestTemplate等等,下面将展开详细的讨论。

二、http调用概述

2.1 什么是http调用

HTTP调用是指客户端(如浏览器、移动应用程序等)使用HTTP协议向服务器发出请求,并且服务器响应这些请求的过程。

在HTTP调用中,客户端发送HTTP请求,服务器接收并处理该请求,最后服务器返回HTTP响应给客户端。这种模式使得客户端能够与服务器进行通信,并获取所需的数据或服务。

2.1.1 http调用步骤

HTTP调用通常涉及以下几个基本步骤:

  • 客户端发起请求:客户端通过HTTP协议构建一个请求消息,包括请求方法(比如GET、POST)、请求头、请求体等信息。然后将请求消息发送给服务器。

  • 服务器处理请求:服务器接收到客户端发送的请求消息后,根据请求消息中的信息进行相应的处理,可能是查询数据库、生成动态内容、执行业务逻辑等。

  • 服务器返回响应:服务器处理完请求后,生成一个包含响应状态码、响应头和响应体的HTTP响应消息,并将其发送回客户端。

  • 客户端接收响应:客户端接收到服务器发送的HTTP响应消息后,根据响应消息中的信息进行相应的处理,可能是解析响应内容、展示数据、执行下一步操作等。

2.2 HTTP调用特点

对于HTTP调用来说,具有如下的特点

  • 简单易用:HTTP协议采用文本格式交换数据,易于人类理解和调试。HTTP调用的请求和响应消息都以明文形式传输,便于开发人员进行调试和排查问题。

  • 跨平台兼容性强:HTTP是一种跨平台的协议,可以在不同操作系统、不同编程语言的环境下使用。这使得HTTP调用能够在各种设备和系统之间进行通信。

  • 状态无关:HTTP协议是一种状态无关的协议,每个请求都是相互独立的,服务器不会保存客户端的状态信息。这样设计简化了服务器的管理,也使得系统更易于扩展和维护。

  • 支持多种请求方法:HTTP支持多种请求方法,如GET、POST、PUT、DELETE等,可以实现不同类型的操作。通过选择合适的请求方法,可以对资源进行查询、创建、更新、删除等操作。

  • 可扩展性强:HTTP协议支持头部字段自定义,允许扩展协议功能。开发人员可以利用HTTP头部字段来传递自定义信息,实现更复杂的交互逻辑。

  • 无连接:HTTP是一种无连接的协议,即每次请求和响应完成后就关闭连接。这样设计降低了服务器的负担,但也导致了每次请求都需要建立新的连接,可能会增加延迟。

2.3 HTTP调用应用场景

HTTP调用在各种应用场景下都有广泛的应用,主要包括以下几个方面:

  • Web开发:在Web开发中,HTTP调用被广泛应用于浏览器与服务器之间的通信。通过HTTP请求和响应,网页可以获取动态内容、发送表单数据、上传文件等操作。

  • API服务:许多应用程序使用HTTP调用来访问第三方API服务,以获取数据或执行特定操作。例如,社交媒体平台提供的API可以通过HTTP调用来实现用户身份验证、发布内容等功能。

  • 微服务架构:在微服务架构中,不同的服务之间通过HTTP调用进行通信,实现服务之间的解耦和协作。每个微服务都可以通过HTTP接口向其他微服务发送请求并获取响应。

  • 移动应用开发:移动应用通常需要与服务器进行数据交换,而HTTP调用是实现移动应用与服务器通信的主要方式。移动应用可以通过HTTP调用获取最新数据、同步用户信息等操作。

  • 云计算:在云计算环境下,不同的服务和资源可能分布在不同的服务器上,通过HTTP调用可以实现这些资源之间的通信和协作。云计算平台也提供了HTTP调用的API接口,供开发者管理和控制云资源。

  • 数据交换:HTTP调用也可以用于数据交换,如传输XML、JSON等格式的数据。应用程序可以通过HTTP调用将数据上传到服务器或从服务器下载数据,实现数据交换和同步。

总的来说,HTTP调用在现代互联网应用开发中扮演着重要的角色,为不同系统、服务和设备之间提供了一种标准化的通信方式,使得它们能够相互协作、交换数据,并提供各种功能和服务。

三、微服务场景下http调用概述

随着微服务开发模式盛行,为了方便开发人员更便捷的进行服务之间接口的调用,微服务框架做了更进一步的封装,比如springcloud系列的feign,即是对http调用的组件封装,而dubbo则是利用其他的协议,总的来说都是为了简化传统的http调用过程的繁琐的代码编写。

3.1 微服务开发中http调用场景

在微服务架构下,HTTP调用被广泛应用于各种场景,具体来说,主要包括以下几个方面:

  • 服务间通信

    • 微服务架构将一个应用程序拆分为多个独立的服务,每个服务都可以通过HTTP调用与其他服务进行通信。服务之间通过HTTP接口发送请求和获取响应,实现了服务之间的解耦和互操作。

  • 负载均衡

    • 由于微服务架构中服务数量较多,需要使用负载均衡器来均衡流量和请求。通过HTTP调用,负载均衡器可以将请求分发给不同的服务实例,以提高系统的性能和可伸缩性。

  • 弹性设计

    • 在微服务架构中,服务可能会出现故障或者需要进行水平扩展。通过HTTP调用,服务可以动态地注册、发现和调用其他服务,从而实现弹性设计,确保系统的可靠性和可用性。

  • 网关路由

    • 微服务架构通常会使用API网关来管理和路由所有的HTTP请求。API网关可以根据请求的路径、参数等信息将请求路由到相应的服务实例,同时还可以进行认证、安全检查等操作。

  • 分布式事务

    • 在微服务架构中,一个业务操作可能涉及多个不同的服务,需要保证这些服务的操作是一致和原子的。通过HTTP调用,可以使用分布式事务协调器来实现跨服务的分布式事务管理。

总结来说,在微服务架构下,HTTP调用作为一种轻量、灵活的通信方式,为不同服务提供了标准化的交互方式,使服务间能够相互通信、协作,实现复杂业务逻辑和功能。HTTP调用在微服务架构中具有重要的作用,已成为许多企业和组织构建分布式系统的首选通信方式。

3.2 微服务组件中http的应用

在微服务架构体系中,各个服务组件的协同,其底层通信机制离不开http的调用,具体来说,在使用HTTP调用微服务组件时,通常会涉及以下几个关键组件和方面:

  1. 服务提供者:服务提供者是暴露API接口的微服务实例,负责提供特定的功能或服务。服务提供者通过HTTP暴露RESTful风格的API接口,使其他服务可以通过HTTP调用来请求其提供的服务。

  2. 服务消费者:服务消费者是需要调用其他服务的微服务实例,它们通过HTTP发起请求,并获取服务提供者返回的响应数据。服务消费者根据需求向服务提供者发起HTTP调用,以获取所需的数据或执行相应的操作。

  3. 服务注册与发现:在微服务架构中,通常会有一个服务注册与发现组件用于管理所有微服务的注册和发现。服务提供者会将自己的信息注册到服务注册中心,而服务消费者可以通过服务发现机制查询并找到需要调用的服务实例。

  4. 负载均衡器:负载均衡器用于平衡不同服务实例之间的负载,确保流量被合理地分发到各个服务实例上。通过负载均衡器,可以提高系统的性能和可伸缩性,避免单个服务实例被过度请求。

  5. 断路器模式:为了防止故障的扩散,微服务架构中的HTTP调用通常会采用断路器模式。当某个服务出现故障或请求超时时,断路器会打开,阻止进一步的调用,并提供降级策略或错误处理机制。

  6. 安全认证与授权:在微服务架构中,由于服务数量众多,安全认证和授权变得尤为重要。通过HTTP调用,可以在请求头中传递认证信息,如JWT令牌、OAuth令牌等,以确保只有授权的服务能够调用受保护的服务。

以上组件和机制共同协作,实现了微服务架构中基于HTTP调用的服务之间的通信和协作。这些组件为微服务架构中的HTTP调用提供了必要的基础设施和支持,确保了系统的稳定性、可靠性和可扩展性。

四、常用的http调用组件

4.1 java中常用的http组件介绍

Java中常用的HTTP调用组件有很多,其中比较常见且使用广泛的包括:

  • Apache HttpClient

    • Apache HttpClient是Apache软件基金会提供的一个开源的HTTP客户端库,它提供了灵活和强大的功能,可以用来发送HTTP请求、处理响应等。

  • OkHttp

    • OkHttp是Square公司开发的一个高性能的HTTP客户端库,它支持HTTP/2 和 SPDY,使用简单且性能优秀。

  • Spring的RestTemplate

    • Spring框架提供了RestTemplate类来简化HTTP访问,它封装了大量的HTTP操作方法,使用方便。

  • HttpURLConnection:

    • Java标准库中自带的HttpURLConnection类也可以用来进行HTTP调用,虽然相对简单,但足以满足一些基本的需求。

4.1.1 HttpClient

HttpClient是Apache软件基金会提供的一个开源的HTTP客户端库,它是一个功能强大且灵活的工具,用于发送HTTP请求、处理响应等操作。HttpClient可以用于构建各种类型的HTTP客户端,从简单的GET请求到复杂的RESTful API调用都可以方便地实现。其主要特点如下:

  • 支持HTTP/1.1和HTTP/2:HttpClient支持HTTP/1.1和HTTP/2协议,可以根据需要选择合适的协议版本进行通信。

  • 灵活的请求和响应处理:HttpClient提供了丰富的API来构建HTTP请求,并能够灵活地处理响应数据,包括文本、二进制数据、JSON等格式。

  • 连接管理和连接池:HttpClient内置了连接管理器和连接池,能够有效地管理HTTP连接,并提供了连接重用、路由优化等功能,提高了性能和效率。

  • 支持HTTPS:HttpClient支持HTTPS协议,可以与安全的SSL/TLS加密连接进行通信,确保数据传输的安全性。

  • 拦截器和过滤器:HttpClient提供了拦截器和过滤器机制,可以在请求发送前或响应返回后添加自定义的逻辑处理,例如日志记录、身份验证等。

  • 配置选项丰富:HttpClient提供了丰富的配置选项,可以进行各种定制化的设置,如超时时间、代理设置、重试策略等。

4.1.2 OkHttp

OkHttp是Square公司开发的一个高性能的HTTP客户端库,用于在Android和Java应用中进行网络通信。它提供了简洁易用的API,支持HTTP/2和SPDY协议,并具有连接池、请求压缩、缓存等优秀的特性。其主要特点如下:

支持HTTP/2和SPDY

OkHttp支持现代的HTTP/2和SPDY协议,可以实现多路复用和头部压缩,提高网络传输效率。

简洁且易用的API

OkHttp提供了清晰简洁的API设计,使用起来非常方便,可以轻松构建各种类型的HTTP请求。

高性能

OkHttp采用了异步和非阻塞的设计,能够处理大量并发请求,并通过连接池和缓存机制提高性能。

自动重试机制

OkHttp内置了自动重试机制,当网络请求失败时可以根据配置自动进行重试,提高请求的成功率。

拦截器

OkHttp提供了拦截器机制,可以在网络请求的不同阶段添加自定义逻辑,如日志记录、身份验证等。

支持WebSocket

除了HTTP通信外,OkHttp还支持WebSocket协议,可以方便地实现实时通信功能。

可扩展性强

OkHttp支持插件式的拓展机制,可以通过插件来增加额外的功能或定制化需求。

总的来说,OkHttp是一个功能强大且性能优异的HTTP客户端库,在Android和Java应用中被广泛使用,对于需要进行网络通信的应用来说是一个很好的选择。

4.1.3 RestTemplate

RestTemplate是Spring框架提供的一个用于简化HTTP访问的工具类,它封装了大量的HTTP操作方法,使得在Java应用中进行RESTful风格的API调用变得更加简单和方便。

  • 支持多种HTTP请求方式

    • RestTemplate支持GET、POST、PUT、DELETE等多种HTTP请求方式,可以满足各种不同类型的API调用需求。

  • 内置消息转换器

    • RestTemplate内置了消息转换器,可以自动将HTTP请求和响应转换成Java对象,简化了数据的处理流程。

  • 支持异步请求

    • RestTemplate提供了异步请求的功能,可以发送异步的HTTP请求并处理返回结果,避免阻塞主线程。

  • 拦截器和过滤器

    • 类似于HttpClient,RestTemplate也支持拦截器和过滤器机制,可以在请求发送前或响应返回后添加自定义的逻辑处理。

  • 集成Spring生态系统

    • 作为Spring框架的一部分,RestTemplate可以与其他Spring组件无缝集成,如Spring Boot、Spring Cloud等,方便进行全栈开发。

  • 简化RESTful API调用

    • 由于RestTemplate封装了大量HTTP操作方法,因此使用它可以简化对RESTful API的调用,减少了代码重复性。

RestTemplate是一个功能丰富且易于使用的HTTP客户端库,在Spring应用中被广泛应用于RESTful API的调用和HTTP通信操作。

4.2 RestClient介绍

RestClient 是一个类似于 RestTemplate 的的同步接口调用工具。相比于 RestTemplate 采用的是 template 设计模式,RestClient 采用了 fluent API 风格,简单灵活,易于阅读和维护。

在Spring Boot 3.2版本中引入了RestClient,这是一个建立在WebClient之上的更高级抽象。RestClient通过提供更直观流畅的API并减少样板文件代码,进一步简化了HTTP请求的生成过程。它保留了WebClient的所有功能,同时提供了一个对开发人员更友好的界面。

五、http组件调用实践

下面通过案例来演示下http常用的几种组件如何使用。

5.1 前置准备

创建两个springboot工程,工程1提供2个用于测试的接口,然后再在工程2中编写接口,使用不同的http组件进行调用。

5.1.1 工程1提供两个测试接口

提供一个GET和POST接口

import com.congge.entity.DbUser;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@RestController
@RequestMapping("/user")
public class DbUserController {

    @PostMapping("/add")
    public Map<String,Object> addUser(@RequestBody DbUser dbUser){
        if(StringUtils.isBlank(dbUser.getUserName())){
            throw new RuntimeException("用户名不能为空");
        }
        DbUser user = new DbUser();
        user.setId(1);
        user.setUserName(dbUser.getUserName());
        user.setAddress("杭州");
        Map<String,Object> resMap = new HashMap<>();
        resMap.put("code",200);
        resMap.put("msg","success");
        resMap.put("data",user);
        return resMap;
    }

    @GetMapping("/detail")
    public DbUser getById(@RequestParam Integer id){
        if(Objects.isNull(id)){
            throw new RuntimeException("id不能为空");
        }
        DbUser user = new DbUser();
        user.setId(1);
        user.setUserName("jerry");
        user.setAddress("杭州");
        return user;
    }

}

测试一下结果

5.2 OkHttp使用

参照如下步骤进行使用

5.2.1 引入OkHttp依赖

版本可以根据自身的需求进行选择

    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.9.0</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.78</version>
        </dependency>

5.2.2 创建OkHttp配置类

类似于其他的一些技术组件,在与springboot整合时,为了更好的控制该bean的使用,提供一个全局的配置bean,可以在bean的设置中优化相关的参数设置

import okhttp3.OkHttpClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OkHttpConfig {

    @Bean
    public OkHttpClient okHttpClient() {
        return new OkHttpClient();
    }
}

5.2.3 OkHttp工具类

提供一个OkHttp的工具类,后续在调用的地方直接注入该工具类即可使用

package com.congge.utils;

import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.Iterator;
import java.util.Map;

@Component
public class OkHttpUtils {

    private static final Logger logger = LoggerFactory.getLogger(OkHttpUtils.class);

    @Autowired
    private OkHttpClient  okHttpClient;

    /**
     * @param url     请求的url
     * @param paramMap 请求的参数,在浏览器?后面的数据,没有可以传null
     * @return
     */
    public  String get(String url, Map<String, Object> paramMap) {
        paramCheck(url, paramMap);

        String resBody = "";
        StringBuffer sb = new StringBuffer(url);
        boolean firstFlag = true;
        Iterator iterator = paramMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry<String, String>) iterator.next();
            if (firstFlag) {
                sb.append("?" + entry.getKey() + "=" + entry.getValue());
                firstFlag = false;
            } else {
                sb.append("&" + entry.getKey() + "=" + entry.getValue());
            }
        }

        Request request = new Request.Builder()
                .url(sb.toString())
                .build();
        Response response = null;
        try {
            response = okHttpClient.newCall(request).execute();
            int status = response.code();
            if(200 !=status){
                throw new RuntimeException("调用失败");
            }
            if (response.isSuccessful()) {
                return response.body().string();
            }
        } catch (Exception e) {
            logger.error("okhttp3 put error >> ex = {}", e.getMessage());
        } finally {
            if (response != null) {
                response.close();
            }
        }
        return resBody;
    }

    private void paramCheck(String url, Map<String, Object> paramMap) {
        if(StringUtils.isEmpty(url)){
            throw new RuntimeException("url不能为空");
        }
        if(null == paramMap || paramMap.isEmpty()){
            throw new RuntimeException("参数不能为空");
        }
    }

    /**
     * post请求
     * @param url
     * @param json
     * @return
     * @throws Exception
     */
    public  String post(String url, String json) throws Exception {
        RequestBody body = RequestBody.create(json, MediaType.get("application/json"));
        Request request = new Request.Builder()
                .url(url)
                .post(body)
                .build();
        Response response = okHttpClient.newCall(request).execute();
        return response.body().string();
    }

    /**
     * post
     *
     * @param url    请求的url
     * @param params post form 提交的参数
     * @return
     */
    public String post(String url, Map<String, Object> params) {

        paramCheck(url, params);
        String responseBody = "";
        FormBody.Builder builder = new FormBody.Builder();
        //添加参数
        if (params != null && params.keySet().size() > 0) {
            for (String key : params.keySet()) {
                builder.add(key, params.get(key).toString());
            }
        }
        Request request = new Request.Builder()
                .url(url)
                .post(builder.build())
                .addHeader("Content-Type", "application/json")
                .build();
        Response response = null;
        try {
            response = okHttpClient.newCall(request).execute();
            int status = response.code();
            if(200 !=status){
                throw new RuntimeException("调用失败");
            }
            if (response.isSuccessful()) {
                return response.body().string();
            }
        } catch (Exception e) {
            logger.error("okhttp3 post error >> ex = {}", e.getMessage());
        } finally {
            if (response != null) {
                response.close();
            }
        }
        return responseBody;
    }

    /**
     * @param url     请求的url
     * @param paramMap 请求的参数,在浏览器?后面的数据,没有可以传null
     * @return
     */
    public String getForHeader(String url, Map<String, Object> paramMap) {
        paramCheck(url,paramMap);
        String responseBody = "";
        StringBuffer sb = new StringBuffer(url);
        boolean firstFlag = true;
        Iterator iterator = paramMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry<String, String>) iterator.next();
            if (firstFlag) {
                sb.append("?" + entry.getKey() + "=" + entry.getValue());
                firstFlag = false;
            } else {
                sb.append("&" + entry.getKey() + "=" + entry.getValue());
            }
        }
        Request request = new Request.Builder()
                .addHeader("key", "value")
                .url(sb.toString())
                .build();
        Response response = null;
        try {
            response = okHttpClient.newCall(request).execute();
            int status = response.code();
            if(200 !=status){
                throw new RuntimeException("调用失败");
            }
            if (response.isSuccessful()) {
                return response.body().string();
            }
        } catch (Exception e) {
            logger.error("okhttp3 put error >> ex = {}", e.getMessage());
        } finally {
            if (response != null) {
                response.close();
            }
        }
        return responseBody;
    }

    /**
     * Post请求发送JSON数据....{"name":"zhangsan","pwd":"123456"}
     * 参数一:请求Url
     * 参数二:请求的JSON
     * 参数三:请求回调
     */
    public String postJsonParams(String url, String jsonParams) {
        String responseBody = "";
        RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), jsonParams);
        Request request = new Request.Builder()
                .url(url)
                .post(requestBody)
                .build();
        Response response = null;
        try {
            response = okHttpClient.newCall(request).execute();
            int status = response.code();
            if(200 !=status){
                throw new RuntimeException("调用失败");
            }
            if (response.isSuccessful()) {
                return response.body().string();
            }
        } catch (Exception e) {
            logger.error("okhttp3 post error >> ex = {}", e.getMessage());
        } finally {
            if (response != null) {
                response.close();
            }
        }
        return responseBody;
    }

    /**
     * Post请求发送xml数据....
     * 参数一:请求Url
     * 参数二:请求的xmlString
     * 参数三:请求回调
     */
    public String postXmlParams(String url, String xml) {
        String responseBody = "";
        RequestBody requestBody = RequestBody.create(MediaType.parse("application/xml; charset=utf-8"), xml);
        Request request = new Request.Builder()
                .url(url)
                .post(requestBody)
                .build();
        Response response = null;
        try {
            response = okHttpClient.newCall(request).execute();
            int status = response.code();
            if(200 !=status){
                throw new RuntimeException("调用失败");
            }
            if (response.isSuccessful()) {
                return response.body().string();
            }
        } catch (Exception e) {
            logger.error("okhttp3 post error >> ex = {}", e.getMessage());
        } finally {
            if (response != null) {
                response.close();
            }
        }
        return responseBody;
    }
}

5.2.4 提供两个测试接口

使用上述的工具类,编写两个测试接口远程调用一下工程1中的两个接口

import com.alibaba.fastjson.JSONObject;
import com.congge.entity.DbUser;
import com.congge.utils.OkHttpUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@Slf4j
public class OkHttpController {

    @Autowired
    private OkHttpUtils okHttpUtils;

    //http://localhost:8088/remote/get
    @GetMapping("/remote/get")
    public Object getRemoteUser(Integer id){
        String url = "http://localhost:8087/user/detail";
        Map<String,Object> paramMap = new HashMap<>();
        paramMap.put("id",id);
        String s = okHttpUtils.get(url, paramMap);
        System.out.println(s);
        DbUser dbUser = JSONObject.parseObject(s, DbUser.class);
        return dbUser;
    }

    //http://localhost:8088/remote/post
    @GetMapping("/remote/post")
    public Object postRemoteUser() throws Exception{
        String url = "http://localhost:8087/user/add";
        Map<String,Object> paramMap = new HashMap<>();
        paramMap.put("userName","jerry");
        paramMap.put("address","杭州");
        String resStr = okHttpUtils.post(url, JSONObject.toJSONString(paramMap));
        System.out.println(resStr);
        return resStr;
    }

}

请求GET接口效果

请求post接口效果

5.2.5 Okhttp使用优化

在本案例中,我们通过配置类提供了Okhttp全局的bean,不难发现,尽管在调用时没有什么区别,但是为了更好的优化Okhttp配置bean的使用,可以在创建OkHttpClient过程中,添加更多的参数从而更好的管理和优化OkHttpClient,类似于我们在使用线程池的时候通常使用自定义的线程池,参考下面的示例;

在配置文件添加如下参数

okhttp:
  proxy:
    enable: true # 启用代理
    host: 127.0.0.1 #代理IP(默认为本机)
    port: 15777 #端口(默认使用本机springboot应用端口)
    connectTimeout: 50 #链接超时时间
    readTimeout: 50 # 读取超时时间
    writeTimeout: 10 # 写入超时时间
    connectionPool: # 连接池配置
      maxConnect: 10 # 连接池的最大空闲连接数
      keepAlive: 20 # 每个连接的最大请求数

OkHttpClient配置类改写如下

@Configuration(value = "okHttpConfig")
public class OkHttpConfig {

    @Value("${okhttp.proxy.enable}")
    private boolean enable;

    @Value("${okhttp.proxy.host}")
    private String proxyHost;

    @Value("${okhttp.proxy.port}")
    private int proxyPort;

    @Value("${okhttp.proxy.connectTimeout}")
    private int connectTimeout;

    @Value("${okhttp.proxy.readTimeout}")
    private int readTimeout;

    @Value("${okhttp.proxy.writeTimeout}")
    private int writeTimeout;

    /**
     * 连接池的最大空闲连接数
     */
    @Value("${okhttp.proxy.connectionPool.maxConnect}")
    private int maxConnect;

    /**
     * 每个连接的最大请求数
     */
    @Value("${okhttp.proxy.connectionPool.keepAlive}")
    private int keepAlive;

    @Bean
    public OkHttpClient okHttpClient() {
        OkHttpClient.Builder builder = new OkHttpClient.Builder()
                .callTimeout(connectTimeout, TimeUnit.SECONDS)
                .connectTimeout(connectTimeout, TimeUnit.SECONDS) // 连接超时时间
                .readTimeout(readTimeout, TimeUnit.SECONDS) 		// 读取超时时间
                .writeTimeout(writeTimeout, TimeUnit.SECONDS) 		// 写入超时时间
                .retryOnConnectionFailure(false)			 // 是否自动重试连接失败的请求
                .connectionPool(new ConnectionPool(maxConnect, keepAlive, TimeUnit.MINUTES)); 	// 设置连接池;
        if (enable) {
            builder.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)));
        }
        return builder.build();
    }
}

5.3 HttpClient使用

HttpClient在日常的项目开发中也是高频使用的http客户端调用组件,参考下面的步骤进行使用

5.3.1 导入httpclient依赖

版本可以自行选择,与springboot版本匹配即可

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.6</version>
        </dependency>

5.3.2 添加httpclient工具类

工具类中分别提供了一个GET和POST请求的工具方法

package com.congge.utils;

import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;

public class HttpClientUtil {

    /**
     * get请求
     * @param url
     * @param params
     * @return
     */
    public static String get(String url, Map<String,Object> params) {
        CloseableHttpClient httpClient = HttpClientBuilder.create().build();
        String sendUrl = url;
        // 拼接参数
        if (Objects.nonNull(params) && params.size() > 0) {
            sendUrl = connectParams(url, params);
        }
        HttpGet httpGet = new HttpGet(sendUrl);
        CloseableHttpResponse response = null;
        try {
            response = httpClient.execute(httpGet);
            HttpEntity httpEntity = response.getEntity();
            if (HttpStatus.SC_OK == response.getStatusLine().getStatusCode() && null != httpEntity) {
                return EntityUtils.toString(httpEntity);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                close(httpClient, response);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        throw new RuntimeException("调用GET请求失败!");
    }

    /**
     * post请求
     * @param url
     * @param params
     * @param paramBody
     * @return
     */
    public static String post(String url, Map<String,Object> params, Map<String,Object> paramBody) {
        CloseableHttpClient httpClient = HttpClientBuilder.create().build();
        String sendUrl = url;
        // 1.拼接参数
        if (Objects.nonNull(params) && params.size() > 0) {
            sendUrl = connectParams(url, params);
        }
        HttpPost httpPost = new HttpPost(sendUrl);
        httpPost.setHeader("Content-Type", "application/json;charset=utf8");

        //2.设置request-body
        String jsonBody = JSONObject.toJSONString(paramBody);
        StringEntity stringEntity = new StringEntity(jsonBody, "UTF-8");
        stringEntity.setContentEncoding("UTF-8");
        stringEntity.setContentType("application/json");
        httpPost.setEntity(stringEntity);

        CloseableHttpResponse response = null;
        try {

            // 2.设置request-body
//            ByteArrayEntity entity = new ByteArrayEntity(requestBody.getBytes(StandardCharsets.UTF_8));
//            entity.setContentType("application/json");
//            httpPost.setEntity(entity);

            response = httpClient.execute(httpPost);
            HttpEntity httpEntity = response.getEntity();
            if (HttpStatus.SC_OK == response.getStatusLine().getStatusCode() && null != httpEntity) {
                return EntityUtils.toString(httpEntity);
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                close(httpClient, response);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        throw new RuntimeException("调用POST请求失败!");
    }

    private static String connectParams(String url, Map<String,Object> params) {
        StringBuffer buffer = new StringBuffer();
        buffer.append(url).append("?");
        params.forEach((x, y) -> buffer.append(x).append("=").append(y).append("&"));
        buffer.deleteCharAt(buffer.length() - 1);
        return buffer.toString();
    }

    public static void close(CloseableHttpClient httpClient, CloseableHttpResponse httpResponse) throws IOException{
        if (null != httpClient) {
            httpClient.close();
        }
        if (null != httpResponse) {
            httpResponse.close();
        }
    }
}

5.3.3 添加httpclient测试接口

添加两个用于测试的接口

@RestController
@Slf4j
public class HttpClientController {

    private static HttpClientUtil httpClientUtil = new HttpClientUtil();

    //http://localhost:8088/http-client/get?id=1
    @GetMapping("/http-client/get")
    public Object getRemoteUserGet(Integer id){
        String url = "http://localhost:8087/user/detail";
        Map<String,Object> paramMap = new HashMap<>();
        paramMap.put("id",id);
        String s = httpClientUtil.get(url, paramMap);
        System.out.println(s);
        DbUser dbUser = JSONObject.parseObject(s, DbUser.class);
        return dbUser;
    }

    //http://localhost:8088/http-client/post
    @GetMapping("/http-client/post")
    public Object getRemoteUserPost() throws Exception{
        String url = "http://localhost:8087/user/add";
        Map<String,Object> paramMap = new HashMap<>();
        paramMap.put("userName","jerry");
        paramMap.put("address","杭州");
        String resStr = httpClientUtil.post(url,null,paramMap);
        System.out.println(resStr);
        return resStr;
    }

}

5.3.4 测试调用效果

get请求

post请求

5.3.5 HttpClient配置优化

为了更好的管理HttpClient的连接配置,比如连接参数相关的设置,我们也可以使用配置类的方式对其进行优化,如下在配置文件中增加如下参数;

#最大连接数
http:
  maxTotal: 100
  #并发数
  defaultMaxPerRoute: 20
  #创建连接的最长时间
  connectTimeout: 2000
  #从连接池中获取到连接的最长时间
  connectionRequestTimeout: 5000
  #数据传输的最长时间
  socketTimeout: 10000
  #提交请求前测试连接是否可用
  staleConnectionCheckEnabled: true

增加一个HttpClientConfig配置类

package package com.congge.config;
 
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class HttpClientConfig {

    //最大连接数
    @Value("${http.maxTotal}")
    private Integer maxTotal;
 
    //并发数
    @Value("${http.defaultMaxPerRoute}")
    private Integer defaultMaxPerRoute;
 
    //创建连接的最长时间
    @Value("${http.connectTimeout}")
    private Integer connectTimeout;
 
    //从连接池中获取到连接的最长时间
    @Value("${http.connectionRequestTimeout}")
    private Integer connectionRequestTimeout;
 
    //数据传输的最长时间
    @Value("${http.socketTimeout}")
    private Integer socketTimeout;
 
    //提交请求前测试连接是否可用
    @Value("${http.staleConnectionCheckEnabled}")
    private boolean staleConnectionCheckEnabled;
 
    /**
     * 实例化一个连接池管理器,设置最大连接数、并发连接数
     *
     * @return httpClientConnectionManager
     */
    @Bean(name = "httpClientConnectionManager")
    public PoolingHttpClientConnectionManager getHttpClientConnectionManager() {
        PoolingHttpClientConnectionManager httpClientConnectionManager = new PoolingHttpClientConnectionManager();
        //设置最大连接数
        httpClientConnectionManager.setMaxTotal(maxTotal);
        //设置并发数
        httpClientConnectionManager.setDefaultMaxPerRoute(defaultMaxPerRoute);
        return httpClientConnectionManager;
    }
 
    /**
     * 实例化连接池,设置连接池管理器
     * 这里需要以参数形式注入上面实例化的连接池管理器
     *
     * @param httpClientConnectionManager 连接池管理器
     * @return httpClientBuilder 连接池
     */
    @Bean(name = "httpClientBuilder")
    public HttpClientBuilder getHttpClientBuilder(@Qualifier("httpClientConnectionManager") PoolingHttpClientConnectionManager httpClientConnectionManager) {
        //HttpClientBuilder中的构造方法被protected修饰,所以这里不能直接使用new来实例化一个HttpClientBuilder,可以使用HttpClientBuilder提供的静态方法create()来获取HttpClientBuilder对象
        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        httpClientBuilder.setConnectionManager(httpClientConnectionManager);
        return httpClientBuilder;
    }
 
    /**
     * 注入连接池,用于获取httpClient
     *
     * @param httpClientBuilder 连接池
     * @return 获取httpclient
     */
    @Bean
    public CloseableHttpClient getCloseableHttpClient(@Qualifier("httpClientBuilder") HttpClientBuilder httpClientBuilder) {
        //        try {
//            //HttpClient设置忽略SSL,实现HTTPS访问, 解决Certificates does not conform to algorithm constraints
//            //http://www.manongjc.com/detail/13-cpmpdgbiafnliyo.html
//            SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
//                @Override
//                public boolean isTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
//                    return true;
//                }
//            }).build();
//            SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
//            return HttpClients.custom().setSSLSocketFactory(sslConnectionSocketFactory).build();
//        } catch (Exception e) {
//            e.printStackTrace();
//        }
//        return null;
        return httpClientBuilder.build();
    }
 
 
    /**
     * Builder是RequestConfig的一个内部类
     * 通过RequestConfig的custom方法来获取到一个Builder对象
     * 设置builder的连接信息
     * 这里还可以设置proxy,cookieSpec等属性,有需要的话可以在此设置
     *
     * @return builder
     */
    @Bean(name = "builder")
    public RequestConfig.Builder getBuilder() {
        RequestConfig.Builder builder = RequestConfig.custom();
        //将属性 set进 builder 中
        builder.setConnectTimeout(connectTimeout)
                .setConnectionRequestTimeout(connectionRequestTimeout)
                .setSocketTimeout(socketTimeout)
                .setStaleConnectionCheckEnabled(staleConnectionCheckEnabled);
        return builder;
    }
 
    /**
     * 使用builder构建一个RequestConfig对象
     *
     * @param builder 连接信息
     * @return builder.build()
     */
    @Bean
    public RequestConfig getRequestConfig(@Qualifier("builder") RequestConfig.Builder builder) {
        return builder.build();
    }
 
}

5.4 RestTemplate使用

RestTemplate是Spring框架中的一个类,用于RESTful风格的HTTP请求调用。

  • 它提供了简单方便的方式来处理HTTP请求和响应,可以发送GET、POST、PUT、DELETE等类型的请求,并处理响应数据。

  • RestTemplate封装了常见的HTTP操作,使得开发者能够更加方便地与RESTful API进行交互。

  • 使用RestTemplate时,开发者可以通过调用不同的方法来发送HTTP请求,并根据需要设置请求头、请求参数、请求体等信息。同时,RestTemplate还支持将响应数据转换为Java对象或其他数据结构,简化了数据处理过程。

下面介绍RestTemplate的使用过程。

5.4.1 添加依赖

如果是springboot工程,默认情况下只要你添加了web依赖,就可以使用RestTemplate。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

5.4.2 添加初始化配置

也可以不配,有默认的设置

注意RestTemplate只有初始化配置,没有什么连接池

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
        return new RestTemplate(factory);
    }

    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
        //默认的是JDK提供http连接,需要的话可以//通过setRequestFactory方法替换为例如Apache HttpComponents、Netty或//OkHttp等其它HTTP library。
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        //单位为ms
        factory.setReadTimeout(5000);
        //单位为ms
        factory.setConnectTimeout(5000);
        return factory;
    }
}

5.4.3 测试GET接口

使用restTemplate进行调用很灵活,API有多种形式,下面依次列举;

方式1:restTemplate.getForObject

使用这种方式,可以直接指定返回值类型,就不要对请求响应的结果单独处理了

    @Autowired
    private RestTemplate restTemplate;

    //http://localhost:8088/rest/getForObject?id=1
    @GetMapping("/rest/getForObject")
    public Object restGetRemote(Integer id){
        String url = "http://localhost:8087/user/detail?id="+id;
        DbUser dbUser = restTemplate.getForObject(url, DbUser.class);
        return dbUser;
    }

方式2:restTemplate.getForEntity

这种方式返回结果外面包装了一层ResponseEntity

    //http://localhost:8088/rest/getForEntity?id=1
    @GetMapping("/rest/getForEntity")
    public Object getForEntity(Integer id){
        String url = "http://localhost:8087/user/detail?id="+id;
        ResponseEntity<DbUser> responseEntity = restTemplate.getForEntity(url, DbUser.class);
        DbUser dbUser = responseEntity.getBody();
        return dbUser;
    }

方式3:restTemplate.exchange

这种方式比较灵活,即可以执行GET请求,POST请求,也可以自定义请求参数

写法1:

    //http://localhost:8088/rest/getExchange?id=1
    @GetMapping("/rest/getExchange")
    public Object getExchange(Integer id){
        String url = "http://localhost:8087/user/detail?id="+id;
        DbUser dbUser = restTemplate.exchange(url, HttpMethod.GET, null, DbUser.class).getBody();
        return dbUser;
    }

写法2:

通过ParameterizedTypeReference指定返回值类型

    //http://localhost:8088/rest/getExchange2?id=1
    @GetMapping("/rest/getExchange2")
    public Object getExchange2(Integer id){
        String url = "http://localhost:8087/user/detail?id="+id;
        //ParameterizedTypeReference<Map<String, Object>> responseBodyType = new ParameterizedTypeReference<Map<String, Object>>() {};
        ParameterizedTypeReference<DbUser> responseBodyType = new ParameterizedTypeReference<DbUser>() {};
        DbUser dbUser = restTemplate.exchange(url, HttpMethod.GET, null, responseBodyType).getBody();
        return dbUser;
    }

5.4.4 测试POST接口

方式1:

使用 restTemplate.postForObject

    //http://localhost:8088/rest/testPost1
    @GetMapping("/rest/testPost1")
    public Object getRemoteUserPost() throws Exception{
        String url = "http://localhost:8087/user/add";
        HttpHeaders headers = new HttpHeaders();
        MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
        headers.setContentType(type);
        headers.add("Accept", MediaType.APPLICATION_JSON.toString());

        Map<String,Object> paramMap = new HashMap<>();
        paramMap.put("userName","jerry");
        paramMap.put("address","杭州");
        String requestJson = JSONObject.toJSONString(paramMap);
        HttpEntity<String> entity = new HttpEntity<String>(requestJson,headers);
        String result = restTemplate.postForObject(url, entity, String.class);
        System.out.println(result);
        return result;
    }

在这个过程中,如果响应中出现中文乱码,需要在配置类中添加一段配置:

	@Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
        RestTemplate restTemplate = new RestTemplate(factory);
        restTemplate.getMessageConverters().set(1,new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return restTemplate;
    }

方式2:

使用 restTemplate.postForEntity

@GetMapping("/rest/testPost2")
    public Object getRemoteUserPost2() throws Exception {
        String url = "http://localhost:8087/user/add";
//        Map<String, Object> requestMap = new HashMap<>();
//        requestMap.put("userName", "jerry");
//        requestMap.put("address", "杭州");
//        ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, requestMap , String.class);

        DbUser dbUser = new DbUser();
        dbUser.setAddress("杭州");
        dbUser.setUserName("jerry");
        HttpEntity<DbUser> request = new HttpEntity<>(dbUser);
        ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, request, String.class);

        return responseEntity.getBody();
    }

方式3:

使用 restTemplate.exchange

    @GetMapping("/rest/testPost3")
    public Object getRemoteUserPost3() throws Exception {
        String url = "http://localhost:8087/user/add";
        // 创建RestTemplate实例
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        DbUser dbUser = new DbUser();
        dbUser.setAddress("杭州");
        dbUser.setUserName("jerry");
        String requestBody =JSONObject.toJSONString(dbUser);
        HttpEntity<String> entity = new HttpEntity<>(requestBody, headers);
        ResponseEntity<String> response = restTemplate.exchange(
                url,
                org.springframework.http.HttpMethod.POST,
                entity,
                String.class
        );
        // 输出响应体
        System.out.println(response.getBody());
        return response.getBody();
    }

5.5 RestClient使用

RestClient是在springboot3.X之后新增的一种http调用,可以说集成了上述所有http组件的优点,因此在使用之前,需要做两个准备:

  • 升级springboot版本;
  • 升级jdk为java17;

升级springboot

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version> <!-- 使用 Milestone 版本进行测试 -->
        <relativePath/>
    </parent>

测试接口

    RestClient restClient = RestClient.create();

    @GetMapping("/rest/client")
    public Object getFromRestClient(Integer id) {
        String url = "http://localhost:8087/user/detail?id=" + id;
        String body = restClient
                .get()
                .uri(url)
                .retrieve()
                .body(String.class);
        return body;
    }

六、写在文末

本文通过较大的篇幅详细介绍了java中常用的http组件的使用,http的调用可以说贯穿到日常开发中放方面面,因此熟练掌握http的组件非常重要,希望本篇对看到的同学有用,本篇到此结束,感谢观看。

版权声明:本文为博主作者:小码农叔叔原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/zhangcongyi420/article/details/138253041

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2024年5月6日
下一篇 2024年5月6日

相关推荐