WebClient 是 Spring 5 引入的一个非阻塞、响应式的 HTTP 客户端,属于 Spring WebFlux 框架的一部分,是官方推荐的 RestTemplate 现代化替代方案。
| 特点 | 说明 |
|---|---|
| 非阻塞 I/O | 使用少量线程处理大量并发请求,资源利用率高 |
| 响应式编程 | 基于 Reactor,返回 Mono/Flux,支持异步和流式处理 |
| 流式 API | 提供流畅的链式调用风格,代码简洁直观 |
| 功能丰富 | 支持同步/异步、流式数据、大文件传输、超时重试、熔断等 |
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
注意:若在代码中直接使用 Jackson 的JsonNode解析响应,需确保jackson-databind依赖存在。较新版本 Spring Boot 可能不会自动传递引入,若报「程序包 com.fasterxml.jackson.databind 不存在」,需显式添加:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
// 方式一:默认设置(无基础 URL)
WebClient client = WebClient.create();
// 方式二:指定基础 URL
WebClient client = WebClient.create("https://api.example.com");
// 方式三:使用 Builder 自定义(推荐)
WebClient client = WebClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
Spring Boot 中推荐:通过 @Configuration 定义成 Bean,便于统一管理和注入
@Configuration
public class AppConfig {
@Value("${external.api.base-url}")
private String baseUrl;
@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl(baseUrl)
.build();
}
}
public Mono<User> fetchUser(String userId) {
return webClient.get()
.uri("/users/{id}", userId)
.retrieve()
.bodyToMono(User.class);
}
public Flux<Order> fetchUserOrders(String userId) {
return webClient.get()
.uri("/orders?userId={userId}", userId)
.retrieve()
.bodyToFlux(Order.class);
}
public User fetchUserSync(String userId) {
return webClient.get()
.uri("/users/{id}", userId)
.retrieve()
.bodyToMono(User.class)
.block(); // block() 阻塞等待结果,不推荐在响应式链路中使用
}
public Mono<User> createUser(User newUser) {
return webClient.post()
.uri("/users")
.bodyValue(newUser)
.retrieve()
.bodyToMono(User.class);
}
webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/v1/search")
.queryParam("name", location) // 中文会自动 URL 编码
.queryParam("count", 1)
.build())
.retrieve()
.bodyToMono(JsonNode.class);
retrieve() 遇到 4xx/5xx 会直接抛 WebClientResponseException,需转换成自己的业务异常,否则会被全局兜底当成 500 处理。
return webClient.get()
.uri("/posts/{id}", id)
.retrieve()
.bodyToMono(PostVo.class)
.onErrorMap(WebClientResponseException.NotFound.class,
e -> new BusinessException("帖子不存在"));
return webClient.get()
.uri("/posts/{id}", id)
.retrieve()
.bodyToMono(PostVo.class)
.switchIfEmpty(Mono.error(new BusinessException("帖子不存在")));
小结:
switchIfEmpty 处理「空结果」(empty signal),只在 2xx 但响应体为空时触发onErrorMap / onStatus 处理「错误状态码/异常」(error signal),如 404当项目中定义了多个类型相同的 WebClient Bean 时,Spring 无法确定注入哪一个,会报 NoUniqueBeanDefinitionException,需用 @Qualifier 按名字指定。
// 定义处(AppConfig)——给每个 Bean 起名字
@Bean
@Qualifier("weatherWebClient")
public WebClient weatherWebClient() { ... }
@Bean
@Qualifier("geocodingWebClient")
public WebClient geocodingWebClient() { ... }
// 注入处(Service)——手写构造器,给参数加 @Qualifier
public WeatherServiceImpl(
@Qualifier("weatherWebClient") WebClient weatherWebClient,
@Qualifier("geocodingWebClient") WebClient geocodingWebClient) {
this.weatherWebClient = weatherWebClient;
this.geocodingWebClient = geocodingWebClient;
}
注意:不能用 Lombok 的@RequiredArgsConstructor自动生成构造器,因为 Lombok 不会自动给参数加@Qualifier,会导致多个同类型 Bean 注入失败。
WebClient 支持配置超时时间(如 HttpClient 的 responseTimeout),并能与 Resilience4j 等库集成,实现失败重试、熔断和超时控制。
提供两个接口,以中文返回天气信息:
| 用途 | API 地址 | 说明 |
|---|---|---|
| 天气数据 | https://api.open-meteo.com/v1/forecast | 只接受经纬度查询,返回温度、湿度、风速、天气代码等 |
| 地理编码 | https://geocoding-api.open-meteo.com/v1/search | 输入城市名,返回经纬度 |
| IP 定位 | http://ip-api.com/json/{ip} | 输入 IP,返回经纬度和城市(免费版仅 HTTP) |
示例请求地址:
# 天气数据
https://api.open-meteo.com/v1/forecast?latitude=39.90&longitude=116.40¤t=temperature_2m,weather_code&timezone=auto
# 地理编码
https://geocoding-api.open-meteo.com/v1/search?name=%E5%8C%97%E4%BA%AC&count=1&language=zh&format=json
# IP 定位
http://ip-api.com/json/8.8.8.8?lang=zh-CN&fields=status,lat,lon,city
步骤 1:配置文件(application.properties)
spring.application.name=Thread
# 天气 API 相关地址
weather.api.base-url=https://api.open-meteo.com
weather.api.geocoding-url=https://geocoding-api.open-meteo.com
weather.api.ip-url=http://ip-api.com/json/{ip}?lang=zh-CN&fields=status,lat,lon,city
步骤 2:配置多个 WebClient Bean(AppConfig.java)
@Configuration
public class AppConfig {
@Value("${weather.api.base-url}")
private String weatherBaseUrl;
@Value("${weather.api.geocoding-url}")
private String geocodingBaseUrl;
@Bean
@Qualifier("weatherWebClient")
public WebClient weatherWebClient() {
return WebClient.builder().baseUrl(weatherBaseUrl).build();
}
@Bean
@Qualifier("geocodingWebClient")
public WebClient geocodingWebClient() {
return WebClient.builder().baseUrl(geocodingBaseUrl).build();
}
}
步骤 3:定义数据对象(dto / vo)
// dto:经纬度定位结果
public record GeoLocation(String city, double latitude, double longitude) {
}
// vo:返回给前端的天气信息(中文)
@Data
@Builder
public class WeatherVo {
private String city; // 城市
private Double latitude; // 纬度
private Double longitude; // 经度
private Double temperature; // 当前温度
private Double apparentTemperature; // 体感温度
private Integer humidity; // 湿度
private Double precipitation; // 降水量
private Double windSpeed; // 风速
private String weather; // 天气现象
private String weatherDetail; // 详细描述
}
步骤 4:实现 Service(WeatherServiceImpl.java)
@Service
public class WeatherServiceImpl implements WeatherService {
private final WebClient weatherWebClient; // 查天气
private final WebClient geocodingWebClient; // 地名转经纬度
private final WebClient externalWebClient; // 发完整 URL(IP 定位)
private final String ipUrl;
public WeatherServiceImpl(
@Qualifier("weatherWebClient") WebClient weatherWebClient,
@Qualifier("geocodingWebClient") WebClient geocodingWebClient,
@Qualifier("externalWebClient") WebClient externalWebClient,
@Value("${weather.api.ip-url}") String ipUrl) {
this.weatherWebClient = weatherWebClient;
this.geocodingWebClient = geocodingWebClient;
this.externalWebClient = externalWebClient;
this.ipUrl = ipUrl;
}
// 输入地点天气:地名 -> 经纬度 -> 天气
public Mono<WeatherVo> getWeatherByLocation(String location) {
return resolveByLocation(location) // 第一步:地名转经纬度
.flatMap(this::fetchWeather) // 第二步:经纬度查天气
.switchIfEmpty(Mono.error(new BusinessException("未找到该地点的天气信息")));
}
// 地名转经纬度
private Mono<GeoLocation> resolveByLocation(String location) {
return geocodingWebClient.get()
.uri(uriBuilder -> uriBuilder
.path("/v1/search")
.queryParam("name", location)
.queryParam("count", 1)
.queryParam("language", "zh")
.queryParam("format", "json")
.build())
.retrieve()
.bodyToMono(JsonNode.class)
.map(node -> {
JsonNode results = node.path("results");
if (!results.isArray() || results.isEmpty()) {
throw new BusinessException("未找到该地点:" + location);
}
JsonNode first = results.get(0);
double lat = first.path("latitude").asDouble();
double lon = first.path("longitude").asDouble();
String city = first.path("name").asText();
return new GeoLocation(city, lat, lon);
});
}
// 查天气(经纬度 -> 天气,转换天气代码为中文)
private Mono<WeatherVo> fetchWeather(GeoLocation geo) {
return weatherWebClient.get()
.uri(uriBuilder -> uriBuilder
.path("/v1/forecast")
.queryParam("latitude", geo.latitude())
.queryParam("longitude", geo.longitude())
.queryParam("current", "temperature_2m,apparent_temperature,relative_humidity_2m,precipitation,weather_code,wind_speed_10m")
.queryParam("timezone", "auto")
.build())
.retrieve()
.bodyToMono(JsonNode.class)
.map(node -> {
JsonNode current = node.path("current");
int code = current.path("weather_code").asInt();
return WeatherVo.builder()
.city(geo.city())
.latitude(geo.latitude())
.longitude(geo.longitude())
.temperature(current.path("temperature_2m").asDouble())
.apparentTemperature(current.path("apparent_temperature").asDouble())
.humidity(current.path("relative_humidity_2m").asInt())
.precipitation(current.path("precipitation").asDouble())
.windSpeed(current.path("wind_speed_10m").asDouble())
.weather(WeatherCode.toText(code))
.weatherDetail(WeatherCode.toDetail(code))
.build();
});
}
}
步骤 5:实现 Controller(WeatherController.java)
@RestController
@RequestMapping("/api/weather")
@RequiredArgsConstructor
@Validated
public class WeatherController {
private final WeatherService weatherService;
// 获取当前所在地天气(根据请求方 IP 定位)
@GetMapping("/current")
public Mono<Result<WeatherVo>> currentWeather(ServerHttpRequest request) {
String ip = resolveClientIp(request);
return weatherService.getCurrentWeather(ip).map(Result::success);
}
// 根据输入地点获取天气
@GetMapping("/location")
public Mono<Result<WeatherVo>> weatherByLocation(
@RequestParam @NotBlank(message = "地点不能为空") String location) {
return weatherService.getWeatherByLocation(location).map(Result::success);
}
}
请求:
GET /api/weather/location?location=北京
返回:
{
"code": "200",
"message": "操作成功",
"data": {
"city": "北京",
"latitude": 39.9042,
"longitude": 116.4074,
"temperature": 26.4,
"apparentTemperature": 27.1,
"humidity": 55,
"precipitation": 0.0,
"windSpeed": 12.6,
"weather": "局部多云",
"weatherDetail": "局部多云"
}
}
flatMap 串联「先定位、再查天气」两步异步操作UriBuilder 拼接 URL,自动处理中文编码