Springboot中发送Http请求和处理响应

1,051 阅读11分钟

前言

在Springboot中,我们需要给第三方服务需要发送请求,并接受处理请求的处理结果。这时候我们就需要自带的请求工具,或者依赖第三方的服务来实现请求的发送和处理。

Spring内置的使用

Http请求信息常量

Spring框架中已经有很多有关Http请求和响应的相关信息的封装常量信息。

HttpMethod : 定义了常见的 HTTP 请求方法。包括GETPOSTPUTDELETEHEADOPTIONSTRACE

HttpStatus : 定义了 HTTP 响应状态码及其相关的描述信息 , 例如 OK(200) , NOT_FOUND(404), INTERNAL_SERVER_ERROR(500)

MediaType : : 定义了HTTP常用的ContentType的信息, 如 application/json , application/x-www-form-urlencoded 等信息

HttpHeaders : 定义了HTTP常用的请求头的信息方法,如可以设置一些常用的请求头 setContentType

Http相关的工具类

Spring框架内置了好用的工具类

UriComponentsBuilder : 用于构建组装操作的请求路径及其参数的工具类。

注意: 会自动对路径和查询参数进行编码,确保特殊字符(如空格、中文等)在 URL 中能够正确表示

// fromUriString : 完整的路径字符串
UriComponentsBuilder.fromUriString(uriString).build().toUriString()
// queryParam : 增加参数
UriComponentsBuilder.fromUriString(uriString).queryParam("param","123").build().toUriString()

MultiValueMap : 用于构建multipart/form-data请求的参数,用于传递文件和参数。

MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
// 增加文件的参数
ByteArrayResource fileResource = new ByteArrayResource(file.getBytes()) {
    @Override
    public String getFilename() {
        return file.getOriginalFilename(); // 确保在 Content-Disposition 中包含文件名
    }
};
formData.add("file", fileResource);

// 增加普通参数
formData.add("param", "paramResult");

RestTemplate 发送请求

RestTemplate是 Spring 框架提供的用于访问 RESTful 服务的客户端工具。它简化了在 Java 应用中发送 HTTP 请求和处理响应的过程。

在构建发送请求的过程中,会使用 HttpEntity,它封装了请求的头信息和请求体等所有元素 。 在接受请求的时候,会使用ResponseEntity ,它封装包含了响应状态码、响应头和响应体等信息。

发送简单的Get请求

  • getForEntity : 获取ResponseEntity格式数据
  • getForObject : 直接获取响应的body数据
// 使用getForEntity方法
ResponseEntity<String> forEntity = restTemplate
                        .getForEntity("http://localhost:8080/hello", String.class);
// 使用getForObject方法
String forObject = resTemplate.getForObject("http://localhost:8080/hello", String.class);

如果请求需要携带对应的参数, url则需要提供对应参数值的占位符,或者使用UriComponentsBuilder来统一构建路径和参数

// 使用占位符参数赋值
ResponseEntity<String> forEntity = restTemplate
                .getForEntity("http://localhost:8080/hello?param={param}", String.class,"123");

// 使用UriComponentsBuilder来统一构建
String url = UriComponentsBuilder.fromUriString("http://localhost:8080/hello")
                .queryParam("param", "123").build().toUriString();
ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class,"123");

简单的发送Post请求

  • postForObject : 直接获取响应的body数据
  • postForEntity : 获取ResponseEntity格式数据
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);

// 构建请求参数
TestParam testParam = new Testparam();
testParam.setName("gjc");

// 封装到请求HttpEntity
HttpEntity<User> requestEntity = new HttpEntity<>(testParam, headers);

// 发送请求
String response = restTemplate.postForObject(url, requestEntity, String.class);

使用formData格式发送文件

使用MultiValueMap来构造参数

注意: 我这边的file是使用的controller层接受到的 MultipartFile类型的文件

// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// 使用MultiValueMap构造formdata的参数
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
ByteArrayResource fileResource = new ByteArrayResource(file.getBytes()) {
    @Override
    public String getFilename() {
        return file.getOriginalFilename(); // 确保在 Content-Disposition 中包含文件名
    }
};
formData.add("file", fileResource);
// 发送请求
HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(formData, headers);
ResponseEntity<String> stringResponseEntity = restTemplate.postForEntity(url, httpEntity, String.class);

自定义使用exchange方法

使用exchange提供了更强大的灵活性来发送 HTTP 请求并处理响应。不过也是同样使用HttpHeaders构建请求头,使用HttpEntity组装构建发送的相关信息, exchange方法提供组装url,请求方法,请求体内容等内容。

