去年底做跨境电商后台的时候,运营提了一个需求:海外仓商品的详情页上要展示本地实时天气,方便买家判断收货时会不会碰上暴雨暴雪。项目组讨论了一圈,最后定下来用百度天气API来对接海外城市天气,由Java服务端统一封装查询入口。标题里的“海外城市实时天气”听起来不大,但真正落地时涉及AK申请、经纬度坐标、HTTP客户端选型、JSON解析、缓存和重试,每一个环节都有可以写出来的细节。这篇文章就把完整的实现过程和踩过的坑记录下来,给要做类似国际业务、又不想引太大第三方依赖的Java开发者做个参考。
1. 这个需求是怎么来的:一个海外仓页面上的“实时天气”接在哪
1.1 业务场景拆解
先说需求本身。跨境电商后台的商品详情页有一个区块,需要展示“发货地天气”,发货地是海外仓所在城市,比如东京、纽约、悉尼。天气信息不是核心订单数据,但要稳定展示、加载快、不能因为天气服务挂掉拖垮整个页面。
这个场景有几个硬约束:接口必须在国内网络环境下可直连;数据更新频次不需要毫秒级,天气本身15到30分钟更新一次就够了;成本要低,不能为一个小功能单独采购商业天气数据源;后续要支持的城市不固定,可能随时加新仓,接口参数要好配。
表面上看,查天气是一个很成熟的能力,但真正接起来并没有想象中那么无脑。光是在“用哪家数据源”这个问题上,项目组就做了两轮调研。
1.2 为什么最终选了百度,而不是高德、和风和OpenWeatherMap
先看几个候选方案。
高德的天气API做得不错,返回字段也全,但它的城市代码体系主要面向国内行政区划,海外城市支持很弱,官方文档里基本只覆盖国内,直接Pass。
和风天气的海外数据源质量很好,覆盖全球城市,开发者文档也清晰,但免费版每日调用量有限,申请开发者权限流程相对重,做小功能有点杀鸡用牛刀。而且一旦业务量上来,需要付费套餐,商务流程又要走一遍。
OpenWeatherMap在国际开发者里很流行,支持全球城市,但国内部分网络环境访问它不够稳定,响应延迟经常让人怀疑人生,不适合做线上页面直连。
最后看百度天气API。它在百度地图开放平台里,服务端接口可以通过经纬度精确查询海外城市天气,国内网络直连快,个人开发者免费配额对中小项目足够,返回结构也比较简单。海外仓这种“按城市查天气”的场景,反而是所有方案里最省事的。最终技术方案定下来:Java后端封装,内部先维护一份仓库城市坐标表,定时请求百度天气接口,本地做TTL缓存,页面通过JSON接口消费。
2. 百度天气API的调用前提:AK申请套路与海外城市的坐标约束
2.1 服务端AK的申请流程
用百度天气API的前提是有一个合法的AK。这里有两个容易搞混的点,先说明白。
第一,天气API不是单独注册产品,而是百度地图开放平台里的一项服务。你需要先注册百度账号,进入控制台,在“应用管理”里创建一个应用。创建时应用类型要选“服务端”,选错成“浏览器端”的话,后面做服务端调用会有一堆莫名其妙的鉴权问题。第二,创建完成后拿到的AK就是请求参数里的ak,不需要额外做签名,也不用计算sn校验,直接拼到URL里就能用。
创建应用时通常会让你配置IP白名单。开发阶段建议先把白名单留空,等代码部署到服务器之后,再把服务器出口IP填进去。这个配置坑得很,后文会专门说。
2.2 location和district_id:海外城市必须走经纬度
百度天气API有两类查询参数,国内城市可以用district_id,这是行政区划编码,比如北京是110000。但district_id这套编码体系只覆盖国内,海外城市查不到。
海外城市要用location参数,格式是经度,纬度,注意顺序,先经度后纬度。比如东京是139.6917,35.6895,拼出来的请求就是:
https://api.map.baidu.com/weather/v1/?location=139.6917,35.6895&data_type=all&ak=你的AK同时需要传data_type,可选值是now(实况)、forecast(预报)、all(实况加预报)。如果页面只需要当前天气,建议用now,响应体小,解析也快。
这里有个容易忽略的点:location只接受经纬度,不接收城市名。你传location=东京是查不到数据的。所以要在服务端维护一份“城市名 -> 经纬度”的映射。
2.3 常用海外城市坐标参考表
下面是我实际维护的仓库城市坐标表,按城市名映射经纬度,城市名用英文小写,方便做路径参数或者缓存key。
| 城市 | 经度 | 纬度 |
|---|---|---|
| tokyo(东京) | 139.6917 | 35.6895 |
| osaka(大阪) | 135.5023 | 34.6937 |
| seoul(首尔) | 126.9780 | 37.5665 |
| bangkok(曼谷) | 100.5018 | 13.7563 |
| singapore(新加坡) | 103.8198 | 1.3521 |
| london(伦敦) | -0.1276 | 51.5074 |
| paris(巴黎) | 2.3522 | 48.8566 |
| berlin(柏林) | 13.4050 | 52.5200 |
| newyork(纽约) | -74.0060 | 40.7128 |
| losangeles(洛杉矶) | -118.2437 | 34.0522 |
| sydney(悉尼) | 151.2093 | -33.8688 |
| dubai(迪拜) | 55.2708 | 25.2048 |
注意北半球纬度为正,南半球的悉尼纬度为负。经纬度是双精度浮点数,不要用int类型放,否则会把0.5度这种精度丢掉。
3. Java调用层实现:从URL拼接、HTTP请求到JSON解析的完整封装
3.1 技术选型:JDK 11 HttpClient + Jackson,为什么不引入Spring
先回应一个常见疑问:用Java调HTTP接口,为什么不直接上Spring的RestTemplate或者Feign?
原因是这个功能在项目里只是一个很小的内部模块,不希望为了一个天气查询把Spring全家桶的依赖带进来。JDK 11开始自带的java.net.http.HttpClient足够稳定,支持超时、异步、连接池,对天气这种低频接口完全够用。JSON解析用Jackson,只引一个jackson-databind依赖,其余的都不需要。
Maven依赖就这一个:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.17.1</version> </dependency>如果你用的是Gradle,把坐标换成就行。
3.2 请求封装:URL构造与HTTP调用的细节
先定义响应模型WeatherResult,这个类承载解析结果,后续给上层业务使用。
public class WeatherResult { private String city; private String country; private double temperature; private double feelsLike; private String text; private int humidity; private String windDir; private String windClass; private String lastUpdate; // getter / setter 省略,可以交给 IDE 生成 @Override public String toString() { return String.format("%s(%s):%s,当前温度 %.1f℃,体感 %.1f℃,湿度 %d%%,%s %s,数据时间 %s", city, country, text, temperature, feelsLike, humidity, windDir, windClass, lastUpdate); } }核心客户端类BaiduWeatherClient,负责构造URL、发起请求、解析响应。下面这段代码是完整可运行的:
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; public class BaiduWeatherClient { private static final String WEATHER_API = "https://api.map.baidu.com/weather/v1/"; private final String ak; private final HttpClient httpClient; private final ObjectMapper objectMapper; public BaiduWeatherClient(String ak) { this.ak = ak; this.httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); this.objectMapper = new ObjectMapper(); } /** * 查询海外城市实时天气 * * @param lng 经度 * @param lat 纬度 * @return 解析后的天气结果 */ public WeatherResult getOverseasWeather(double lng, double lat) throws IOException, InterruptedException { String url = buildUrl(lng, lat); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .timeout(Duration.ofSeconds(10)) .GET() .build(); HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); return parse(response.body()); } private String buildUrl(double lng, double lat) { // location 参数格式固定为“经度,纬度”,经纬度都是数字和逗号,无需额外 URL 编码 return WEATHER_API + "?location=" + lng + "," + lat + "&data_type=all&ak=" + ak; } private WeatherResult parse(String responseBody) throws IOException { JsonNode root = objectMapper.readTree(responseBody); int status = root.path("status").asInt(-1); String message = root.path("message").asText(); if (status != 0) { throw new RuntimeException("百度天气接口返回异常,status=" + status + ", message=" +