SpringBoot微服务实战:天气API聚合、响应式调用与JPA持久化

发布时间:2026/9/17 0:48:22
SpringBoot微服务实战:天气API聚合、响应式调用与JPA持久化
简介这是一份面向Java后端初学者与SpringBoot入门开发者的实战型天气预报系统源码聚焦RESTful接口开发、第三方API集成及前后端基础交互帮助学习者掌握企业级Web应用的快速搭建流程。资源共25个文件包含19个Java类涵盖Controller、Service、Entity及启动主类、2个XML配置文件Maven依赖与MyBatis映射、1个JavaScript前端交互脚本、1个HTML页面模板、1个application.properties配置文件及1个.gitignore整体压缩包仅57KB轻量易读结构清晰便于逐层理解MVC分层设计与Spring Boot自动配置机制。已有493人学习下载可直接导入IDE运行完整呈现天气数据获取、解析、缓存与页面渲染全流程附带可扩展的多城市查询逻辑与标准化异常处理是练手Spring BootHTTP客户端简单前端集成的理想小项目。1. 这不是个“查天气”的玩具项目而是一套可落地的 SpringBoot 微服务骨架你打开这个[springboot项目源码]天气预报系统.zip第一眼看到pom.xml里spring-boot-starter-web和spring-boot-starter-data-jpa并存就该意识到它压根没打算只做个静态页面调 API 的 Demo。真实业务中用户要查北京明天降水概率系统得先校验城市是否存在查本地缓存或数据库再决定是否穿透调用 OpenWeatherMap历史查询请求要落库归档避免高频重放前端分页拉取多城市预报时后端必须做响应体压缩和字段裁剪——这些都不是RestController加个GetMapping就能扛住的。本项目完整覆盖了「第三方 API 聚合 本地数据持久化 请求链路治理」三层结构适合 Java 工程师在 2 小时内跑通、3 天内改造成企业级气象服务中间件。尤其对刚从 SSH 迁移过来、还在纠结ConfigurationProperties怎么绑定嵌套 YAML 的开发者这里application.yml里weather.api.timeout: 3000和weather.cache.ttl: 3600的写法就是标准答案。2. 第三方天气 API 集成与容错设计为什么不用 RestTemplate 而选 WebClient2.1 选型依据响应式编程对 IO 密集型调用的天然优势天气接口本质是高延迟、低计算量的网络 IO 操作。SpringBoot 2.0 默认推荐 WebClient 替代 RestTemplate核心在于其非阻塞特性单线程可并发处理数百个 HTTP 请求而 RestTemplate 每次调用都独占一个 Tomcat 线程。本项目WeatherService.java中webClient.get().uri(...)的写法正是利用 Reactor 的Mono/Flux实现异步编排。若强行用 RestTemplate在 50 QPS 下线程池极易耗尽这点在application.yml的server.tomcat.max-threads: 200配置中已埋下伏笔——它暗示了同步模型的硬性瓶颈。2.2 关键代码解析带熔断与降级的 API 调用链// src/main/java/com/example/weather/service/WeatherService.java public MonoWeatherResponse fetchWeatherByCity(String city) { return webClient.get() .uri(uriBuilder - uriBuilder .path(/data/2.5/weather) .queryParam(q, city) .queryParam(appid, apiKey) .queryParam(units, metric) .build()) .retrieve() .onStatus(HttpStatus::is4xxClientError, response - Mono.error(new WeatherApiException(客户端参数错误: city))) .onStatus(HttpStatus::is5xxServerError, response - Mono.error(new WeatherApiException(服务端异常请稍后重试))) .bodyToMono(WeatherResponse.class) .timeout(Duration.ofMillis(3000)) // 超时熔断 .onErrorResume(WeatherApiException.class, e - cacheService.getFallbackWeather(city)); // 降级返回缓存 }提示timeout()必须放在bodyToMono()之前否则仅对响应体解析生效网络连接超时仍会阻塞。此处 3000ms 是根据 OpenWeatherMap 公开 SLA 设定的——其 P95 延迟约 1200ms预留 2.5 倍缓冲。2.3 配置驱动的 API 管理application.yml 的实战写法# src/main/resources/application.yml weather: api: base-url: https://api.openweathermap.org app-id: ${WEATHER_API_KEY:your_default_key_here} # 支持环境变量覆盖 timeout: 3000 max-retry: 2 cache: ttl: 3600 # 缓存 1 小时单位秒 max-size: 1000 # 最大缓存条目数注意${WEATHER_API_KEY:...}的默认值仅用于本地开发生产环境必须通过-DWEATHER_API_KEYxxx或 Kubernetes Secret 注入。若直接写死密钥git commit时会被 SonarQube 扫出高危漏洞CVE-2023-XXXXX 类似风险。2.4 容错验证模拟网络中断的单元测试// src/test/java/com/example/weather/service/WeatherServiceTest.java Test void whenApiUnreachable_thenReturnFallback() { // 模拟 WebClient 抛出 ConnectException Mockito.when(mockWebClient.get()).thenThrow(new ConnectException(Connection refused)); StepVerifier.create(weatherService.fetchWeatherByCity(beijing)) .expectNextMatches(response - response.getMain().getTemp() 0 response.getName().equals(Beijing)) // 断言降级数据结构合法 .verifyComplete(); }此测试强制触发onErrorResume分支验证降级逻辑是否真正生效。关键点在于StepVerifier断言的是response对象的业务字段而非简单检查是否不为空——因为缓存可能返回空对象必须确认降级数据符合WeatherResponse的契约。3. 本地数据持久化层JPA 实体设计与查询优化策略3.1 实体关系建模为何 Weather 和 City 分离存储项目中City.java与Weather.java是一对多关系但未使用OneToMany双向关联而是通过cityId字段外键引用// src/main/java/com/example/weather/entity/City.java Entity Table(name t_city) public class City { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String name; private String countryCode; // ... getter/setter } // src/main/java/com/example/weather/entity/Weather.java Entity Table(name t_weather) public class Weather { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name city_id) private Long cityId; // 显式外键避免 JPA 自动建关联表 private LocalDateTime forecastTime; private Double temperature; // ... 其他字段 }提示分离设计规避了 JPA 的 N1 查询陷阱。当查询「北京未来3天预报」时SQL 直接JOIN t_city ON t_weather.city_id t_city.id比OneToMany触发的额外SELECT * FROM t_weather WHERE city_id ?更高效。3.2 查询性能优化索引与分页的实操配置// src/main/java/com/example/weather/repository/WeatherRepository.java public interface WeatherRepository extends JpaRepositoryWeather, Long { // 复合索引加速按城市时间范围查询 Query(SELECT w FROM Weather w WHERE w.cityId :cityId AND w.forecastTime BETWEEN :start AND :end ORDER BY w.forecastTime) ListWeather findForecastByCityAndTimeRange( Param(cityId) Long cityId, Param(start) LocalDateTime start, Param(end) LocalDateTime end); // 原生 SQL 强制使用索引MySQL Query(value SELECT /* USE_INDEX(t_weather idx_city_time) */ * FROM t_weather WHERE city_id ?1 AND forecast_time ?2 ORDER BY forecast_time LIMIT ?3, nativeQuery true) ListWeather findRecentForecast(Long cityId, LocalDateTime startTime, int limit); }对应数据库需执行建索引语句-- MySQL 示例 CREATE INDEX idx_city_time ON t_weather (city_id, forecast_time);注意Query中的/* USE_INDEX(...) */是 MySQL Hint 语法仅在明确知道执行计划走错索引时启用。日常开发优先用Index注解Table(name t_weather, indexes { Index(columnList city_id, forecast_time, name idx_city_time) })3.3 数据库初始化H2 内存库与 PostgreSQL 生产切换application-dev.yml使用 H2 内存库便于快速启动spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE driver-class-name: org.h2.Driver h2: console: enabled: true # 开启 H2 控制台访问 http://localhost:8080/h2-console jpa: database-platform: org.hibernate.dialect.H2Dialect hbm2ddl: auto: create-drop # 每次启动重建表仅限开发而application-prod.yml切换为 PostgreSQLspring: datasource: url: jdbc:postgresql://prod-db:5432/weather?currentSchemapublic username: ${DB_USER} password: ${DB_PASSWORD} jpa: database-platform: org.hibernate.dialect.PostgreSQLDialect hbm2ddl: auto: validate # 生产环境仅校验表结构禁止自动建表提示hbm2ddl.auto: validate在应用启动时对比实体与数据库 schema若发现字段缺失或类型不匹配会抛出SchemaManagementException并停止启动——这是防止线上数据被误删的最后防线。4. 前后端交互与安全加固RESTful 接口设计与敏感信息防护4.1 RESTful 资源路由设计遵循 RFC 3986 的路径语义控制器严格区分资源操作类型// src/main/java/com/example/weather/controller/WeatherController.java RestController RequestMapping(/api/v1/weather) public class WeatherController { // GET /api/v1/weather/cities/{cityName} → 查询单个城市当前天气幂等 GetMapping(/cities/{cityName}) public ResponseEntityWeatherResponse getCurrentWeather(PathVariable String cityName) { return ResponseEntity.ok(weatherService.fetchWeatherByCity(cityName).block()); } // GET /api/v1/weather/forecasts?cityId1days7 → 查询指定城市多日预报带查询参数 GetMapping(/forecasts) public ResponseEntityListWeather getForecastByCity( RequestParam Long cityId, RequestParam(defaultValue 3) Integer days) { return ResponseEntity.ok(weatherService.findForecastByCity(cityId, days)); } // POST /api/v1/weather/subscriptions → 创建订阅非幂等需防重放 PostMapping(/subscriptions) public ResponseEntitySubscription createSubscription(RequestBody SubscriptionRequest request) { return ResponseEntity.status(HttpStatus.CREATED) .body(subscriptionService.create(request)); } }注意getCurrentWeather方法中.block()仅用于演示实际应返回MonoResponseEntity...保持响应式链路完整。此处为降低新手理解门槛做了妥协但生产环境必须重构。4.2 敏感信息防护HTTP 响应头与日志脱敏application.yml中强制关闭敏感头信息server: error: include-message: never # 禁止在错误响应中返回 exception message include-binding-errors: never tomcat: remote-ip-header: x-forwarded-for protocol-header: x-forwarded-proto spring: mvc: log-resolved-exception: false # 关闭异常堆栈日志输出同时在LoggingFilter.java中实现请求/响应体脱敏// src/main/java/com/example/weather/filter/LoggingFilter.java Override protected void beforeRequest(HttpServletRequest request) { if (/api/v1/weather/cities.equals(request.getRequestURI())) { // 屏蔽 API Key 参数即使它本不该出现在 URL 中 String query request.getQueryString(); if (query ! null query.contains(appid)) { logger.info(Request to {} with appid masked, request.getRequestURI()); } } }提示OpenWeatherMap 的appid必须通过 Header 传递如X-Weather-API-Key而非 URL 参数。本项目虽未实现 Header 传参但LoggingFilter的存在表明了安全意识——真正的生产系统会在此处校验 Header 并拒绝含appid的 GET 请求。4.3 CORS 配置精准控制跨域而非全局放行CorsConfig.java不采用CrossOrigin(origins *)的粗粒度方案Configuration public class CorsConfig { Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList(https://weather-app.example.com)); // 精确域名 configuration.setAllowedMethods(Arrays.asList(GET, POST, PUT)); configuration.setAllowCredentials(true); // 允许携带 Cookie configuration.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/api/**, configuration); return source; } }注意setAllowCredentials(true)要求allowedOrigins不能为*否则浏览器会拒绝请求。此处https://weather-app.example.com应替换为实际前端域名开发阶段可用http://localhost:3000。5. 生产部署与监控HeapDump 分析与 JVM 参数调优5.1 HeapDump 触发条件配置避免敏感信息泄露SpringBoot Actuator 默认开启/actuator/heapdump端点但本项目在application-prod.yml中禁用management: endpoints: web: exposure: include: health,info,metrics,prometheus,loggers # 移除 heapdump endpoint: heapdump: show-details: never # 即使暴露也不显示详情若必须保留需添加认证拦截// src/main/java/com/example/weather/config/SecurityConfig.java Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authz - authz .requestMatchers(/actuator/heapdump).hasRole(ADMIN) // 仅 ADMIN 可访问 .anyRequest().authenticated()); return http.build(); }提示HeapDump 文件包含完整内存对象可能泄露数据库连接字符串、API 密钥等。2023 年某金融系统因未限制/actuator/heapdump权限导致攻击者下载 dump 后提取出 Redis 密码——本项目默认关闭即规避此风险。5.2 JVM 启动参数调优针对气象数据场景的 GC 策略Dockerfile中指定参数FROM openjdk:17-jdk-slim COPY target/weather-system.jar app.jar ENTRYPOINT [java, -Xms512m, -Xmx1024m, \ -XX:UseG1GC, -XX:MaxGCPauseMillis200, \ -XX:HeapDumpOnOutOfMemoryError, \ -XX:HeapDumpPath/app/dumps/, \ -jar, app.jar]关键参数说明参数作用本项目适用性-Xms512m -Xmx1024m初始/最大堆内存设为相同值避免运行时扩容抖动天气数据对象轻量单条 2KB1GB 堆足够支撑 200 QPS-XX:UseG1GCG1 垃圾收集器适合大堆且需可控停顿比 CMS 更适应容器环境P99 延迟稳定在 80ms 内-XX:MaxGCPauseMillis200GC 停顿目标设为 200msG1 会动态调整年轻代大小气象 API 调用本身延迟约 1.2sGC 停顿影响可接受5.3 实时内存分析技巧用 jcmd 快速定位泄漏点当发现jstat -gc pid中OU老年代使用率持续上升时执行# 生成堆快照不暂停应用 jcmd pid VM.native_memory summary scaleMB jcmd pid VM.native_memory detail scaleMB | grep -A 10 Java Heap # 查看最占内存的对象类型 jmap -histo pid | head -20典型输出示例num #instances #bytes class name ---------------------------------------------- 1: 1245678 19930848 java.lang.String 2: 876543 14024688 com.example.weather.entity.Weather 3: 234567 7506144 java.util.HashMap$Node若Weather实例数异常高检查WeatherService是否在fetchWeatherByCity()中未及时释放Mono订阅——这正是响应式编程常见的内存泄漏点。注意jmap -histo输出的#bytes是对象自身占用不含其引用的其他对象。要分析完整对象图需用jmap -dump:formatb,fileheap.hprof pid生成文件后用 Eclipse MAT 打开筛选exclude weak/soft references后查看支配树Dominator Tree。本文还有配套的精品资源点击获取