String url = "http://example.com/api/create";
// 创建请求实体
TestParam testParam = new Testparam();
testParam.setName("gjc");
// 请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<MyRequestObject> requestEntity = new HttpEntity<>(testParam, headers);
// 可以额外的指定请求方法
ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);

配置超时等综合信息

通过RestTemplatesetRequestFactory方法设置请求超时等参数,手动构建SimpleClientHttpRequestFactory来设置连接超时和读取超时时间

SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
// 设置连接超时时间为3秒(单位:毫秒)
requestFactory.setConnectTimeout(3000);
// 设置读取超时时间为5秒(单位:毫秒)
requestFactory.setReadTimeout(5000);

RestTemplate restTemplate = new RestTemplate(requestFactory);

RestTemplate 默认使用的是 HttpsURLConnection, 如果配置更换RequestFactory为HttpComponentsClientHttpRequestFactory 则可以使用Apache HttpComponents这个Http请求客户端库。

针对Https请求: 默认使用的是java 的标准安全机制来建立 SSL/TLS 连接。 如果要修改取消验证,就需要更换HttpClient的 RequestFactory进行配置。

WebClient的发送

Spring 5 引入了新的 WebClientAPI,取代了现有的 RestTemplate 客户端。ResTemplate是基于传统的同步阻塞式 I/O 模型。当使用RestTemplate发送请求时,线程会被阻塞,直到服务器返回响应。而WebClient是反应式编程模型,基于 Reactor 库构建,是非阻塞式的。

综合来讲,WebClient就是为了异步和反应式编程而设计的。

基本

  • retrieve : 用于指定如何提取响应
    • bodyToMono : 表示将响应体转换为一个Mono<>对象
    • bodyToFlux : 将响应结果处理为 Flux 对象
  • subscribe : 订阅这个对象,异步获取信息
  • block : 同步获取这个信息
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

发送Get请求

使用我们前面提到的UriComponentsBuilder来定义路径,能更方便的拼接路径。然后使用构建webClient使用get方法请求数据。

UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("/hello")
        .queryParam("param", "test");
WebClient webClient = WebClient.create("http://localhost:8080");
// 发送get请求,获取responseMono对象来获取异步响应结果
Mono<String> responseMono = webClient.get()
        .uri(builder.toUriString())
        .retrieve()
        .bodyToMono(String.class);
responseMono.subscribe(System.out::println);

发送Post请求

  1. 针对application/json 或者 Text_Plian 等body数据时

使用 BodyInserters.fromValue : 直接传输json字符串数据来填充body数据

Mono<String> responseMono = webClient.post()
        ....省略...
        .contentType(MediaType.APPLICATION_JSON)
        .body(BodyInserters.fromValue(JSONObject.toJsonString(user)))....
  1. 针对表单数据 applicaiton/x-www-form-urlencoded

BodyInserters.fromFormData方法提供了一种方便的方式来构建和插入表单数据。可以通过链式调用with方法来添加多个键值对

Mono<String> responseMono = webClient.post()
        ....省略...
        .contentType(MediaType.APPLICATION_FORM_URLENCODED)
        .body(BodyInserters.fromFormData("param1", "value1")
                      .with("param2", "value2"))
  1. 针对文件等表单数据 multipart/form-data

BodyInserters.fromMultipartData 方法将MultiValueMap插入到body中。构造数据的方法类似于RestTemplate构造form-data请求内容数据。

// 创建请求内容的参数
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
ByteArrayResource fileResource = new ByteArrayResource(file.getBytes()) {
    @Override
    public String getFilename() {
        return file.getOriginalFilename(); // 确保在 Content-Disposition 中包含文件名
    }
};
formData.add("file", fileResource);

Mono<String> responseMono = webClient.post()
        ....省略...
        .contentType(MediaType.MULTIPART_FORM_DATA)
        .body(BodyInserters.fromMultipartData(formData))

OpenFeign

Feign 最早是由 Netflix 公司进行维护的,后来 Netflix 不再对其进行维护,最终 Feign 由社区进行维护,更名为 Openfeign , 且在Feign的基础上支持了SpringMVC的注解。

前置配置

在 Spring Boot 的主应用类上添加@EnableFeignClients注解,这样 Spring 会扫描并创建 Feign 客户端接口的代理实现,用于发送请求。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<!-- 配合使用 -->
<dependencyManagement>
    <dependencies>
        <!-- SpringCloud netflix微服务 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2023.0.3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

注意:上面SpringCloud的版本与Springboot的对应 ,我当前springboot的版本是3.3.4,所以选择对应的2023.0.3版本。

如果不确定对应的版本可以去官网查看spring.io/projects/sp… 或者start.spring.io/actuator/in… 都可以找到对应的版本

发送请求

