Spring Boot+Spark气象数据分析毕设全流程实战指南 我最近在毕设资源库里翻项目发现各种“Spring Boot Spark”的组合标题特别多比如“基于Spark的西南天气数据的分析与应用”几乎每隔几页就能看到一次。这个现象背后其实有很实际的原因Spring Boot负责业务接口Spark负责批量数据分析一个偏工程落地、一个偏数据处理两者缝在一起既有技术深度又能做得完、讲得清非常适合作为本科毕业设计的选题。今天我就拿这个题目当例子完整拆解一下这类项目从选题分析、环境搭建、数据清洗、分析建模到可视化展示的全过程。这篇文章不是让你去下载一个源码包然后跑一下就完事而是把每个环节的设计思路和踩坑点都说清楚帮你真正能复现、能答辩、能扩展。1. 选题拆解这个毕设到底在做什么1.1 一眼看懂项目的技术栈和核心任务这个项目的核心关键词有三个西南天气数据、Spark、Spring Boot。西南天气数据指的是以成都、重庆、昆明、贵阳、拉萨等西南主要城市为代表的气象观测数据常见字段包括日期、城市、最高气温、最低气温、平均气温、降雨量、相对湿度、风速、天气现象等。数据来源可以是公开气象数据集、老师提供的模拟数据或者通过一些天气接口批量采集。Spark负责对这批历史天气数据进行离线分析比如按月统计各城市温度变化趋势、对比不同城市降雨量差异、基于温湿度做气候类型聚类等。分析结果会写入MySQL这样的关系型数据库。Spring Boot则负责把分析结果暴露成RESTful接口供前端网页调用展示。也就是说Spark是“幕后计算”的角色Spring Boot才是用户实际接触到的系统入口。这个选题最大的优点是技术栈非常“标准”Spring Boot做Web服务、Spark做分布式计算、MySQL做持久化、ECharts做可视化每个环节都有成熟解决方案组合起来又是一个完整的大数据应用闭环特别适合用来展示学生的工程能力和数据分析思维。1.2 为什么选“西南天气数据”作为切入点做毕设最怕的就是选题“假大空”。如果题目是“基于Spark的数据分析平台”听上去很宽泛但答辩时老师一问“你分析了什么数据、得出了什么结论”很容易答不上来。而“西南天气数据”把数据域缩得非常具体有了具体数据分析目标就清晰了。西南地区本身也很适合做气象数据分析地形从盆地到高原纵向跨度大气候类型多样同样是冬天成都湿冷而昆明温暖这种差异化对比在可视化图表上非常直观。用真实数据进行分析能得出“昆明四季如春、重庆夏季高温天数多、拉萨昼夜温差大”这类有现实意义的结论比虚构的模拟数据更有说服力。数据量级也很合适。按20个城市、每城市每天一条记录、连续存10年计算也就7万条左右单机Spark完全可以高效处理。如果换成全国数据或更细粒度的站点数据数据量上去了但清洗和存储成本也上去了对毕设来说反而容易失控。1.3 系统整体架构与数据流向整个系统的数据流可以用一句话概括原始天气数据文件 → Spark离线清洗与统计 → 结果入库MySQL → Spring Boot查询接口 → 前端ECharts可视化。具体拆成四层看数据层CSV或JSON格式的原始天气数据文件也可以直接从公开API批量拉取后落盘。计算层Spark读取文件执行数据清洗去重、缺失值填充、异常值剔除和统计分析聚合、排序、聚类。服务层Spring Boot提供城市天气概览、趋势对比、分析报告等接口同时用定时任务周期性地触发Spark任务更新数据。展示层前端页面通过HTTP请求获取后端接口数据渲染成折线图、柱状图、热力图和地图。这里有一个很多新手容易搞混的点Spark并不是常驻在Spring Boot进程里的而是独立提交的离线任务。Spring Boot工程和Spark任务的关系不是“调一个方法”那么简单而是通过命令行提交、定时调度或者调用工具类的方式把Spark任务的结果落到数据库再由Spring Boot去读库。这个设计决定了项目的整体结构别搞反了。2. 环境准备与开发基础搭建2.1 JDK、Maven、Scala 版本怎么选做这种项目最常见的问题就是版本不一致官网文档和博客教程各说各话照着配了一下午最后项目启动直接报错。我在这个项目上最终采用的是这套相对稳妥的组合JDK 8Spring Boot 2.x和Spark 3.x都兼容网上资料最全遇到问题还能搜到答案。Maven 3.6管理依赖用的版本不要太老。Spark 3.3.1选这个版本是因为它对JDK 8支持非常成熟同时也支持Spark SQL的高级功能。Spring Boot 2.7.x和Spark 3.x的Jackson依赖冲突最小后面我专门写一节讲这个坑。Scala 2.12.15Spark 3.3系列编译用的就是Scala 2.12本地写Spark代码时依赖Scala库要对应上。有条件的话建议用IDEA开发装上Scala插件Spark代码最好做成一个独立的Maven模块和Spring Boot模块分开。这样依赖隔离更干净提交任务的时候也能单独打包不用把整个Web应用一起打进去。2.2 Spark 开发环境的两种玩法本地模式与集群模式新手入门建议先用本地模式也就是把Spark跑在IDEA里通过master(local[*])指定用本机所有可用线程来执行。这种方式不需要搭建任何集群调试时还支持断点非常适合开发阶段。val conf new SparkConf() .setAppName(LocalWeatherAnalysis) .setMaster(local[*]) val spark SparkSession.builder() .config(conf) .enableHiveSupport() // 如果不需要访问Hive可以去掉 .getOrCreate()等本地调通之后再考虑提交到Spark Standalone集群。Standalone是Spark自带的集群模式不需要额外装Hadoop生态对毕设来说够用了。集群搭建的核心是配置spark-env.sh、workers文件然后在主节点执行start-master.sh和start-workers.sh。这里有个经验如果只是演示本地模式完全够用但如果答辩时想强调“分布式计算”的能力还是提前录好集群模式下的运行日志和监控截图现场演示集群容易翻车。2.3 初始化 Spring Boot 工程与核心依赖Spring Boot工程用IDEA的Spring Initializr创建依赖选Spring Web、MyBatis、MySQL Driver。如果要显眼一点可以加上Redis做热点数据缓存。pom.xml里核心依赖大致是这样dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency这里特别提醒一下如果后面还要在Spring Boot工程里直接写Spark代码进行联调就需要把spark-core、spark-sql也加进依赖但提交运行的时候不要把Spring Boot的依赖打进去否则会因为依赖冲突或者包体积暴增出现各种问题。最干净的做法是Spark分析模块单独一个工程Spring Boot只负责读取结果数据。3. 天气数据的采集与预处理决定分析质量的前置环节3.1 西南天气数据从哪来、长什么样毕设常用的天气数据来源有两类。第一类是公开数据平台下载的历史气象数据通常是CSV格式包含日期、站点编号、气温、降水、风速、相对湿度等字段数据质量高但可能需要筛选出西南区域的站点。第二类是调用免费的天气API按城市抓取这种方法更灵活但要注意接口的调用频次限制建议批量抓取一次后存成文件不要每次分析都现场拉取。我这里的操作建议是把原始文件放在/data/weather/raw/目录下文件名带上日期后缀比如weather_chengdu_2020.csv这样后续做增量更新时方便识别。一份典型的CSV数据大概长这样city,date,temp_max,temp_min,temp_avg,precipitation,humidity,wind_speed,weather 成都,2020-01-01,10,4,7,0.0,76,1.2,阴 成都,2020-01-02,8,3,5,2.5,88,1.8,小雨这里要注意原始数据通常不是完美的。比如某个站点某一天没有观测记录气温字段可能是空值或者降雨量用-999这种特殊值表示缺测甚至可能出现日期重复、城市名称大小写不统一的问题。这些都需要在Spark任务里完成清洗。3.2 清洗规则与异常值处理在Spark里做数据清洗我习惯用DataFrame API因为可以直接基于列名操作代码可读性高。清洗主要分四步第一步读取原始数据时指定schema避免字段类型推断出错。val schema StructType(Array( StructField(city, StringType, nullable false), StructField(date, StringType, nullable false), StructField(temp_max, DoubleType, nullable true), StructField(temp_min, DoubleType, nullable true), StructField(precipitation, DoubleType, nullable true) )) val rawDF spark.read .option(header, true) .schema(schema) .csv(/data/weather/raw/)第二步去重。按城市和日期两个字段判定重复保留第一条。val dedupDF rawDF.dropDuplicates(city, date)第三步处理缺失值。气温类的字段用同城市、同月份的均值填充降雨量缺失则按0处理因为降雨量平均到日维度大多为0用均值填充反而会带来偏差。第四步剔除异常值。比如气温绝对值超过50就认为是传感器异常降雨量为负也直接过滤。可以先用describe()方法看看每列的最大最小值结合西南地区实际情况设定合理阈值。清洗完成后把结果写成一个新的Parquet文件或者直接写入MySQL的weather_clean表。这一步很关键后续所有分析都基于清洗后的数据不要再回到原始CSV反复读取。3.3 数据存储设计MySQL 表结构怎么设计MySQL里至少需要四张核心表设计如下city_info城市基本信息表字段有city_id、city_name、province、longitude、latitude。weather_clean清洗后的天气数据明细表字段有city_id、date、temp_max、temp_min、temp_avg、precipitation、humidity、wind_speed、weather。weather_analysis_result分析结果表存各分析任务的输出比如按月平均气温、城市降雨总量等字段可以设计成analysis_type、city_id、stat_date、metric_name、metric_value。analysis_task_logSpark任务执行日志表用来记录每次分析任务的启动时间、结束时间、状态和结果行数。表结构设计的时候注意一点weather_analysis_result是典型的长表结构虽然看起来行数多但扩展性非常好。后续新增分析指标时不需要改表结构只要往analysis_type里加新类型就行。前端查询时用analysis_type city_id stat_date组合条件过滤性能也够用。4. 核心分析功能设计与实现4.1 用 Spark 做温度趋势统计温度趋势分析是这个项目的标配功能实现起来不算复杂但能体现对Spark SQL的掌握程度。分析目标很明确按城市、按月份统计平均最高气温、平均最低气温、平均气温并计算出同比变化。val resultDF cleanDF .withColumn(year, year(col(date))) .withColumn(month, month(col(date))) .groupBy(city, year, month) .agg( round(avg(temp_max), 1).alias(avg_temp_max), round(avg(temp_min), 1).alias(avg_temp_min), round(avg(temp_avg), 1).alias(avg_temp) ) .orderBy(city, year, month)这段代码的核心是groupBy加agg的组合。这里我给一个建议分析逻辑尽量用Spark SQL来写而不是用RDD算子硬算。因为Spark SQL自带Catalyst优化器能自动做谓词下推、列剪枝处理小型数据时性能差异不明显但代码层面简洁很多答辩时也很好解释。分析结果写回MySQL时可以用df.write.mode(SaveMode.Overwrite).jdbc(...)但我更推荐把结果先转成DataFrame再批量写入或者干脆用foreachPartition做批量插入避免小文件过多和连接频繁开销。4.2 用 Spark 做城市气候聚类分析聚类分析是让项目“上档次”的功能。传统的统计只是算平均值而聚类能从数据本身出发把城市按气候特征分成几类比如“冬冷夏热型”“四季如春型”“高原温差型”结论非常直观。实现聚类有两种路径。一种是直接用Spark MLlib的KMeans算法把城市的月均气温、月均降水等特征向量化后聚类。另一种是自己写简单的规则分类比如根据年均温差、年降水量阈值给城市打标签。如果要用KMeans关键步骤是先做特征向量化import org.apache.spark.ml.feature.VectorAssembler import org.apache.spark.ml.clustering.KMeans val featureDF cityFeatureDF .select(city, avg_temp_annual, avg_temp_range, annual_precipitation, avg_humidity) val assembler new VectorAssembler() .setInputCols(Array(avg_temp_annual, avg_temp_range, annual_precipitation, avg_humidity)) .setOutputCol(features) val vectorDF assembler.transform(featureDF) val kmeans new KMeans().setK(3).setSeed(2024L) val model kmeans.fit(vectorDF) val clusteredDF model.transform(vectorDF)聚类的K取值不要太大西南城市本身不多K3或4就够了。聚类完成之后一定要回看每个簇包含哪些城市给每个簇赋予一个业务含义这才是数据分析和“只会跑代码”的区别。4.3 分析结果如何接入 Spring Boot 接口Spark分析完成后分析结果表里已经有聚合好的数据Spring Boot这边就变得非常简单了。典型的接口是这样设计的GET /api/weather/city/list返回城市列表。GET /api/weather/trend?cityId1metricavgTemp返回某城市指定指标的趋势数据。GET /api/weather/compare?cities1,2,3year2023多城市指标对比。GET /api/weather/cluster返回城市聚类结果。Controller层只做参数校验和结果封装业务逻辑全部放到Service层。这里有个很实用的小技巧分析结果表的数据是“算一次查很多次”的类型非常适合加缓存。用Spring Cache配上Redis查询接口的响应时间能从几百毫秒降到个位数毫秒。GetMapping(/trend) Cacheable(value weather:trend, key #cityId : #metric) public ResultListWeatherTrendVO trend(RequestParam Integer cityId, RequestParam String metric) { return Result.success(weatherService.getTrend(cityId, metric)); }注意缓存KEY设计要考虑数据刷新策略。如果Spark任务是每天凌晨跑一次那么Redis缓存失效时间设置为12小时或24小时都行确保用户看到的数据不会过期太久。4.4 定时调度与数据刷新毕设里如果只有手动触发分析其实也能通过验收但加一个定时调度能力会让项目完整度上一个档次。实现方式有两类。第一类是Spring自带的Scheduled注解在服务端定时调用Spark提交脚本。例如每天凌晨2点刷新数据Component public class WeatherAnalysisTask { Scheduled(cron 0 0 2 * * ?) public void runSparkAnalysis() { // 调用Shell脚本提交Spark任务 Process process Runtime.getRuntime() .exec(sh /opt/scripts/submit_weather_analysis.sh); } }第二类是纯命令行。把Spark任务打成Jar包后用spark-submit命令提交./bin/spark-submit \ --class com.example.WeatherAnalysisJob \ --master spark://localhost:7077 \ --executor-memory 2g \ weather-analysis.jar \ --date 2026-01-01我自己的经验是提交脚本里把日期参数传进去这样重跑某一天的数据时不用改代码。同时每次任务启动和结束时都要记录日志状态方便排查问题。5. 可视化展示与应用落地5.1 后端接口的 VO 设计与返回格式统一可视化的前提是后端返回的数据结构足够清晰。很多毕设项目的前后端对接出问题本质上就是VO设计太随意前端不知道拿到的JSON结构是什么。我建议所有接口统一返回这样的格式{ code: 200, message: success, data: { cityName: 成都, trendData: [ { month: 2023-01, value: 7.5 }, { month: 2023-02, value: 9.2 } ] } }统一封装一个ResultT类泛型里放真实业务数据。这样前端写图表渲染时只需要关注data字段其他元信息不用管。为了减少重复代码可以用MyBatis写一个针对weather_analysis_result表的通用查询方法按analysis_type和city_id过滤后映射成VO对象。5.2 前端图表选型与页面搭建如果不想做复杂的前后端分离工程直接用常规的Vue 3加ECharts是最稳妥的方案。ECharts对折线图、柱状图、热力图、地图都能渲染而且网上有大量示例源码可以借鉴。首页大屏建议放四个核心模块左上展示西南主要城市近一个月平均气温的折线变化趋势。右上展示各城市年降雨量对比柱状图。左下展示城市气候聚类结果的散点图。右下展示一个带地图的站点分布或者气温热力图。图表渲染的核心是接收后端返回的JSON数据然后用setOption填充到图表里。这里有一个常见的坑如果后端返回的日期格式不统一比如一个是2023-01另一个是2023/01ECharts的坐标轴就会错位。最稳的做法是在后端就统一格式化好前端不做二次转换。6. 常见坑位与排查实录6.1 Spark 与 Spring Boot 的依赖冲突问题这个坑我踩了整整两天印象极其深刻。Spring Boot 2.7默认依赖Jackson 2.13.x而Spark 3.3自带Jackson 2.12.x。当你在同一个工程里同时引入spark-core和spring-boot-starter-web时Maven依赖仲裁可能选错版本运行时报出各种NoSuchMethodError。解决办法是把两个模块彻底分离Spark分析工程单独打包Spring Boot工程只通过读取数据库或调用Shell脚本与Spark交互。如果坚持要在Spring Boot里直接调用Spark API做联调就要在pom里显式排除Spark传递的Jackson依赖并统一Jackson版本。6.2 Spark 内存溢出与分区调优本地模式处理几万条数据一般不会OOM但如果你把数据量放大到几年全国数据或者集群模式下执行内存设置不当很容易报java.lang.OutOfMemoryError或Container killed by YARN for exceeding memory limits。排查思路是先看任务日志里是Driver还是Executor出了问题。Driver内存不够就调spark.driver.memoryExecutor内存不够就调spark.executor.memory和spark.executor.cores。如果数据倾斜严重比如某个城市的记录数远多于其他城市需要考虑对city字段做盐值加扰后分多个分区处理。我的处理经验是像天气数据这种维度不多、数据量不大的场景设置spark.sql.shuffle.partitions为4~8个就足够了不要盲目调大分区太多反而增加调度开销。6.3 本地跑得通打成Jar提交集群就报错这个经典问题通常是打包方式不对。用maven-shade-plugin把所有依赖打成一个大Jar听起来省事但实际上Spark本身自带的类会和Jar里的类冲突。正确做法是使用maven-assembly-plugin或者直接在IDEA里配置provided作用域让Spark相关依赖在提交时由集群环境提供。判断方法很简单本地运行时去掉--master local[*]改提交到集群如果报ClassNotFoundException优先检查是否是打包的时候把Spark自带类也打进去了。6.4 前端图表不显示数据的定位方法图表没数据时不要先去改样式打开浏览器F12看Network请求的响应体。第一步确认接口是否返回200第二步看JSON的code是不是200第三步看data字段是不是空数组或空对象。这三个节点的排查逻辑要刻在脑子里能解决80%的前端调试问题。7. 答辩角度与项目扩展思路7.1 把“数据分析闭环”讲清楚答辩时老师最在意的不是代码写了多少行而是你清不清楚自己在做什么。建议准备一张“数据流转示意图”从原始数据到清洗后数据再到分析结果再到可视化展示每一步说明输入是什么、输出是什么、为什么这么做。特别是Spark部分要能回答这几个高频问题Spark和传统Java集合处理数据的区别是什么分布式、内存计算、懒执行RDD、DataFrame、Dataset有什么区别DataFrame有schema信息Catalyst能优化执行计划为什么不用Hadoop MapReduce而用Spark中间结果落内存迭代计算更快如何保证分析结果正确用SQL跑一遍同口径数据对比7.2 项目还可以怎么扩展如果时间充裕有几个扩展方向性价比很高。第一个方向是引入实时流处理比如对接天气API的实时数据用Spark Streaming或Flink做实时温度监测告警。第二个方向是加机器学习预测用历史气温数据训练一个ARIMA或Prophet模型预测未来7天的气温趋势。第三个方向是增加用户权限和收藏功能把系统从纯展示升级成多用户平台。这三个方向里我建议优先做预测功能因为它和“分析”的契合度最高而且能在答辩现场演示“输入历史数据、输出预测曲线”的效果比较容易得高分。最后再分享一个做毕设的小习惯每天跑完Spark任务后把控制台里关键的统计日志复制到一个logs文本里保存起来。等写毕业论文的时候你会发现这些日志截图、运行时间、数据量级的记录比临时去重新跑一遍数据要值钱得多。别问我是怎么知道的都是我当年熬了几个通宵换来的经验。