重学SpringBoot3-WebClient配置与使用详解

CoderJia 77 2024-12-04

1. 简介

WebClient是Spring 5引入的响应式Web客户端,用于执行HTTP请求。相比传统的RestTemplate,WebClient提供了非阻塞、响应式的方式来处理HTTP请求,是Spring推荐的新一代HTTP客户端工具。本文将详细介绍如何在SpringBoot 3.x中配置和使用WebClient。

2. 环境准备

2.1 依赖配置

pom.xml中添加必要的依赖:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.10</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

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

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

3. WebClient配置

3.1 基础配置

@Configuration
public class WebClientConfig {
    
    @Bean
    public WebClient webClient() {
        return WebClient.builder()
                .baseUrl("https://echo.apifox.com")
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
                .build();
    }
}

3.2 高级配置

package com.coderjia.boot3webflux.config;

import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;

import java.time.Duration;

/**
 * @author CoderJia
 * @create 2024/12/3 下午 09:42
 * @Description
 **/
@Slf4j
@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient() {
        // 配置HTTP连接池
        ConnectionProvider provider = ConnectionProvider.builder("custom")
                .maxConnections(500)
                .maxIdleTime(Duration.ofSeconds(20))
                .build();

        // 配置HTTP客户端
        HttpClient httpClient = HttpClient.create(provider)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                .responseTimeout(Duration.ofSeconds(5))
                .doOnConnected(conn ->
                        conn.addHandlerLast(new ReadTimeoutHandler(5))
                                .addHandlerLast(new WriteTimeoutHandler(5)));

        // 构建WebClient实例
        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .baseUrl("https://echo.apifox.com")
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
                // 添加请求日志记录功能
                .filter(ExchangeFilterFunction.ofRequestProcessor(
                        clientRequest -> {
                            log.debug("Request: {} {}",
                                    clientRequest.method(),
                                    clientRequest.url());
                            return Mono.just(clientRequest);
                        }
                ))
                // 添加响应日志记录功能
                .filter(ExchangeFilterFunction.ofResponseProcessor(
                        clientResponse -> {
                            log.debug("Response status: {}",
                                    clientResponse.statusCode());
                            return Mono.just(clientResponse);
                        }
                ))
                .build();
    }
}

3.3 retrieve()和exchange()区别

在使用 WebClient 进行 HTTP 请求时,retrieve() 和 exchange() 方法都可以用来处理响应,但它们有不同的用途和行为。以下是它们的主要区别:
retrieve()

  • 用途:retrieve() 方法用于简化响应处理,特别是当你只需要响应体时。
  • 自动错误处理:retrieve() 会自动处理 HTTP 错误状态码(例如 4xx 和 5xx),并抛出 WebClientResponseException 及其子类。
  • 返回值:通常用于直接获取响应体,例如 bodyToMono(String.class) 或 bodyToFlux(String.class)。
  • 适用场景:适用于大多数常见的请求处理场景,特别是当你不需要手动处理响应状态码时。

exchange()

  • 用途:exchange() 方法提供了更底层的控制,允许你手动处理响应,包括响应状态码和响应头。
  • 手动错误处理:exchange() 不会自动处理 HTTP 错误状态码,你需要手动检查响应状态码并进行相应的处理。
  • 返回值:返回 ClientResponse 对象,你可以从中提取响应状态码、响应头和响应体。
  • 适用场景:适用于需要手动处理响应状态码或响应头的复杂场景。

示例对比

retrieve()

public Mono<JSONObject> get(String q1) {
    return webClient.get()
            .uri(uriBuilder -> uriBuilder
                    .path("/get")
                    .queryParam("q1", q1)
                    .build())
            .accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .bodyToMono(JSONObject.class);
}

exchange()

public Mono<JSONObject> get(String q1) {
    return webClient.get()
            .uri(uriBuilder -> uriBuilder
                    .path("/get")
                    .queryParam("q1", q1)
                    .build())
            .accept(MediaType.APPLICATION_JSON)
            .exchangeToMono(response -> {
                if (response.statusCode().is2xxSuccessful()) {
                    return response.bodyToMono(JSONObject.class);
                } else {
                    return Mono.error(new RuntimeException("Request failed with status code: " + response.statusCode()));
                }
            });
}

4. 使用示例

4.1 基本请求操作

package com.coderjia.boot3webflux.service;

import com.alibaba.fastjson.JSONObject;
import jakarta.annotation.Resource;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

/**
 * @author CoderJia
 * @create 2024/12/3 下午 10:22
 * @Description
 **/
@Service
public class ApiService {

    @Resource
    private WebClient webClient;

