
简介面向Java后端与数据可视化学习者这份源码实现了一个基于ECharts的疫情分析系统采用Spring Boot搭建后端服务、MyBatis操作MySQL数据库配合Jsoup等工具自动爬取疫情数据最终通过ECharts以折线图、柱状图、地图等形式动态展示确诊、治愈、死亡等趋势与统计结果适合课程设计、毕业设计或数据分析入门实践。整个压缩包共106个文件约10MB包含28个Java源文件、13个JavaScript脚本、13个XML配置、10个CSS样式以及SQL初始化脚本、HTML页面和少量图片资源目录划分清晰便于按功能模块理解前后端交互与数据流转。目前已有1275人学习下载。借助这套源码可完整走通数据采集、清洗入库、接口开发到前端可视化的全链路掌握Spring Boot自动配置、MyBatis动态SQL、ECharts图表封装等关键技能并能为二次开发疫情数据看板或相关可视化项目提供可直接修改的工程模板。1. 疫情数据可视化大屏为什么值得花一个下午把它跑通疫情分析系统在毕业设计里满天飞但大多数源码下载下来就只有一堆Controller和静态页面连数据都拿不出手。这份Java基于ECharts的数据可视化疫情分析系统给了我一个完整闭环Jsoup定向抓取公开页面数据MyBatis写入MySQLSpring Boot暴露REST接口前端ECharts多图渲染。项目文件名Covid-19-Data-Showing-master从爬虫到图表一条链路是通着的不是只有展示壳子。适合三类人Java后端想补前端可视化技能的毕设需要可运行项目的学生以及工作中要快速搭数据看板的开发者。我拆完源码后发现它的价值不在“疫情”二字而在“数据抓取、入库、接口、可视化”这条标准流水线完全可迁移。你换一套数据源换几张图表就是一个新项目。2. 技术栈拆解Spring Boot、MyBatis、MySQL、ECharts各自扛哪一块2.1 为什么是这四件套而不是JSP和Servlet老式Java Web项目用JSP加原生Servlet就能做页面但疫情数据大屏讲究的是页面局部刷新和图表动态渲染JSP每次整页刷新体验和开发效率都不行。ECharts是前端库专门解决图表问题Spring Boot则把Servlet那套繁琐配置全部内嵌——内嵌Tomcat、自动装配、Starter依赖管理让一个后端项目从零到能启动只需要一个主类和几个配置项。MyBatis干的活是Java对象和SQL之间的映射。疫情数据查询多数是“按日期、按地区、按状态”多条件组合MyBatis的动态SQL标签可以免去拼接字符串的脏活比如if标签能在某个查询条件为空时自动跳过该片段。MySQL则负责存储和聚合三层结构一搭后端就稳定了。技术选型对比方案后端数据访问前端图表是否推荐原生JSP ServletServletJDBCJSP内置表格不推荐开发慢维护难Spring Boot MyBatis MySQL EChartsSpring BootMyBatisECharts推荐前后端分离清晰Vue Spring BootSpring BootMyBatisECharts也可行但上手门槛更高这套源码选的是第二种对Java开发者最友好ECharts通过一个HTML页面引入不碰Node.js也不碰Webpack后端只需要输出JSON前端用原生Ajax拉取整个链路没有一处需要额外构建工具。2.2 源码目录核心结构从Covid-19-Data-Showing-master看项目组织项目使用Maven管理压缩包里能看到mvnw和mvnw.cmd这是Maven Wrapper脚本。它的作用是锁定Maven版本换一台机器执行./mvnw spring-boot:run就能自动下载对应版本Maven并启动项目不需要全局预装Maven。目录上是一套标准Spring Boot分层结构Covid-19-Data-Showing-master ├── mvnw / mvnw.cmd ├── pom.xml ├── src/main/java/com/example/... │ ├── controller/ # REST接口 │ ├── service/ # 业务逻辑 │ ├── mapper/ # MyBatis的Mapper接口 │ ├── entity/ # 对应数据表的实体类 │ └── Covid19Application.java # 主启动类 └── src/main/resources/ ├── application.yml # 数据源、端口配置 └── mapper/*.xml # MyBatis动态SQL文件主启动类是带有SpringBootApplication注解的Covid19Application它同时集合了Configuration、EnableAutoConfiguration和ComponentScan三个注解的功能。浏览器访问时请求先进ControllerController调用Service接口Service再通过Mapper接口进数据库拿到数据拼成JSON返回给前端ECharts拿到JSON后渲染图表。启动前先确认pom.xml里有spring-boot-starter-web、mybatis-spring-boot-starter和mysql-connector-java三组依赖。缺少任何一个启动阶段都会直接报错。2.3 数据模型设计疫情表字段与查询维度疫情趋势分析需要支撑两种典型查询某地区一段时间的累计确诊走势以及某一天全国各地区的数据对比。MySQL表建议至少设计以下字段CREATE TABLE covid_data ( id BIGINT AUTO_INCREMENT PRIMARY KEY, area VARCHAR(50) NOT NULL COMMENT 地区名称, province VARCHAR(50) DEFAULT NULL COMMENT 省份/国家, confirmed INT NOT NULL DEFAULT 0 COMMENT 累计确诊, cured INT NOT NULL DEFAULT 0 COMMENT 累计治愈, dead INT NOT NULL DEFAULT 0 COMMENT 累计死亡, suspected INT NOT NULL DEFAULT 0 COMMENT 现存疑似, update_time DATETIME NOT NULL COMMENT 数据抓取时间, UNIQUE KEY uk_area_time (area, update_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT疫情数据表;uk_area_time联合唯一索引有两个实际作用一是防爬虫重复执行时产生相同时间点的重复记录二是让INSERT ... ON DUPLICATE KEY UPDATE语句能稳定触发更新逻辑。查询趋势时配合update_time字段做范围过滤配合area字段做分组聚合索引都能命中。这里一个容易漏掉的细节是update_time不能直接用DATE类型因为一天里可能爬取多次只精确到天会导致一天多条记录覆盖混乱。用DATETIME保留时分秒前端做趋势图时再按天聚合展示。3. 数据管道实操Jsoup爬虫采集、清洗与MySQL动态SQL入库3.1 为什么爬虫选Jsoup而不是Selenium疫情数据一般挂在公开统计页面这类页面多数是服务端渲染的静态表格或列表。Jsoup是Java里最常用的HTML解析器提供类似jQuery的选择器语法几行代码就能从HTML中抽取目标字段。Selenium虽然能处理JavaScript动态渲染的页面但必须额外启动浏览器驱动占资源不说部署到服务器上还容易因为缺headless模式配置而失败。我的判断方式是目标页面直接用浏览器“查看网页源代码”能找到数据就选Jsoup必须等JS异步加载才能看到数据的才考虑Selenium。这份源码里的公开数据页面属于前者用Jsoup是合理选择。3.2 爬虫抓取的核心代码HttpClient拿页面Jsoup解析DOM实际操作中完整流程分两步先用HttpClient发送请求拿回HTML字节流再用Jsoup去解析。HttpClient的优势是能控制请求头、超时时间、代理等细节比Jsoup自带的connect()更接近真实浏览器的请求环境。public ListCovidData fetchFromRemote() throws IOException { // 1. 用HttpClient模拟浏览器请求带上User-Agent和Referer CloseableHttpClient client HttpClients.custom() .setUserAgent(Mozilla/5.0 (Windows NT 10.0; Win64; x64)) .setConnectionTimeToLive(10, TimeUnit.SECONDS) .build(); HttpGet request new HttpGet(https://example-covid-stats-page.com/data); request.setHeader(Referer, https://example-covid-stats-page.com/); try (CloseableHttpResponse response client.execute(request)) { String html EntityUtils.toString(response.getEntity(), UTF-8); // 2. 用Jsoup解析HTML选择器定位到数据行 Document doc Jsoup.parse(html); Elements rows doc.select(table#listTable tbody tr); ListCovidData list new ArrayList(); for (Element row : rows) { Elements cells row.select(td); if (cells.size() 5) continue; // 过滤不完整的行 CovidData data new CovidData(); data.setArea(cells.get(0).text().trim()); data.setConfirmed(parseIntSafe(cells.get(1).text())); data.setCured(parseIntSafe(cells.get(2).text())); data.setDead(parseIntSafe(cells.get(3).text())); data.setUpdateTime(new Date()); list.add(data); } return list; } } private int parseIntSafe(String text) { try { return Integer.parseInt(text.replace(,, )); } catch (NumberFormatException e) { return 0; } }这段代码有几个关键点说明。HttpClients.custom().setUserAgent()是模拟浏览器指纹很多统计页面会拦截无User-Agent的机器请求设置后能显著降低403概率。EntityUtils.toString(response.getEntity(), UTF-8)第二个参数显式指定编码避免中文乱码——如果页面实际是GBK编码这里要改成GBK判断依据是页面meta charset标签。parseIntSafe里的replace(,, )用于处理“1,234”这种带千分位逗号的数字文本直接Integer.parseInt会抛错这是统计数据页面的常见坑。3.3 清洗与入库用ON DUPLICATE KEY UPDATE实现幂等写入爬虫拿到的是原始HTML文本入库前必须做两类清洗空字符串和“-”占位符统一替换成0地区名称字段去掉首尾空格和全角空格。清洗逻辑直接写在Service层不要放在SQL里硬处理原因是一旦SQL写复杂了索引就失效了。MySQL的写入语句我常用INSERT ... ON DUPLICATE KEY UPDATE配合联合唯一索引做到“有则更新、无则插入”!-- CovidDataMapper.xml -- insert idupsertBatch parameterTypelist INSERT INTO covid_data (area, province, confirmed, cured, dead, suspected, update_time) VALUES foreach collectionlist itemitem separator, (#{item.area}, #{item.province}, #{item.confirmed}, #{item.cured}, #{item.dead}, #{item.suspected}, #{item.updateTime}) /foreach AS new ON DUPLICATE KEY UPDATE confirmed new.confirmed, cured new.cured, dead new.dead, suspected new.suspected, update_time new.update_time /insertforeach标签里的separator,用于拼接多行VALUESMyBatis会遍历List逐个生成元组。AS new是MySQL 8.0之后支持的别名语法旧版本MySQL或MariaDB不支持如果数据库版本低于8.0需要改成VALUES(confirmed)这种旧式写法。我用这个语句的体会是批量插入5000条数据比逐条插入快一个数量级但foreach一次性拼接太长的SQL也会触达max_allowed_packet上限实测一次500条以内最稳妥。4. 后端接口与查询Controller-Service-Mapper三层怎么对外输出JSON4.1 REST接口设计趋势、分布、排行三个端点后端不需要把查询逻辑暴露给前端只需要按前端图表的消费方式设计端点。疫情大屏典型的三类图表对应三个接口趋势折线图要一条按时间序列聚合的曲线地图或柱状图要按地区分组的当前值排行榜要排序后的TOP N列表。RestController RequestMapping(/api/covid) public class CovidController { Autowired private CovidService covidService; /** 某地区累计确诊趋势返回按日期升序的列表 */ GetMapping(/trend) public ListMapString, Object trend(RequestParam String area, RequestParam(required false) String startDate, RequestParam(required false) String endDate) { return covidService.queryTrend(area, startDate, endDate); } /** 最新一天全国各地区数据分布 */ GetMapping(/latest) public ListCovidData latest() { return covidService.queryLatestGroupByArea(); } /** 累计确诊数TOP10地区排行 */ GetMapping(/top) public ListMapString, Object top(RequestParam(defaultValue 10) int limit) { return covidService.queryTop(limit); } }RequestParam(required false)让趋势接口可以不带日期参数调用不带时Service层自动回退到最近30天这个兜底逻辑避免前端少传参就报400。RequestParam(defaultValue 10)限定排行榜条数上限防止前端传一个limit100000直接把数据库拖垮。一个值得注意的接口设计细节latest接口返回的是ListCovidData实体字段名直接序列化成JSON趋势接口返回MapString, Object是因为SQL聚合结果没有对应实体类。二选一时优先用Map减少为临时结果集创建一堆VO类的成本。4.2 聚合查询SQL按时间分组和按地区分组趋势接口的SQL用DATE_FORMAT把update_time截断到天再按天聚合取每天的最后一条快照。这里有个隐含逻辑爬虫一天可能抓取多次同一天有多条记录前端折线图只需要一天一个点。如果直接按天取MAX(update_time)再连回原表可以得到当天最新值但这写法复杂。我的做法是先用子查询取每天最大时间再连接到数据表select idqueryTrend resultTypemap SELECT DATE_FORMAT(t.update_time, %Y-%m-%d) AS date, t.confirmed, t.cured, t.dead FROM covid_data t INNER JOIN ( SELECT area, MAX(update_time) AS max_time FROM covid_data WHERE area #{area} if teststartDate ! null and startDate ! AND DATE(update_time) gt; #{startDate} /if if testendDate ! null and endDate ! AND DATE(update_time) lt; #{endDate} /if GROUP BY area, DATE(update_time) ) m ON t.area m.area AND t.update_time m.max_time ORDER BY date ASC /select子查询按“地区天”分组取最大时间外查询再回表拿完整记录确保每个日期只出一条最新数据。gt;和lt;是XML文件里对和的转义不转义MyBatis会把它当成标签开始符直接解析报错。Mapper接口对应这个SQL时传一个MapString, Object参数即可Mapper方法签名是ListMapString, Object queryTrend(MapString, Object params)XML里用#{area}、#{startDate}直接引用Map的键。4.3 application.yml数据源和MyBatis关键配置历经多次跑不通之后我总结出一份能直接复用的配置模板spring: datasource: url: jdbc:mysql://localhost:3306/covid19?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl server: port: 8080serverTimezoneAsia/Shanghai必须显式指定MySQL 8.0默认时区是UTC不指定的话Java里的new Date()写入数据库会差8小时折线图的横轴日期会整体偏移一天。map-underscore-to-camel-case开启后数据库字段update_time自动映射到实体属性updateTime不用在SQL里写一长串别名。log-impl配置成StdOutImpl能直接在控制台打印完整SQL和参数排查“数据查不出来”时是最直接的工具。启动后验证接口用curl快速确认JSON结构curl http://localhost:8080/api/covid/trend?area湖北startDate2024-01-01endDate2024-02-01返回的JSON数组里每个元素应该有date、confirmed、cured、dead四个字段和ECharts的data数组一一对应。5. ECharts可视化实战折线图、柱状图、中国地图的配置与调参5.1 数据加载方式Ajax拉接口再setOptionECharts默认支持把option.series[0].data写成静态数组但实际项目里数据必须来自接口。标准做法是页面加载完成后用fetch或axios请求后端拿到数据后再执行myChart.setOption()。这个过程的先后顺序很重要myChart.init()必须先执行不然容器还没有实例而setOption()可以执行多次数据刷新时直接覆盖旧配置。script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script div idtrendChart stylewidth:100%;height:450px;/div script // 1. 初始化图表实例 var trendChart echarts.init(document.getElementById(trendChart)); fetch(/api/covid/trend?area湖北) .then(function(resp) { return resp.json(); }) .then(function(data) { // 2. 把接口返回的日期和统计值拆成两个数组 var dates data.map(function(item) { return item.date; }); var confirmed data.map(function(item) { return item.confirmed; }); // 3. 设置图表配置项 trendChart.setOption({ title: { text: 湖北省累计确诊趋势 }, tooltip: { trigger: axis }, legend: { data: [累计确诊] }, xAxis: { type: category, data: dates }, yAxis: { type: value }, series: [{ name: 累计确诊, type: line, smooth: true, areaStyle: { opacity: 0.2 }, data: confirmed }] }); }); /scripttooltip.trigger: axis对折线图最实用鼠标在图表区域内移动时展示整条竖线对应X轴位置的所有系列数值。smooth: true让折线变成曲线视觉上更平滑但要注意它并不改变数据本身。areaStyle.opacity控制折线下方渐变面积的不透明度取0.2比较淡雅数字调大后会遮挡网格线。重置表格大小。ECharts实例一旦初始化就绑定了容器宽度窗口缩放时图表不会自动跟着变必须监听window.resize事件并调用chart.resize()否则大屏适配时会出现图表溢出或留白。5.2 柱状图、中国地图、饼图的配置关键点柱状图适合展示各省当天数据对比和折线图共用一套xAxis/yAxis结构只需要把series.type改成bar。柱状图有个常见需求是柱子顶部的数值标签对应配置项是label: { show: true, position: top }。横轴标签太多时设xAxis.axisLabel.interval: 0强制显示全部省份配合rotate: 45把文字旋转45度避免互相重叠。中国地图是ECharts可视化里最容易翻车的地方。从ECharts 5开始官方包里不再内置中国地图的GeoJSON数据直接设置type: map并指定map: china会报错Map china not exists。解决方法是单独引入地图GeoJSON文件用echarts.registerMap(china, geoJson)注册后再使用。地图的series配置里visualMap组件是数据映射的核心它把数值范围映射成颜色深浅。除了地图饼图的radius参数可以控制内外半径做环形图label开启formatter可以在扇区外显示百分比和地区名。饼图数据量超过10个分类时建议把占比小于3%的分类合并成“其他”不然小扇区的标签会挤成一团。6. 避坑指南跑不起来、数据为空、图表不渲染的五个常见原因6.1 启动报错Cannot determine embedded database driver class现象运行mvn spring-boot:run时应用启动失败控制台提示找不到数据库驱动类。原因pom.xml里缺少mysql-connector-java依赖或者application.yml里的driver-class-name配置和MySQL版本不匹配。解决先在pom.xml里加入mysql-connector-java版本号跟着Spring Boot父工程走不需要显式指定。如果MySQL是5.x旧版本driver-class-name要改成com.mysql.jdbc.DriverMySQL 8.0以上才用com.mysql.cj.jdbc.Driver。改完执行mvn clean再重启避免旧依赖残留。6.2 接口返回数据但图表一片空白现象浏览器访问接口能正常返回JSON数机构都正确但页面图表区域空白。原因最常见的是setOption在fetch返回之前就执行了data数组为空其次JSON里的字段名和ECharts的data索引对不上比如后端返回date前端却读了day。解决在then回调里加console.log(data)确认字段名再映射。另一种情况是容器高度为0echarts.init拿到的是一个0像素高度的元素图表渲染出来是空的。检查div是否设置了heightECharts渲染需要容器有明确的宽高。6.3 中国地图一直显示“Map china not exists”现象折线图和柱状图正常唯独地图图表报错控制台提示注册地图不存在。原因ECharts 5.x不再内置中国地图数据必须显式注册GeoJSON。解决下载china.js文件或从阿里云DataV的GeoJSON地址引入然后执行echarts.registerMap(china, geoJson)。引入后把series里的map: china保持不变visualMap的min和max根据实际数据范围设置例如累计确诊设置min: 0, max: 50000颜色用浅色到深色渐变。6.4 爬虫抓回来全是乱码现象Java端打印爬取到的HTML字符串中文全部变成菱形问号。原因页面实际编码是GBK或GB2312但代码里统一用UTF-8解析了。解决解析前先看页面meta charset...声明然后在Jsoup.parse时传入对应编码。稳妥写法是Jsoup.parse(InputStream in, String charsetName, String baseUri)第三个参数传即可。我习惯把解析方法设计成自动探测编码——先读取前几个字节的BOM标记没有BOM就按页面的meta声明取编码这样换数据源不用改代码。6.5 批量写入数据库时提示PacketTooBigException现象调用批量upsert接口时MySQL报错Packet for query is too large。原因foreach一次性拼接了太多条记录超过MySQL的max_allowed_packet上限。解决把List按500条一批切分逐批调用mapper方法然后commit。文件里预置了批处理逻辑的话注意默认值可能设置得过大改成分段提交即可。7. 进阶把毕业设计改成生产可用的可视化大屏7.1 定时刷新从手动爬虫到Scheduled自动更新毕业设计能做到定时自动抓取就是加分项。在Spring Boot里加一个定时任务类注解就能搞定Component public class CovidScheduleTask { Autowired private CrawlerService crawlerService; Scheduled(initialDelay 5000, fixedRate 3600000) public void refreshData() { ListCovidData latest crawlerService.fetchFromRemote(); crawlerService.cleanAndSave(latest); } }initialDelay 5000表示应用启动5秒后先执行一次避免服务刚启动就立刻跑爬虫导致数据库连接还没准备好。fixedRate 3600000是每小时执行一次爬取频率不宜超过这个值——公开页面承载能力有限频繁抓取容易被封IP。定时任务跑起来后前端不用刷新页面ECharts图表再做一次setOption就能更新。7.2 大屏自适应一个resize事件解决多分辨率问题可视化大屏和普通网页不同它要在1080P甚至2K屏上都铺满整个屏幕。所有图表容器不要写死像素宽度用百分比布局然后统一监听窗口变化按比例调整。把这段逻辑写进主页面底部所有图表实例都会跟随窗口尺寸缩放。7.3 多数据源切换用配置切换而不是改代码后端接入第二个数据源时不要改代码重新编译。在application.yml里预留两个数据源连接串用一个开关切换。常见做法是用Spring Boot的Profile机制application-dev.yml指向测试库application-prod.yml指向生产库启动时--spring.profiles.activedev即可切换。这个方法在交接项目时特别实用组员本地连本地库服务器上连服务器库一套代码不用动。最后说一个我自己踩过的教训写爬虫更新逻辑时第一次偷懒没判断“页面数据是否有变化”就把旧数据全部覆盖结果网页临时出乱码数据库里的历史曲线直接被冲乱了。从那以后凡是做数据管道我都强制加一遍“先查update_time最新值只有页面数据时间新于库里的才允许写入”这种增量判断步骤。这份源码帮你把链路跑通已经完成了80%剩下20%是把数据校验和刷新节奏补上。希望帮到你。本文还有配套的精品资源点击获取