JSP+MySQL个人日记本源码运行全攻略:从环境搭建到避坑指南 简介基于jspmysql的JSP个人日记本源码是一份面向Java Web初学者与课程设计场景的完整Web应用项目。资源以JSP作为视图层、Servlet处理控制逻辑结合MySQL存储用户、日记与分类数据覆盖用户登录、会话保持、日记增删改查、分类管理、个人中心等典型功能模块可帮助读者理解JSP与传统Servlet/MVC开发模式的配合方式。包体共90个文件主要包含8个jsp页面、18个java源文件、27个html静态页及多张png图片另附README.md与README.en.md说明文档整体压缩包仅256KB结构紧凑、便于快速部署学习。目前已有222人学习/下载。源码中涉及的JDBC连接、表单提交与验证、session会话管理等知识点较为典型适合用来对照练习Java Web基础开发并可作为课程设计或项目实训的参考模板。1. 这套JSPMySQL个人日记本源码到底值不值得跑你是不是也遇到过这种课题老师甩过来一个标题叫“基于JSPMySQL的个人日记本”代码包是个zip里面塞着几层文件夹、一堆JSP页面和一个.sql文件——看着很全真打开却不知道先点哪个文件。先给你一个反直觉的结论这种“老掉牙”的JSPMySQL组合恰恰是Java Web课程设计和毕业设计里通过率最高、复查最容易说清楚的一类选题。它麻雀虽小但Servlet、JSP、JDBC、会话管理、SQL操作全都要你亲手碰一遍面试官问起来你能讲出的细节反而比Spring Boot多。这篇笔记我从零给你拆一遍环境怎么配、数据库表怎么建、注册登录和日记增删改查怎么写、部署到Tomcat会遇到哪些奇怪现象以及你拿到一个同样结构的zip之后怎么在最短时间内把它跑起来并改造成能答辩的样子。全文不依赖任何IDE的图形化向导所有代码都能在命令行和记事本级别跑通。2. 搭建最小运行环境JDK、Tomcat、MySQL 8.0 与 IDEA 动态 Web 工程2.1 版本搭配的底层逻辑为什么是JDK 1.8、Tomcat 8.5 和 MySQL 8.0老项目源码最常见的翻车点不是代码本身而是环境版本对不上。JSP个人日记本这种项目的典型年代背景是JDK 1.8 Tomcat 8.x配套的Servlet规范是3.1JSP规范是2.3。如果你电脑里装着JDK 17再去跑Tomcat 8.5虽然能勉强启动但很多旧代码里用了% page importjava.util.* %这类老写法在更高版本的Servlet容器里对EL表达式和JSTL的解析方式有差异表现就是页面能打开但c:forEach循环一个数据都不渲染还不报错。MySQL版本上老源码的SQL文件多数按MySQL 5.7导出字符集写的是utf8。装MySQL 8.0也没关系但要注意两个点一是连接驱动要换mysql-connector-java8.x版本不能再拿老项目lib里的5.1.x去连二是MySQL 8.0默认认证插件是caching_sha2_password老驱动会直接拒绝连接现象就是JDBC报Public Key Retrieval is not allowed。这两条是整篇笔记里最频繁出现的坑后面避坑章节单独展开。我在本地长期维护一套固定组合JDK 1.8.0_202、Tomcat 8.5.100、MySQL 8.0.36、IntelliJ IDEA。不需要最新版但每个软件要锁死具体小版本。Tomcat 11这种新版别碰Servlet规范跳到6.0javax.servlet变jakarta.servlet老JSP代码引用全炸。2.2 拿到一个zip源码后的第一步剖析目录结构不要把zip里的文件直接拖到Tomcat的webapps下就完事。先用命令行解开压缩包把项目结构拉到眼前看一遍。常见的课程设计源码包结构一般长这样JSP个人日记本源码/ ├── sql/ │ └── diary.sql # 建库建表脚本通常包含create database和insert测试数据 ├── src/ │ ├── com/ │ │ └── diary/ │ │ ├── dao/ # UserDao.java, DiaryDao.java │ │ ├── entity/ # User.java, Diary.java │ │ ├── servlet/ # LoginServlet.java, DiaryServlet.java等 │ │ └── util/ # DBUtil.java, 连接数据库的工具类 ├── web/ # 已编译后的Web根目录或源码里的WebRoot │ ├── WEB-INF/ │ │ ├── lib/ # 依赖JAR包mysql-connector-java-x.jar, jstl.jar │ │ └── web.xml # Servlet映射与欢迎页配置 │ ├── css/ │ ├── jsp/ │ │ ├── login.jsp │ │ ├── register.jsp │ │ └── diaryList.jsp └── pom.xml # 如果是Maven工程部分源码包里没有第一阶段只做三件事确认sql目录里有没有建库脚本确认WEB-INF/lib里有没有数据库驱动确认web.xml里配置的Servlet路径和JSP页面的表单action是否能对上。这三件事不出错后面跑通的概率有七成。如果zip里没有sql目录只有一堆.java编译后的class文件那这个包大概率是别人从Tomcat的webapps目录里直接拷出来的没有数据库脚本别浪费时间逆向自己建库建表参考第三章给的表结构。2.3 IDEA 里初始化一个Web工程不借助向导的最小配置如果你手里的源码本身就带.idea或.project文件导入即可但很多第三方下载的zip是纯源码没有IDE配置。我给你一套从空目录到能跑通的最小路径全程不依赖IDEA的企业级向导。新建目录DiaryProject手工创建src、web两个子目录。在web/WEB-INF下面建一个手写web.xml?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 display-nameJSP Personal Diary/display-name welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list !-- 登录Servlet映射示例完整映射按源码里的类名对应 -- servlet servlet-nameLoginServlet/servlet-name servlet-classcom.diary.servlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping !-- 全局编码过滤器解决POST中文乱码 -- filter filter-nameencoding/filter-name filter-classcom.diary.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencoding/filter-name url-pattern/*/url-pattern /filter-mapping /web-app这里参数说明几点web-app的version填3.1对应Tomcat 8.5url-pattern里的/login必须和JSP表单里的actionlogin配合斜杠的有无要一致很多人在这上面翻车——页面表单写actionloginweb.xml映射写/loginTomcat找不到路径直接404编码过滤器要放在所有Filter第一位不然后端读到的POST参数全是乱码。2.4 把MySQL 8.0 装到可用的状态初始化、起服务、建库Windows下装MySQL 8.0我推荐用ZIP压缩包方式而不是MSI安装器原因是你更容易看清它的启动和排错逻辑。去官网下载mysql-8.0.x-winx64.zip解压到比如D:\mysql-8.0.36然后按下面顺序操作# 1. 在bin目录下执行初始化生成data目录和root临时密码 mysqld --initialize-insecure # 2. 注册Windows服务并启动 mysqld --install MySQL80 net start MySQL80 # 3. 进入命令行客户端 mysql -u root -p # 这里因为--initialize-insecure密码为空直接回车初始化这一步要专门解释参数--initialize-insecure会生成一个密码为空的root账号适合本地开发。官方默认的--initialize会生成随机临时密码写在data目录下的*.err文件里新手经常找不到还以为是安装失败。--install注册的是windows服务名后面排查“端口被占用”“服务起不来”都靠这个名字。进入mysql后用下面SQL建库库名和源码里DBUtil.java的URL写的库名保持一致常见就叫diary_dbCREATE DATABASE IF NOT EXISTS diary_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE diary_db; -- 用户表 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 日记表 CREATE TABLE t_diary ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, title VARCHAR(200) NOT NULL, content TEXT, mood VARCHAR(20) DEFAULT , weather VARCHAR(20) 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), CONSTRAINT fk_diary_user FOREIGN KEY (user_id) REFERENCES t_user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意这里用了utf8mb4而不是老脚本里的utf8MySQL 8.0里utf8实际是utf8mb3存emoji表情会报“Incorrect string value”错误。ON UPDATE CURRENT_TIMESTAMP是给编辑功能用的每次修改日记自动更新时间。索引idx_user_time对应日记列表页最常见的查询是按用户ID加时间倒序不加这个查询在数据量大了之后会扫全表。3. 数据库设计与JDBC访问层从表结构到连接串参数3.1 日记本的核心数据结构三张表还是一张表个人日记本的功能边界很清楚用户注册、用户登录、写日记、看列表、看详情、改日记、删日记。围绕这个功能最合理的表设计是两张业务表加一张可选的分类表。我这里给出的设计是t_user和t_diary两张表。为什么要单独拆t_user因为日记的增删改查全部要绑定用户ID如果不拆分所有用户共用一个表登录认证后根本无法区分“我的日记”只能靠每次查询传username做过滤一旦用户名修改就全线崩溃。两张表的业务边界是用户表只管账号密码和个人信息日记表只存日记内容和归属。第三张分类表要不要加取决于源码里日记编辑页有没有“分类”下拉框。有就建t_category(id, user_id, name),日记表加category_id外键没有就保持两张表。我见过很多候选人在答辩时被问“你的系统支持按分类筛选吗”然后支支吾吾答不上来——功能没做不可怕可怕的是数据库设计里明明留了字段却没实现。3.2 写一个够用的JDBC工具类连接串参数逐个拆解不管源码包里有没有现成的DBUtil.java你都应该自己能手写出来。这个类就是整个项目的数据访问底座所有DAO都通过它拿连接。下面是我们在MySQL 8.0下的标准写法package com.diary.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { // 驱动名com.mysql.cj.jdbc.Driver 是8.x的类名 private static final String DRIVER com.mysql.cj.jdbc.Driver; // 连接串useSSLfalse 避免本地自签名证书警告 private static final String URL jdbc:mysql://127.0.0.1:3306/diary_db ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai allowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, java.sql.Statement stmt, java.sql.ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { } } } }这段代码里有几个参数是必须写全的每一个都有明确的坑对应。serverTimezoneAsia/Shanghai不写MySQL 8.0会报“The server time zone value Öйú±ê׼ʱ¼ä is unrecognized”因为新版驱动要求显式指定时区。useSSLfalse是针对本地环境不加会用自签名证书走SSL握手慢且不可靠。allowPublicKeyRetrievaltrue对应MySQL 8.0的caching_sha2_password授权插件不加就报Public Key Retrieval is not allowed。值得单独强调URL里为什么写127.0.0.1而不是localhost。在老版本驱动里两者都可以但现代MySQL客户端碰到localhost会优先走Unix socketLinux/macOS或命名管道Windows装了两个版本MySQL或socket文件路径不对时JDBC会报Cannot connect to MySQL server through socket /tmp/mysql.sock。写127.0.0.1强制走TCP协议绕开这一层这也是避坑章里最重要的一条。3.3 从DriverManager到连接池小项目有没有必要上Druid个人日记本这种并发量几乎为零的课程设计完全没必要引入Druid或HikariCP连接池。原因很简单连接池解决的是“频繁创建连接”的性能问题而你的项目每秒根本不会有几十个请求引入连接池反而多了一个配置文件、多一套初始化逻辑答辩老师问起来还容易卡壳。但有一件事值得做把DBUtil改成支持外部配置而不是把数据库密码硬编码在Java类里。常见的做法是用Properties文件读取// db.properties 文件内容 jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/diary_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.userroot jdbc.password123456然后DBUtil里用Properties.load(Thread.currentThread().getContextClassLoader().getResourceAsStream(db.properties))加载这样以后部署到别人机器上只需要改配置文件不用重新编译。这一步对源码二次开发来说是性价比极高的改动也就二十行代码但会让你的码比原包明显干净。数据库账号权限这里顺便提一句开发环境用root没问题但如果这个系统真要部署到公网服务器建议单独建一个账号diary_user只授予diary_db库的SELECT,INSERT,UPDATE,DELETE权限不要用root裸奔。一条授限SQL就够CREATE USER diary_userlocalhost IDENTIFIED BY YOUR_PASSWORD; GRANT SELECT, INSERT, UPDATE, DELETE ON diary_db.* TO diary_userlocalhost; FLUSH PRIVILEGES;3.4 老源码的SQL脚本是乱码或导入失败怎么抢救下载的zip里那个diary.sql打开一看要么是全部注释要么一导入MySQL就报错这是常见现象。常见的失败原因有两种一是SQL文件编码不对Windows记事本保存的UTF-8带了BOM头MySQL 8.0的source命令会把它当成不可见字符解析失败二是脚本里有DROP DATABASE但你没执行权限。推荐的导入姿势是用命令行而不是图形客户端mysql -u root -p --default-character-setutf8mb4 diary_db D:/sql/diary.sql--default-character-setutf8mb4这个参数很关键它告诉客户端“脚本文件本身是utf8mb4编码”没有它Windows PowerShell里执行导入时中文全变问号。如果脚本开头有CREATE DATABASE建议你手动先执行一次建库再只导入表和数据避免权限边界问题。4. 日记本核心功能落地注册登录、会话与日记增删改查4.1 注册功能与密码存储为什么不能明文入库很多老源码里的register.jsp处理逻辑极其简单前端表单提交到RegisterServletServlet里request.getParameter(username)拿到值拼个INSERT INTO t_user就完事。这种做法放在课程设计里虽然能跑通但只要答辩老师问一句“密码为什么要加密”你就只能干瞪眼。我先给出正确的注册Servlet核心逻辑再解释密码处理的细节package com.diary.servlet; import java.io.IOException; import java.security.MessageDigest; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.diary.dao.UserDao; import com.diary.entity.User; WebServlet(/register) public class RegisterServlet extends HttpServlet { private static final long serialVersionUID 1L; private UserDao userDao new UserDao(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); String confirmPwd request.getParameter(confirmPwd); String nickname request.getParameter(nickname); if (username null || !username.matches(^[a-zA-Z0-9_]{3,20}$)) { request.setAttribute(msg, 用户名需为3-20位字母数字下划线); request.getRequestDispatcher(/register.jsp).forward(request, response); return; } if (password null || password.length() 6) { request.setAttribute(msg, 密码至少6位); request.getRequestDispatcher(/register.jsp).forward(request, response); return; } if (!password.equals(confirmPwd)) { request.setAttribute(msg, 两次密码不一致); request.getRequestDispatcher(/register.jsp).forward(request, response); return; } // 加盐MD5盐取用户名的反向字符串避免同样密码产生相同摘要 String salted password { username }; String md5 md5(salted); User user new User(); user.setUsername(username); user.setPassword(md5); user.setNickname(nickname); try { int result userDao.insert(user); if (result 0) { response.sendRedirect(request.getContextPath() /login.jsp); } else { request.setAttribute(msg, 用户名已存在); request.getRequestDispatcher(/register.jsp).forward(request, response); } } catch (Exception e) { e.printStackTrace(); request.setAttribute(msg, 系统异常请稍后再试); request.getRequestDispatcher(/register.jsp).forward(request, response); } } private String md5(String input) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(input.getBytes(UTF-8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (Exception e) { throw new RuntimeException(e); } } }逻辑说明用户名白名单正则^[a-zA-Z0-9_]{3,20}$是很多老源码缺失的关键校验不加它SQL注入和非法字符都能绕过后端一拼接SQL就出问题。密码校验放在Servlet而不是JSP是因为JSP里做同样的校验即使绕过去Servlet这层还是能防住。密码加盐的方式是password {username}盐与用户名绑定这样两个用户即使密码一样库里的MD5摘要也不一样。这个方案在真实生产环境只算入门级但对于课程设计项目已经足够应付大部分安全提问。如果你想再专业一步把盐改成UUID随机值存到独立字段代码量多五行但含金量会明显不同。4.2 登录与HttpSession记住用户的两种方式登录成功之后要让系统“认识”你JSP时代的标准答案是HttpSession。登录Servlet里的关键动作// 登录成功后写入SessionsetAttribute的key要和JSP里取的key一致 HttpSession session request.getSession(); session.setAttribute(loginUser, user); session.setAttribute(userId, user.getId()); // 设置会话超时时间为30分钟单位是秒 session.setMaxInactiveInterval(30 * 60); // 重定向到日记列表页 response.sendRedirect(request.getContextPath() /diary/list);setMaxInactiveInterval(1800)这个参数值得单独解释。不设置的默认值是Tomcat的web.xml里配的30分钟但有些源码为了让演示方便会把这个值调成60*60*24即一天内免登录。这在你演示项目给老师和验收人员看时很爽但要清楚还有一个隐患loginUser存的是整个User对象如果你的User实体里包含了数据库的自增ID和时间字段JSP里直接${loginUser.nickname}没问题但绝对不能把password字段也放进Session。我见过候选人在JSP上直接用${loginUser.password}调试然后忘记删掉的尴尬场景。“记住我”功能要不要做看源码包有没有这个需求。老项目的常规做法是登录成功后把用户ID和用户名存入Cookie有效期7天下次请求进来先查Session没有Session再用Cookie里的信息去数据库查询。具体在EncodingFilter里加一层判断即可。新手不建议动手写因为Cookie里存明文用户ID会留下安全问题等你把ID加密、加过期时间都做完了会发现还不如直接用Session方便。4.3 日记的增删改查PreparedStatement 与 JSP 显示层的配合日记核心操作集中在DiaryServlet里用请求里的action参数区分增删改查。推荐把CRUD写在一个Servlet里分发比拆成四个Servlet维护成本低也符合老项目源码里最常见的组织方式。先看DAO层更新日记的方法同时解决两个高频坑一是UPDATE语句的SET子句不能把主键写进去二是PreparedStatement传参从1开始计数package com.diary.dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import com.diary.entity.Diary; import com.diary.util.DBUtil; public class DiaryDao { // 新增日记 public int insert(Diary diary) { String sql INSERT INTO t_diary(user_id, title, content, mood, weather, create_time) VALUES(?, ?, ?, ?, ?, NOW()); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, diary.getUserId()); ps.setString(2, diary.getTitle()); ps.setString(3, diary.getContent()); ps.setString(4, diary.getMood()); ps.setString(5, diary.getWeather()); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } } // 查询某个用户的全部日记按时间倒序 public ListDiary findByUserId(int userId) { ListDiary list new ArrayList(); String sql SELECT id, user_id, title, LEFT(content, 80) AS content, mood, weather, create_time FROM t_diary WHERE user_id ? ORDER BY create_time DESC; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, userId); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Diary d new Diary(); d.setId(rs.getInt(id)); d.setTitle(rs.getString(title)); d.setContent(rs.getString(content)); d.setMood(rs.getString(mood)); d.setWeather(rs.getString(weather)); d.setCreateTime(rs.getTimestamp(create_time)); list.add(d); } } } catch (SQLException e) { e.printStackTrace(); } return list; } // 按主键查一条日记 public Diary findById(int id) { String sql SELECT * FROM t_diary WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Diary d new Diary(); d.setId(rs.getInt(id)); d.setUserId(rs.getInt(user_id)); d.setTitle(rs.getString(title)); d.setContent(rs.getString(content)); d.setMood(rs.getString(mood)); d.setWeather(rs.getString(weather)); d.setCreateTime(rs.getTimestamp(create_time)); return d; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }这段DAO代码里有两个容易被忽略的设计点。第一个是LEFT(content, 80)这是在列表页只显示摘要的通用写法避免列表接口一次性把整篇日记的TEXT字段全查出来尤其日记里如果存了图片的Base64字符串全查出来直接可以把Tomcat内存打满。第二个是ORDER BY create_time DESC这是日记列表最基本的排序规则你会在源码里看到有同学把DESC丢掉然后发现最新写的日记永远排在页面最底下。JSP显示层对应diaryList.jsp里最核心的循环片段% taglib urihttp://java.sun.com/jsp/jstl/core prefixc % div classdiary-list c:forEach items${diaryList} vardiary div classdiary-item h3a hrefdetail?id${diary.id}${diary.title}/a/h3 p${diary.content}/p span${diary.mood} / ${diary.weather}/span spanfmt:formatDate value${diary.createTime} patternyyyy-MM-dd HH:mm//span a hrefedit?id${diary.id}编辑/a a hrefdelete?id${diary.id} onclickreturn confirm(确定删除吗?)删除/a /div /c:forEach /divitems${diaryList}这个EL表达式的名称必须来自Servlet里request.setAttribute(diaryList, list)的key大小写完全一致。这是JSP项目里极其顽固的坑Servlet里写diaryListJSP里写成DiaryList一运行列表就是空白页控制台还不报错只会看到JSP里单独写了个% for(...) %的老式循环的人才明白那种痛苦。fmt:formatDate需要导入fmt标签库对应lib里必须有jstl.jar和standard.jar不少zip包漏放其中某一个页面上就会报418错误。4.4 删除日记的参数安全用POST还是GET老源码里删除日记无一例外用a hrefdelete?id1删除/a也就是GET请求。这在答辩演示时操作方便但懂行的老师会指出问题GET请求可以被人手工构造比如用户在浏览器地址栏输入/diary/delete?id1直接删掉任意一篇日记。更严重的搜索引擎爬虫或预加载工具可能顺着链接就触发了删除。正确的做法是把删除按钮改成一个表单用POST提交form actiondelete methodpost styledisplay:inline; onsubmitreturn confirm(确定删除这篇日记吗?); input typehidden nameid value${diary.id}/ button typesubmit删除/button /form同时Servlet端要二次校验当前登录用户的ID和日记的user_id是否一致。只判断这张日记存不存在就删同理也可以删别人的。加一行校验Diary diary diaryDao.findById(id); if (diary null) { response.sendError(404); return; } if (diary.getUserId() ! currentUserId) { response.sendError(403); return; } diaryDao.deleteById(id);这个改动几乎不增加工作量但在答辩时能展示你有基本的安全边界意识比你多写一百行业务代码都有说服力。5. 部署与运行避坑Tomcat 乱码、MySQL 8.0 认证与端口占用排查5.1 Tomcat 启动就报“端口被占用”先别急着改端口现象IDEA里点运行Tomcat控制台直接出现Port 8080 required by Tomcat v8.5 Server at localhost is already in use或者“Address already in use: JVM_Bind”。原因最常见的两个一是之前注册了MySQL80服务并开了另一个是你电脑上装过Oracle或旧版Tomcat8080端口被其他进程占着。很多人一看到端口被占立刻去改Tomcat的server.xml端口为8081这个操作没错但掩盖了问题——如果下一次启动又报8081被占你就得一直往下改。先找出是谁占了端口更重要。排查手段Windows下用命令行查占用进程。# 查看8080端口被哪个进程占用 netstat -ano | findstr 8080 # 结果最后一列是PID比如 12345继续查是哪个程序 tasklist /fi PID eq 12345看到结果后对应处理如果占用进程是mysqld.exe说明MySQL把端口也占了见过这种情况但比较少见如果是java.exe多半是你电脑里另一个Tomcat没关干净去任务管理器结束进程即可如果是oracle.exe可能是你装过Oracle数据库但没禁用监听建议进入服务管理器找到Oracle相关服务设置为手动启动而不是和Tomcat抢端口。如果确定要改端口记住三处要同步server.xml里的Connector port8080、项目内部JSP里如果有硬编码hrefhttp://localhost:8080/...的绝对路径引用、以及数据库连接串里如果写了端口。第三处很多人会漏掉因为连接串经常写的是jdbc:mysql://127.0.0.1:3306/diary_db如果你把MySQL端口从3306改成了3307这一处没改项目启动后任何数据库操作都会抛Communications link failure。5.2 中文乱码三连JSP 页面乱码、POST 乱码、数据库中文乱码现象页面上中文标题变成我的日记一类的堆字符表单提交的中文在列表页显示成问号数据库里存进去的中文用Navicat看正常但页面上显示乱码。这三类乱码的根源完全不同处理方式也不能混为一谈。第一类JSP页面本身的乱码原因是没有声明页面编码或声明的位置不对。老的JSP写法常见% page contentTypetext/html;charsetISO-8859-1%把charset改成UTF-8是第一步。同时HTML的meta charsetUTF-8要放在head最上面。如果浏览器仍显示乱码按CtrlShiftR强制刷新不要用缓存里的旧页面做判断。第二类POST提交中文乱码根源是Tomcat 8.5默认对POST请求按ISO-8859-1解码除非你在Servlet里request.setCharacterEncoding(UTF-8)。我推荐的统一解法是编写一个EncodingFilter在web.xml里配置为拦截/*在doFilter开头执行request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); chain.doFilter(request, response);注意这里的顺序setCharacterEncoding在读取getParameter()之前调用才有效。如果Servlet里已经getParameter之后你再setCharacterEncoding这次请求里的参数已经按ISO-8859-1解码过了再设也没用。Tomcat还有一部分配置叫URIEncodingUTF-8那是给GET请求的QueryString解码用的在server.xml的Connector标签上加属性。第三类数据库中文乱码主要在MySQL连接串和表字符集两端排查。表已经建了utf8mb4连接串里带characterEncodingutf8这种情况下还乱码就去查MySQL服务端变量SHOW VARIABLES LIKE character_set%;看到character_set_serverlatin1说明初始化时没设置默认字符集解决方式是在my.ini的[mysqld]段加[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_general_ci改完重启MySQL服务。注意my.ini文件本身用记事本保存后默认编码是ANSI里面的中文注释保存或读取都可能出问题建议用VS Code打开右下角确认编码是UTF-8后再保存。5.3 MySQL 8.0 连接被拒ERROR 2002 (HY000) 和 Public Key Retrieval现象Navicat或命令行能连上但Tomcat项目一执行DAO查询就报Public Key Retrieval is not allowed或者是ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock (2)。第一类Public Key Retrieval的根因在MySQL 8.0把默认认证插件换成了caching_sha2_password。老项目JDBC驱动如果还在用5.1.x版本压根不认识这个插件需要allowPublicKeyRetrievaltrue参数配合useSSLfalse或者重写连接串如果把allowPublicKeyRetrieval改成true之后还是报错那要考虑把你的旧驱动JAR替换成mysql-connector-java-8.0.x.jar。第二类ERROR 2002最像的坑出在命令行客户端上。你执行mysql -u root -p没带-h参数默认走socket文件而不是TCP如果MySQL服务没启动、socket文件路径不对或者你用了localhost而实际客户端在容器里就会报这个。解决方法是显式指定TCP连接mysql -h 127.0.0.1 -P 3306 -u root -p同时检查服务状态# Linux / macOS systemctl status mysql # Windows net start | findstr MySQL如果确定了MySQL服务在跑但还是连不上用telnet 127.0.0.1 3306看端口是否真的在监听。Windows需要先在控制面板打开Telnet功能也可以用Test-NetConnectionPowerShell命令。5.4 IDEA 里部署到 TomcatArtifact 配置与 jar 包缺失现象IDEA里点运行Tomcat能启动、日志正常但浏览器打开http://localhost:8080就是404或者在JSP页面里报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因要从IDEA的部署模型说起。IDEA里配置Tomcat时有一个“Deployment”选项卡下面会显示一个ArtifactArtifact的全称是war exploded也就是解压后的目录。如果你之前是从旧版本的IDEA导入老项目或者直接改过Modules结构的Artifact输出的目录里没有包含WEB-INF/lib数据库驱动JAR就不会被打进去。解决办法分两步。第一步检查项目结构确认依赖库是否被加入到WEB-INF/libProject Structure - Artifacts - 选中你的Web项目 - 右侧有Available Elements 把你要的库如mysql-connector-java-8.0.36.jar右键选 Put into /WEB-INF/lib如果列表里根本没有这个库先到File - Project Structure - Libraries确认有没有添加外部JAR。没有就通过号选择Java找到你下载的驱动JAR加入。第二步重新构建并清理目录。IDEA里坑最多的是它不会自动清理旧Artifact产出目录旧的out目录里残留了旧版本的class文件或原本就没带lib你以为删了类实际加载的还是旧的。选择Build - Rebuild Project然后再运行Tomcat。顺便说一个检验部署完整性的土办法项目跑起来之后到Tomcat解压目录里的webapps/你的项目名/WEB-INF/lib下面手动看一下有没有那几个关键JAR和.properties文件。有说明部署产物完整没有页面报什么错都正常。6. 源码二次开发前的验证技巧从接口断言到可维护性收尾把源码跑通只是第一步真正让这个项目成为你能在答辩里抬起头来的东西是验证与收尾阶段的做法。我这里给你一套可复制的最小验证清单。第一用浏览器开发者工具做接口断言。打开Chrome DevTools的Network面板勾选“Preserve log”按下面的顺序手动过一遍流程注册新用户、用新用户登录、写一篇含中英文符号和emoji的日记、回列表页确认摘要显示、进详情确认全文、编辑并再次保存、删除这篇日记。每一步看Network里对应请求的Status Code注册成功是302跳转登录页查询成功是200删除成功如果是POST也应该是302或200任何一步出现500点开Response窗口看异常堆栈定位到具体代码行。第二用命令行curl做登录状态下的接口验证这个对后端逻辑更重要# 先登录-c 保存Cookie到本地文件 curl -c cookie.txt -d usernametestpassword123456 http://localhost:8080/diary/login # 用Cookie请求日记列表-b 从文件读取Cookie curl -b cookie.txt http://localhost:8080/diary/list # 未登录直接访问列表页应该被过滤器拦截并跳转到login.jsp curl -L http://localhost:8080/diary/list最后一行-L跟随跳转看最终是否落到/login.jsp验证你没有忘记加登录拦截过滤器。这是很多老源码的漏网之鱼Session为空时访问列表页会直接抛NullPointerException而不是友好地跳转登录页。第三代码收尾阶段做三件小事。把打印System.out.println(rs.getString(password))这类调试语句全部删除把JSP里% page importjava.sql.*%这种直接在视图层写SQL的坏味道改掉数据逻辑统一收进DAO层确认web.xml里有错误页配置。关于错误页你不需要写一个很好看的404页面但至少要加这一行error-page error-code500/error-code location/error.jsp/location /error-page如果你之后打算把项目往Spring Boot 2方向升级JSP视图层的迁移有一个已知边界Spring Boot默认不支持JSP需要单独引入tomcat-embed-jasper依赖并把视图物理路径放到src/main/webapp/WEB-INF/jsp下同时spring.mvc.view.prefix/WEB-INF/jsp/suffix.jsp。这一段不是必需的但讲出来会让你的系统演进思路更完整。我自己的习惯是拿到任何一份老源码第一件事永远是先跑通原版再动手改代码。很多人一上来就改数据库表结构、换页面模板结果到答辩前一天发现原版的登录逻辑都还走不通那种紧张感会直接影响你的状态。记住这个顺序原版跑一遍 - 熟悉每个Servlet对应的功能 - 按验证清单过一遍 - 再决定改哪里。希望这篇笔记能帮你少踩几个坑把本子上的日记安安稳稳地跑起来。本文还有配套的精品资源点击获取