去年接了一个课程设计,题目就是“智能家居门户网站”,当时第一反应是这玩意儿不都烂大街了吗,随便找个Spring Boot加Vue的模板改改交差就行。但真拆开需求一看,要求限定在JSP这一套技术栈里,还得带完整源码、数据库脚本、调试部署说明,这就不是简单搬模板能糊弄过去的事了。
做完之后回头再看,我反而觉得这个题目选得挺有意思。JSP虽然听起来老,但它恰好能帮你把Web开发最底层的那些逻辑彻底摸透:页面怎么渲染、请求怎么流转、Session怎么维持、SQL怎么拼接。以前用Spring Boot写东西,注解一标、依赖一拉,什么都给你安排好,反而不知道底层发生了什么。这个项目做完,我把Servlet生命周期、JDBC连接管理、Filter过滤器这些概念全盘活了。
这篇文章我就把这套系统的完整实现过程、数据库设计思路、调试部署的坑和后续改造方向全部写出来,适合正要做课设、毕设,或者纯粹想补一补JSP/Java Web基础的同学参考。项目本身不复杂,但牵扯到的知识点密度很高,吃透了很值。
1. 智能家居门户到底要做什么——先想清楚业务再动手写代码
很多同学拿到题目就开始建表写页面,写到一半发现逻辑乱套。我习惯先把业务理清楚,把用户是谁、要干什么、数据怎么流转都画明白,再碰代码。
1.1 这个系统解决的真实生活场景
智能家居门户网站,本质上是一个远程控制与状态可视化的入口。我给它定义了三类核心用户:管理员、家庭成员、访客。
- 管理员:负责管理系统里的所有设备,比如添加一个新买的智能灯泡、删除已经淘汰的旧插座、配置场景模式。管理员还可以管理家庭成员的账号,分配控制权限。
- 家庭成员:是日常使用最多的角色。早上出门前看一眼今天家里的温湿度,下班路上提前打开空调,用app或者网页把客厅的灯光调成暖色调,这些都是成员的基本操作。
- 访客:权限最低,一般只能看到房屋公共区域的状态信息,比如客厅温度、大门是否关闭,不能操作任何设备。这适合临时给保姆、钟点工或者来访的亲戚开个只读权限。
搞清楚角色之后,核心业务流程就很清晰了。我整理成了一张简单的表格:
| 模块 | 功能点 | 涉及角色 | 数据流转 |
|---|---|---|---|
| 用户管理 | 登录、注册、权限控制 | 管理员、成员、访客 | 用户表 + Session会话 |
| 设备管理 | 添加、删除、修改、状态切换 | 管理员操作、成员使用 | 设备表 + 状态表 |
| 场景控制 | 一键离家/回家/睡眠模式 | 管理员配置、成员使用 | 场景表 + 关联表 |
| 环境监测 | 温湿度、光照、空气质量展示 | 所有人可见 | 环境数据表 |
| 日志审计 | 记录谁在什么时候做了什么 | 管理员 | 操作日志表 |
1.2 为什么限定用JSP而不是前后端分离
评论区肯定有人会问,都2024年了,为什么还抱着JSP不放手?说实话,如果让我给公司做产品,我也不会上JSP。但作为一个教学型项目,JSP的定位恰好是其他技术栈替代不了的。
JSP最宝贵的地方在于,它把“后端数据”和“前端页面渲染”直接塞在同一个文件里。你写一段<%Java代码循环取出设备列表,然后用HTML拼出一个表格,这个过程的因果关系极其直观——你一眼就能看见数据库里的行是怎么变成浏览器上的像素的。而前后端分离之后,前端是Vue的v-for,后端是JSON,中间还有跨域、Token、接口联调,链路拉得太长,对初学者来说根本没法形成完整的认知闭环。
所以我给这个项目定的基调是:技术不求新,但要求每一步都吃得透。JSP + Servlet + JavaBean的经典MVC组合,加上MySQL存储数据,就是最直白、最不容易出错的方案。等这套东西跑通了,你再去看Spring Boot,会发现理解成本断崖式下降。
1.3 需求拆分与工作量评估
拿到题目之后,我只花了半天就确定了自己的交付清单:
- 一套完整的Java Web工程源码,结构按MVC分层
- 完整的MySQL建库建表脚本,包含测试数据
- 一套可直接运行的调试部署文档
- 至少6个核心功能页面:登录、设备列表、设备详情、场景控制、环境监测、用户管理
这个量级的工作量,一个人正常开发,大概两到三个全天就能全部搞定。如果只是照着我这个思路走,速度还能更快。下面我按模块把实现细节逐一展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术栈选型与环境搭建——每一步选择背后都有原因
这个项目虽然叫JSP开发,但真正写起来,里面涉及的不仅是JSP一门前端技术。一套完整可运行的工程,至少包括页面渲染、后端控制、数据持久化、数据库管理、开发调度等多个环节。我详细说一下这套技术组合是怎么搭起来的,以及每一步为什么要选它。
2.1 技术栈清单及各层职责
| 层级 | 技术选型 | 职责说明 |
|---|---|---|
| 前端页面 | JSP + CSS3 + JavaScript + jQuery + Ajax | 页面结构、样式、用户交互与局部刷新 |
| 后端控制 | Servlet | 接收请求、调用业务逻辑、控制跳转 |
| 业务逻辑 | Java Bean(普通类) | 封装设备控制、温湿度计算、用户校验等业务规则 |
| 数据访问 | JDBC + DBCP连接池 | 数据库的连接与增删改查操作 |
| 数据存储 | MySQL 5.7 | 存储用户、设备、场景、环境数据、日志 |
| 服务器 | Tomcat 9.0 | 运行Servlet与JSP的容器 |
| 开发工具 | IDEA或Eclipse + Navicat + Maven(可选) | 编写代码与数据库调试 |
这个搭配的语言逻辑是:JSP只负责展示、Servlet只负责调度、JavaBean只负责业务逻辑、JDBC只负责访问数据库。每一层的职责都极其单一,排查问题时定位很精准。比如页面上数据不显示,你只需要检查两种情况:是Servlet没有把数据放到request里,还是JSP页面里取数据的时候写错了属性名。
2.2 为什么引入连接池而不直接用DriverManager
很多教材里教的是用Class.forName("com.mysql.jdbc.Driver")加DriverManager.getConnection()来连接数据库。这种写法在小项目里能跑,但致命问题在于每次数据库操作都要创建一次物理连接,用完了再断开。在并发量稍微上来一点或者页面刷新频繁的情况下,数据库会因为上下文的反复创建销毁而负载飙高,页面响应速度也会有肉眼可见的卡顿。
我在这里用DBCP连接池来解决这个问题。连接池的核心原理,就是预先创建一批数据库连接,放到一个池子里,谁需要就直接从池里拿,用完了再还回去,而不是真正关闭。这样数据库连接对象的创建只做一次,后续所有请求都复用这一批连接,性能提升是非常明显的。
用DBCP只需要在src目录下放一个dbcp.properties配置文件,然后在Java代码里用BasicDataSourceFactory创建数据源。配置如下:
properties复制# dbcp.properties 连接池配置文件
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/smart_home?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username=root
password=123456
# 初始连接数
initialSize=5
# 最大活动连接数
maxActive=20
# 最大空闲连接数
maxIdle=10
# 最小空闲连接数
minIdle=5
# 获取连接的最长等待时间(毫秒)
maxWait=5000
然后在工具类DBUtils.java里初始化连接池:
java复制import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.dbcp2.BasicDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
public class DBUtils {
private static DataSource dataSource = null;
static {
try {
InputStream is = DBUtils.class.getClassLoader().getResourceAsStream("dbcp.properties");
Properties props = new Properties();
props.load(is);
// 根据配置文件创建数据源
dataSource = BasicDataSourceFactory.createDataSource(props);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
// 从连接池中取一个连接
return dataSource.getConnection();
}
}
注意看第5行到第9行的配置,maxWait=5000的意思是:如果连接池里的连接全被占用,新的请求最多等5秒,超出直接报错。这个参数很关键,它能防止某一个请求长时间占用连接导致整个系统假死。
2.3 Tomcat环境部署与工程目录结构
环境我踩过一次比较浪费时间的坑,就是Tomcat的目录权限问题。在Linux或Mac上,如果你把Tomcat解压到了系统目录(比如/usr/local/),启动时经常会碰到Permission denied。我当时搞了十分钟没反应,最后在终端敲ls -l一看,发现bin目录下根本没有可执行权限。解决办法很简单:
bash复制# 给Tomcat目录赋予读写执行权限
chmod -R 755 /Users/你的用户名/apache-tomcat-9.0.xx/
# 启动Tomcat
cd /Users/你的用户名/apache-tomcat-9.0.xx/bin
./startup.sh
Windows下则要特别注意环境变量JAVA_HOME有没有配好。Tomcat启动脚本会用它去定位Java的安装目录,配错了直接黑窗口闪退。你可以用echo %JAVA_HOME%检查,确保路径不写多不少、没有多余空格。
我在IDEA里创建的是一个标准的Maven Web工程。目录结构如下:
code复制smart-home/
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ ├── com.smarthome.servlet/ # Servlet控制层
│ │ │ │ ├── LoginServlet.java
│ │ │ │ ├── DeviceServlet.java
│ │ │ │ ├── SceneServlet.java
│ │ │ │ └── EnvironmentServlet.java
│ │ │ ├── com.smarthome.service/ # 业务逻辑层
│ │ │ │ ├── UserService.java
│ │ │ │ ├── DeviceService.java
│ │ │ │ └── SceneService.java
│ │ │ ├── com.smarthome.dao/ # 数据访问层(DAO)
│ │ │ │ ├── UserDao.java
│ │ │ │ ├── DeviceDao.java
│ │ │ │ └── EnvironmentDao.java
│ │ │ ├── com.smarthome.model/ # 实体类
│ │ │ │ ├── User.java
│ │ │ │ ├── Device.java
│ │ │ │ └── EnvironmentData.java
│ │ │ └── com.smarthome.util/ # 工具类
│ │ │ ├── DBUtils.java
│ │ │ └── SessionFilter.java
│ │ └── webapp
│ │ ├── WEB-INF
│ │ │ ├── web.xml # Web部署描述文件
│ │ │ └── pages/ # JSP页面(受保护,不能直接访问)
│ │ ├── static/ # 静态资源
│ │ │ ├── css/
│ │ │ ├── js/
│ │ │ └── images/
│ │ ├── index.jsp # 门户首页
│ │ ├── login.jsp # 登录页
│ │ └── register.jsp # 注册页
│ └── main/resources
│ └── dbcp.properties # 数据库连接配置
└── sql
└── smart_home.sql # 建库建表脚本
有一点要注意,我把JSP页面放在了WEB-INF/pages/目录下。这样做的原因是,WEB-INF目录下的资源不能通过URL直接访问,只能通过Servlet内部转发来渲染。比如用户想访问设备管理页,URL访问的是/device/list,Servlet处理完数据之后,再request.getRequestDispatcher("/WEB-INF/pages/device-list.jsp").forward(request, response)。这样能防止用户绕过登录、直接输入xxx.jsp的地址越权访问页面,安全性会好很多。
3. 数据库设计——设备、场景和环境数据的表怎么建才合理
数据库是整套系统里最容易拖后腿的环节。很多课设的数据库就两三张表,表单提交之后数据能不能存进去都不管。我这次把表设计得稍微有层次一些,既不过度设计,也把智能家居里最核心的几个概念都覆盖了。
3.1 核心数据表结构与字段说明
我设计了六张表:用户表t_user、设备类型表t_device_type、设备表t_device、设备状态日志表t_device_status、场景表t_scene、场景设备关联表t_scene_device、环境数据表t_environment_data。列一下建表语句:
sql复制-- 用户表
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` varchar(50) NOT NULL COMMENT '用户名(登录账号)',
`password` varchar(100) NOT NULL COMMENT '密码(MD5加密存储)',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`role` tinyint(4) NOT NULL DEFAULT '2' COMMENT '角色:0=管理员,1=家庭成员,2=访客',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1=启用,0=禁用',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 设备类型表
CREATE TABLE `t_device_type` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`type_name` varchar(30) NOT NULL COMMENT '类型名称,如灯光、空调、窗帘',
`type_code` varchar(30) NOT NULL COMMENT '类型编码,如light、aircon、curtain',
`icon` varchar(255) DEFAULT NULL COMMENT '设备图标路径',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_type_code` (`type_code`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='设备类型表';
-- 设备表
CREATE TABLE `t_device` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`device_name` varchar(100) NOT NULL COMMENT '设备名称,如客厅主灯',
`device_type_id` int(11) NOT NULL COMMENT '设备类型ID,关联t_device_type',
`room` varchar(50) NOT NULL COMMENT '所在房间,如客厅、卧室',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '开关状态:0=关,1=开',
`is_online` tinyint(4) NOT NULL DEFAULT '1' COMMENT '在线状态:0=离线,1=在线',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_room` (`room`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='设备表';
-- 设备状态日志表
CREATE TABLE `t_device_status` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`device_id` int(11) NOT NULL COMMENT '设备ID',
`status` tinyint(4) NOT NULL COMMENT '开关状态:0=关,1=开',
`operator_id` int(11) NOT NULL COMMENT '操作人ID,关联t_user.id',
`operate_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
`remark` varchar(255) DEFAULT NULL COMMENT '备注信息',
PRIMARY KEY (`id`),
KEY `idx_device_id` (`device_id`),
KEY `idx_operate_time` (`operate_time`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='设备状态日志表';
-- 场景表
CREATE TABLE `t_scene` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`scene_name` varchar(50) NOT NULL COMMENT '场景名称:离家模式、回家模式、睡眠模式',
`scene_code` varchar(50) NOT NULL COMMENT '场景编码:leave、home、sleep',
`description` varchar(255) DEFAULT NULL COMMENT '场景描述',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_scene_code` (`scene_code`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='场景表';
-- 场景设备关联表
CREATE TABLE `t_scene_device` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`scene_id` int(11) NOT NULL COMMENT '场景ID',
`device_id` int(11) NOT NULL COMMENT '设备ID',
`target_status` tinyint(4) NOT NULL COMMENT '该设备在场景中的目标状态:0=关,1=开',
PRIMARY KEY (`id`),
KEY `idx_scene_id` (`scene_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='场景设备关联表';
-- 环境数据表
CREATE TABLE `t_environment_data` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`temperature` decimal(5,2) NOT NULL COMMENT '温度,单位℃',
`humidity` decimal(5,2) NOT NULL COMMENT '湿度,单位%',
`light_intensity` int(11) NOT NULL COMMENT '光照强度,单位Lux',
`pm25` int(11) DEFAULT NULL COMMENT 'PM2.5浓度,单位μg/m³',
`record_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '采集时间',
PRIMARY KEY (`id`),
KEY `idx_record_time` (`record_time`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='环境数据表';
3.2 为什么要设计“设备类型表”而不是把类型直接写成字段
这是一个很值得说的小设计。如果图省事,你完全可以在t_device表里直接放一个device_type varchar(20)字段,存“灯光”“空调”这样的字符串。但问题是,如果后面想给某一类设备增加属性字段,比如照明设备加一个“色温”、空调加一个“模式”,你就得去修改t_device表的结构,而与之相关的代码、页面、SQL都会跟着变动。
拆出一张设备类型表之后,设备与类型是多对一关系,每种类型的特有属性可以通过关联表或者额外字段去扩展,整套系统的扩展性会好很多。类似的思路也适用于场景和设备的多对多关系——一个场景对应多个设备,一个设备也可以出现在多个场景中,所以中间表的引入是必须的。
3.3 测试数据的sql插入技巧
建表之后,我写了一些模拟数据方便测试联调。设计测试数据的技巧是,要让不同状态下都能覆盖到。比如设备表中,有在线的、有离线的、有打开的、有关闭的;环境数据表里,温度湿度要有高低差异,这样页面图表渲染出来视觉效果才丰富。这里放一段示例:
sql复制-- 插入设备类型
INSERT INTO `t_device_type` (`type_name`, `type_code`, `icon`) VALUES
('灯光', 'light', '/static/images/icon-light.png'),
('空调', 'aircon', '/static/images/icon-aircon.png'),
('窗帘', 'curtain', '/static/images/icon-curtain.png'),
('插座', 'socket', '/static/images/icon-socket.png');
-- 插入设备
INSERT INTO `t_device` (`device_name`, `device_type_id`, `room`, `status`, `is_online`) VALUES
('客厅主灯', 1, '客厅', 1, 1),
('卧室床头灯', 1, '卧室', 0, 1),
('客厅空调', 2, '客厅', 1, 1),
('主卧空调', 2, '主卧', 0, 0),
('客厅窗帘', 3, '客厅', 0, 1),
('书房插座', 4, '书房', 1, 1);
-- 插入场景
INSERT INTO `t_scene` (`scene_name`, `scene_code`, `description`) VALUES
('离家模式', 'leave', '关闭所有灯光和电器,窗帘自动拉合'),
('回家模式', 'home', '客厅灯光打开,空调调至舒适温度'),
('睡眠模式', 'sleep', '所有灯光关闭,卧室空调开启睡眠模式');
-- 离家模式下:所有设备目标状态为关闭
INSERT INTO `t_scene_device` (`scene_id`, `device_id`, `target_status`) VALUES
(1, 1, 0), (1, 2, 0), (1, 3, 0), (1, 4, 0), (1, 5, 0), (1, 6, 0);
-- 回家模式下:客厅主灯、客厅空调、客厅窗帘打开
INSERT INTO `t_scene_device` (`scene_id`, `device_id`, `target_status`) VALUES
(2, 1, 1), (2, 3, 1), (2, 5, 1);
-- 睡眠模式下:卧室床头灯关闭、主卧空调打开
INSERT INTO `t_scene_device` (`scene_id`, `device_id`, `target_status`) VALUES
(3, 2, 0), (3, 4, 1);
这套数据设计好后,你写页面时基本每个模块都有现成的测试对象。
4. 核心功能模块实现——登录、设备控制、场景联动、环境监测的完整逻辑
业务逻辑是整套系统的灵魂。我用一个典型的“设备控制”流程来拆解数据是怎么从浏览器一路流到数据库的,再说明场景联动和环境监测的特殊性。
4.1 登录认证与权限拦截的实现
登录模块虽然简单,但它是过滤器逻辑的落脚点。我对密码做了MD5加盐处理,而不是明文存储。注册时前端把密码传到RegisterServlet,后端用MD5Util.md5(password + salt)生成密文再入库。
登录成功之后,我把用户信息放入Session:
java复制// LoginServlet.java 核心逻辑
User user = userService.login(username, password);
if (user != null) {
// 登录成功,保存用户信息到Session
HttpSession session = request.getSession();
session.setAttribute("loginUser", user);
session.setMaxInactiveInterval(30 * 60); // Session半小时无操作自动失效
// 获取跳转来源,如果没有则返回首页
String redirectUrl = (String) session.getAttribute("redirectUrl");
if (redirectUrl == null || redirectUrl.isEmpty()) {
redirectUrl = "index.do";
}
response.sendRedirect(redirectUrl);
} else {
// 登录失败,返回登录页并带出错误信息
request.setAttribute("loginError", "用户名或密码错误,请重新输入");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
上面代码第6行的setMaxInactiveInterval很多人会忽视。默认情况下Tomcat的Session超时时间是30分钟,但如果你通过代码或者web.xml设置了其他值,行为会不一样。这个超时时间设置得太短,用户操作稍微慢一点就得重新登录;设置太长,又会有安全隐患。30分钟是我在多方权衡后选的体验值。
权限拦截我用了Filter过滤器,这是Servlet规范里一个非常经典的设计。有了它,就不用在每个Servlet里重复写“判断是否登录”的代码了。我写了一个SessionFilter,统一处理所有需要登录才能访问的路径:
java复制// SessionFilter.java 登录拦截过滤器
@WebFilter(urlPatterns = {"/device/*", "/scene/*", "/environment/*", "/user/*"})
public class SessionFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
HttpSession session = request.getSession(false);
// 判断用户是否已登录
if (session != null && session.getAttribute("loginUser") != null) {
// 已登录,放行请求
chain.doFilter(request, response);
} else {
// 未登录,记录来源并跳转到登录页
String requestURI = request.getRequestURI();
String queryString = request.getQueryString();
if (queryString != null && !queryString.isEmpty()) {
requestURI = requestURI + "?" + queryString;
}
session.setAttribute("redirectUrl", requestURI);
response.sendRedirect(request.getContextPath() + "/login.jsp");
}
}
}
request.getSession(false)里面的参数false很关键,意思是如果当前请求里没有Session,不要帮我创建新的,直接返回null。如果不加这个false,没有登录的用户访问任何页面时,系统都会自动创建一个空Session,既浪费内存,又会干扰后面的判断逻辑。
4.2 设备控制的前后端完整链路
设备控制是系统的核心功能,涉及点很多。用户在前端点击一个开关,整个请求链路如下:
- 前端页面通过JavaScript发起Ajax异步请求,URL是
/device/control,参数包含deviceId和targetStatus - 请求到达
DeviceServlet,Servlet先拿到参数并校验合法性 - Servlet调用
DeviceService.controlDevice(deviceId, targetStatus, loginUser)方法 - Service方法里做了三件事:更新设备表
t_device的当前状态;向设备状态日志表t_device_status插入一条操作记录;返回操作结果 - Servlet根据Service返回结果,把结果响应给前端
- 前端收到响应后,动态修改页面的开关状态和样式
controlDevice的Service实现如下:
java复制// DeviceService.java
public boolean controlDevice(int deviceId, int targetStatus, User loginUser) {
// 1. 更新设备状态
int updateCount = deviceDao.updateStatus(deviceId, targetStatus);
if (updateCount <= 0) {
return false;
}
// 2. 添加操作日志
boolean logResult = deviceStatusLogDao.insertLog(deviceId, targetStatus, loginUser.getId(), "页面远程控制");
return logResult;
}
这里需要注意第2步中,我默认把日志插入结果也当作设备控制成功的必要条件。这样做好处是,日志必定和设备状态保持同步,以后做审计时不会出现“设备状态变成了开但日志里没记录”的情况。
数据库操作里的“开启事务”同样重要。如果第1步更新成功、第2步日志失败,事务就要回滚,保证原子性:
java复制// DeviceDao.java 在Service层开启事务
Connection conn = null;
try {
conn = DBUtils.getConnection();
// 手动关闭自动提交模式
conn.setAutoCommit(false);
deviceDao.updateStatus(conn, deviceId, targetStatus);
deviceStatusLogDao.insertLog(conn, deviceId, targetStatus, operatorId, remark);
// 全部成功,提交事务
conn.commit();
} catch (Exception e) {
// 出现异常,回滚事务
if (conn != null) {
conn.rollback();
}
throw new RuntimeException("设备控制失败", e);
} finally {
if (conn != null) {
// 归还连接到连接池(会自动恢复自动提交模式)
conn.setAutoCommit(true);
conn.close();
}
}
4.3 场景联动——一键控制多个设备的批量操作
场景联动是我个人觉得最有“智能家居”味道的模块,因为它不是普通的单设备开关,而是批量指挥多个设备执行预定状态。
以“离家模式”为例,用户点击执行离家模式之后,系统需要做的事情是:查出场景ID=1,然后查出这个场景关联的所有设备及目标状态(屋子里所有灯和家电都要关掉),依次执行控制逻辑。如果场景里有10台设备,就循环执行10遍。
核心代码在SceneService.executeScene(int sceneId):
java复制// SceneService.java
public void executeScene(int sceneId) {
// 1. 查出场景下的所有设备及目标状态
List<SceneDevice> sceneDevices = sceneDao.findDevicesBySceneId(sceneId);
if (sceneDevices == null || sceneDevices.isEmpty()) {
throw new RuntimeException("该场景下没有配置任何设备");
}
// 2. 逐台设备执行控制操作
for (SceneDevice sd : sceneDevices) {
deviceService.controlDevice(sd.getDeviceId(), sd.getTargetStatus(), currentUser);
}
// 3. 添加“执行场景”的操作日志
sceneLogDao.insertLog(sceneId, currentUser.getId(), "用户手动执行了场景");
}
这里有一个值得思考的细节:如果第3台设备执行失败,前两台已经成功了,这种情况怎么处理?按严格的“全成功才算成功”标准,需要把设备控制作为同一事务的一部分,但我当初实现的时候没有在executeScene里强制开启事务,因为一台设备控制失败往往意味着硬件层面的问题(比如设备离线),即使回滚成功,也无法真正让那台设备离线状态生效。所以在实际项目里,我采用了部分成功策略:执行场景时,如果某台设备失败,记录日志,继续执行其他设备;前端返回“有N台设备执行成功,1台执行失败”的结果。这更贴合真实场景。
4.4 环境监测数据的动态展示与历史曲线
环境监测模块的数据来源,在真实系统中是传感器定时上报到数据库的。在本项目里,我用定时任务模拟了数据产生过程。Servlet层用EnvironmentServlet查询最近一小时的数据返回给JSP页面。
页面展示采用了“ECharts”折线图,用JavaScript从后台接口动态拉取JSON数据然后渲染。这里不贴太多代码,但有一个非常实用的技术点值得说:把Java集合转换为JSON。我引入了Fastjson框架,只需一行代码:
java复制// EnvironmentServlet.java
List<EnvironmentData> list = environmentService.getRecentData(60);
String json = JSON.toJSONString(list);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(json);
前端的JSP页面里用Ajax定时轮询这个接口,然后更新图表。Ajax的好处是,页面不必整体刷新,用户体验会更接近现代Web应用。我在页面里设置一个定时器:
javascript复制// 每30秒拉取一次最新环境数据
setInterval(function () {
$.ajax({
url: '/environment/realtime',
type: 'GET',
dataType: 'json',
success: function (data) {
// 更新对应的DOM节点
$('#temperature').text(data.temperature + ' ℃');
$('#humidity').text(data.humidity + ' %');
$('#light').text(data.lightIntensity + ' Lux');
}
});
}, 30000);
4.5 前端页面设计与用户体验上的几点讲究
这里要给页面布局提两条实在的建议。第一,仪表盘首页一定要有全局状态总览,例如“当前家中5台设备在线、2台离线、3台开启”,让用户打开网站第一眼就知道全屋状态。我用的是给t_device表做分组统计SQL:
sql复制SELECT
COUNT(*) AS total,
SUM(CASE WHEN is_online = 1 THEN 1 ELSE 0 END) AS online_count,
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS device_on_count
FROM t_device;
第二,设备卡片的设计建议带上“设备类型图标 + 房间名 + 状态开关”,操作一个设备尽量不超过两次点击。我当时用纯CSS Grid实现了响应式卡片布局,不同屏幕自动适配,视觉上比表格好看很多。
5. 调试部署——从本机到数据库再到云服务器的完整过程
课设提交要求里那句“程序+源码+数据库+调试部署+开发环境”最容易被低估。很多人的项目源码能跑,但换一台电脑就起不来,原因基本都是环境不一致。这一部分我详细写一下怎么做到“换设备也能快速跑起来”。
5.1 调试过程中的三个致命坑
第一个坑是JDBC驱动版本与MySQL版本不匹配。MySQL 8.0以上的驱动类名和MySQL 5.7不一样。MySQL 5.7用com.mysql.jdbc.Driver,而8.0以后要用com.mysql.cj.jdbc.Driver,并且URL中必须带serverTimezone参数,否则会报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。这个中文乱码的时区错误信息第一次见很懵,其实就是时区配置的问题。
第二个坑是数据库字符集不统一。如果表的字符集是latin1,而JSP页面是UTF-8,中文数据就会出现乱码。我建议建库时明确指定:
sql复制CREATE DATABASE smart_home DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
同时,在JDBC连接串中明确声明字符集:
code复制jdbc:mysql://localhost:3306/smart_home?useUnicode=true&characterEncoding=utf-8
第三个坑是Tomcat端口被占用。如果你同时启动了多个项目,或者另一个程序占用了8080端口,Tomcat启动日志里会报Port 8080 required by Tomcat v9.0 Server is already in use。这时可以修改conf/server.xml里的端口号,或者用命令查占用进程:
bash复制# 查看8080端口被哪个进程占用
netstat -ano | findstr 8080
# Linux下
lsof -i:8080
5.2 数据库导入与环境配置流程
我保证换一台电脑也能快速跑起来的操作流程是固定的五步:
- 安装JDK 8+,配置
JAVA_HOME环境变量 - 安装MySQL 5.7+,用Navicat或命令行执行
smart_home.sql建库 - 安装Tomcat 9,把工程打成war包放入
webapps目录 - 修改
dbcp.properties中的账号密码,对准本地数据库 - 启动Tomcat,访问
http://localhost:8080/项目名/login.jsp
用命令行导入数据库脚本可以这样:
bash复制mysql -u root -p < smart_home.sql
如果脚本里有中文字符注释,执行前还要加上字符集参数:
bash复制mysql -u root -p --default-character-set=utf8mb4 < smart_home.sql
我在提供课设材料的时候,会在工程根目录放置一个README.md,把这个流程用图文对照写清楚。能让老师或者接手的人三分钟跑起来,这份交付就成功了一半。
5.3 部署到云服务器的细节
如果你想把项目部署到云服务器上给更多人演示,操作也很直接。以一台CentOS服务器为例:
bash复制# 1. 安装JDK
yum install -y java-1.8.0-openjdk
# 2. 安装MySQL
yum install -y mysql-server
systemctl start mysqld
# 3. 上传war包到服务器
scp smart-home.war root@服务器IP:/opt/tomcat/webapps/
# 4. 启动Tomcat
/opt/tomcat/bin/startup.sh
部署到公网后,要特别注意数据库账号安全问题,不建议使用root直接连接。我新建了专用账号,只给项目库分配增删改查权限:
sql复制CREATE USER 'smart_user'@'localhost' IDENTIFIED BY 'Sm@rtHome2024';
GRANT SELECT, INSERT, UPDATE, DELETE ON smart_home.* TO 'smart_user'@'localhost';
FLUSH PRIVILEGES;
5.4 调试部署中的常见错误速查表
| 错误现象 | 根本原因 | 解决办法 |
|---|---|---|
| 页面显示404 | URL路径或Servlet映射错误 | 检查web.xml或@WebServlet注解中的url-pattern |
| 数据库中文乱码 | 连接串没有指定UTF-8 | 在JDBC URL中加入characterEncoding=utf-8 |
| ClassNotFoundException | 缺少MySQL驱动jar包 | 检查WEB-INF/lib下是否放入mysql-connector-java.jar |
| 用户登录后重新跳回登录页 | Session失效或Filter拦截路径写错 | 检查Session超时时间,检查Filter的urlPattern |
| 设备状态一直不更新 | 缓存问题 | 清浏览器缓存,加一个时间戳参数强制刷新 |
| 静态资源(css/js)加载不出来 | 静态资源路径错误 | 用${pageContext.request.contextPath}拼接相对路径 |
6. 这个课设项目还能怎么改进——从“能用”走向“像一个产品”
把课设交上去之后,如果你还有精力,我强烈建议往下面这几个方向再做一轮改造。不仅答辩时更好讲,你在这轮改造里学到的东西也会比课设本身多。
6.1 安全层面的四个补强点
我这个项目为了方便演示,密码只是做了加盐MD5,权限拦截也只在Servlet层做了。其实比如SQL注入防护、XSS过滤、CSRF Token防跨站请求伪造,这些常见漏洞在这个系统里都还存在。最简单的加固方式是把所有SQL改成PreparedStatement预编译方式,杜绝拼接字符串导致的注入风险;登录表单加入一次性Token,防止跨站请求伪造;页面输出时对用户可控内容做HTML转义。
以SQL预编译为例,正确写法是:
java复制// 错误示范(存在SQL注入风险)
String sql = "SELECT * FROM t_user WHERE username = '" + username + "' AND password = '" + password + "'";
// 正确写法(PreparedStatement预编译)
String sql = "SELECT * FROM t_user WHERE username = ? AND password = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, username);
ps.setString(2, password);
ResultSet rs = ps.executeQuery();
6.2 从JSP向后端分离演进
JSP这套体系的维护痛点在于,HTML代码和Java代码混在一个文件里,做大型项目时前端和后端很难并行工作。等到你真正工作了,大概率接触的是前后端分离架构。我建议你做完这个项目,试着用Vue或者React重写前端页面,后端只提供JSON接口。这时候你会发现,所有Servlet返回的内容从forward到JSP变成了response.getWriter().write(JSON.toJSONString(result)),其他业务逻辑完全不变。这个迁移过程就是珍贵的学习过程。
6.3 让“智能”名副其实:引入消息队列和MQTT协议
真实的智能家居设备不会像数据库里那样只是改一行状态,而是要通过MQTT等物联网协议和硬件实时通信。如果你愿意在这个项目里继续深挖,可以把设备控制从“直接修改数据库”变成“发布一条MQTT消息到设备主题”,让真实的硬件设备订阅主题并执行指令。这样,数据库里的状态更新只是操作成功后的回显,而不再是操作本身。这两者之间本质上的区别非常大。
我当时做这个改造时,引入了一个轻量级MQTT客户端库,控制逻辑变成:
java复制// MQTT发送控制指令
MqttMessage message = new MqttMessage();
message.setPayload("ON".getBytes());
message.setQos(1);
mqttClient.publish("home/device/" + deviceId + "/command", message);
然后等待设备上报状态,设备上报后回写数据库并实时推送到前端页面。这个进阶改造让项目从“模拟仿真”变成了“真实可用的物联网雏形”,答辩的时候价值会完全不一样。
结语与几点真实体会
把JSP智能家居门户网站这个项目从零完整做下来,最大的收获不是掌握了某个具体技术,而是体验了一次“从需求分析到部署上线”的完整闭环。以前用Spring Boot跟着教程敲代码,根本没想过Servlet容器是怎么把请求交给代码处理的,也没理解为什么要分MVC三层。经过这个项目,这些概念终于变成了自己的肌肉记忆。
最后想跟大家说一句掏心窝的话:做课设或者练手项目,最忌讳的是“贪多求快”。一个JSP项目虽然“老”,但只要能把它背后的请求流转、数据库设计、权限控制、部署调试这些链路彻底吃透,它的价值比水十个“短视频速成项目”要高得多。如果你正在做类似的系统,代码跑不通、逻辑理不顺,欢迎到评论区留言交流。我最清楚那种卡在一个莫名Bug上两小时的心烦状态,咱们互相帮着看看,问题往往三句话就解决了。
