1. 老技术新问题:这个成绩管理系统到底要解决什么
JSP + Servlet + Bootstrap + MySQL,这个组合放到2025年看确实不算新潮——Spring Boot 已经占领了绝大多数新项目的默认起点。但在学校里的课程设计、毕业设计,以及不少老单位正在运行的教务系统里,这套技术栈依然是实打实的生产主力。我见过太多学生拿着"学生成绩管理系统"这个题目,直接照着一个 Spring Boot 课程视频改改就交了,结果问 Servlet 生命周期、Session 原理、JDBC 连接池是什么,一脸茫然。反而那些老老实实用 JSP + Servlet 把每一个请求、每一次数据库操作都摸清楚的人,后面学框架的时候,速度和质量都明显吊打前者。
这个项目的核心需求非常明确:给教务处提供一个管理学生、课程、成绩的 Web 系统,学生能查成绩,教师能录成绩,管理员能统筹全部数据。听起来功能不多,但仔细拆一遍,它天然覆盖了 JavaWeb 最关键的几个知识点:三层架构设计、Servlet 生命周期、JSP 九大内置对象、四大作用域、JDBC 编程、数据库连接池、Filter 拦截器、EL 表达式和 JSTL 标签库。任何一个环节没搞懂,系统跑起来就会出各种莫名其妙的 bug。
所以这篇文章,我不打算给你贴一个完整源码让你直接复制交差了事。我想把它当成一个完整的工程案例,从技术选型的思考、环境搭建的参数细节、后端分层的代码写法,到前端 Bootstrap 整合、打包部署的完整流程,把每一步的关键决策和踩坑点都讲清楚。这篇文章适合三类人:正在做 JavaWeb 课程设计的学生、想补传统 JavaWeb 技术细节的框架使用者、以及需要维护老项目的初级开发。看完你不仅能把这套系统跑起来,还能明白它为什么这么设计。
2. 从零搭环境:IDEA配置JavaWeb项目与MySQL一次跑通
先说句实话:这个项目超过三分之一的坑,都出在环境搭建阶段。代码还没写几行,光是在 IDEA 里把 Tomcat 跑起来、让 MySQL 连上,就劝退了不少人。所以环境这部分我讲得细一点,版本搭配、参数配置、可能出现的报错一次性说清楚,后面你照着做基本能一把过。
2.1 环境版本怎么搭配才不折腾
我自己在教学中验证过很多套组合,最省心的搭配是这样的:JDK 1.8(真的不用上 17,传统 JavaWeb 项目上 17 反而容易出现各种兼容问题)、IDEA 2023 或 2024 版本、Tomcat 8.5、MySQL 5.7 或者 8.0 都行。这里有个重要提醒:如果选 MySQL 8.0,连接驱动必须用mysql-connector-java 8.0.x,别用老旧的 5.1.x 驱动硬怼,不然会报 SSL 连接错误或者Public Key Retrieval is not allowed这类问题。
具体版本对照我整理在下面,照着配可以少踩很多坑。
| 软件 | 推荐版本 | 选择理由与注意点 |
|---|---|---|
| JDK | 1.8(8u202+) | 兼容 Tomcat 8.5 和传统 JSP/Servlet 规范,避免新版本模块化限制 |
| IDEA | 2023.x 或 2024.x | 自带 Tomcat 集成,社区版也够用但配置稍微绕,建议 Ultimate |
| Tomcat | 8.5.x | 对应 Servlet 3.1 规范,支持 JSP 2.3,对传统项目最友好 |
| MySQL | 5.7 或 8.0 | 8.0 注意时区参数,5.7 直接一把梭 |
| 驱动 | mysql-connector-java 8.0.33 | 8.0 数据库必须配 8.x 驱动;5.7 用 5.1.49 也行 |
提醒一点:如果之前机器上装过 MySQL 5.7,这次想换 8.0,卸载时一定记得把数据目录、服务注册、环境变量全部清理干净,不然
net start mysql经常会报"服务无法启动",大概率就是注册表和残留目录冲突了。
2.2 数据库建库脚本与连接串的关键细节
数据库设计是整个系统的地基。我习惯按照"学生、教师、课程、成绩、班级"这五张核心表来做,教务系统的典型场景就齐了。设计时要注意:学生表里加班级字段或者单独拆班级表都有道理,我建议拆出来,因为后续做"按班级查成绩汇总"会特别方便。
建库脚本核心部分长这样:
CREATE DATABASE edu_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE edu_system; CREATE TABLE tb_student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', stu_name VARCHAR(50) NOT NULL COMMENT '姓名', gender CHAR(1) DEFAULT '男', class_id INT COMMENT '班级外键', created_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE tb_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(100) NOT NULL, credit INT DEFAULT 2 ); CREATE TABLE tb_score ( id INT PRIMARY KEY AUTO_INCREMENT, stu_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2), exam_type VARCHAR(20) DEFAULT '期末', FOREIGN KEY (stu_id) REFERENCES tb_student(id), FOREIGN KEY (course_id) REFERENCES tb_course(id), UNIQUE KEY uk_stu_course (stu_id, course_id) );字符集这里我用的是utf8mb4而不是utf8,原因很简单:MySQL 的utf8最多只能存 3 字节的字符,碰上生僻字或者部分特殊符号会直接报错,utf8mb4才是完整兼容 Unicode 的。JSP 页面里也保持 UTF-8,然后在 JDBC 连接串里再指定一遍,三层统一后中文乱码问题能省掉一半。
JDBC 连接串我推荐直接这么写,注意characterEncoding、serverTimezone和useSSL这三个参数:
String url = "jdbc:mysql://localhost:3306/edu_system?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4&allowPublicKeyRetrieval=true";useSSL=false:本地开发不需要走 SSL 证书,避免握手报错;serverTimezone=Asia/Shanghai:MySQL 8.0 默认时区和本地系统不一致时会报错,指定之后一劳永逸;allowPublicKeyRetrieval=true:MySQL 8.0 配合 caching_sha2_password 认证插件时的必选项,不加就报访问被拒绝;characterEncoding=utf8mb4:和数据库、页面三级统一,彻底根治中文乱码。
2.3 IDEA部署Tomcat的完整配置流程
IDEA 里跑 JavaWeb 项目,很多新手卡在不知道"项目怎么才能访问到"。传统 JavaWeb 项目在 IDEA 里的部署逻辑和 Spring Boot 完全不同:Spring Boot 内嵌容器点运行就行,JSP + Servlet 项目必须手动把 Web 项目打包或"爆炸目录"部署进 Tomcat 的 webapps 下再启动。
具体步骤是这样的:
- 新建项目时选择 Java Enterprise,勾选 Web Application,这样 IDEA 会生成标准的
web/WEB-INF目录结构; - 打开 Run/Debug Configurations,点加号选择 Tomcat Server -> Local;
- 在 Deployment 标签页点加号,选择 Artifact,通常用
项目名:war exploded; - Application context 填
/或者/edu_system,推荐后者,方便后面部署多个项目不冲突; - 回到 Server 标签页,确认 HTTP port 默认 8080(如果被占用改成 8081 就行,端口冲突是非常常见的问题)。
明说一个经验:如果不是特殊需要,建议用 war exploded 而不是 war 包。exploded 模式本质是把编译后的目录直接映射到 Tomcat,支持 JSP 修改后刷新即生效,开发体验比打 war 包热部署舒服太多。等最后要部署到服务器,再手动打成 war 包也不迟。
配置完成后点运行,IDEA 会自动启动 Tomcat 并打开浏览器访问你的项目路径。如果能看到默认的 index.jsp 页面,环境就通了。
3. 后端核心代码:Servlet生命周期、DAO与三层架构落地
环境通了之后,接下来的核心就是把后端代码写清楚。我不打算把所有代码全文贴出来,那样文章会变成代码仓库。我挑四个最核心、最影响整体质量的点来说:Servlet 生命周期如何体现在项目里、数据库连接池封装与 DAO 层写法、登录认证与 Session 机制、成绩分页查询的 Servlet 实现。这四个点吃透了,整个系统的主干就通了。
3.1 Servlet生命周期到底怎么体现在项目里
面试和考试总爱问 Servlet 生命周期,很多人背得滚瓜烂熟,但真到写代码的时候就忘了。其实这个概念非常简单:Servlet 从创建到销毁一共经历三个阶段——init、service、destroy。init 在容器启动后首次收到请求时被调用,且只调用一次,适合做资源初始化;service 每次请求都会进来,负责分发到 doGet 或 doPost;destroy 在项目停止或 Servlet 移除时调用,适合释放资源。
在这个系统里,我实际用到了这个生命周期的地方有两个。第一个是在登录接口里重写了init()方法读取初始化参数,比如验证码开关,虽然这个项目里我直接用硬编码了,但这是一个很好的扩展点;第二个是在整个应用层面,我用 ServletContextListener 来初始化 Druid 连接池,而不是在每次请求里新建数据库连接。你写代码的时候要有这个意识:一切只需要初始化一次的资源,都应该放进 init 或者 context 生命周期里,而不是丢到每次请求的处理逻辑中。
看一个最典型的登录 Servlet,感受一下 doGet 和 doPost 的分工:
@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { request.getRequestDispatcher("login.jsp").forward(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { req.getSession().setAttribute("loginUser", user); if ("admin".equals(user.getRole())) { resp.sendRedirect("admin/index.jsp"); } else { resp.sendRedirect("student/index.jsp"); } } else { req.setAttribute("errorMsg", "用户名或密码错误"); req.getRequestDispatcher("login.jsp").forward(req, resp); } } }这里必须解释一个很多新手绕不清的点:为什么登录失败用 forward,而登录成功用 sendRedirect?因为 forward 是服务器内部的请求转发,浏览器地址栏不会变,request 域里的errorMsg还能传给 JSP 显示;sendRedirect 是让浏览器重新发起一次请求,地址栏会变成目标路径,能避免刷新页面弹窗提示"确认重新提交表单"。这正是 request 作用域和 session 作用域的典型使用场景:一次性提示消息用 request,跨页面保持登录状态用 session。
3.2 数据库连接池封装与 DAO 层写法
传统 JDBC 编程最容易被喷的问题就是每次请求都DriverManager.getConnection(),这个操作非常昂贵,性能差不说,连接数一多数据库直接扛不住。所以这个项目我直接用 Druid 连接池。Druid 是国产的开源连接池组件,监控页面和 SQL 日志对调试特别友好,配置文件放在src/druid.properties:
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/edu_system?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4&allowPublicKeyRetrieval=true username=root password=123456 initialSize=5 maxActive=20 maxWait=60000然后在项目里写一个 DBUtil 工具类,用静态代码块加载配置,把getConnection()和close()封装好。这里用静态代码块其实就体现了 Servlet 生命周期思想在普通 Java 里的延伸:连接池是整个应用共享的资源,初始化一次就够了。
public class DBUtil { private static DruidDataSource dataSource; static { try { Properties props = new Properties(); props.load(DBUtil.class.getClassLoader().getResourceAsStream("druid.properties")); dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError("数据库连接池初始化失败"); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }DAO 层写法的核心准则就一条:一个方法只做一件事,SQL 全部用 PreparedStatement 预编译,参数用?占位。这里我不厌其烦地强调:不要在 DAO 里拼字符串 SQL,SQL 注入的教训太多,尤其在成绩管理系统这种登录和查询场景密集的项目里,拼字符串等于把数据库裸奔给人看。
看一个典型的查询方法:
public List<ScoreVO> findScorePage(int pageNo, int pageSize) { List<ScoreVO> list = new ArrayList<>(); String sql = "SELECT s.stu_no, s.stu_name, c.course_name, sc.score " + "FROM tb_score sc " + "JOIN tb_student s ON sc.stu_id = s.id " + "JOIN tb_course c ON sc.course_id = c.id " + "ORDER BY sc.id DESC LIMIT ?, ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, (pageNo - 1) * pageSize); ps.setInt(2, pageSize); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { ScoreVO vo = new ScoreVO(); vo.setStuNo(rs.getString("stu_no")); vo.setStuName(rs.getString("stu_name")); vo.setCourseName(rs.getString("course_name")); vo.setScore(rs.getBigDecimal("score")); list.add(vo); } } } catch (SQLException e) { e.printStackTrace(); } return list; }LIMIT ? 分页参数的计算是个高频易错点:第一页是(1 - 1) * pageSize = 0,从第 0 条开始取;第二页是(2 - 1) * pageSize = pageSize,跳过第一页。注意这里用try-with-resources把 Connection、PreparedStatement、ResultSet 三个资源都自动关闭了,不会因为忘写 close 导致连接泄漏。
3.3 登录认证与成绩分页的Servlet实现
登录功能是这个系统最核心的拦截点。光在登录 Servlet 里判断用户名密码还不够,你还得保证:没登录的人不能直接访问管理页面,不同角色的人不能互相越权。这个需求用 Filter 过滤器来做最优雅。
写一个 LoginFilter,拦截所有/admin/*和/student/*路径的请求,检查 session 里有没有用户对象,没有就重定向回登录页:
@WebFilter(urlPatterns = {"/admin/*", "/student/*"}) public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; HttpSession session = req.getSession(false); Object loginUser = (session != null) ? session.getAttribute("loginUser") : null; if (loginUser == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }整个请求流转的链路是这样的:浏览器发起请求 -> Tomcat 接收 -> 经过 Filter 过滤器链 -> 匹配到对应 Servlet 的 service 方法 -> 根据请求方式分发到 doGet/doPost -> Servlet 调 DAO 查询数据 -> 把结果放进 request 域 -> forward 到 JSP 渲染 -> JSP 返回 HTML。
这个链路在框架里被隐藏得太深,很多只学过 Spring Boot 的开发真不知道请求经过了什么。但在传统 JavaWeb 里,每一步都是代码里实打实看得见的,这也是我推荐学生做这个项目的原因。
成绩查询我做了分页,分页算法本身不复杂但很值得掌握。前端传 pageNo 和 pageSize,默认 pageNo=1、pageSize=10,后端先查总记录数计算出总页数,再查当前页数据,把数据、当前页、总页数全部塞进 request 域,跳转到 JSP。总页数的计算就一行:int totalPages = (int) Math.ceil(totalCount * 1.0 / pageSize);,这里用Math.ceil向上取整,保证最后不满一页的数据也能有单独的一页展示。
4. 前端页面:JSP + Bootstrap整合的实用写法
后端写得再好,页面一塌糊涂也不行。Bootstrap 在这个项目里的定位是"低成本快速做出能看的后台界面",不用自己写复杂的 CSS。但传统 JSP + Bootstrap 项目的页面写法有一些特有的讲究,很多用惯了 Vue 的人上手反而不适应。
4.1 Bootstrap引入方式的选择
一个很实际的问题:Bootstrap 用 CDN 还是本地文件?如果你是在实验室、内网环境做课程设计交付,我强烈建议把 Bootstrap 的 css/js 文件下载到项目web/static目录下,通过本地路径引用。原因很简单:你答辩的时候如果现场网络不好,CDN 加载失败,整个项目的样式全部崩掉,那画面太尴尬了。
本地引入的标准写法:
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="${pageContext.request.contextPath}/static/bootstrap/css/bootstrap.min.css"> </head> <body> <script src="${pageContext.request.contextPath}/static/bootstrap/js/bootstrap.bundle.min.js"></script> </body>注意这里必须用${pageContext.request.contextPath}动态拼接项目名。因为项目部署时 context path 是/edu_system,如果你在 JSP 里写死href="/static/...",浏览器解析的时候会跑到 localhost 根路径下面去找 static 目录,结果就是 404。这个问题困扰了无数刚接触 JavaWeb 的新手。
4.2 列表页与表单页的 Bootstrap 组装
页面结构我用三块来组织:顶部导航栏、侧边功能菜单、主体内容区。顶部用 Bootstrap 的 Navbar,侧边栏用 Nav 组件搭配flex-column,主体内容用container-fluid加卡片。典型的成绩列表页 JSP 片段长这样:
<div class="container-fluid"> <div class="card shadow-sm"> <div class="card-header d-flex justify-content-between"> <h5 class="mb-0">成绩列表</h5> <button class="btn btn-primary btn-sm">String scoreStr = request.getParameter("score"); if (scoreStr == null || scoreStr.trim().isEmpty()) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "成绩不能为空"); return; } try { BigDecimal score = new BigDecimal(scoreStr); if (score.compareTo(BigDecimal.ZERO) < 0 || score.compareTo(new BigDecimal("100")) > 0) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "成绩必须在0-100之间"); return; } } catch (NumberFormatException e) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "成绩必须是数字"); return; }这一层校验在大型系统里通常会配合 Hibernate Validator 之类的框架来做,但传统 JSP + Servlet 项目里就手写,逻辑清楚且零依赖。前端校验保证普通用户操作流畅,后端校验保证数据安全,两条腿走路。
5. 常见问题排查表与性能优化心得
说实话,写这个项目的过程,基本就是一部"报错处理手册"。我把自己在教学和开发中真实遇到过的问题整理成一个速查表,每一项都是实战经验,不是从文档里抄的。这些问题如果等你上了线才发现,排查成本会高出十倍,提前看完能帮你省下大量时间。
5.1 高频报错问题与解决方案
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| JSP 页面中文全部乱码 | 页面编码、数据库编码、连接串编码不统一 | 三级统一为 UTF-8 / utf8mb4;JSP 头部加pageEncoding="UTF-8";response 设置setCharacterEncoding("UTF-8") |
| 浏览器直接下载 JSP 文件而不是执行 | Tomcat 配置了 JSP 引擎但没能正确编译 | 检查新建项目时是否生成了 WEB-INF/web.xml;确认文件扩展名是.jsp而不是.html |
| 访问 Servlet 报 404 | Servlet 映射路径写错或注解路径没带@WebServlet | 检查注解或 web.xml 中 servlet-mapping 的 URL 是否和请求路径一致;确认 context path 正确 |
| MySQL 连接报 Public Key Retrieval is not allowed | MySQL 8 使用 caching_sha2_password 插件时缺参数 | 连接串加allowPublicKeyRetrieval=true;或者在 MySQL 里把加密规则改回 mysql_native_password |
| tomcat 8080 端口被占用 | 其他程序占用端口 | 命令行 `netstat -ano |
| 修改 JSP 后页面不生效 | war 包模式部署没有热更新 | 开发阶段用 war exploded;确认 IDEA 层面开启了 build project automatically |
| 点击删除按钮不弹确认框就删了 | 没有做二次确认 | 在页面加 JS 的 confirm 弹窗,确认后再跳转;正式场景建议改 POST + fetch 删除 |
| 登录后刷新页面直接退出登录 | Session 过期或浏览器禁用了 Cookie | 设置 session 超时时间;检查 Tomcat context.xml 是否配置了 Cookie 接收规则 |
这里特别想强调 MySQL 8 的认证插件问题。MySQL 8 默认使用caching_sha2_password,而老的应用和驱动默认期望的是mysql_native_password。如果你连的是 8.0 数据库但驱动还是 5.x,就会报认证失败。两条路:一是升级驱动到 8.x 并在连接串里加allowPublicKeyRetrieval=true;二是执行一条 SQL 把用户加密规则改回去。我把第二种方法也贴出来,方便需要在老代码环境里快速解决问题的情况:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;这个方法治标,但有时候就是最快能救场的解法。
5.2 细节优化:预编译、事务、连接复用
除了报错排查,这个项目里还有几处值得做的细节优化,直接决定系统是"能跑"还是"跑得耐艹"。
第一个是 SQL 预编译。前面 DAO 部分我已经强调过了,这里再展开说一个数据安全问题:如果你把 SQL 写成SELECT * FROM tb_student WHERE stu_no = '" + stuNo + "'",那么一个恶意构造的学号参数' OR '1'='1就能让你把整个学生表的数据全部拉出来。这不是理论,这是真实发生过无数次的攻击方式。PreparedStatement 预编译机制保证了参数作为数据被传递,永远不会被解释为 SQL 指令。
第二个是事务处理。成绩录入经常是批量操作,比如一个老师录全班 30 个人的成绩。如果每录一条就自动提交一次,中途第 20 条出错,前 19 条已经写进数据库了,数据就不一致。正确做法是:开启事务、批量插入、全部成功才提交、失败则回滚。核心代码模式如下:
Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关掉自动提交 for (Score score : scoreList) { scoreDao.insert(conn, score); } conn.commit(); // 全部成功才提交 } catch (SQLException e) { if (conn != null) conn.rollback(); // 出错回滚 throw e; } finally { if (conn != null) conn.setAutoCommit(true); DBUtil.close(conn); }事务这块很多初学者总觉得用不上,但课程设计答辩的时候,只要你在成绩录入这个功能里提到事务处理,基本就能让答辩老师高看你一眼。因为这说明你已经意识到真实业务系统里"数据一致性"这个核心问题。
第三个是连接复用。很多人写完项目发现 Tomcat 启动后,MySQL 服务时不时报 "Too many connections",原因就是代码里每次都DriverManager.getConnection(),用完又不归还。用数据库连接池之后,连接是复用的,用完归还给池子而不是销毁,高并发场景下性能差距极其明显。我上面给出的 DBUtil 里用 Druid 就是这个目的。
6. 部署发布:war打包与后续扩展方向
项目写完开发完还不算完,你得能把它打包部署到服务器上。传统 JSP 项目的部署方式说简单也简单,说绕也绕。主要分两步:打 war 包,然后放到 Tomcat 的 webapps 目录里。
IDEA 里打 war 包很简单:Project Structure -> Artifacts -> 点加号 -> Web Application -> Archive -> 选对应模块,然后 Build -> Build Artifacts,选择 build 或者 rebuild,war 文件就会生成到你配置的 output 目录。生成的 war 包直接拷贝到 Tomcat 的 webapps 目录下,启动 Tomcat,它会自动解压并部署。
这里要特别注意一个问题:war 包里的WEB-INF/web.xml会被部署时读取,而如果你是纯注解方式(@WebServlet、@WebFilter)写的项目,没有 web.xml,那也没关系。但如果你用的是 IDEA 自动生成的 web.xml,里面通常有一个<welcome-file-list>默认指向 index.jsp,这个路径要和你的项目结构对得上,不然访问根路径会 404。
另外一个部署后的常见问题是数据库连接配置。本地开发时druid.properties里的用户名密码可能是root/123456,但部署到服务器上时密码可能不同。这里我建议把数据库连接信息做成外部可配置的,最简单粗暴的方式就是改 properties 文件,但更好的方式是让系统在启动时读取环境变量或外置配置文件,这样换环境部署不用重新打 war 包。
最后说说这个项目的扩展方向。如果你打算在这个项目基础上继续做毕业设计或者个人项目,我有几个建议的演进路径:
- 把 DAO 层换成 MyBatis-Plus,保留 Servlet 层不动,体验一下框架带来的开发效率提升;
- 把 JSP 替换成 Vue + Element UI 做前后端分离,Servlet 改成提供 JSON 接口,这样界面能漂亮一大截;
- 引入 Redis 缓存成绩查询热点数据,感受缓存穿透、击穿这些名词到底是什么场景;
- 给系统加登录验证码、操作日志、数据导出 Excel 功能,让功能完整度更像真实产品。
我自己实际带过不少学生走完这套路径,最有意思的现象是:做完 JSP + Servlet 项目再学 Spring Boot,很多人不再觉得 Bean、自动配置、起步依赖是什么魔法,因为他们已经在最底层体验过 Tomcat 处理请求的完整流程了。框架终究是对传统模式的封装,理解了底层,框架就只是工具,而不是黑盒。
如果你真遇到卡住的地方,把自己断言的代码路径、报错日志、Tomcat 的 localhost 日志发出来,把问题描述清楚,通常很快就能定位。这个项目本身不复杂,它难的是对整体流程的把握和理解,希望你不仅把它跑起来,还能把它吃透。