Java环境监测系统源码拆解:Servlet+Service+DAO三层架构与CO2/温度/PH采集实现
简介这份资源是面向计算机专业学生与Java初学者的一套环境监测系统完整源码可直接用于毕业设计、课程设计或项目练手帮助解决从零搭建监测类Web应用时缺少可运行参考的问题。压缩包共46个文件约29KB以35个Java源文件为核心辅以DAO层、DTO数据、Controller层、数据库设计与更新脚本及说明文档覆盖数据采集、业务处理与接口暴露等环节。代码按监控管理方案与用户管理两条主线组织包含CO2、温度、PH值等环境指标的Service、Dao与Controller实现并配有登录模块便于理解分层架构与数据库交互流程。目前已有702人学习下载适合希望借鉴目录结构、梳理后端分层写法或快速搭建环境监测原型的开发者参考。1. 环境监测系统源码拆包从 school-master 看 Java 毕业设计的真实骨架很多同学拿到“环境监测系统”这个题目时第一反应是去搜现成的 Java 源码但下载下来往往发现要么跑不起来要么代码结构混乱到没法写论文。我最近把一份名为 school-master 的 Java 环境监测系统源码完整拆了一遍它覆盖了 CO2、温度、PH 值、光照等典型环境因子的采集与展示后端用 Servlet Service DAO 三层结构数据库表设计清晰前端页面虽然不算精致但功能闭环完整。这份代码适合谁适合正在做环境监测方向毕业设计、需要一套能跑通且能讲清楚架构的 Java 项目底稿的同学。它不是什么工业级系统但作为课程设计或毕业设计的起点骨架是完整的改起来有章可循。下面我从代码结构、数据库设计、核心模块实现到部署踩坑一步步拆给你看。2. 代码结构与分层设计Servlet Service DAO 到底怎么串起来的2.1 从包结构看 MVC 的落地方式打开 school-master 的源码目录能看到典型的 Java Web 项目分层。Controller 层对应 Servlet比如PhInfoController.java、Co2InfoController.java、TemperatureInfoController.java、LoginController.java它们负责接收 HTTP 请求、解析参数、调用 Service 层并返回视图或数据。Service 层是接口与实现分离的比如PhInfoService.java定义接口PhInfoServiceImpl.java写具体逻辑。DAO 层同样如此PhInfoDao.java是接口PhInfoDaoImpl.java是 JDBC 实现。这种接口-实现分离的写法在毕业设计里很加分答辩时你可以说“面向接口编程便于替换底层存储”虽然实际替换的可能性不大但结构上是对的。实体类方面PhdataInfo、TemperaturedataInfo、Co2Info、AdminloginInfo、UserloginInfo分别对应数据库中的表。注意命名上有些混用比如PhdataInfo和PhInfo同时存在前者可能是原始数据实体后者可能是处理后的信息实体。这种命名不够统一的情况在实际项目中很常见你二次开发时最好先画一张类图理清关系。2.2 一个完整的请求链路以 PH 值查询为例假设前端要查询最新的 PH 值数据请求会先到PhInfoController它继承HttpServlet在doGet或doPost方法里调用PhInfoService的查询方法。Service 实现类里通常会做一些业务判断比如数据有效性校验、分页处理然后调用PhInfoDao的findAll或findByCondition方法。DAO 实现类里用 JDBC 连接数据库执行 SQL把结果集映射成PhInfo对象列表返回。下面是一个典型的 DAO 层查询方法我按源码风格还原了核心逻辑// PhInfoDaoImpl.java 中的查询方法 public ListPhInfo findAll() { ListPhInfo list new ArrayList(); String sql SELECT id, ph_value, collect_time, device_id FROM ph_info ORDER BY collect_time DESC; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { PhInfo info new PhInfo(); info.setId(rs.getInt(id)); info.setPhValue(rs.getDouble(ph_value)); info.setCollectTime(rs.getTimestamp(collect_time)); info.setDeviceId(rs.getString(device_id)); list.add(info); } } catch (SQLException e) { e.printStackTrace(); // 实际项目建议用日志框架 } return list; }这段代码的关键点用PreparedStatement防止 SQL 注入用 try-with-resources 自动关闭连接结果集字段名要和数据库列名一致。参数方面ph_value用double接收collect_time用Timestampdevice_id用String。如果你要加条件查询比如按时间段筛选就在 SQL 里加WHERE collect_time BETWEEN ? AND ?然后ps.setTimestamp(1, start)这样传参。2.3 数据库表设计与实体映射从源码中的实体类和 DAO 方法反推核心表至少有这几张表名对应实体关键字段用途ph_infoPhInfoid, ph_value, collect_time, device_id存储 PH 值采集记录temperature_infoTemperatureInfoid, temperature, collect_time, device_id存储温度数据co2_infoCo2Infoid, co2_value, collect_time, device_id存储 CO2 浓度admin_login_infoAdminloginInfoid, username, password管理员账号user_login_infoUserloginInfoid, username, password普通用户账号建表时注意collect_time用DATETIME类型ph_value用DECIMAL(5,2)比DOUBLE更合适因为 PH 值精度要求固定。主键用INT AUTO_INCREMENT。如果你用 MySQL 8记得在 JDBC URL 里加上serverTimezoneAsia/Shanghai否则时间会差 8 小时这个坑后面还会细说。3. 环境数据采集与展示模块CO2、温度、PH 的增删改查实现3.1 数据录入接口的编写与参数校验环境监测系统的核心功能之一是数据录入。源码中Setco2dataInfoController.java这个类名有点绕从命名看应该是“设置 CO2 数据信息”的控制器。它接收前端表单提交的 CO2 浓度值、设备编号、采集时间然后调用Co2InfoService的save方法。Service 实现里通常会先做参数校验比如浓度值不能为负、设备编号不能为空然后再调 DAO 插入数据库。下面是一个 Service 层保存方法的典型写法// Co2InfoServiceImpl.java 中的保存逻辑 public boolean save(Co2Info info) { // 参数校验浓度值必须在合理范围内 if (info.getCo2Value() null || info.getCo2Value() 0 || info.getCo2Value() 10000) { throw new IllegalArgumentException(CO2浓度值不合法); } if (info.getDeviceId() null || info.getDeviceId().trim().isEmpty()) { throw new IllegalArgumentException(设备编号不能为空); } // 补全采集时间如果前端没传就用当前时间 if (info.getCollectTime() null) { info.setCollectTime(new Timestamp(System.currentTimeMillis())); } return co2InfoDao.insert(info); }参数校验这步在毕业设计里容易被忽略但答辩时老师很可能会问“如果用户输入负数怎么办”。加上这段逻辑你就能回答“在 Service 层做了业务校验DAO 层只负责数据持久化”。co2Value的上限我设了 10000实际 CO2 浓度单位通常是 ppm室内正常值在 400-2000 之间10000 作为上限是合理的。3.2 列表展示与分页查询的实现前端展示环境数据时通常需要分页。源码里可能用了简单的LIMIT语句我建议在 DAO 层加一个通用的分页方法。下面以温度数据为例// TemperatureInfoDaoImpl.java 分页查询 public ListTemperatureInfo findByPage(int pageNum, int pageSize) { ListTemperatureInfo list new ArrayList(); String sql SELECT id, temperature, collect_time, device_id FROM temperature_info ORDER BY collect_time DESC LIMIT ? OFFSET ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, pageSize); ps.setInt(2, (pageNum - 1) * pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { TemperatureInfo info new TemperatureInfo(); info.setId(rs.getInt(id)); info.setTemperature(rs.getDouble(temperature)); info.setCollectTime(rs.getTimestamp(collect_time)); info.setDeviceId(rs.getString(device_id)); list.add(info); } } } catch (SQLException e) { e.printStackTrace(); } return list; }这里pageNum从 1 开始OFFSET的计算是(pageNum - 1) * pageSize。如果你用 MySQLLIMIT ? OFFSET ?是标准写法如果用 Oracle得用ROWNUM嵌套改起来麻烦一些。分页查询还要配一个count方法查总记录数否则前端分页组件不知道总页数。3.3 数据可视化用 ECharts 对接后端接口源码的前端部分可能用了 JSP 直接渲染表格但现代毕业设计更推荐前后端分离后端返回 JSON前端用 ECharts 画折线图。你可以在 Controller 里这样返回 JSON// TemperatureInfoController.java 中返回 JSON 数据 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ListTemperatureInfo list temperatureInfoService.findLatest(50); // 取最近50条 response.setContentType(application/json;charsetUTF-8); JSONArray jsonArray new JSONArray(); for (TemperatureInfo info : list) { JSONObject obj new JSONObject(); obj.put(time, info.getCollectTime().toString()); obj.put(value, info.getTemperature()); jsonArray.add(obj); } response.getWriter().write(jsonArray.toString()); }前端拿到 JSON 后ECharts 的xAxis.data放时间数组series.data放温度值数组。注意时间格式要统一Timestamp.toString()出来是2024-01-15 10:30:00.0ECharts 能识别但如果你要更美观可以用SimpleDateFormat格式化成HH:mm。这个接口的findLatest(50)方法需要在 DAO 里写ORDER BY collect_time DESC LIMIT 50然后 Service 层反转一下列表让时间正序。4. 避坑与常见问题排查从数据库连接到中文乱码4.1 数据库连接失败驱动、URL、时区三连坑现象项目启动后访问页面报java.sql.SQLException: No suitable driver found或者连接超时。原因一是 MySQL 驱动 jar 包没放到WEB-INF/lib下二是 JDBC URL 写错三是 MySQL 8 的时区问题。很多同学从网上抄的 URL 是jdbc:mysql://localhost:3306/school但 MySQL 8 需要加?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue。解决先确认mysql-connector-java-8.0.x.jar在 lib 目录然后检查 URL 格式。如果还报时区错误把serverTimezone改成GMT%2B8试试。我一般会在DBUtil类里加一个静态代码块加载驱动Class.forName(com.mysql.cj.jdbc.Driver)虽然 JDBC 4.0 以后可以省略但显式加载能避免一些容器环境下的玄学问题。4.2 中文乱码从 JSP 到数据库的全链路排查现象页面输入中文设备名称存到数据库变成???或者查询出来显示乱码。原因编码问题可能出现在三个环节——JSP 页面编码、Servlet 请求编码、数据库连接编码。源码里如果没统一设置很容易翻车。解决JSP 文件头部加% page contentTypetext/html;charsetUTF-8 languagejava %Servlet 里在doPost第一行加request.setCharacterEncoding(UTF-8)数据库连接 URL 加characterEncodingutf8MySQL 建库时用CREATE DATABASE school DEFAULT CHARACTER SET utf8mb4。四个地方都设了基本就不会乱码了。4.3 时间差 8 小时Timestamp 的时区陷阱现象前端显示的数据采集时间是2024-01-15 02:30但实际是10:30。原因MySQL 驱动默认用 UTC 时区解析Timestamp而国内是东八区。如果你在 URL 里没指定serverTimezone驱动就会按 UTC 处理。解决在 JDBC URL 里明确写serverTimezoneAsia/Shanghai。如果已经存进去的数据时间错了要么改 URL 后重新插入要么在 SQL 里用CONVERT_TZ函数转换。我习惯在DBUtil里统一管理 URL避免每个 DAO 各写各的。4.4 连接泄漏忘记关闭 ResultSet 和 Statement现象系统运行一段时间后报Too many connections数据库连接池耗尽。原因DAO 方法里开了Connection、PreparedStatement、ResultSet但异常发生时没有在finally块里关闭或者只关了Connection没关ResultSet。解决用 try-with-resources 语法把资源声明在 try 的括号里Java 会自动关闭。如果你用的是老版本 JDK就在finally里按ResultSet→Statement→Connection的顺序依次关闭每个关闭操作单独 try-catch避免一个异常影响后面的关闭。4.5 前端图表不显示JSON 格式与跨域问题现象ECharts 图表空白浏览器控制台报Uncaught SyntaxError: Unexpected token或者CORS policy错误。原因一是后端返回的 JSON 格式不对比如多了 BOM 头或者用了单引号二是前后端分离时端口不同浏览器拦截了跨域请求。解决后端返回前设置response.setContentType(application/json;charsetUTF-8)用JSONArray或Gson生成标准 JSON。跨域问题可以在 Servlet 里加response.setHeader(Access-Control-Allow-Origin, *)但生产环境不建议用*应该指定具体域名。如果是本地开发把前端页面和后端部署在同一个 Tomcat 下最省事。5. 二次开发与论文写作把源码变成你自己的东西5.1 功能扩展加一个“噪声监测”模块要改哪些文件假设你要在现有系统上增加噪声监测功能按分层结构需要新增或修改这些文件数据库加一张noise_info表实体类NoiseInfo.javaDAO 接口NoiseInfoDao.java和实现NoiseInfoDaoImpl.javaService 接口NoiseInfoService.java和实现NoiseInfoServiceImpl.javaControllerNoiseInfoController.java前端加一个噪声数据展示页面。如果系统有权限控制还要在菜单表里加一条记录。这套流程走下来你对 MVC 的理解会比只看书深得多。5.2 论文里的系统架构图怎么画才不空洞很多同学的论文架构图就是三层方块加箭头老师一看就知道是模板。我建议你结合源码的实际类名来画Controller 层标出PhInfoController、Co2InfoControllerService 层标出对应的接口和实现类DAO 层标出PhInfoDaoImpl等。然后在旁边注明“接口与实现分离便于单元测试和替换存储方案”。数据库设计部分把ph_info、temperature_info、co2_info的字段和类型列成表格说明每个字段的选取理由比如“ph_value 用 DECIMAL(5,2) 是因为 PH 值范围在 0-14 之间保留两位小数足够”。5.3 从源码到答辩三个必问问题的准备根据我带过的项目经验答辩时老师最可能问这三个问题第一“你的数据采集是实时的吗怎么实现的”你可以回答“当前版本是模拟数据录入但架构上预留了 WebSocket 接口后续可以对接传感器网关”。第二“数据库连接池用了吗”如果源码里没用你就说“当前用 DBUtil 直接获取连接生产环境建议换成 Druid 或 HikariCP”。第三“系统安全性怎么保证的”你可以说“登录密码做了 MD5 加密SQL 用了 PreparedStatement 防注入后续可以集成 Spring Security”。这些问题提前准备好答辩就不会慌。5.4 部署到 Tomcat 的完整步骤与验证方法把项目跑起来是第一步。用 IDEA 的话配置 Tomcat 服务器把school-master作为 Web 模块部署Application context 设为/school。启动后访问http://localhost:8080/school/login.jsp用数据库里的管理员账号登录。如果登录成功但页面样式丢失检查web.xml里的welcome-file和静态资源路径。验证数据录入功能时打开浏览器开发者工具看 Network 面板里 POST 请求的响应状态码是不是 200返回的 JSON 里success字段是不是true。我一般还会在数据库里直接SELECT COUNT(*)确认数据真的写进去了避免前端提示成功但后端没落库的假象。5.5 一个具体技巧用 MyBatis-Plus 替换 JDBC 层如果你想让代码更“现代”一点可以把 DAO 层的 JDBC 实现换成 MyBatis-Plus。以PhInfo为例先加依赖mybatis-plus-boot-starter然后写一个 Mapper 接口继承BaseMapperPhInfo实体类上加TableName(ph_info)和TableId(type IdType.AUTO)。原来PhInfoDaoImpl里的findAll方法直接删掉Service 里调phInfoMapper.selectList(null)就行。分页用PagePhInfo page new Page(pageNum, pageSize); phInfoMapper.selectPage(page, null);。这样改完代码量少一半而且不用手动关连接。但注意MyBatis-Plus 需要 Spring Boot 环境如果你原项目是纯 Servlet得先改成 Spring Boot 项目工作量不小。我建议毕业设计如果时间紧就保留 JDBC 版本把精力放在功能完整性和论文逻辑上如果时间充裕用 MyBatis-Plus 重构能让你在答辩时多一个亮点。从那以后我每次拿到一份陌生源码都强制自己先画类图、再跑通登录、最后改一个字段验证全链路这三步走完代码就不再是黑匣子了。希望帮到你。本文还有配套的精品资源点击获取