具体使用和Spring MVC类似,对@GetMapping,@PostMapping等注解的使用

声明@FeignClient的name名称和url地址,指定访问请求的基础配置。最后再具体使用的位置上注入该bean,调用指定的方法,就能发送对应的请求信息。

@FeignClient(name = "user-service", url = "http://localhost:8080")
public interface UserServiceClient {

    // get请求
    @GetMapping("/get")
    String get(@RequestParam("param") String param);
    
    // post请求体的json内容
    @PostMapping("/post")
    String post(@RequestBody User user);
    
    // 请求文件
    @PostMapping(value = "/upload")
    String uploadFile(@RequestPart("file") MultipartFile file);
    
    // 获取请求响应和结果
    @GetMapping(value = "/responseFile")
    feign.Response responseFile();
}

设置请求超时时间

通过设置配置文件超时的时间,可以指定全局或者局部。

spring:
  cloud:
    openfeign:
      client:
        config:
          // default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
         default:
           connectTimeout: 5000
           readTimeout: 5000
         // 为serviceC这个服务单独配置超时时间,  单个配置的超时时间将会覆盖全局配置。
         serviceC:
           connectTimeout: 30000
           readTimeout: 30000

OkHttp3

OkHttp3 是一个用于 Android 和 Java 应用程序的开源 HTTP 客户端库。它由 Square 公司开发,旨在使 HTTP 通信变得简单、高效且功能丰富。在现代软件开发中,无论是移动应用还是后端服务,与网络资源进行交互是必不可少的,OkHttp3 提供了一种强大的方式来处理 HTTP 请求和响应。

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

基本使用

构建OkHttpClient,设置请求信息Request,发送请求newCall调用方法

// 构建 
OkHttpClient client = new OkHttpClient();
// 请求信息
Request request = new Request.Builder()
              .url("http://www.baidu.com")//请求接口。
              .build();//创建Request 对象
// 发送请求获取响应
Response response = client.newCall(request).execute();

注意: reponse.body().string() ,获取流的读操作, 所以只能执行一次

Basic Authentication

通过OkHttpClient.Buider 构建 authenticator , 往请求头上塞Authorization , 有关credential 可以通过 Credentials.basic方法来构建

client = new OkHttpClient.Builder()
  .authenticator(new Authenticator() {
      @Override
      public Request authenticate(Route route, Response response) {
          String credential = Credentials.basic("username", "password");
          return response.request().newBuilder()
                  .header("Authorization", credential)
                  .build();
      }
  })
  .build();

同步/异步请求

execute() 方法直接执行就是同步请求,enqueue() 方法执行接受回调信息

OkHttpClient okHttpClient = new OkHttpClient();
final Request request = new Request.Builder()
        .url(url)
        .get()//默认就是GET请求,可以不写
        .build();

// 同步请求
Response response = okHttpClient.newCall(request).execute();
String string = response.body().string();


// 异步请求
okHttpClient.newCall(request).enqueue(new Callback((){
    // 处理失败的场景
    @Override
    public void onFailure(Call call, IOException e) {
        e.printStackTrace();
    }

    // 处理成功的场景
    @Override
    public void onResponse(Call call, Response response) throws IOException {
        log.info(TAG, "okHttpGet enqueue: onResponse:"+ response.body().string());

        ResponseBody body = response.body();
        String string = body.string();
        byte[] bytes = body.bytes();
        InputStream inputStream = body.byteStream();
    }

})

post json请求

使用 RequestBody 来创建参数

// 构造contentType 和 json的body
MediaType contentType = MediaType.parse("application/json; charset=utf-8");
String json = "jsonString";
RequestBody body = RequestBody.create(contentType, json );

post form请求

使用 muletipartBody.Builder 来构建参数,可以传递普通字符串参数,也可以传递文件内容。 主要用于构建multipart/form - data类型的请求体

MultipartBody.Builder muletipartBodyBuilder = new MultipartBody.Builder()
                      .setType(MultipartBody.FORM);
// 1. 添加key/value
muletipartBodyBuilder.addFormDataPart(key, value);

//文件 MultipartFile类型的fileData
byte[] data = fileData.getBytes();
String name = fileData.getOriginalFilename();
String contentType = fileData.getContentType();

// 2. 添加key/name/文件内容
RequestBody fileBody = RequestBody.create(data, MediaType.parse(contentType));
muletipartBodyBuilder.addFormDataPart(key, name, fileBody);

post wwwform 请求

FormBody.Builder用于构建application/x - www - form - urlencoded类型的请求体。这种类型的请求体主要用于发送简单的表单数据,即键值对形式的数据,其中值是简单的文本内容,没有文件等复杂的数据类型

FormBody.Builder formBuilder = new FormBody.Builder();
formBuilder.addEncoded(key, value);  // 指定默认URL编码的value值
formBuilder.add(key, value); // 如果value前面已经被编码过了或者value是json数据,就直接add

获取响应体数据

可以通过Responsebody内容中获取bytes/string内容或者byteStream

Response response = okHttpClient.newCall(request).execute()
response.body().byteStream() // 获取字节流,需要手动close关闭流数据(流式)
response.body().source() // 流式处理器,需要手动关闭流数据,高效一块一块的读取
    // 配合exhausted判断和read/readUtf8Line读取实现


byte[] res = response.body.bytes(); //获取字节数组内容, 同时触发关闭响应体
String res = reponse.body().string() //获取字符串内容,同时触发关闭响应体

注意:数据只能获取读取一次,不能重复读取

流式的参考案例:获取流式处理器,来实现处理分块读取,

try (BufferedSource source = response.body().source()) {
    Buffer buffer = new Buffer();
    while (!source.exhausted()) {
        // 第一种:每次读取 8KB 数据
        long bytesRead = source.read(buffer, 8192);
        if (bytesRead == -1) break;
        // 第二种:按行读取
        // String line = source.readUtf8Line(); // 按行读取
        // if (line == null) break; // 数据流结束
        // 处理读取的数据
        System.out.println("Read " + bytesRead + " bytes");
    }
}

忽略SSL证书

X509TrustManager用于管理和验证服务器证书,HostnameVerifier用于验证服务器的主机名是否与证书中的主机名匹配。

所以我们可以配置一个不验证证书的TrustManager,空实现方法内容。和配置一个HostnameVerifier的verify返回true的方法

public X509TrustManager x509TrustManager() {
    // 配置一个不验证证书的TrustManager
    return new X509TrustManager() {
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) {
        }
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) {
        }
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
    };
}


