大概在三年前,我第一次完整跑通一个 JavaWeb 项目时,用的就是 JSP + Servlet + MySQL + Bootstrap 这套组合。今天想认真拆解一下这个经典课设/毕设项目——基于 JavaWeb 和 MySQL 的日记系统。它麻雀虽小,五脏俱全:用户注册登录、日记的新增编辑删除、数据库设计、前端页面美化,恰好把 JavaWeb 从浏览器到数据库的整条数据链路串了一遍。这篇博客会覆盖完整的环境搭建、建库建表脚本、核心代码思路、页面排版细节,以及我在实际运行中踩过的各种坑,适合正在学 JavaWeb 的同学、准备课程设计的朋友,以及那些不想一上来就扎进 Spring Boot 注解海洋、想先把底层 Web 原理摸透的人。
1. 项目概述与需求拆解:日记系统到底要解决什么
1.1 为什么需要一个在线日记系统
先聊聊业务背景。很多人写日记的习惯是用 Word 文档、txt 或者手机备忘录,但这类方式有个共同问题:数据太分散,时间一长就乱了。换设备想看之前的日记,要么靠同步盘,要么手动转存,时间久了基本就找不到。日记这个场景的核心需求其实很明确:私密、能分类、能随时回溯历史记录。这个项目就是在 Web 端实现了一个轻量级日记管理工具,解决记录、查询、编辑、删除这四件高频操作。
从技术角度看,这又是一个非常标准的 JavaWeb CRUD 项目:用户注册时把账号密码写入 MySQL,登录成功后把用户信息放进 Session,写日记就是一次 INSERT,列表页就是一个带排序的 SELECT。全程没有中间件、没有分布式、没有缓存,有的就是最纯粹的 Web 应用数据流转。也正因为链路短、知识密度高,它特别适合作为理解和掌握 JavaWeb 主线的入门实战项目。我在帮人改代码的过程中发现,凡是能把这个项目里数据流讲清楚的人,后续学框架普遍顺畅得多。
1.2 功能清单与边界控制
做课程设计或者毕业设计,最忌讳一上来就什么都想做。这个日记系统我建议把功能控制在下面这个范围内:
| 模块 | 功能点 | 说明 | 优先级 |
|---|---|---|---|
| 用户模块 | 注册 | 用户名唯一性校验、密码加密存储 | 高 |
| 用户模块 | 登录 | Servlet 处理请求、Session 保存登录态 | 高 |
| 用户模块 | 退出登录 | 销毁 Session 并跳转登录页 | 高 |
| 日记模块 | 新增日记 | 标题、内容、天气、心情、配图 | 高 |
| 日记模块 | 日记列表 | 按时间倒序、分页展示 | 高 |
| 日记模块 | 日记详情 | 根据 ID 查询单条完整记录 | 高 |
| 日记模块 | 编辑与删除 | 必须校验当前用户是作者本人 | 高 |
| 检索模块 | 关键词搜索 | 标题或内容模糊匹配 | 中 |
边界也提前划好:不做复杂的好友关系、不做评论点赞、不做消息推送,就把自己定位成一个"个人工具型应用"。数据权限同样简化:每个用户只能操作自己创建的日记,通过 SQL 条件WHERE user_id = ?来实现,不做细粒度的角色体系。这个设计既保证了项目的完整性,又不至于把工程量撑到一个人做不完的程度。
2. 技术选型与工具链:jsp+servlet+mysql这套组合凭什么还能打
2.1 Servlet 与 JSP 在 Web 开发里的真实定位
先纠正一个被很多人误解的点:Servlet 并不过时,它只是被 Spring MVC 封装了。Spring MVC 的前端控制器 DispatcherServlet 本身就是一个 Servlet,这一点在任何一本框架源码分析的书里都能看到。所以,在还没有建立"请求如何进入 Java 程序、响应如何写回浏览器"这个底层认知之前,直接上手 Spring Boot,遇到各种自动配置异常会很难定位。
JSP 的本质也是一个 Servlet,容器会把 JSP 文件编译成 Java 类再执行。在这个项目里,JSP 负责视图层渲染,Servlet 负责接收请求、调用业务逻辑、决定跳转到哪个页面,JDBC 负责和 MySQL 打交道。三层各司其职,就是标准的 MVC 思想。很多网上笔记喜欢把 JSP+Servlet 说成"老技术",但对教学项目而言,把这一套手工流程跑通,后面的框架学习就是查漏补缺而不是从零开始。
2.2 为什么前端用 Bootstrap 而不是 Vue 或 React
如果这是一个纯后端练习项目,前端用原生 HTML+CSS 也说得过去。但课设和毕设答辩时,页面观感真的很影响第一印象。Bootstrap 的优势在于:现成的 CSS 类和组件、响应式栅格系统、不需要 Node.js 环境、不需要 npm install,下载本地文件或者直接引用 CDN 就能用。对传统 JavaWeb 项目来说,引入成本几乎为零。
选择 Bootstrap 还有一个隐形好处:它是响应式的。日记列表在电脑上显示三列,在手机浏览器上自动堆叠成一列,这个能力是 Bootstrap 底层栅格系统已经处理好的,不需要额外写媒体查询。对不熟悉前端工程化的 Java 学习者来说,这是性价比最高的选型。Vue 和 React 本身没有问题,但它们需要 Node 环境、构建工具、组件库配合,复杂度会明显高出这个项目的定位。
2.3 MySQL 与 JDBC 驱动选型的版本细节
数据库这块选 MySQL 基本没争议:免费、跨平台、资料多,JDBC 驱动足够稳定。真正容易踩坑的是驱动版本和类名对应关系。8.0 版本的 mysql-connector-java,驱动类名是com.mysql.cj.jdbc.Driver,而 5.x 版本是com.mysql.jdbc.Driver,名字差一个cj。很多新手复制了旧资料里的驱动类名,连 5.7 的 MySQL 却报 ClassNotFoundException,就是因为驱动和类名不匹配。建议统一用 8.0.x 的驱动,连接串里再带上useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8这几个参数,能避免大量连接层面的兼容性问题。
2.4 项目包结构与分层设计
一个规范的 Web 项目,包结构第一天就要理清楚,后面才不会越来越乱。我常用的分层方式是这样的:
src ├─ com.diary.entity // 实体类:User、Diary ├─ com.diary.dao // 数据访问层:UserDao、DiaryDao ├─ com.diary.service // 业务逻辑层:UserService、DiaryService ├─ com.diary.servlet // 控制层:登录、注册、日记相关 Servlet ├─ com.diary.filter // 过滤器:编码过滤器、登录过滤器 ├─ com.diary.util // 工具类:DBUtil、MD5Util web ├─ index.jsp ├─ login.jsp ├─ register.jsp ├─ diary │ ├─ list.jsp │ ├─ add.jsp │ ├─ edit.jsp │ └─ detail.jsp └─ static ├─ css ├─ js └─ upload核心原则就是分层:Servlet 不直接写 JDBC 代码,DAO 层不出现 HttpServletRequest,业务逻辑尽量放在 Service 层。这个习惯坚持下来,以后如果想把 DAO 换成 MyBatis,只需要动 DAO 层,其他层完全不受影响,这也是我说这套结构"能打"的原因——它尊重了职责分离的底线。
3. 环境搭建与数据库设计:从IDEA配置到MySQL建表
3.1 IDEA 创建并运行 JavaWeb 项目的完整配置
"idea 运行 javaweb 项目配置"是很多人卡住的第一关,我按步骤拆一遍。
第一步,IDEA 里新建 Project,选 Java Empty Project 就行,不要用 Java Enterprise 模板,否则会把 Maven 和 Spring 相关的插件全部带进来,对理解底层流程反而是干扰。第二步,选中模块右键,Add Framework Support,勾选 Web Application,IDEA 会自动生成 web 目录和 web.xml。第三步,配置 Tomcat:Run → Edit Configurations → 加号 → Tomcat Server → Local,在 Application Server 里选择本地解压好的 Tomcat 路径。第四步也是最关键的一步,Deployment 选项卡里点加号,把当前项目的 Artifact 添加进去,Application context 建议设为/或/diary。这一步漏掉的话,启动 Tomcat 后访问任何页面都会 404。
还有两个细节值得注意:IDEA 里的 Project SDK 建议选 JDK8 或 JDK11,要和 Tomcat 版本的兼容性对应;运行前检查一下 8080 端口是否被占用,如果被占,可以把 Tomcat 的 HTTP port 改成 8081,或者到 conf/server.xml 里统一修改。
3.2 MySQL 安装与连接:绕不开的几个坑
MySQL 安装教程到处都是,但真正容易出问题的集中在这几个位置。
如果用的是免安装版,也就是 zip 解压版,需要在 MySQL 目录下手动创建 my.ini,里面至少要包含 basedir 和 datadir 两个配置项,并且 datadir 对应的 data 目录必须通过初始化命令生成。初始化命令是mysqld --initialize-insecure,这样 root 账号初始密码为空,方便本地调试;如果用了--initialize,系统会生成一个临时随机密码,存放在 data 目录下的 .err 日志文件里,很多人找不到密码就是因为没看这个文件。
启动服务的时候,在管理员命令行执行net start mysql之前,先确认服务存在。如果提示服务名无效,需要先执行mysqld --install mysql注册 Windows 服务。至于最常见的"net start mysql 服务无法启动",绝大多数原因是 my.ini 里的路径写错,或者 data 目录没有成功初始化。排查方法很直接:打开 data 目录下的 .err 日志,看最后几行报错信息,比在网上盲目搜索高效得多。
JDBC 连接阶段还有一个高频报错是 SSL connection error。解决方法是连接串加参数:jdbc:mysql://localhost:3306/diary?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8。8.0 驱动默认开启 SSL,本地调试用useSSL=false关掉即可;serverTimezone 不指定的情况下会报 CST 时区相关的异常,这也是必带的。
3.3 数据库表设计与建表 SQL
数据库叫 diary,核心两张表:用户表、日记表。
CREATE DATABASE IF NOT EXISTS diary DEFAULT CHARACTER SET utf8mb4; USE diary; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(50) DEFAULT '', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_diary ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, title VARCHAR(100) NOT NULL, content TEXT NOT NULL, weather VARCHAR(20) DEFAULT '', mood VARCHAR(20) DEFAULT '', image_path VARCHAR(200) DEFAULT '', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_user_time (user_id, create_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;几个设计点说明一下。user_id 上建索引,因为列表查询永远带WHERE user_id = ?,数据量上来以后,没有索引就是全表扫描。create_time 和 update_time 用 DATETIME 加 DEFAULT 值,减少业务代码负担。内容字段用 TEXT 而不是 VARCHAR,因为 VARCHAR 在 MySQL 的行长度限制下不适合存长文本。字符集统一 utf8mb4,兼容 emoji,避免用户输入特殊字符时入库报错。
3.4 JDBC 工具类的封装
数据访问层依赖一个统一的连接工具类,这里给出基础版本:
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/diary?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USERNAME = "root"; private static final String PASSWORD = "root"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL 驱动加载失败", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt != null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }教学项目里用 DriverManager 完全够用,不需要一开始就上连接池。每个 DAO 方法结束之后必须关闭连接,否则时间一长就会触发 MySQL 的 max_connections 上限,出现 Too many connections 错误,这个错误在演示现场非常尴尬。等系统需要更稳定并发的时候,再换成 Druid 或 HikariCP 不迟。
4. 核心功能实现:注册登录、日记增删改查的完整链路
4.1 注册与登录:密码怎么存才不算白干
注册功能是第一个入口,也是最容易写出安全漏洞的地方。最忌讳的是明文密码入库。这里用 MD5 做一次摘要再存,虽然 MD5 现在已经不是最推荐的加密方案,但作为教学项目,比明文强太多了——至少日志泄露或者数据库被拖走时,密码不会直接暴露。想更安全一些,可以加盐,例如MD5(password + username),让相同密码产生不同的摘要值。
登录逻辑分三步:接收参数、按用户名查用户、比对密码摘要。比对通过后,把 user 对象放入 Session,再重定向跳转到首页;这里用重定向而不是转发,是为了避免刷新页面时重复提交表单。注册时同样需要处理用户名唯一的场景:先按 username 查一次,存在就返回错误提示,否则执行 INSERT。
Session 的用途不只是保存登录态,后面的列表页、详情页都会通过 Session 里的 user_id 来过滤数据,所以权限控制的起点其实是在登录成功那一刻。
4.2 登录拦截:过滤器让未登录用户无处可去
有了登录功能,必须配套一个登录过滤器,否则用户直接在地址栏输入/diary/list.jsp就能绕过登录,看到其他用户的日记列表。这是最典型、也最容易在演示时翻车的安全缺口。
@WebFilter("/diary/*") public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; Object user = req.getSession().getAttribute("user"); if (user == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }用 @WebFilter 注解可以省去 web.xml 里的配置。过滤路径设为/diary/*,覆盖所有日记相关页面和 Servlet。这里有个很容易忽略的细节:login.jsp 本身不能放在被过滤的路径下,否则过滤器会一直拦截登录页,形成死循环跳转。
4.3 新增、列表、编辑、删除:核心 CRUD 的编码思路
新增日记的 Servlet 逻辑很简单,核心是接收参数、组装对象、调用 DAO 插入。唯一要留意的是,创建时间不要信任前端传来的字符串,直接交给数据库的 DEFAULT CURRENT_TIMESTAMP 生成,避免用户伪造时间。内容字段做一下长度校验,防止提交超大文本拖垮数据库。
列表查询是最能体现"mysql 排序"价值的场景。日记列表天然需要按时间倒序,让最新内容排在最前面:
public List<Diary> findByUserId(int userId, int offset, int size) { String sql = "SELECT id, title, weather, mood, image_path, create_time FROM t_diary " + "WHERE user_id = ? ORDER BY create_time DESC, id DESC LIMIT ?, ?"; // 执行查询并封装为 List<Diary> }ORDER BY create_time DESC是倒序,再加id DESC作为次级排序,防止同一秒写入多条数据时出现顺序抖动。LIMIT 的两个参数,第一个是起始偏移量,第二个是每页条数,分页的核心就是这条 SQL。
编辑和删除的权限校验要特别重视:update 语句的 WHERE 条件带上AND user_id = ?,删除同理写成DELETE FROM t_diary WHERE id = ? AND user_id = ?。这样即使有人手动篡改请求里的 id 参数,也改不到别人的数据。这是数据隔离最后一道闸门,绝对不能漏。
4.4 事务处理:什么时候必须用、怎么用
单表单语句的 CRUD 不需要事务,但出现"两件事必须同时成功或同时失败"的场景时,事务就是刚需。比如删除日记的同时更新用户的统计字段;再比如注册时插入用户记录、同时初始化用户的个性化配置表,都属于典型的双写场景。
MySQL 事务处理的标准写法是:
Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 操作1:执行删除 // 操作2:执行统计更新 conn.commit(); // 全部成功才提交 } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } throw new RuntimeException("事务执行失败", e); } finally { DBUtil.close(null, null, conn); }核心就是三句话:关闭自动提交、全部执行后 commit、任何一环异常就 rollback。这里有一个非常容易踩的坑:同一个事务里的所有操作必须使用同一个 Connection。如果每个 DAO 方法内部都单独 getConnection 再 close,事务就完全失效了。很多同学写了事务代码,数据还是不一致,根源就在这里——每个方法各拿各的连接,自动提交还是开着,commit 和 rollback 根本管不到别的连接。
5. 前端页面与 Bootstrap美化:jsp页面的排版与交互细节
5.1 页面公共部分的抽取与复用
JSP 项目最怕的是页面之间大量复制粘贴,头部导航、CSS 引入、JS 引入每个文件都重复一遍,后期想改一个类名就要翻遍所有页面。解决办法是把公共部分抽成独立的 JSP 文件,用静态 include 引进来:
<%@ include file="/common/header.jsp" %>被引入的 header.jsp 里放 DOCTYPE、html 标签、Bootstrap 的 CSS 链接、导航栏 HTML。新增页面时只需要引 header.jsp 和 footer.jsp,主体只写本页独有内容。这里建议把公共文件放在 web 目录下的/common子目录,并始终用绝对路径引用,避免不同层级页面里的相对路径计算错误。
5.2 Bootstrap 栅格与卡片组件的实际运用
日记列表页适合用 Bootstrap 的 card 组件做三列网格展示:
<div class="container mt-4"> <div class="row"> <c:forEach items="${diaryList}" var="diary"> <div class="col-md-4 mb-3"> <div class="card"> <div class="card-body"> <h5 class="card-title">${diary.title}</h5> <p class="card-text text-truncate">${diary.content}</p> <a href="detail?id=${diary.id}" class="btn btn-primary btn-sm">查看详情</a> </div> </div> </div> </c:forEach> </div> </div>col-md-4是栅格系统的精髓:中等宽度屏幕以上,一行放三列,每列占 4/12;窄屏上自动堆叠为单列。text-truncate类可以让超长内容自动省略,不需要写字符串截断逻辑。登录页的表单同样可以利用 Bootstrap 的 form-group、form-control、btn 系列类,组合起来页面立刻有正规感。配合 HTML5 的 required、minlength 属性,不写 JS 也能实现基础校验。
5.3 删除确认:用模态框提升完成度
原生 confirm 弹窗功能上没有问题,但观感太工程风。Bootstrap 的 modal 组件可以实现更好的交互:点击删除按钮,弹出带有日记标题的确认框,点击确认才真正提交删除请求。具体做法是在列表页放一个 modal,根据当前行数据设置弹窗里隐藏字段的值;确认按钮绑定 click 事件,跳转到删除 Servlet。这一步做完,项目的完成度会有肉眼可见的提升,这也是我坚持推荐 Bootstrap 的原因——组件都给你了,剩下的只是拼装逻辑。
5.4 页面加载完成后自动刷新一次
有些场景需要页面加载后自动刷新一次,比如登录跳转后希望列表页重新拉取数据,或者编辑保存后希望详情页展示最新内容。这个需求对应实际开发里常见的"jsp 页面让加载完后刷新一次"。
最简单的是在 JSP 页面加 meta 标签:
<meta http-equiv="refresh" content="0">页面一加载就立刻刷新。另一种方式是 JS:
window.addEventListener('load', function () { location.reload(); });两种方式适用场景不同:meta 刷新简单直接,适合无条件刷新;JS 方式可以加条件判断,比如只有请求参数带 refresh=1 时才刷新,灵活性更高。演示项目里建议用 JS 方式,后续扩展时不用改 HTML 结构。
5.5 图片上传与坐标定位
这个项目里的配图功能涉及两类图片问题:上传和定位。
上传流程是:前端表单设置enctype="multipart/form-data",后端 Servlet 用 Part 接口接收文件,保存到 web 目录下的 upload 文件夹,把相对路径写入数据库的 image_path 字段。关键细节有三个:文件名必须重命名,否则多人上传同名文件会互相覆盖,建议用时间戳加随机数;上传目录要保证可写,不同环境下部署时最容易忽略;页面显示时用相对路径,不要硬编码本地磁盘绝对路径。
"jsp 图片如何对坐标定位"这个问题,在实际项目里通常这样处理:如果是简单的位置标记,在图片的父容器上加position: relative,标记元素用position: absolute并设置 left 和 top 的百分比值;如果要精确到像素,就得结合图片容器实际尺寸换算,课程设计阶段用百分比方案完全够用。
5.6 中文乱码的三处排查位置
JSP 项目里中文乱码是一个逃不掉的话题,排查顺序基本固定。
第一处是 JSP 文件本身:<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>必须写全。第二处是请求参数:Servlet 里要在所有 getParameter 之前执行request.setCharacterEncoding("UTF-8"),对 GET 请求,Tomcat 8 之后默认按 UTF-8 处理,8 之前的版本还需要在 server.xml 里配置 URIEncoding。第三处是数据库:JDBC URL 带characterEncoding=utf8,表结构统一 utf8mb4。三处全部对齐,中文才能畅通无阻;任何一处遗漏,都可能出现前端显示正常但数据库里是问号,或者反过来。
6. 常见问题与踩坑记录:从MySQL服务启动失败到404排查
6.1 MySQL 服务无法启动的完整排查链路
这个问题值得单独拿出来讲。现象是执行net start mysql提示服务无法启动,或者服务启动后立刻停止。按下面顺序排查,命中率极高:
- 打开 data 目录下的 .err 日志,看最后几行报错信息,这是最直接的线索。
- 检查 my.ini 里 basedir 和 datadir 是否使用绝对路径,Windows 下注意路径分隔符和末尾的反斜杠。
- 确认 data 目录是否初始化成功,没有 data 目录时先执行
mysqld --initialize-insecure。 - 确认 3306 端口没有被其他进程占用,用
netstat -ano | findstr :3306查看。 - 确认服务注册信息,路径不对时先执行
mysqld --remove mysql再重新mysqld --install。
90% 以上的情况出在前三步。日志文件永远是排查的第一老师,比在网上漫无目的地搜索有效得多。
6.2 JDBC 连接报错的典型场景
JDBC 连接报错里,除了 SSL connection error,还有三个高频问题。Access denied for user 表示用户名或密码错误,连本地 root 都进不去时,检查 MySQL 服务运行时用的是哪个配置文件和数据目录。Unknown database 表示连接串里的库名不存在,先登录 MySQL 执行 CREATE DATABASE。Communications link failure 通常是数据库服务没启动,或者防火墙拦截了 3306 端口,本地调试可以先临时关闭防火墙验证。
6.3 404 与 500 的定位思路
访问 JSP 页面 404,不要急着怀疑代码,按顺序排查:
- IDEA 控制台 Tomcat 是否启动成功,出现 Failed to start component 说明启动阶段就失败了。
- 访问路径和 web 目录下的真实路径是否一致,注意大小写和 .jsp 后缀。
- Deployment 里的 Artifact 是否添加,Application context 是否和访问 URL 前缀一致。
页面 500 的话,把 Tomcat 控制台的异常信息往上翻,找最底部的 Caused by,大部分集中在 SQL 语法、空指针、ClassNotFoundException 三类。我改过很多项目,发现 500 错误的定位能力,直接决定了你能不能独立完成课设——耐心读异常栈,比反复刷新页面有用得多。
6.4 跑通之后还能怎么升级
功能全部跑通之后,如果时间和精力允许,按以下顺序优化性价比最高:
- 引入 Druid 连接池替换 DriverManager,解决并发连接数不足的问题。
- 全部 SQL 改用 PreparedStatement 参数化,统一防 SQL 注入。
- 密码存储从纯 MD5 升级为加盐 MD5 或 BCrypt。
- 把 DAO 从接口加实现类的结构整理好,为以后替换 MyBatis 做准备。
- 引入 Log4j 或 SLF4J,把关键操作记录下来,方便后期排查。
任何一项做完,都能在答辩时拿出实际的东西,证明你不只是"跑通了流程",而是理解了项目的性能瓶颈和数据安全。
最后说一点个人体会。这个项目我不仅当年写过,后来帮人改过的版本也不下十次。最大的感受是:很多同学能跑通流程,但对"数据流转"没有形成完整的闭环认知。登录为什么用 Session、删除为什么要带 user_id、列表顺序为什么不对——这些细节才是真正值钱的东西。把这套 JSP + Servlet + MySQL + Bootstrap 的"土办法"玩明白,再去看 Spring Boot,你会发现它不过是把 Servlet 的模板化和参数封装做了升级,底层还是请求、处理、响应、持久化这一套。先把地基打稳,框架只是新瓶装旧酒。后面如果你们在做类似的项目,遇到具体报错或者拿不准的设计点,欢迎随时带着问题来交流。