最近总有读者拿着“JSP记账本的设计与实现”这个题目来问我,说课程设计或毕业设计抽到了它,手头只有一份标题里写着“程序+源码+数据库+调试部署+开发环境”的压缩包,却不知道从哪下手。这类项目的本质,是用一套完整的Java Web技术栈把记账业务跑通,听起来简单,但真正动手时会发现:数据库怎么建、登录状态怎么维持、报表怎么统计、部署到别人电脑上为什么乱码,每一个环节都能卡住人。这篇就围绕JSP记账本从设计、编码、调试到部署的全过程,把我实际做这个项目时踩过的坑、总结的经验一次讲清楚。
1. 记账本项目为什么选JSP:一个成熟技术栈在课程设计里的现实价值
1.1 这个题目到底在考什么
JSP记账本是一个很典型的Java Web入门综合项目。它不像商城、论坛那么复杂,但又完整覆盖了一个Web应用从后端到前端、从数据库到页面渲染的所有环节。考察点其实非常明确。
- 能用JSP+Servlet处理HTTP请求和响应
- 能用JDBC操作MySQL数据库,完成增删改查
- 能用Session/Cookie管理用户登录状态
- 能用JSTL、EL表达式在页面展示动态数据
- 能理清项目目录结构、配置文件、部署方式
这些点合在一起,对应的是企业对Java Web开发的入门要求。所以哪怕JSP现在在生产环境中的新项目用得少了,这个题目仍然是很多学校课程设计的常客,因为它能在写代码的过程中把Web开发的骨架练一遍。
1.2 JSP/Servlet在这个场景下的真实优势
有人会问,现在新项目都用Spring Boot了,为什么不直接学Spring Boot?这个问题我在带学生做项目时被问过很多次。
从学习角度讲,JSP+Servlet是理解Web底层原理最好的方式。Spring Boot帮你封装了内嵌Tomcat、自动配置、依赖管理,你写起来是快,但你对HTTP请求如何被处理、Session怎么创建、Servlet生命周期是什么样的,实际上是模糊的。而JSP记账本逼着你手动处理这些细节——你写一个Servlet,手动配置web.xml,手动获取Connection,手动关闭资源。这个过程虽然啰嗦,但扎实。
从课程设计答辩角度讲,JSP项目更容易被老师问到细节,也更容易答上来。因为每一步都是你自己写出来的,没有框架帮你兜底。老师问“你这个登录状态存在哪里”,你可以直接说Session;问“数据库连接怎么管理的”,你可以把JDBC的步骤背出来。
JSP本身在服务端渲染场景下并没有完全过时。在一些老系统的维护、内部管理系统、快速原型开发中,JSP仍然在运行。学会读JSP代码,是接手很多老旧项目的必备技能。
1.3 技术栈清单和版本选型
做JSP记账本,我建议的技术栈如下,版本经过实践验证,按这个组合能少踩很多坑。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 或 11 | 8最稳,兼容性最好,Tomcat支持最全 |
| IDE | Eclipse EE 或 IDEA Ultimate | 两个都行,IDEA对JSP语法提示更好 |
| Web服务器 | Tomcat 8.5 或 9.0 | 9.0对应Servlet 4.0,8.5够用 |
| 数据库 | MySQL 5.7 或 8.0 | 5.7经典,8.0注意驱动和时区问题 |
| 前端技术 | JSP + JSTL + EL + jQuery | 不需要前端框架,够用 |
| 项目管理 | Maven 3.6+ | 方便管理依赖,但也可以不用 |
提示:如果你的题目要求里没有强制Maven,用传统方式在Eclipse里建Dynamic Web Project(动态Web项目),把jar包手动放到WEB-INF/lib下,反而更直观,也更好控制版本。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计:记账本的核心不在页面,而在数据模型怎么搭
2.1 三张核心表的设计思路
很多同学拿到题目第一反应是去写登录页面、记账页面,这是本末倒置。记账本的核心是数据,数据模型设计得不好,后面所有页面都会跟着改。我先设计的是数据库,而且只用了三张表,就把这个项目的基本业务全部撑起来了。
第一张是用户表。这个表是记账本所有数据归属的前提,没有用户体系,账本数据就没法和人关联。第二张是分类表,用来管理收入支出分类,比如餐饮、购物、工资、奖金。第三张是账单流水表,这是整个项目最核心的业务表,每一笔收入或支出都对应一条记录。
2.2 建表SQL脚本和表关系说明
我直接给出一份可运行的建表脚本,MySQL 5.7和8.0都能跑。
sql复制-- 用户表
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码',
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 分类表
CREATE TABLE `t_category` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '所属用户',
`name` varchar(50) NOT NULL COMMENT '分类名称',
`type` tinyint(4) NOT NULL COMMENT '1收入 2支出',
`sort` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 账单流水表
CREATE TABLE `t_record` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '所属用户',
`category_id` int(11) NOT NULL COMMENT '分类ID',
`type` tinyint(4) NOT NULL COMMENT '1收入 2支出',
`amount` decimal(10,2) NOT NULL COMMENT '金额',
`note` varchar(255) DEFAULT NULL COMMENT '备注',
`record_date` date NOT NULL COMMENT '消费日期',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_date` (`user_id`, `record_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
表关系非常简单:t_user 是一侧,t_category 和 t_record 都通过 user_id 关联到用户。t_record 又通过 category_id 关联到 t_category。
这里有一个设计细节值得说明:为什么账单表里要冗余一个 type 字段,而不是全靠 category_id 去关联分类表再查类型?因为按类型统计是记账本最高频的操作,比如统计本月总收入、总支出。如果每次都去关联分类表才能区分收支,SQL 会变复杂,性能也差。冗余这个字段后,统计就直接 WHERE type = 1 就完了,非常干净。
2.3 数据建模时容易忽略的细节
记账本项目数据库设计阶段,有几个细节特别容易被忽略,会导致后期返工。
- 金额字段要用
decimal,不要用float或double。浮点数在计算合计时会有精度问题,比如 0.1+0.2 算出来变成 0.30000000000000004。用 decimal(10,2)既保证精度,也符合金额的实际场景。 - 字符集统一用
utf8mb4。如果只用utf8,遇到用户输入特殊表情符号或生僻字时,会报“Incorrect string value”错误。 - 流水表的
record_date设置成date类型,而不是datetime。因为记账关心的核心维度是哪一天花的钱,不需要精确到时分秒。如果你把时间精度设太高,反而会增加分组统计的复杂度,每次都要先格式化日期。 - 在写业务代码之前,一定要先准备一些测试数据。至少插入一个测试用户、6个左右分类、20多笔跨越两三个月的流水。没有测试数据,页面统计功能根本无法有效验证。
3. 核心代码拆解:从登录鉴权到统计报表的一整条业务链路
3.1 登录会话管理:用Session还是Cookie
记账本必然需要用户登录。用户登录后,系统要记住这个用户,后面的所有写账、查账操作都得知道“我是谁”。这就涉及到会话管理。
我的做法是在用户登录成功后,把用户ID和用户名放进Session:
java复制// LoginServlet.java 核心逻辑
String username = request.getParameter("username");
String password = request.getParameter("password");
User user = userDao.findByUsername(username);
if (user != null && user.getPassword().equals(MD5Util.md5(password))) {
HttpSession session = request.getSession();
session.setAttribute("loginUser", user);
// 可选:记住登录状态
if ("on".equals(request.getParameter("remember"))) {
Cookie cookie = new Cookie("loginUser", user.getId().toString());
cookie.setMaxAge(60 * 60 * 24 * 7);
response.addCookie(cookie);
}
response.sendRedirect("record/list.jsp");
} else {
request.setAttribute("errorMsg", "用户名或密码错误");
request.getRequestDispatcher("login.jsp").forward(request, response);
}
用Session是JSP项目管理登录状态的默认方案。原因很简单:Session保存在服务器端,客户端只存一个JSESSIONID,安全风险小。而Cookie是把用户信息放在客户端,容易被篡改。
但这里有个很容易被忽略的坑:Session默认有效时间是Tomcat配置的30分钟。用户如果登录后放着页面不动超过30分钟,再点提交,Session已经失效了。所以在写鉴权代码时,必须统一处理Session为空的情况,跳转回登录页。
3.2 记账增删改查的完整链路
以“新增一笔支出”为例,完整的请求链路是:
- 用户在 record_add.jsp 填写表单
- 表单提交到 RecordAddServlet
- Servlet 获取参数,封装成 Record 对象
- 调用 RecordDao.insertRecord(record)
- DAO 执行 INSERT 语句
- 成功则重定向回列表页,失败则返回错误信息
这个链路看着简单,但代码结构如果乱了,后面维护会很难受。我在项目里用的是经典的 JSP + Servlet + DAO 三层结构。
RecordDao 里插入流水的方法大致是这样:
java复制public int insertRecord(Record record) {
String sql = "INSERT INTO t_record (user_id, category_id, type, amount, note, record_date) VALUES (?, ?, ?, ?, ?, ?)";
try (Connection conn = DbUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setInt(1, record.getUserId());
ps.setInt(2, record.getCategoryId());
ps.setInt(3, record.getType());
ps.setBigDecimal(4, record.getAmount());
ps.setString(5, record.getNote());
ps.setDate(6, Date.valueOf(record.getRecordDate()));
return ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
return 0;
}
}
注意我用的是 PreparedStatement,这非常重要。如果你偷懒用 Statement 直接拼接SQL字符串,用户输入里带一个单引号就能让你的SQL崩掉,甚至会引发SQL注入攻击,这可是课程设计答辩时的扣分重灾区。
删除和修改的思路和插入一样,只是SQL语句不同。修改时我建议先根据ID查询出原记录回显到表单,再让用户修改后提交。这样用户能知道自己原来填的是什么,体验更好。
3.3 月度统计报表的SQL和页面展示
记账本除了增删改查,统计报表是另一个必须具备的功能。一个合格的记账本,至少要有“本月收入”“本月支出”“本月结余”这三个数字,最好还能按分类分析支出占比。
月度统计的SQL可以这样写:
sql复制-- 某用户某月的收支汇总
SELECT
SUM(CASE WHEN type = 1 THEN amount ELSE 0 END) AS total_income,
SUM(CASE WHEN type = 2 THEN amount ELSE 0 END) AS total_expense
FROM t_record
WHERE user_id = ?
AND DATE_FORMAT(record_date, '%Y-%m') = ?
按分类统计支出金额和笔数,用 GROUP BY:
sql复制SELECT c.name, COUNT(r.id) AS cnt, SUM(r.amount) AS total
FROM t_record r
LEFT JOIN t_category c ON r.category_id = c.id
WHERE r.user_id = ? AND r.type = 2 AND DATE_FORMAT(r.record_date, '%Y-%m') = ?
GROUP BY c.name
ORDER BY total DESC
这里有一个关于 DATE_FORMAT 的性能提醒。如果数据量只有几百条,这么写完全没问题。但如果你把项目扩展到上万条数据,DATE_FORMAT(record_date, '%Y-%m') 会导致索引失效。更好的写法是改成范围查询:
sql复制WHERE r.user_id = ?
AND r.record_date >= '2024-06-01'
AND r.record_date < '2024-07-01'
在课程设计阶段,数据量小,两种写法都行,但如果你在报告里写的是范围查询,并且解释了为什么,这会让答辩老师对你的印象分明显提升。
统计页面我用 JSTL 结合图表显示。JSP中可以先用SQL的查询结果构造好数据,再用JSTL的 <c:forEach> 循环输出到HTML表格中。如果需要图形展示,可以引入轻量级的 ECharts,从Servlet返回JSON数据,前端用Ajax加载。但这一步可选,做出来是加分项,不做不影响及格。
3.4 JSP脚本片段和JSTL的选择
在使用JSP时,最容易被批“代码混乱”的问题,是在JSP页面里直接写大量Java代码片段,也就是 <% ... %>。比如有人喜欢这样写:
jsp复制<%
RecordDao dao = new RecordDao();
List<Record> list = dao.listRecordsByDate("2024-06-01");
for (Record r : list) {
%>
<tr><td><%=r.getAmount()%></td></tr>
<%
}
%>
这种写法不是不能跑,但会有一个问题:页面里揉杂了大量Java逻辑,HTML标签和Java代码混在一起,后期维护极其痛苦。更规范的做法是用 JSTL 标签配合 EL 表达式,把Java代码从页面中剥离出去。
jsp复制<c:forEach items="${recordList}" var="r">
<tr>
<td>${r.categoryName}</td>
<td>${r.type == 1 ? '收入' : '支出'}</td>
<td>${r.amount}</td>
<td>${r.recordDate}</td>
<td>${r.note}</td>
</tr>
</c:forEach>
两种方式功能一样,但 JSTL 版本的页面可读性好很多,也更符合 MVC 理念。我在做这个项目时,前前后后把最初写在 JSP 里的脚本片段全部重构成了 JSTL + EL,虽然多花了一点时间,但页面清爽太多,后面加功能也快了很多。
4. 开发环境搭建与调试部署全记录:换个电脑就崩的问题怎么解决
4.1 开发环境搭建的关键点
JSP项目的开发环境搭建,我按下面的顺序来做,每一步都能确认无误再进下一步,可以避免很多初学者“一到环境就懵”的问题。
- 安装JDK并配置
JAVA_HOME环境变量,path里加%JAVA_HOME%\bin,命令行验证java -version - 安装IDE,Eclipse的Enterprise Java版本自带Web开发插件,IDEA需要Ultimate版
- 安装Tomcat,在IDE里配置本地Tomcat路径。注意Tomcat 8.5以上要求JDK 7+,Tomcat 9要求JDK 8+
- 安装MySQL,设置root密码,记住字符集选 utf8mb4
- 创建数据库和表,导入建表脚本和测试数据
- 在IDE里创建Dynamic Web Project,Target Runtime选你的Tomcat版本
- 把MySQL驱动jar包放到
WEB-INF/lib目录,并Build Path
这里面最容易出问题的是第7步。很多人图省事,只在Build Path里引了驱动包,但没放进 WEB-INF/lib。结果IDE里编译不报错,一运行就报 ClassNotFoundException: com.mysql.cj.jdbc.Driver。因为Tomcat运行项目时,类加载范围是 WEB-INF/lib 和 WEB-INF/classes,IDE的Build Path不能代表运行时的类路径。
4.2 调试阶段最常见的几个报错
我在跑通这个项目的过程中,遇到过几个高频报错,每一个都卡了不短的时间,在这里列出来给各位排雷。
第一个是数据库连接不上。报错信息一般是 java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)。这个问题的原因很简单:数据库连接URL、用户名或密码写错了。大部分情况是密码不匹配,或者在URL里没指定 useSSL=false。MySQL 8.0的驱动要求把URL写成 jdbc:mysql://localhost:3306/db_bookkeeping?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 这样,少了 serverTimezone 会直接报时区错误。
第二个是页面中文乱码。这个问题有多个层面的原因:数据库表的字符集不是utf8mb4;JDBC连接URL里没有 characterEncoding=utf8;JSP页面没有设置 pageEncoding="UTF-8";Servlet里 request.setCharacterEncoding("UTF-8") 没写。排查思路是从数据库往页面逐层检查,先确认数据库内容是否正常,再确认页面输出是否正常。我写了个工具方法来统一初始化,避免每次都忘。
java复制public static Connection getConnection() throws SQLException {
String url = "jdbc:mysql://localhost:3306/db_bookkeeping?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8";
String user = "root";
String password = "123456";
return DriverManager.getConnection(url, user, password);
}
第三个是404错误。项目部署后访问 http://localhost:8080/Bookkeeping/ 报404。这个多半是部署的上下文路径不对。在Eclipse中右键项目 Properties -> Web Project Settings 可以看 Context Root,或者在 Tomcat 的 server.xml 里看 <Context docBase="..." path="/xxx" />。确保访问路径和 context path 一致即可。
第四个是端口被占用。Tomcat启动时报 Port 8080 required by Tomcat v9.0 Server at localhost is already in use。命令行执行 netstat -ano | findstr 8080 找到占用进程PID,再到任务管理器结束进程。如果机器上装了多个Java服务,建议把Tomcat端口改到 8081 或 9090,修改 conf/server.xml 里的 <Connector port="8080"> 即可。
我用一个表格把这些常见问题整理在方案文档里,答辩演示时老师问到了直接翻给他看,非常加分。
| 现象 | 根因 | 解决方案 |
|---|---|---|
| Access denied for user | 密码或用户名错误 | 检查JDBC账号密码 |
| ClassNotFoundException | 驱动jar未加入WEB-INF/lib | 把驱动jar复制过去 |
| UnknownHostException | 网络或主机名问题 | 检查数据库地址和端口 |
| 中文乱码 | 字符集不统一 | 统一utf8mb4和characterEncoding |
| 端口占用 | Tomcat端口被占 | 换端口或结束占用进程 |
| 500 Internal Server Error | Java异常或空指针 | 查看Tomcat日志定位 |
注意:遇到500错误,第一时间去Tomcat的
logs目录下看localhost.当天日期.log文件,里面会有完整的异常堆栈。很多同学一慌就到处乱改代码,其实日志里早就写清楚了是哪一行报空指针。
4.3 部署到Tomcat时最容易忽略的细节
Tomcat部署有几种方式,最常见的两种:一种是在IDE里直接Run On Server,适合开发调试;另一种是打WAR包放到Tomcat的 webapps 目录下,适合交付部署。课程设计最终通常要第二种方式,因为要让老师在任意电脑上都能跑起来。
打WAR包有几种方式:Eclipse里右键项目 -> Export -> WAR file;IDEA里 Build -> Build Artifacts -> Build;如果用Maven,mvn clean package 一条命令搞定。
WAR包放到Tomcat的webapps目录后,启动Tomcat,它会自动解压。这里有几个坑:
- 如果WEB-INF/lib目录下只有MySQL驱动,没有JSTL的jar,页面用了JSTL标签会报错。确认一下
jstl-1.2.jar和standard.jar是否在lib里。 - WAR包解压后的目录名就是访问路径。如果包名是
Bookkeeping.war,访问地址就是http://localhost:8080/Bookkeeping/index.jsp。 - 交付给老师时,光给WAR包不够,要写一个
部署说明.txt,写清楚JDK版本、Tomcat版本、MySQL版本、数据库初始化脚本的执行方法、数据库连接账号密码配置位置。我见过很多项目代码写得不错,但老师部署不起来,最后得分没那么理想,全是栽在部署说明上。
5. 拿到源码后怎么快速跑通:项目结构、配置修改和二次开发扩展
5.1 读懂项目目录结构
一个规范的JSP记账本项目,目录结构应该是这样:
code复制Bookkeeping/
├── src/
│ ├── com/example/model/ 实体类:User.java, Category.java, Record.java
│ ├── com/example/dao/ 数据访问层:UserDao.java, RecordDao.java, CategoryDao.java
│ ├── com/example/servlet/ 控制器:LoginServlet.java, RecordAddServlet.java
│ └── com/example/util/ 工具类:DbUtil.java, MD5Util.java
├── WebContent/
│ ├── WEB-INF/
│ │ ├── lib/ MySQL驱动jar、JSTL jar
│ │ └── web.xml 部署描述符
│ ├── css/ 静态样式
│ ├── js/ jQuery、ECharts等
│ └── *.jsp login.jsp, register.jsp, record/list.jsp, stats.jsp
拿到源码后,先别急着跑,按顺序做以下动作:
- 用IDE导入项目,确认是Dynamic Web Project或Maven项目
- 看
src/com/example/util/DbUtil.java,确认数据库连接方式和账号密码 - 打开
sql或db目录下的建表脚本,在本地MySQL里执行 - 启动项目,用测试账号登录
5.2 必改的三个配置文件
- 数据库连接配置:DbUtil里的URL、用户名、密码。如果数据库版本是MySQL 8.0,确保驱动也是8.x版本,否则驱动类和URL写法都不兼容。
- web.xml里的欢迎页面和Servlet映射。确认
<welcome-file-list>指向 login.jsp,确认Servlet的<url-pattern>和实际访问路径一致。 - 字符编码过滤器。如果项目没有统一编码,建议在web.xml里加一个过滤器。
xml复制<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
5.3 二次开发的功能扩展方向
如果你不满足于只把基础功能跑通,想拿高分,可以考虑加这些功能,它们都建立在现有代码结构之上,不需要推翻重写。
- 增加图表统计:用ECharts饼图展示支出分类占比。后端写一个统计接口返回JSON,前端Ajax调用并渲染图表。这个功能实现简单,视觉效果好,答辩现场特别亮眼。
- 增加账单搜索:在列表页加日期范围、分类筛选。DAO层加一个组合查询方法,用StringBuilder拼接动态SQL。
- 增加数据导出:用POI导出当月账单为Excel文件。这个功能虽然要用到新jar包,但逻辑很简单,查询记录、循环写Excel、返回下载流。
- 增加密码找回或验证码:登录页面加一个简单验证码,用Servlet负责生成图片。这个能体现对安全性的考虑。
5.4 编码安全和规范:别在答辩时被问住
最后说一个容易被忽视的点:代码规范和安全,这往往是答辩老师最常提问的领域。
你用了 PreparedStatement 防止SQL注入,加分。如果用了 Statement 拼SQL,一定要在项目说明里回避或者提前想好怎么解释。
用户密码不建议明文存储。课程设计可以不用太复杂的加密,但至少用MD5加盐处理。MD5虽然不算安全的加密算法,但比明文好太多,而且代码实现简单。如果老师问“为什么不用MD5”,你可以说“MD5是项目基础版本的密码摘要方案,如果要上生产环境,会换成BCrypt”。
页面底部写上统一导航和版权信息,避免出现“暂无数据”时页面空白的尴尬。列表页加一个简单的分页,至少用LIMIT做假分页。分页代码不复杂,但能体现你对大数据场景的思考,这在答辩时是很大的亮点。
我个人在实际把玩这个项目时的体会是:JSP记账本真正难的不是某个单个技术点,而是把所有技术点串成一条完整业务线的过程。一开始我也觉得登录、增删改查、统计这些功能各做各的,谁都不难,但把它们整合进一个项目里,边边角角的问题就全冒出来了。等你把这个项目完整跑通,再回头看Servlet的生命周期、JDBC的连接管理、Session的作用范围,都会有一种“原来是这样”的通透感。这个过程本身,就是做课程设计最大的收获。