public SSLSocketFactory sslSocketFactory() {
    try {
        // 信任任何链接
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, new TrustManager[]{x509TrustManager()}, new SecureRandom());
        return sslContext.getSocketFactory();
    } catch (NoSuchAlgorithmException | KeyManagementException e) {
        e.printStackTrace();
    }
    return null;
}

public static HostnameVerifier getUnsafeHostnameVerifier() {
    // 返回 true ,不验证主机名与证书是否匹配
    return (hostname, session) -> true;
}

构建请求头

使用Request.Builder的方法来构建请求头

Request.Builder requestBuilder = builder.url(urlBuilder.build()).post(requestBody);
  if (header != null) {
      for (String key : header.keySet()) {
          requestBuilder.addHeader(key, header.get(key));
      }
  }

配置连接池

配置连接数目和连接空闲时间

public ConnectionPool pool() {
    // 最大空闲连接数 200个
    int maxIdleConnections = 200;
    // 连接空闲时间最多为300秒
    long keepAliveDuration = 300L;
    return new ConnectionPool(maxIdleConnections, keepAliveDuration, TimeUnit.SECONDS);
}

综合配置应用上Client

对OkHttpClient的综合配置,例如忽略SSL证书,连接池,拦截器,代理缓存等。

public OkHttpClient okHttpClient() {
    return new OkHttpClient.Builder()
            // 配置忽略SSL证书
            .sslSocketFactory(sslSocketFactory(), x509TrustManager())
            .hostnameVerifier(getUnsafeHostnameVerifier())
            // 是否开启缓存
            .retryOnConnectionFailure(false)
            // 连接池
            .connectionPool(pool())
            .connectTimeout(connectTimeout, TimeUnit.SECONDS)
            .readTimeout(readTimeout, TimeUnit.SECONDS)
            .writeTimeout(writeTimeout,TimeUnit.SECONDS)
            // 设置代理
            // .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 8888)))
            // 拦截器
            // .addInterceptor()
            .build();
}

总结

RestTemplate

优点:简单易用,集成Spring生态系统。 缺点:不支持异步,需要自定义配合Future实现。性能高并发场景下比较局限。没有响应式的支持

WebClient

优点: 支持响应式编程,适合高并发。灵活的配置 缺点: 学习成本高,需要对响应式编程的理解比较深刻。生态集成传统框架不友好,可能存在兼容问题。

OpenFeign

优点: 接口式的编程,熟悉springmvc的很容易上手,集成SpringCloud生态,配置服务发现和负载均衡方便。 缺点: 因代理机制性能要求比较高。需要依赖SpringCloud环境,配置相对有点麻烦。

OkHttpClient :

优点: 高性能高并发,连接池复用连接。支持请求的请求形式丰富。 缺点: 与Spring生态集成不够前面的那些好。缺少响应式编程支持。配置相对也有点复杂。