很多第一次接触“JSP中小型艺术培训机构管理系统”这套题目的朋友,拿到手的第一反应往往是:这么多 JSP 文件、一堆 Servlet、几个 SQL 脚本,到底从哪开始看?我最初接手这种带源码、带数据库、还要自己调试部署的 Java Web 项目时,也花了不止一个晚上才把环境跑通。所以这篇我打算不按官方式的项目说明书来讲,而是直接按我自己动手梳理和部署这套系统的真实顺序来写,把业务建模、表结构设计、开发环境搭建、核心编码思路和部署过程中的坑一次性讲透。
如果你是做课程设计、毕业设计,或者刚接手一套 JSP 老项目需要本地跑起来,又或者想从零模仿着写一个中小型机构管理系统,这篇文章都适用。技术栈很经典:JSP + Servlet + JDBC + MySQL + Tomcat,没有花哨的微服务,就是一个标准的 Java Web 单体应用。它能覆盖艺术培训机构的学员档案、课程开班、报名排课、课时消耗、缴费统计和教师授课安排,规模不大,但对理解 Web 项目的完整链路非常合适。
下面我按照自己的实现和调试顺序,把整套东西拆开讲。
1. 中小型艺术培训机构真正需要的业务闭环
1.1 机构日常运营里逃不掉的那些事
我最早以为艺术培训机构的系统就是“把学生信息存进数据库”,真正和开舞蹈班、美术班的老师聊完才发现,核心是按课时消耗来管理预收款项。这和普通的商品进销存有本质区别。
一家钢琴培训工作室的典型业务流是这样的:新生到店咨询后,先登记基本信息,比如姓名、年龄、家长联系电话、想学的科目。销售或前台老师会推荐课程包,例如“钢琴一对一,20节课,单价 160 元/节,总价 3200 元”。家长缴费后,这名学生才正式成为学员,并且系统里应该有一个可用课时数 = 20的记录。之后每次来上课,老师或者教务人员在点名表上勾一下,系统自动把剩余课时扣掉 1 节。课程上到还剩两三节时,机构需要能看到哪些学员快没课时了,方便安排续费提醒。
这里有一个容易搞混的点:报名不等于缴费,缴费不等于实际消课。如果数据库设计时只做一张“报名表”,不区分“缴费订单”和“上课记录”,那后面统计机构这个月到底消耗了多少课时、还有多少预收款没有消课,就会算成一笔糊涂账。
1.2 我敲定功能清单的过程
当时我给自己列功能清单时,并没有一上来就写代码,而是先模拟了一个月内机构前台要做的所有操作。原型很简单:
- 管理员登录系统,修改密码;
- 教师管理,维护授课教师和所教科目;
- 课程管理,定义“钢琴”“美术”“街舞”“素描”等课程,以及标准单价;
- 班级或开班计划,比如“周六上午 10 点钢琴一对一 A 班”;
- 学员管理,新生登记、编辑、转班、停课;
- 报名选班,记录学生报了哪个班级,生成缴费单;
- 上课签到,记录每次到课和扣减课时;
- 课时统计,查询某个学员剩余课时,全班到课情况;
- 缴费与退费记录,算机构流水。
真实做到最后,你可以把模块合并成三大块:基础档案(教师、学员、课程)、业务过程(报名、签到、缴费)、统计查询(课时余量、收入汇总)。这套功能划分刚好也对应了 JSP 项目里的包结构,后续写代码会非常顺畅。
提示:如果只是应付答辩或二次开发,没必要去加复杂的排课算法,艺术培训机构多数是固定课程包+固定班级,一个学员在一个周期内对应一个班、一个授课教师,这种业务模型用关系型数据库处理是最舒服的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么坚持用 JSP + Servlet + MySQL,而不是一上来就 Spring Boot
2.1 这套老组合在今天仍有价值
现在大家写项目动不动就是 Spring Boot + MyBatis Plus,一看到 JSP 项目就皱眉。我的观点是:你要先搞清楚这个项目的使用场景。
JSP + Servlet 是 Java Web 最底层的运行模型。JSP 本质上最终会被容器翻译成 Servlet,再由 Tomcat 加载执行,所以它不存在“过时到没法用”的问题。对于中小型管理系统,页面数量大约在 20 到 40 个之间,表数量在 8 到 12 张之间,JSP + Servlet 完全能扛住,而且部署时不需要额外的依赖管理,把 WAR 包丢进 Tomcat 就能跑。这对没有 Maven 私服、网络也不一定顺畅的教室环境来说,反而是优点。
另外,很多带“程序+源码+数据库”的项目包,源码里根本没有 pom.xml,而是直接用了 Eclipse 的动态 Web 工程目录结构。你如果非要把它强行改成 Spring Boot,会遇到大量路径、注解、JSP 视图解析的兼容问题,改造时间比重新写一个还长。
2.2 技术分层怎么分才不混乱
标准做法是用 MVC 思想来约束自己,而不是真的引入复杂的 Spring MVC。我建议这样分:
- View 层:JSP 页面,只负责展示数据和提交表单;
- Controller 层:Servlet,接收请求、解析参数、调用 Service、决定跳转哪个 JSP;
- Service 层:写业务逻辑,比如报名时校验班级人数上限、扣课时时要先判断余量;
- DAO 层:用 JDBC 操作 MySQL,封装对每张表的增删改查。
对应到项目目录就是 src/com/xxx/entity、src/com/xxx/dao、src/com/xxx/service、src/com/xxx/servlet,加上 WebContent 目录下的 admin、student、course 等 JSP 子目录。这样分层之后,哪怕代码里 JSP 仍然夹杂了少量 Java 脚本片段,整体结构也不会烂到没法维护。我在实际改代码时,发现大多数“JSP 项目烂代码”的根本原因不是用了 JSP,而是把<% ... %>脚本片段写得到处都是,业务逻辑又全堆在页面里,这种代码后期基本不敢动。为了保证源码整洁,我给自己定了一条规矩:JSP 里原则上只留 JSTL 标签和 EL 表达式,Java 逻辑一律放 Servlet 和 Service。虽然 JSP 项目不强制这么做,但答辩或面试时这会是很好的加分点。
2.3 开发运维角度的匹配性
从调试部署角度讲,JSP 项目在 Tomcat 下排错很方便。修改 JSP 页面后,无需重启 Tomcat 就能看到效果;修改 Servlet 或者 Java 类后,在 IDE 中重启 Tomcat 即可。MySQL 5.7 配合 JDBC 驱动 mysql-connector-java 5.1.49 是兼容性最稳的组合,现在很多新数据库版本反而在 SSL 连接参数、时区问题上给新手添堵。
3. 数据库建模:把课时、续费、排班这些业务捋直
3.1 核心表划分与命名
数据库是整个系统的地基。我习惯先画一张表关系图,但在文章里我不用流程图,直接说设计和表字段,因为这部分才是真正决定项目质量的地方。我当时给这套系统设计的核心表如下:
| 表名 | 中文含义 | 关键字段与说明 |
|---|---|---|
| admin | 后台用户表 | id, username, password, real_name, role。角色可区分超级管理员和普通教务 |
| teacher | 教师表 | id, name, phone, course_id, hire_date, status |
| student | 学员表 | id, name, gender, age, parent_phone, course_id, enroll_date, status |
| course | 课程表 | id, course_name, course_type, price_per_period, valid_months, description |
| banji | 班级开班表 | id, course_id, teacher_id, class_name, start_date, class_time, max_student, current_student |
| sign_up | 报名表 | id, student_id, banji_id, sign_time, total_period, remain_period, payment_status |
| attendance | 上课签到表 | id, student_id, banji_id, teacher_id, class_date, is_attend, remark |
| payment | 缴费记录表 | id, sign_up_id, student_id, amount, pay_time, pay_type, operator_id |
需要注意,专业术语里尽量避免使用 class 作为表名,因为它是数据库和 Java 里的保留字,我用 banji 代替,兼容性更好。
3.2 为什么把“剩余课时”冗余在报名表里
很多人会把剩余课时实时计算出来,每次查询都去 attendance 表里 count(*),看这个学生缺勤了几次再拿总课时去减。这样做在数据量小的时候没问题,但机构每天上下课打卡很频繁,每次都全表统计会浪费性能。更关键的是,机构的业务规则不只是“到课扣课”,还有请假补课、教师临时调课等情况,留一个 remain_period 字段在报名表里,签到成功时在同一个事务中更新,业务逻辑会更直观。
当然冗余字段会带来一致性问题,所以我在签到模块里把“判断剩余课时大于 0”和“扣减剩余课时”放到同一个数据库事务里执行,避免了并发情况下的超扣。
3.3 外键是用还是不用
不少 JSP 源码里建表时喜欢把所有外键加满,这看起来关系清晰,实际导入数据时经常因为表顺序问题报错:“Cannot add or update a child row”。我的做法是:表结构设计中保留逻辑外键,但不一定强制加物理外键约束。也就是说,在创建表时不再用 FOREIGN KEY 约束,而是在使用 JDBC 查询时用 JOIN 维护引用关系。这并不代表不重视数据完整性,而是为了降低导入数据和分模块调试的成本,程序里的业务校验兜底。
建表 SQL 示例可以这么写:
sql复制CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '学员姓名',
`gender` tinyint(4) DEFAULT '1' COMMENT '1男 2女',
`age` int(11) DEFAULT NULL,
`parent_phone` varchar(20) DEFAULT NULL COMMENT '家长联系电话',
`course_id` int(11) DEFAULT NULL COMMENT '主学课程',
`enroll_date` date DEFAULT NULL COMMENT '报名日期',
`status` tinyint(4) DEFAULT '1' COMMENT '1在读 2停课 3毕业',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学员表';
数据库字符集一定要用 utf8mb4,别再用 utf8。因为 utf8 在 MySQL 里并不是真正的全量 Unicode,遇到家长姓名里的生僻字或者特殊符号会变成乱码,甚至直接报错。这个细节是我在真机部署时踩过的坑,下面调试部分我会再讲一次。
3.4 初始数据和演示数据怎么准备
网上拿到的源码一般会附带 db_art_school.sql。我发现很多初学者导入失败是因为直接双击运行整个 SQL 文件,而文件里的库名、编码和本地 MySQL 配置不一样。稳妥的导入步骤是:
- 用文本编辑器打开 SQL 文件,确认有没有
CREATE DATABASE语句; - 没有的话,自己在 MySQL 里先建库:
CREATE DATABASE art_school DEFAULT CHARACTER SET utf8mb4; - 再用命令行导入:
bash复制mysql -uroot -p art_school < db_art_school.sql
如果 SQL 文件里已经包含 CREATE DATABASE 和 USE 语句,就只需要在命令行执行 mysql -uroot -p < db_art_school.sql。我自己更推荐用 Navicat 或 DataGrip 导入,遇到中途报错还能精确定位到具体行。
4. 开发环境一套流程:JDK 8、Tomcat 9、MySQL 5.7 到 Eclipse
4.1 版本组合怎么选才不闹心
JSP 系统最怕的就是环境版本前后不匹配。我建议你整套环境按照下面这个组合来配:
| 组件 | 推荐版本 | 理由 |
|---|---|---|
| JDK | JDK 8(1.8.0_291) | 兼容老 Web 工程,不需要额外处理模块化参数 |
| Tomcat | Tomcat 9.0.x | 支持 Servlet 4.0,稳定且完美兼容 JSP 2.3 |
| MySQL | MySQL 5.7 | 和 5.1.x 驱动搭配最省心 |
| JDBC 驱动 | mysql-connector-java 5.1.49 | 放在 WebContent/WEB-INF/lib 下 |
| IDE | Eclipse IDE for Enterprise Java and Web Developers | 导入 Dynamic Web Project 最友好 |
不要一激动去装最新的 JDK 17 或 Tomcat 10。Tomcat 10 之后,包名从 javax.servlet 变成了 jakarta.servlet,大量老 JSP 源码编译会直接报错。JDK 8 + Tomcat 9 + MySQL 5.7 是兼容最稳的三角组合。
4.2 导入乱码工程的正确姿势
从网上下载的源码如果压缩包解压后没有 .project 和 .classpath 文件,直接把文件夹放进 Eclipse 工作是导不进去的。我的做法是:
- Eclipse 中新建一个 Dynamic Web Project,项目名和源码包名保持一致;
- 将下载的
src目录下的 Java 文件复制到新项目的src目录; - 将下载的
WebContent(也可能叫WebRoot)下的文件复制到新项目的WebContent目录; - 在项目构建路径中把
WebContent/WEB-INF/lib下的 MySQL 驱动 JAR 加入 Build Path; - 检查项目右键 Properties > Project Facets,确保 Dynamic Web Module 版本是 3.1 或 4.0,Java 版本是 1.8;
- 最后在 Servers 面板中关联本机 Tomcat 9。
如果源码里本身带了 .project,直接 Import > Existing Projects into Workspace 也行,但导入后第一件事就是检查字符集。我遇到过源码用 UTF-8 保存,而 Eclipse 默认工作区是 GBK,一打开全变乱码的情况。统一处理办法是:Window > Preferences > General > Workspace,把 Text file encoding 设为 UTF-8;再检查项目级别的编码设置,右键项目 > Properties > Resource,也改成 UTF-8。
4.3 Tomcat 如何与 IDE 正确联动
很多新手配置 Tomcat 时直接去 IDE 里添加了一个 Runtime Environment,但 Server Locations 默认使用的是 Eclipse 的临时目录。你在 Eclipse 里 Run on Server 能运行,可是页面上修改 JSP 后,浏览器如果访问的是 Tomcat 安装目录下的 webapps,自然看不到效果。解决方法是:
- 在 Servers 面板双击已配置的 Tomcat Server;
- 在 Server Locations 里选择 Use Tomcat installation;
- Deploy Path 保持
webapps; - 保存并重启 Server。
这样做的好处是,你可以直接从 Tomcat 安装目录的 webapps 下找到项目的 WAR 包或展开目录,方便再把项目单独导出去部署到别的电脑上。
5. 从登录到课时扣减:核心模块的实现细节
5.1 登录认证用 Session + 一个干净的 Filter
老项目里最常见的登录逻辑是每个页面加载时判断 session.getAttribute("adminUser"),代码重复又容易漏。我建议用一个 Filter 统一拦住所有需要登录的请求。这样既能保护后面的 JSP 页面,也能保护 Servlet 接口。
java复制@WebFilter("/*")
public class LoginFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
String uri = request.getRequestURI();
// 放行登录页、登录接口和静态资源
if (uri.endsWith("/login.jsp") || uri.endsWith("/login")
|| uri.contains("/css/") || uri.contains("/js/")) {
chain.doFilter(req, resp);
return;
}
Object user = request.getSession().getAttribute("adminUser");
if (user == null) {
response.sendRedirect(request.getContextPath() + "/login.jsp");
return;
}
chain.doFilter(req, resp);
}
}
这段代码同时解决了另一个问题:在 JSP 页面里做跳转时,不要写死绝对路径 /login.jsp,在项目名变化后容易 404。用 request.getContextPath() 动态拼接项目上下文,部署到任何名字的 WAR 包下都没问题。
5.2 学员模块:别把表单参数一个一个塞进对象
如果学员表有十来个字段,最原始的写法是从 request.getParameter("name") 取一个塞一个。这样也不是不行,但代码极其啰嗦,后期加字段要改动的地方多。我推荐简化思路:用 JavaBean + 手动封装相结合。没有反射工具的前提下,写一个静态的 requestToStudent(HttpServletRequest request) 方法,把所有字段提取统一放到一个方法里,至少让 Servlet 的请求处理保持清爽。
同时,新增学员时要做的校验不能省:
- 姓名非空,长度不超过 50;
- 家长手机号格式要校验,比如
^1[3-9]\\d{9}$; - 年龄范围 3 到 60 之间。
如果数据库驱动用的是 MySQL 5.1.49,JDBC 连接串最好这样写:
java复制jdbc:mysql://localhost:3306/art_school?useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true
没有 characterEncoding=utf8,插入中文姓名时很容易出现“?”乱码。
5.3 报名和缴费:事务范围要覆盖两张表
一个学员报名一个班级,数据库操作至少涉及三张表:报名表、缴费表、班级的当前人数更新。这里绝不能只用三条独立的 executeUpdate,否则中途报错会产生“钱收了但班级人数没增”的脏数据。
典型实现思路:
java复制Connection conn = null;
try {
conn = DBUtil.getConnection();
conn.setAutoCommit(false);
// 1. 插入报名记录
int signUpId = addSignUp(conn, studentId, banjiId, totalPeriod);
// 2. 插入缴费记录
addPayment(conn, signUpId, studentId, amount);
// 3. 班级当前人数加 1
updateBanjiCurrentStudent(conn, banjiId, 1);
conn.commit();
} catch (Exception e) {
if (conn != null) {
conn.rollback();
}
throw e;
}
这一点在面试中非常加分,因为很多 JSP 课程设计项目虽然能跑通,但你问他“如果第一步成功第二步失败怎么办”,他多半答不上来。事务控制是判断一个项目有没有生产意识的试金石。
5.4 签到消课:扣减时先校验剩余课时
上课签到页面的做法是:选择一个班级,页面列出该班所有学员,老师勾选本次到课的人,提交到 Servlet。这时会出现“老师选了几个学生,但有的学生剩余课时已经不够”的情况。
稳妥写法是在循环扣课之前先做一次查询:
java复制for (Integer studentId : attendStudentIds) {
int remain = signUpDao.findRemainPeriod(studentId, banjiId);
if (remain <= 0) {
// 记录异常,并在页面提示该学员课时不足
} else {
attendanceDao.insert(studentId, banjiId, teacherId, today);
signUpDao.decreaseRemainPeriod(studentId, banjiId);
}
}
这里其实也有并发问题,两个老师同时给一个学员签到几乎不可能,但要严谨一点,扣课时 SQL 可以写成原子操作:
sql复制UPDATE sign_up SET remain_period = remain_period - 1
WHERE student_id = ? AND banji_id = ? AND remain_period > 0
如果 executeUpdate 返回 0,就代表这个学员已经没课时了,程序可以立刻抛业务提醒,不需要先查余量再更新。这是比“先查后改”更高级的做法。
5.5 统计报表:直接 SQL 聚合比在 Java 里循环高效
很多 JSP 项目的统计页会在 Java 里循环累加,数据一多页面就卡。实际上这种报表完全可以交给 MySQL 的聚合函数:
sql复制-- 统计某个课程已经产生的总缴费金额
SELECT COALESCE(SUM(amount), 0) AS total_amount
FROM payment p
JOIN sign_up s ON p.sign_up_id = s.id
JOIN banji b ON s.banji_id = b.id
WHERE b.course_id = ?;
-- 查询剩余课时已经少于 5 节的学员
SELECT s.name AS student_name, s.parent_phone, su.remain_period
FROM sign_up su
JOIN student s ON su.student_id = s.id
WHERE su.remain_period < 5 AND su.payment_status = 1
ORDER BY su.remain_period ASC;
对于课设来说,“剩余课时预警”就是一个非常实在的功能,甚至比毫无依据的“智能推荐”更打动人。
6. 调试部署里那些能让人熬到凌晨的坑
6.1 404:Tomcat 访问路径与应用上下文不一致
我自己刚跑 JSP 项目时,最常遇到的是项目明明部署成功,浏览器输入 http://localhost:8080/项目名/login.jsp 却 404。这时候先不要查代码,按顺序排查:
- 项目是否成功部署到了 Tomcat 的 webapps;
- 项目名是什么,URL 里的上下文路径是否一致;
WEB-INF下的web.xml中 context root 有没有被 IDE 改掉;- 如果项目没有
index.jsp,直接访问根路径会找不到默认首页,需要手动补后续路径。
如果是在 Eclipse 中 Run on Server 后出现 404,大概率是因为项目没有被加入到 Server 的配置里。打开 Servers 面板,右键 Tomcat,选择 Add and Remove,把项目加入到 Configured 列表里。
6.2 数据库中文乱码的三种可能
中文乱码是最劝退的问题,它可能发生在三个位置:
- 数据库本身:库和表的字符集不是 utf8mb4;
- JDBC 连接:连接串没加
characterEncoding=utf8; - 页面请求编码:POST 请求没有设置统一编码过滤器。
我建议在 web.xml 里加一个字符编码过滤器,让所有请求都按 UTF-8 处理:
xml复制<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>com.xxx.filter.EncodingFilter</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>
同时检查 MySQL 的 my.ini:
ini复制[mysqld]
character-set-server=utf8mb4
修改后重启 MySQL。乱码如果还残留,把原来的表 DROP 后重新导入 SQL,因为已经变成乱码的数据即使改了字符集也不会自动变回来。
6.3 JDBC 驱动加载失败和 MySQL 版本冲突
错误信息如果出现 ClassNotFoundException: com.mysql.jdbc.Driver,表示驱动 JAR 没在类路径下。网上有些项目用的是 com.mysql.cj.jdbc.Driver,那需要新版驱动。我们前面推荐的 5.1.49 驱动,类名仍然是 com.mysql.jdbc.Driver。
如果出现 Public Key Retrieval is not allowed 或者 SSL connection error,说明 MySQL 版本较新又没加 useSSL=false。换成 5.7 或把连接参数补齐为:
java复制jdbc:mysql://localhost:3306/art_school?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
serverTimezone 参数高版本 MySQL 驱动必须显式配置,否则日期类型会报错。
6.4 Tomcat 端口被占用:查杀进程比盲目改端口更靠谱
Tomcat 默认 8080 端口被占用时,控制台会报 Port 8080 required by Tomcat v9.0 Server is already in use。很多人直接改 Tomcat 端口为 8081,确实能启动,但如果项目里写死了某些页面跳转或 JSP 中引用了带端口的地址,又会带来新的坑。
我建议先查端口占用:
bash复制netstat -ano | findstr :8080
taskkill /f /pid 进程号
如果是在 IDE 中反复重启导致的端口占用,甚至有可能是因为 Tomcat 没有完全关闭,到任务管理器结束 java.exe 进程即可。
6.5 WAR 包发布到独立 Tomcat 时的小问题
在本地 Eclipse 里运行正常,但导出成 WAR 包部署到另一台电脑的 Tomcat,有时候会碰到数据库连接不上。这是因为数据库配置写死在代码里,比如连接的是 localhost:3306,换了机器后数据库如果不在本机,就要改成目标机器的 IP。
更稳妥的方法是:写一个 db.properties 放在 src 目录下,启动时用 Properties 类读取,避免每次改连接信息都要重新编译源码。类似下面这样:
properties复制jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/art_school?useUnicode=true&characterEncoding=utf8&useSSL=false
jdbc.username=root
jdbc.password=123456
部署新环境时,只需要改这个文件,不用动 Java 代码,重新打 WAR 时把这个文件一并打包进去即可。
6.6 页面引用了不存在的 JSP 片段
JSP 源码经常使用 include 指令引入公共头部和菜单,比如:
jsp复制<%@ include file="common/header.jsp" %>
如果项目部署后整页空白或某一块空白,控制台没有明显报错,优先检查 include 的路径是不是大小写或目录层级有问题。Linux 服务器上的 Tomcat 对路径大小写敏感,Windows 本地不敏感,所以在本地好好的项目一旦部署到线上就找不到文件,这个坑也常见。
7. 让这套系统从“功能能用”变得“答辩能打”的细节
7.1 分页查询与搜索,哪怕丑也要有
课程设计、毕业设计做出来的管理系统中,“列表页”往往很粗暴:一次性把所有学员显示在表格里,没有搜索也没有分页。这在数据量只有几十条时看不出问题,但你自己要在答辩时演示,最好做一个带条件和分页的查询。毕竟真实机构一定会搜索“姓王的钢琴班学员”或“上周欠费的人”。
分页 SQL 用 LIMIT ?, ? 实现:
sql复制SELECT * FROM student
WHERE name LIKE CONCAT('%', ?, '%')
AND status = 1
ORDER BY id DESC
LIMIT ?, ?
页面底部的分页条不需要做得像大型管理系统那么复杂,显示“上一页、下一页、当前第 x/y 页”就足够了。把当前页、每页条数、总记录数、总页数放在一个 PageBean 里,在 Servlet 中封装后传到 JSP,是 JSP 项目的经典范式。
7.2 用 JSTL 标签替代 JSP 脚本片段
很多老代码里长这样:
jsp复制<%
List<Student> students = (List<Student>) request.getAttribute("studentList");
for (Student s : students) {
%>
<tr>
<td><%=s.getName()%></td>
</tr>
<% } %>
代码能跑,但可读性差。建议改成 JSTL 循环:
jsp复制<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<c:forEach var="s" items="${studentList}">
<tr>
<td>${s.name}</td>
</tr>
</c:forEach>
如果下载的源码里没有引入 JSTL 依赖,记得把 jstl-1.2.jar 放到 WEB-INF/lib。改造之后页面干净很多,自己也更容易通过代码审查。
7.3 SQL 注入防护是最容易被问到的安全点
凡是带查询条件的模块,都可能被 SQL 注入。尤其是登录验证,如果用字符串拼接 SQL:
java复制String sql = "SELECT * FROM admin WHERE username='" + username + "' AND password='" + password + "'";
这就是典型的漏洞,输入 ' or 1=1 -- 可能直接绕过登录。不要嫌麻烦,一律改成 PreparedStatement 预编译。
这个点几乎是任何一次技术面试或课程答辩必问的问题。你只要答出“使用预编译 + 参数占位符,而不是字符串拼接 SQL”,就已经能证明你有基本的安全意识了。再配合统一登录 Filter 的思路,整个项目的安全层次比大多数课设高出一截。
8. 从这套 JSP 系统往后走的扩展方向
很多同学会纠结:我花时间把 JSP 老项目跑完、改完,是不是白费力气?我倒认为不是。JSP 系统对理解 HTTP 请求处理流程非常有帮助,它没有框架帮你自动封装,你能清楚看到一个请求怎样从 JSP 页面发起,到 Servlet 后被接收,再调用 Service、DAO,最后回到页面渲染。这份“底层手感”,直接上手 Spring Boot 的人是体会不到的。
如果这个项目想继续扩展,我会建议这样演进:
- 保留现有的数据库设计,这是整个系统的核心资产;
- 把 Servlet + JSP 的请求处理逐步替换成 Spring MVC 或者 Spring Boot;
- 引入 MyBatis 或 MyBatis Plus 替换 DAO 层的 JDBC 代码;
- 登录认证升级为 Spring Security 或 Shiro;
- 把 JSP 页面替换成 Vue + Element UI 之类的前后端分离方案。
这些步骤不需要推翻重来,而是逐层替换。数据库表结构里只要把“课时”“缴费”“签到”这三条业务线设计对了,换任何框架都能复用。我在实际参与过的项目里见过好几套类似机构管理系统,最终商业版本也都保留着报名表、缴费表、课耗表这三个核心概念,只是换了一身新框架的外衣。
所以我的建议是:不要嫌这套系统“老”。先把它吃透,跑通,再动手改一遍。
我自己把这套艺术培训机构管理系统完整调试完以后,最大的感受是:拿到一个项目,别急着钻代码,先花一个晚上把业务闭环和表结构的关系搞明白,再启动环境,效率会高很多。项目里那么多 JSP 文件,其实真正值得细看的无非是登录、报名、签到和缴费统计这几个页面,其余基本都是同类增删改查。只要业务链路在自己脑子里清晰了,代码和部署中的问题大多是查得着的配置问题,而不是高深莫测的架构问题。
最后再分享一个小技巧:如果你手里这套源码的数据库脚本导入后总报错,可以先把脚本里的 DROP TABLE 和 CREATE DATABASE 单独复制出来执行,再分段执行建表语句,最后执行插入语句。这种“分段排错”的思路对整个项目的调试同样适用——遇到问题先缩小范围,再逐步定位,比对着日志乱猜要快得多。