    // GET请求
    public Mono<JSONObject> get(String q1) {
        return webClient.get()
                .uri(uriBuilder -> uriBuilder
                        .path("/get")
                        .queryParam("q1", q1)
                        .build())
                .accept(MediaType.APPLICATION_JSON)
                .retrieve()
                .bodyToMono(JSONObject.class);
    }

    // POST请求
    public Mono<JSONObject> post(JSONObject body) {
        return webClient.post()
                .uri("/post")
                .bodyValue(body)
                .retrieve()
                .bodyToMono(JSONObject.class);
    }

    // PUT请求
    public Mono<JSONObject> put(String q1, JSONObject JSONObject) {
        return webClient.put()
                .uri(uriBuilder -> uriBuilder
                        .path("/put")
                        .queryParam("q1", q1)
                        .build())
                .bodyValue(JSONObject)
                .retrieve()
                .bodyToMono(JSONObject.class);
    }

    // DELETE请求
    public Mono<JSONObject> delete(String q1) {
        return webClient.delete()
                .uri(uriBuilder -> uriBuilder
                        .path("/delete")
                        .queryParam("q1", q1)
                        .build())
                .retrieve()
                .bodyToMono(JSONObject.class);
    }
}

效果展示

get

post

put

delete

4.2 处理复杂响应

@Service
public class ApiService {
    
    // 获取列表数据
    public Flux<JSONObject> getAllUsers() {
        return webClient.get()
                .uri("/users")
                .retrieve()
                .bodyToFlux(JSONObject.class);
    }

    // 处理错误响应
    public Mono<JSONObject> getUserWithErrorHandling(Long id) {
        return webClient.get()
                .uri("/users/{id}", id)
                .retrieve()
                .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> Mono.error(new RuntimeException("客户端错误")))
                .onStatus(HttpStatusCode::is5xxServerError, clientResponse -> Mono.error(new RuntimeException("服务器错误")))
                .bodyToMono(JSONObject.class);
    }

    // 使用exchange()方法获取完整响应
    public Mono<ResponseEntity<JSONObject>> getUserWithFullResponse(Long id) {
        return webClient.get()
                .uri("/users/{id}", id)
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .flatMap(response -> response.toEntity(JSONObject.class));
    }
}

4.3 高级用法

@Service
public class ApiService {
    
    // 带请求头的请求
    public Mono<JSONObject> getUserWithHeaders(Long id, String token) {
        return webClient.get()
                .uri("/users/{id}", id)
                .header("Authorization", "Bearer " + token)
                .retrieve()
                .bodyToMono(JSONObject.class);
    }

    // 带查询参数的请求
    public Flux<JSONObject> searchUsers(String name, int age) {
        return webClient.get()
                .uri(uriBuilder -> uriBuilder
                        .path("/users/search")
                        .queryParam("name", name)
                        .queryParam("age", age)
                        .build())
                .retrieve()
                .bodyToFlux(JSONObject.class);
    }

    // 文件上传
    public Mono<String> uploadFile(FilePart filePart) {
        return webClient.post()
                .uri("/upload")
                .contentType(MediaType.MULTIPART_FORM_DATA)
                .body(BodyInserters.fromMultipartData("file", filePart))
                .retrieve()
                .bodyToMono(String.class);
    }
}

5. 最佳实践

  1. 合理使用响应式类型

    • 使用 Mono 用于单个对象
    • 使用 Flux 用于集合数据
    • 注意背压处理
  2. 错误处理

     public Mono<JSONObject> getUserWithRetry(Long id) {
         return webClient.get()
                 .uri("/users/{id}", id)
                 .retrieve()
                 .bodyToMono(JSONObject.class)
                 .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
                 .timeout(Duration.ofSeconds(5))
                 .onErrorResume(TimeoutException.class,
                         e -> Mono.error(new RuntimeException("请求超时")));
     }
    
  3. 资源管理

    • 使用连接池
    • 设置适当的超时时间
    • 实现优雅关闭

6. 注意事项

  1. WebClient 是非阻塞的,需要注意响应式编程的特性
  2. 合理配置连接池和超时参数
  3. 在生产环境中实现适当的错误处理和重试机制
  4. 注意内存使用,特别是处理大量数据时

7. 与RestTemplate对比

特性WebClientRestTemplate
编程模型响应式、非阻塞同步、阻塞
性能更好一般
资源利用更高效一般
学习曲线较陡平缓
适用场景高并发、响应式系统简单应用、传统系统

8. 总结

WebClient 作为 Spring 推荐的新一代 HTTP 客户端,提供了强大的响应式编程能力和更好的性能。虽然相比 RestTemplate 有一定的学习曲线,但在现代微服务架构中,其带来的好处远超过学习成本。建议在新项目中优先考虑使用WebClient,特别是在需要处理高并发请求的场景下。

参考资料