拓冰建站拓冰建站
首页 / 资讯中心 / 正文

高德天气API Java二次开发实战:缓存、熔断与降级设计

简介基于高德地图开放平台生活服务接口实现的实时天气查询Java项目面向学习第三方API对接的开发者适合作为课程设计或接口二次开发入门参考。压缩包共17个文件整体仅1.29MB包含核心源码WeatherDemo.java、六个jar依赖如json-lib、commons-logging等、xml工程配置文件、markdown格式说明文档及license声明结构精简便于快速导入工程并阅读。目前已有897人次浏览学习。代码完整展示了从注册API Key、拼接城市编码请求到使用Java网络库发送HTTP请求、解析返回JSON并提取温湿度等气象字段的过程同时涉及网络异常处理与数据展示逻辑可帮助理解Java网络编程和高德API调用的真实落地方式。对于想快速掌握天气查询功能并希望进一步扩展其他生活服务接口的开发者而言是一份紧凑且实用的参考源码。1. 高德天气 API 二次开发为什么不是调个接口就算完做过 LBS 类系统的人都懂业务方那句“就加个天气功能”背后往往是这样的场景物流大屏要根据目的地下雨提示司机减速、门店选址要把历史降水天数当参考因子、露营类 App 要在地图打点上叠加未来两小时降雨。高德开放平台的天气查询接口属于 Web 服务 API 的 V3 类别按城市 adcode 或经纬度返回实时天气与预报数据。但这只是最外面一层皮真实项目里你还要解决 key 鉴权、经纬度转区域编码、超时怎么处理、缓存要不要做、配额超限怎么降级这些问题。对 Java 后端开发者来说这个标题真正考验的不是“能不能调通一个 HTTP 接口”而是“能不能把第三方天气能力内化成自己系统的可靠服务”。本文会直接用 Java 写一遍从签名参数到二次封装的全部过程重点放在缓存、自动熔断、批量预热这三个容易被忽视的实战环节。新手可以照着前两章把最小链路跑通有经验的人可以直接跳到第三章看容错设计。整个过程中只依赖 JDK 自带类库和 Spring Boot 的基础组件不引入额外重型依赖。2. 高德天气接口的最小接入先用 HttpClient 把链路跑通2.1 高德天气 API 的接口形态与鉴权机制高德开放平台的天气功能挂在 Web 服务 API 下数据分两种实时天气extensionsbase和天气预报extensionsall。请求路径是https://restapi.amap.com/v3/weather/weatherInfo核心参数只有四个key、city、extensions、output。这里的city可以直接填城市 adcode 六位编码也可以填经纬度坐标经度,纬度但经纬度会被高德逆地理编码后匹配到最近的 adcode所以存在边界区域返回邻近城市天气的情况。鉴权机制很朴素在请求参数里带上key不存在 OAuth 签名流程。但上海、北京等城市的 adcode 是稳定的六位码业务中不能假设调用者会正确传码所以二次开发的第一步反而是把“入参归一化”做掉接收任意字符串形式的城市名、经纬度或 IP在服务内部统一换算成 adcode 再发给高德。// 入参规范化城市名/经纬度 → adcode public String resolveAdcode(String input) { // 优先按纯数字识别 adcode如 110000 if (input.matches(\\d{6})) { return input; } // 经纬度形式 116.40,39.90需要调用高德逆地理编码 if (input.matches(\\d\\.?\\d*,\\d\\.?\\d*)) { String[] split input.split(,); String regeoUrl String.format( https://restapi.amap.com/v3/geocode/regeo?location%s,%skey%s, split[0], split[1], apiKey); // 读取 regeocode.addressComponent.adcode 返回 } // 城市名走地理编码取 geocodes[0].adcode }这段代码把高德开放的另外两个接口引入了体系逆地理编码和地理编码。在真实设计里城市的 adcode 不应该每次请求都实时查询而是启动时加载一份城市表到本地缓存配合定时任务每周更新。这样既减少了外部接口调用次数也让后续的天气缓存逻辑能够依赖一个稳定的本地维度。2.2 用 JDK 原生 HttpClient 实现第一版调用高德返回的是 JSON 包装的数据字段结构相对扁平。实时天气返回lives数组里面是省份、城市、天气现象、温度、湿度、风向风力、发布时间等预报天气返回forecasts其中casts数组里按天存放白天和夜间两套天气信息。第一版不要引 RestTemplate直接用java.net.http.HttpClient是最干净的连接超时和读取超时分开配置后续换连接池也更方便。HttpClient client HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .build(); public WeatherLive parseWeather(String cityAdcode) throws Exception { String url String.format( https://restapi.amap.com/v3/weather/weatherInfo?city%skey%sextensionsbase, cityAdcode, apiKey); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(url)) .timeout(Duration.ofSeconds(3)) .GET() .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() ! 200) { throw new RuntimeException(高德天气接口 HTTP response.statusCode()); } JSONObject body JSONObject.parseObject(response.body()); if (!1.equals(body.getString(status))) { throw new RuntimeException(高德天气接口错误: body.getString(info)); } JSONObject live body.getJSONArray(lives).getJSONObject(0); return new WeatherLive(live.getString(city), live.getString(weather), live.getString(temperature), live.getString(humidity)); }这里status字段是判断业务是否成功的唯一依据返回1表示成功0时info字段里带有具体失败原因。常见的失败原因集中在INVALID_USER_KEY和DAILY_QUERY_OVER_LIMIT前者是 key 没配对或没开通天气服务权限后者是当天配额耗尽。建议在代码里把info字段原样打进日志并且把status0与 HTTP 层错误当成两类不同的异常处理HTTP 状态码是非 2xx 时网络链路本身出了问题需要考虑重试status0时高德服务是通的但业务被拒绝盲目重试只会加速配额消耗。2.3 线上最容易被忽略的配置项连接池与字符编码第一版能用和生产能用之间还差一个连接池。如果每次请求都新建HttpClient在高并发下会创建大量 TCP 连接而且每个连接都在 TIME_WAIT 状态里停留 60 秒左右端口耗尽只是时间问题。JDK 的HttpClient内部默认会复用连接但为了更精细地控制建议使用连接池化比较成熟的Apache HttpClient 5或OkHttp。PoolingHttpClientConnectionManager manager new PoolingHttpClientConnectionManager(); manager.setMaxTotal(200); manager.setDefaultMaxPerRoute(50); RequestConfig config RequestConfig.custom() .setConnectTimeout(3000) .setSocketTimeout(3000) .setConnectionRequestTimeout(1000) .build(); CloseableHttpClient httpClient HttpClients.custom() .setConnectionManager(manager) .setDefaultRequestConfig(config) .build();参数说明setMaxTotal是连接池里的总连接数上限setDefaultMaxPerRoute是单个路由的最大并发连接数。高德接口域名只有一个所以DefaultMaxPerRoute决定了实际并发能力。setConnectionRequestTimeout是从连接池借连接的等待时间如果这个值太短线程池里积压的请求会直接抛超时如果太长雪崩时线程会被拖死。三个超时时间建议比例是 3:3:1连接请求超时最短因为它在等待的是本地资源而非远端响应。另外高德接口返回的Content-Type里有charsetutf-8但保险起见解析响应体时强制用UTF-8避免某些边缘代理篡改字符集声明导致中文城市名乱码。3. 二次开发的核心把裸 API 封装成可治理的后端服务3.1 数据模型隔离不要让你的实体类依赖高德 JSON 结构二次开发最容易踩的坑是把高德的lives[0].temperature直接映射到自己系统的数据库字段上。高德的字段名和语义是第三方定义的一旦接口升级改名你的整个业务代码都要跟着动。常见做法是建两层模型AmapWeatherResponse做 JSON 到 Java 的原始映射WeatherInfo作为服务对外输出的领域模型中间通过convert方法转换。这层转换虽然看起来冗余但它是未来替换天气源、增加数据清洗逻辑的唯一扩展点。public class WeatherInfo { private String cityName; // 标准化城市名例如 北京市 private Integer temperature; // 摄氏温度去除 ℃ 后缀 private String weather; // 天气现象如 晴 / 小雨 private Integer humidity; // 相对湿度百分比 private LocalDateTime reportTime; // 高德发布时间已转本地时区 // 构造函数、getter、setter 省略 }转换时注意两点温度字段高德返回的是带单位的字符串如31℃要手动剔除单位后转成Integerreport_time是高德服务器发布数据的时间不是你请求的时间这个值要原样保留传到前端用户看到的“更新于 xx 分钟前”应该基于这个时间计算而不是基于客户端本地时间。另外建议在领域模型里增加一个weatherSource字段值为amap。这样后面接入和风天气或中央气象台作为降级源时前端可以区分数据来自哪个渠道排查问题时不用猜。3.2 缓存策略的四个参数容量、TTL、分层与失效顺序天气数据的特征是变化慢、查询频次高。实时天气基本上是小时级变化预报数据一天更新两次左右。一个合理的缓存设计能把 99% 的重复请求挡在高德配额之外。缓存策略里最关键的四个参数分别是cacheSize缓存容量避免内存里堆积大量无人访问的冷门城市、liveTtl实时天气的过期时间设为 300 秒较合理、forecastTtl预报的过期时间建议 1800 秒、maxIdleTime从缓存被访问的时间算起的最长闲置时间。Component public class WeatherCacheService { // key: adcode, value: 天气数据 过期时间 private final CacheString, CachedEntryWeatherInfo liveCache; public WeatherCacheService() { this.liveCache Caffeine.newBuilder() .maximumSize(500) .expireAfterWrite(Duration.ofMinutes(5)) .expireAfterAccess(Duration.ofHours(24)) .recordStats() .build(); } public WeatherInfo getLiveWeather(String adcode) { CachedEntryWeatherInfo entry liveCache.getIfPresent(adcode); if (entry null || entry.isExpired()) { return null; } return entry.getData(); } public void putLiveWeather(String adcode, WeatherInfo weather) { liveCache.put(adcode, new CachedEntry(weather, System.currentTimeMillis() 300_000)); } }上述实现用了 Caffeine 作为本地缓存expireAfterWrite是写入后 5 分钟过期expireAfterAccess是访问后 24 小时过期两者同时生效时谁先触发谁淘汰。maximumSize(500)意味着内置 LRU 淘汰机制如果业务覆盖全国所有区县这个容量是不够的要按实际城市数上调到 3000 左右。需要特别注意的是缓存空间大小不等于键数量上限因为高德天气是根据 adcode 聚合的一个 adcode 对应一个市或区不像用户维度那样有膨胀风险所以按照全国行政区划总数设置 3500 的容量上限是合理的。3.3 熔断与降级的落地姿势线程池隔离比熔断器状态机更实在第三方接口最怕的是长时间不可用而熔断器的本质是“快速失败避免线程堆积”。业界常见做法是用 Resilience4j 或 Sentinel但很多团队连网关层都没接贸然引入的代价是无谓的运维成本。我更推荐一种轻量方案直接用一个带超时控制的线程池配合简单的失败计数实现自动降级。private final ExecutorService weatherPool new ThreadPoolExecutor( 10, 20, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(100), new ThreadPoolExecutor.CallerRunsPolicy()); public WeatherInfo getWeatherWithFallback(String adcode) { FutureWeatherInfo future weatherPool.submit( () - amapClient.queryLiveWeather(adcode)); try { return future.get(2, TimeUnit.SECONDS); } catch (TimeoutException e) { log.warn(高德天气查询超时adcode{}, adcode); // 回退到本地缓存中的过期数据即使超过 TTL 也勉强可用 return liveCache.getIfPresent(adcode) null ? null : liveCache.getIfPresent(adcode).getData(); } catch (Exception e) { // 还有一层降级返回默认的晴朗天气适合对数据实时性要求不高的场景 return WeatherInfo.unknown(adcode); } }线程池的核心线程数设 10ArrayBlockingQueue容量 100拒绝策略用CallerRunsPolicy。这里CallerRunsPolicy的含义是当线程池和队列都满时任务会被提交线程自己执行。这个策略对第三方 API 调用是有争议的它相当于把压力转移回调用线程但如果调用线程本身就是 Tomcat 工作线程这样做是变相把下游故障传播到了自己的容器线程并不优雅。实际部署中我会把拒绝策略换成AbortPolicy并在外层捕获RejectedExecutionException然后直接走到降级路径返回默认值。关于CallerRunsPolicy和AbortPolicy哪个更好取决于你是否能容忍调用线程的额外耗时我现在的经验是后者更可控因为它让每个请求的耗时有一致上限。3.4 RESTful 风格的自研接口面向调用方设计而非面向高德设计二次开发之后的成果要暴露给内部或前端接口设计不能直接透传高德的 URL 参数。一个符合 REST 风格规范的做法是GET /api/v1/weather/live/{adcode}返回实时天气GET /api/v1/weather/forecast/{adcode}返回未来几天预报。这两个接口内部可以不经过高德而是先查本地缓存缓存未命中才回源。而adcode这个路径参数本身对调用方不友好前端更习惯传城市名所以还可以增加一个 query 参数city让服务端做解析。RestController RequestMapping(/api/v1/weather) public class WeatherController { GetMapping(/live/{adcode}) public ResponseEntityApiResponseWeatherInfo liveByAdcode( PathVariable String adcode) { WeatherInfo weather weatherService.getLiveWeather(adcode); return ResponseEntity.ok(ApiResponse.success(weather)); } GetMapping(/live) public ResponseEntityApiResponseWeatherInfo liveByCity( RequestParam String city) { String adcode adcodeService.resolve(city); return ResponseEntity.ok(ApiResponse.success(weatherService.getLiveWeather(adcode))); } }路由设计上把adcode作为路径参数、city作为 query 参数本质上是一种渐进式接口对高频访问的固定城市用短路径方便缓存治理对低频随意输入的查询走解析链路。ApiResponse是统一的响应包装内部至少包含code、message、data三个字段。有一个容易忽略的细节ApiResponse里不能只放temperature、weather这种业务字段还应该带一个serverTime字段让接口调用方知道当前数据是后端在哪个时间点拿到的这个字段在接口排障时价值极高。4. 参数校准与异常治理把线上日志变成可复盘数据4.1 必调的三个参数connectTimeout、maxConnPerRoute、HTTP 重试次数很多团队在二次开发时参数是复制高德官方示例里的默认值这是灾难的开始。高德官方示例偏重演示它的 HTTP 客户端没有设置连接池上限也没有设置超时这在生产环境的表现就是接口偶尔卡住、线程池被打满。一组经过验证的初始值如下参数项推荐值设置理由connectTimeout3 秒高德接口在全国多地域平均响应 100-300ms3 秒足够识别网络异常socketTimeout5 秒读取响应体超时防止连接建立但响应迟迟不到的情况maxConnPerRoute50单机 50 并发已经覆盖绝大多数中小型业务的峰值调用量maxConnTotal200给其它走同连接池的外部接口留出余量重试次数是最值得争论的配置。默认情况下status0的业务错误和 HTTP 5xx 错误不建议重试EOFException、连接重置这类网络级异常可以重试一次。我在生产环境只允许最多重试一次因为高德天气配额是按日结算的每次重试都在消耗配额。如果你的报价重试次数太多某一天高德接口不稳你的配额会在几分钟内被重试请求打光后面一整天所有用户都拿不到数据。所以正确的做法是连接池层的重试次数设为 1并在应用层额外加一个基于 Redis 的分布式限流门闩当一分钟内错误次数超过 30 次就切换到降级模式。if (isNetworkError(ex) retryCount.incrementAndGet() 1) { log.warn(高德天气网络异常执行第 1 次重试city{}, adcode); return doQuery(adcode); } else { log.error(高德天气请求失败放弃重试city{}, error{}, adcode, ex.getMessage()); return fallback(adcode); }这段逻辑的重点是isNetworkError要精确匹配java.net.ConnectException、SocketTimeoutException和SSLException而不是捕获所有Exception。因为一旦把JSON解析异常也纳入重试遇到高德返回非 JSON 内容时会连续请求两次浪费配额且延长故障恢复时间。4.2 Java 面试题级别的考点天气接口幂等与并发穿透高德天气接口是天然幂等的同一次请求参数返回相同结果所以二次开发中最常见的面试问题是“如何设计缓存防穿透”。所谓穿透是指大量请求查询一个不存在的 adcode缓存里没有这个键请求全部打到高德造成配额虚耗。解决方案有两个层面第一层在入口处维护一个合法 adcode 集合用HashSet装载全国所有有效的 adcode查询前先校验第二层是缓存空值把高德返回status0或lives数组长度为 0 的结果也缓存 60 秒后续同样的非法请求直接命中缓存的空对象。更隐蔽的问题是并发击穿某个 adcode 的缓存刚好过期同一瞬间有 50 个请求同时发现缓存未命中。如果不加控制50 个请求会同时打到高德。这时需要加一个 JVM 级别的ReentrantLock做单机互斥或者用 RedisSETNX做分布式锁。单机场景直接维护一个ConcurrentHashMapString, Lock拿到锁之后二次检查缓存这是最简单的防击穿实现。分布式部署时再用 Redis 锁但要注意锁的粒度是 adcode 而不是整个天气服务否则一个城市的请求就把全站串行化了。private final ConcurrentHashMapString, ReentrantLock lockMap new ConcurrentHashMap(); public WeatherInfo getLiveWeatherWithLock(String adcode) { WeatherInfo cached cacheService.getLiveWeather(adcode); if (cached ! null) return cached; ReentrantLock lock lockMap.computeIfAbsent(adcode, k - new ReentrantLock()); lock.lock(); try { // Double-check拿到锁后缓存可能已被其它线程填充 cached cacheService.getLiveWeather(adcode); if (cached ! null) return cached; WeatherInfo fresh amapClient.queryLiveWeather(adcode); cacheService.putLiveWeather(adcode, fresh); return fresh; } finally { lock.unlock(); // 避免锁对象无限堆积主动清理 lockMap.remove(adcode); } }代码说明lockMap的 key 是 adcodecomputeIfAbsent保证每个 adcode 只有一个锁实例。lock()放在try之外因为如果加锁本身抛异常那说明 JVM 状态已经很不健康不如直接抛出去。finally里unlock()后清掉锁对象防止内存里面堆积大量不再访问的锁。这里lockMap.remove(adcode)有一个微小的时间窗口线程 A 持有锁时线程 B 已经通过computeIfAbsent拿到了同一个锁实例并阻塞A 释放锁并 remove 之后线程 C 进来会创建新锁——此时 B 在旧锁上等待C 在新锁上执行两个线程就不再互斥。解决方式是不 remove让锁对象常驻牺牲少量内存换取正确性。业务上我会选不 remove因为 adcode 的总量是有限的。4.3 确定性排错清单报错时先看哪四个字段高德天气接口报错排查是有路径依赖的。拿到一个线上故障先看status字段0代表业务失败再看info字段里的英文错误码INVALID_USER_KEY是 key 配置错误DAILY_QUERY_OVER_LIMIT是配额超限KEY_DISABLED是 key 被停用。然后把高德返回的原始body完整打印到日志里不要只打印转译后的异常消息因为高德的一些错误码在官方变更时会调整文案原文能帮你快速对比新老文档。最后一步是看时间耗时的分布用System.currentTimeMillis()记录调用前后差值超过 2 秒就要怀疑是本地网络问题还是高德侧抖动再在restapi.amap.com上做curl -w测试本地到高德机房的链路质量。这个顺序能覆盖绝大多数场景。5. 进阶把天气能力从查询接口升级为主动推送服务当核心查询稳定之后可以更进一步把天气从“被动等前端来问”变成“定时主动拉取并推送”。这有两个典型应用一是面向 C 端用户每天早上的天气推送通知二是面向内部决策比如物流调度系统每天早上拉取一批城市的降雨预报自动决策是否调整发车时间。两种场景的共同点是它们都基于固定的城市列表不需要用户实时触发。实现思路比较简单。用 Spring Boot 的Scheduled注解起一个定时任务每 30 分钟拉取全国重点城市的实时天气和未来 24 小时预报写入缓存。这个预热机制比用户触发才回源高明得多高峰期的高德配额消耗被转移到了凌晨这些闲时时段用户访问时命中缓存率接近 100%。Scheduled(cron 0 28 * * * ?) public void preloadMajorCities() { ListString cityAdcodes adcodeService.getMajorCityList(); log.info(开始预热重点城市天气城市数量{}, cityAdcodes.size()); for (String adcode : cityAdcodes) { try { WeatherInfo live amapClient.queryLiveWeather(adcode); cacheService.putLiveWeather(adcode, live); // 控制调用节奏避免短时间内打满配额 TimeUnit.MILLISECONDS.sleep(200); } catch (Exception e) { log.warn(预热失败adcode{}, msg{}, adcode, e.getMessage()); } } }代码里的0 28 * * * ?是 Cron 表达式含义是每小时的第 28 分钟执行一次。之所以选 28 分而不是整点是为了错开电网整点报告的查询高峰降低一时段内集中请求的可能性。sleep(200)是故意限速不是防御高德限流而是避免 300 个城市在极短时间内同时发请求导致本地线程池积压拉长整体任务时间没关系但不要影响正在处理的用户实时请求。推送侧只做一个很薄的服务把预热的缓存数据按用户订阅的城市列表筛出来走公司已有的消息推送通道。这里我要强调一个容易被忽略的点高德预报数据里的dayweather和nightweather是分开的字段推送文案要根据当前时间判断该用哪个不要上午八点推“夜间晴”这种让人摸不着头脑的内容。判断逻辑就是拿当前小时数和预报数据的日期做比对早于 18 点取白天预报晚于 18 点取夜间预报跨天时从casts数组里找第二天元素。这个细节做好整个推送服务的体验会明显上一个台阶。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门