多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Java Web实战:用JSP+MySQL搭建个人备忘系统

Java Web实战:用JSP+MySQL搭建个人备忘系统 简介这是一套基于JSP与MySQL开发的轻量级个人记事备忘系统源码面向Java Web初学者及课程设计实践者解决日常笔记在线化管理需求适用于高校Web开发实训、毕业设计参考或MVC模式入门项目。资源共374个文件涵盖24个Java后端类含Servlet与DAO、9个JSP页面如note.jsp、index.jsp、28个CSS与86个JS前端资源含Bootstrap、UEditor、video.js等成熟组件以及97张PNG界面截图和46个GIF动效图完整呈现前后端交互与UI细节压缩包大小27.51MB。已有109人学习下载提供可直接部署到Tomcat的完整工程结构含web.xml配置、JDBC连接池设置、SQL建表语句及初始化数据附带清晰的目录分层notes-jsp主模块、常见SQL注入防护示例与基础日志记录机制是理解JSPServletMySQL三层架构落地的典型教学案例。1. 为什么一个“个人记事备忘系统”值得用 JSP MySQL 从零搭起它不是练手玩具而是你理解 Web 开发闭环的最小可信单元你可能在简历里写过“熟悉 Java Web 开发”但真能说清请求怎么从浏览器进 Servlet、Session 怎么跨页面存用户状态、MySQL 的datetime和timestamp在插入时到底谁听谁的这个标题里的“个人记事备忘系统”恰恰是唯一能让你把 JDBC 连接池、JSP 页面跳转逻辑、表单 CSRF 防御、甚至 Tomcat 日志定位错误全串起来的真实切口。它不追求高并发但要求每一步都可追踪、可打断、可重放——比如你删掉一条备忘后刷新页面发现它又回来了那一定是事务没提交或者前端缓存没清你改完密码却登不进去大概率是password字段用了VARCHAR(20)却存了 BCrypt 加密后的 60 位字符串。这不是教科书里的 Hello World而是一套带血丝的实战切片它小到能一上午跑通又厚到能挖出你三年都没意识到的底层盲区。适合刚学完 Servlet 生命周期、正卡在“写了代码但不知道哪步断了”的 Java 初学者也适合想带新人快速建立 Web 请求链路直觉的带教工程师。2. 搭建环境与初始化数据库避开 JDK 版本错配和 MySQL 8.0 默认认证插件这两大“静默杀手”2.1 环境版本锁定为什么必须用 JDK 8u202 Tomcat 9.0.83 MySQL 8.0.33而非最新版很多新手直接下最新 JDK 21 或 MySQL 8.4结果连Class.forName(com.mysql.cj.jdbc.Driver)都报ClassNotFoundException。根本原因在于JDK 9 移除了javax.servlet包而老版 JSP 编译器jasper.jar仍强依赖它MySQL 8.0.4 默认启用caching_sha2_password认证插件但mysql-connector-java:5.1.47JSP 项目最常用驱动根本不认识它。我一般会强制降级并固化三件套# 下载地址均来自官方归档页非第三方镜像 # JDK 8u202: https://adoptium.net/zh-CN/temurin/releases/?version8 # Tomcat 9.0.83: https://archive.apache.org/dist/tomcat/tomcat-9/v9.0.83/bin/ # MySQL 8.0.33: https://downloads.mysql.com/archives/community/提示解压后务必检查JAVA_HOME指向 JDK 8 目录且PATH中java -version输出必须为1.8.0_202Tomcat 启动日志第一行若出现Server version: Apache Tomcat/9.0.83才算落定。2.2 创建 MySQL 数据库与用户用utf8mb4而非utf8并显式指定认证插件执行以下 SQL注意不是在 phpMyAdmin 图形界面点点点而是用命令行mysql -u root -p进入后逐行粘贴-- 1. 创建数据库字符集必须为 utf8mb4支持 emoji 和生僻字 CREATE DATABASE IF NOT EXISTS memo_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 2. 创建专用用户禁止 root 远程直连 CREATE USER memo_userlocalhost IDENTIFIED WITH mysql_native_password BY MemoPass123!; -- 3. 授予最小权限只给 memo_db 的 CRUD GRANT SELECT, INSERT, UPDATE, DELETE ON memo_db.* TO memo_userlocalhost; -- 4. 刷新权限立即生效 FLUSH PRIVILEGES;关键参数说明utf8mb4_unicode_ci比utf8mb4_general_ci更准的排序规则对中文标点兼容性更好IDENTIFIED WITH mysql_native_password强制使用旧版认证插件确保 JDBC 5.1.x 驱动能握手成功memo_userlocalhost限定只能本机连接杜绝远程爆破风险。2.3 验证数据库连通性写一个独立的TestDB.java不依赖任何 Web 容器在任意空文件夹下新建TestDB.javaimport java.sql.*; public class TestDB { public static void main(String[] args) { String url jdbc:mysql://localhost:3306/memo_db?useSSLfalseserverTimezoneAsia/Shanghai; String user memo_user; String password MemoPass123!; try (Connection conn DriverManager.getConnection(url, user, password)) { System.out.println(✅ 数据库连接成功); // 额外验证查一下当前时间确认时区设置生效 try (Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(SELECT NOW() as now_time)) { if (rs.next()) { System.out.println(⏰ 当前 MySQL 时间 rs.getTimestamp(now_time)); } } } catch (SQLException e) { System.err.println(❌ 连接失败 e.getMessage()); e.printStackTrace(); } } }编译运行# 先下载 mysql-connector-java-5.1.47.jar官网归档页有 javac -cp .:mysql-connector-java-5.1.47.jar TestDB.java java -cp .:mysql-connector-java-5.1.47.jar TestDB现象 → 原因 → 解决若报Access denied for user密码输错或用户创建时没加localhost重新执行CREATE USER若报Unknown system variable tx_isolationMySQL 版本过高8.0.33降级到 8.0.33若报The server time zone value XXX is unrecognizedURL 中serverTimezoneAsia/Shanghai拼写错误注意大小写和斜杠。3. 设计核心数据表与 JDBC 工具类用PreparedStatement防注入用try-with-resources确保连接释放3.1 两张表足矣users存账号memos存备忘拒绝过度设计很多初学者一上来就加categories表、tags表、share_logs表……结果连登录都调不通。真实需求很简单一个用户对应多条备忘每条备忘有标题、内容、创建时间、是否完成。精简到极致的 SQL-- users 表仅存登录凭证密码必须加密存储后续章节实现 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, -- BCrypt 加密后长度约 60 created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- memos 表user_id 外键关联 users.id软删除用 is_deleted 字段 CREATE TABLE memos ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, title VARCHAR(200) NOT NULL, content TEXT, is_done TINYINT(1) DEFAULT 0, -- 0未完成1已完成 is_deleted TINYINT(1) DEFAULT 0, -- 0正常1已删除回收站逻辑 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意ON DELETE CASCADE是关键——当用户注销时自动删掉其所有备忘避免外键残留。3.2 封装DBUtil用静态块加载驱动用ThreadLocal隔离连接非连接池够用在src/com/utils/DBUtil.java中写package com.utils; import java.sql.*; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/memo_db?useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER memo_user; private static final String PASSWORD MemoPass123!; // 1. 静态块加载驱动JDBC 4.0 可省略但显式写更稳 static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL Driver not found!, e); } } // 2. 获取连接每次调用都新建无连接池教学场景够用 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 3. 关闭资源重点按 ResultSet → Statement → Connection 逆序关 public static void close(Connection conn, Statement stmt, 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) { /* 忽略 */ } } }为什么不用 HikariCP因为初学者先要理解“连接是什么”“为什么不能全局共用一个 Connection”。等你能手动写出事务回滚、批量插入、连接泄漏检测再上连接池不迟。3.3 写第一个 DAOUserDAO的登录校验用PreparedStatement绑定参数防 SQL 注入在src/com/dao/UserDAO.java中package com.dao; import com.utils.DBUtil; import java.sql.*; public class UserDAO { // 根据用户名查用户用于登录校验 public static User findByUsername(String username) { String sql SELECT id, username, password FROM users WHERE username ? AND is_deleted 0; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, username); // ✅ 参数化杜绝 admin OR 11 注入 try (ResultSet rs pstmt.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setPassword(rs.getString(password)); // 密码只取值不暴露 return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; // 用户不存在 } // 插入新用户注册 public static int insert(User user) { String sql INSERT INTO users (username, password) VALUES (?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { pstmt.setString(1, user.getUsername()); pstmt.setString(2, user.getPassword()); // 此处应为 BCrypt 加密后字符串 int affectedRows pstmt.executeUpdate(); if (affectedRows 0) { throw new SQLException(Creating user failed, no rows affected.); } try (ResultSet generatedKeys pstmt.getGeneratedKeys()) { if (generatedKeys.next()) { return generatedKeys.getInt(1); // 返回自增主键 } else { throw new SQLException(Creating user failed, no ID obtained.); } } } catch (SQLException e) { e.printStackTrace(); return -1; } } }关键细节pstmt.setString(1, username)?占位符必须用setXxx()绑定不能字符串拼接Statement.RETURN_GENERATED_KEYS获取AUTO_INCREMENT主键注册后跳转到用户主页要用try-with-resources自动关闭ResultSet/Statement/Connection避免连接泄漏。4. 实现用户注册与登录流程用 Session 管理状态用request.getRequestDispatcher()控制跳转4.1 注册页面register.jsp表单提交到RegisterServlet用EL 表达式回显错误% page contentTypetext/html;charsetUTF-8 languagejava % html headtitle用户注册/title/head body h2注册新账户/h2 !-- 1. 显示上一步传来的错误信息 -- c:if test${not empty errorMsg} p stylecolor:red${errorMsg}/p /c:if form actionRegisterServlet methodpost label用户名input typetext nameusername required/labelbr label密码input typepassword namepassword required/labelbr label确认密码input typepassword nameconfirmPassword required/labelbr input typesubmit value注册 /form /body /html注意c:if是 JSTL 标签需在WEB-INF/lib下放入jstl-1.2.jar并在 JSP 顶部加% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %。4.2RegisterServlet校验逻辑前置密码用 BCrypt 加密注册成功后重定向到登录页WebServlet(/RegisterServlet) public class RegisterServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取表单参数 String username request.getParameter(username); String password request.getParameter(password); String confirmPassword request.getParameter(confirmPassword); // 2. 基础校验空值、长度、一致性 if (username null || username.trim().length() 3) { request.setAttribute(errorMsg, 用户名至少3位); request.getRequestDispatcher(register.jsp).forward(request, response); return; } if (!password.equals(confirmPassword)) { request.setAttribute(errorMsg, 两次输入的密码不一致); request.getRequestDispatcher(register.jsp).forward(request, response); return; } // 3. BCrypt 加密引入 jbcrypt-0.4.jar String hashedPassword BCrypt.hashpw(password, BCrypt.gensalt(12)); // 4. 调用 DAO 插入数据库 User user new User(); user.setUsername(username); user.setPassword(hashedPassword); int userId UserDAO.insert(user); if (userId 0) { // 注册成功重定向到登录页避免刷新重复提交 response.sendRedirect(login.jsp?msg注册成功请登录); } else { request.setAttribute(errorMsg, 注册失败请重试); request.getRequestDispatcher(register.jsp).forward(request, response); } } }为什么用response.sendRedirect()而非forward()因为forward()是服务器内部跳转浏览器地址栏仍是/RegisterServlet用户刷新会重复提交表单sendRedirect()是 302 重定向地址栏变成/login.jsp刷新安全。4.3 登录LoginServlet查库比对密码正确则存HttpSession错误则返回错误提示WebServlet(/LoginServlet) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); // 1. 查询用户 User user UserDAO.findByUsername(username); if (user null) { request.setAttribute(errorMsg, 用户名不存在); request.getRequestDispatcher(login.jsp).forward(request, response); return; } // 2. BCrypt 密码比对明文密码 vs 数据库加密串 if (BCrypt.checkpw(password, user.getPassword())) { // 登录成功存 session并跳转到首页 HttpSession session request.getSession(); session.setAttribute(user, user); // 存整个 User 对象后续页面可取 session.setMaxInactiveInterval(1800); // 30分钟无操作过期 response.sendRedirect(index.jsp); } else { request.setAttribute(errorMsg, 密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } } }Session 安全要点setMaxInactiveInterval(1800)强制会话 30 分钟失效防止长期挂起不存密码、不存敏感字段只存id和username生产环境需配合HttpOnlyCookieTomcat 9 默认开启。5. 备忘 CRUD 与 Session 权限控制用 Filter 拦截未登录访问用is_done字段实现状态切换5.1 写一个AuthFilter所有/memo/*请求必须先过登录校验在src/com/filter/AuthFilter.java中WebFilter(filterName AuthFilter, urlPatterns /memo/*) 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; // 1. 获取 session 中的 user HttpSession session req.getSession(false); User user (User) (session ! null ? session.getAttribute(user) : null); // 2. 未登录则重定向到登录页并保存原请求路径登录后跳回 if (user null) { String originalUrl req.getRequestURL().toString(); if (req.getQueryString() ! null) { originalUrl ? req.getQueryString(); } req.getSession().setAttribute(originalUrl, originalUrl); resp.sendRedirect(req.getContextPath() /login.jsp?msg请先登录); return; } // 3. 已登录放行 chain.doFilter(request, response); } }提示WebFilter注解需 Tomcat 7若用低版本需在web.xml中配置filter。5.2MemoServlet实现增删改查用user_id关联当前登录用户杜绝越权WebServlet(/memo/*) public class MemoServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String pathInfo request.getPathInfo(); // /memo/list → pathInfo/list if (/list.equals(pathInfo)) { listMemos(request, response); } else if (/add.equals(pathInfo)) { request.getRequestDispatcher(/memo/add.jsp).forward(request, response); } else if (/edit.equals(pathInfo)) { editMemo(request, response); } else if (/delete.equals(pathInfo)) { deleteMemo(request, response); } } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String pathInfo request.getPathInfo(); if (/add.equals(pathInfo)) { addMemo(request, response); } else if (/update.equals(pathInfo)) { updateMemo(request, response); } } private void listMemos(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { User user (User) request.getSession().getAttribute(user); ListMemo memos MemoDAO.findByUserId(user.getId()); request.setAttribute(memos, memos); request.getRequestDispatcher(/memo/list.jsp).forward(request, response); } private void addMemo(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { User user (User) request.getSession().getAttribute(user); String title request.getParameter(title); String content request.getParameter(content); Memo memo new Memo(); memo.setUserId(user.getId()); memo.setTitle(title); memo.setContent(content); memo.setIsDone(0); MemoDAO.insert(memo); response.sendRedirect(list); // 重定向到列表页 } private void deleteMemo(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int id Integer.parseInt(request.getParameter(id)); User user (User) request.getSession().getAttribute(user); // ✅ 关键WHERE user_id ? AND id ?防止越权删除他人备忘 MemoDAO.deleteByUserIdAndId(user.getId(), id); response.sendRedirect(list); } }权限控制核心逻辑deleteByUserIdAndId()方法中 SQL 必须同时带user_id ? AND id ?否则攻击者改 URL 中id123就能删别人数据listMemos()只查user.getId()的备忘天然隔离。5.3list.jsp页面用 JSTL 循环展示用is_done控制复选框状态与样式% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % h2我的备忘录/h2 a hrefadd 添加新备忘/a | a hreflogout 退出登录/a table border1 trth标题/thth内容/thth状态/thth操作/th/tr c:forEach items${memos} varm tr td${m.title}/td td${m.content}/td td c:choose c:when test${m.isDone 1} span stylecolor:green✅ 已完成/span /c:when c:otherwise span stylecolor:gray⏳ 进行中/span /c:otherwise /c:choose /td td !-- 切换完成状态GET 请求无 CSRF 风险 -- a hreftoggleDone?id${m.id} ${m.isDone 0 ? 标记完成 : 标记未完成}/a | a hrefedit?id${m.id}✏️ 编辑/a | a hrefdelete?id${m.id} onclickreturn confirm(确定删除)️ 删除/a /td /tr /c:forEach /table玄学细节onclickreturn confirm()删除前二次确认避免误点toggleDone用 GET 是因为只是状态翻转无数据修改且简单stylecolor:green直接内联教学项目不引 CSS降低复杂度。6. 避坑指南5 个让 90% 新手卡住超过 2 小时的真实问题与血泪解法6.1 现象JSP 页面中文显示为乱码如“备忘录”变成“???”但数据库里存的是好的原因Tomcat 默认用 ISO-8859-1 解析 POST 请求体而浏览器发的是 UTF-8 编码解决在web.xml中全局配置CharacterEncodingFilter或在每个 Servlet 的doPost()开头加request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8);血泪经验只设response.setContentType不够POST 表单提交必须request.setCharacterEncoding否则getParameter()拿到的就是乱码。6.2 现象登录成功后跳转到index.jsp但页面里${sessionScope.user.username}显示为空原因% page contentTypetext/html;charsetUTF-8 %缺失或 JSP 文件本身保存为 ANSI 编码Windows 记事本默认解决用 VS Code 或 Notepad 将所有.jsp文件另存为UTF-8 无 BOM格式并确保每页顶部有% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %注意pageEncoding是告诉 JSP 引擎“这个文件本身是 UTF-8 编码”contentType是告诉浏览器“响应内容是 UTF-8”。6.3 现象MemoDAO.insert()执行后数据库没数据但executeUpdate()返回 1原因MySQL 表引擎不是 InnoDB而是 MyISAM不支持事务autocommittrue但实际不生效解决建表时显式指定ENGINEInnoDB并检查SHOW CREATE TABLE memos; -- 输出中必须含 ENGINEInnoDB翻车现场某次用 Navicat 图形化建表勾选了“支持事务”却忘了选引擎结果死活不入库。6.4 现象BCrypt.checkpw()总返回 false但hashpw()生成的串长度是对的原因checkpw()第一个参数是明文密码第二个是数据库里存的加密串顺序反了解决死记硬背口诀“checkpw(plain,hashed)”就像equals()一样前者是待验证的后者是基准// ✅ 正确 if (BCrypt.checkpw(userInputPass, dbHashedPass)) { ... } // ❌ 错误新手高频 if (BCrypt.checkpw(dbHashedPass, userInputPass)) { ... }6.5 现象response.sendRedirect(list)后页面 404但http://localhost:8080/memo/list能直接访问原因sendRedirect()的路径是相对于域名的不是相对于当前 Servlet 的路径解决用request.getContextPath()拼接response.sendRedirect(request.getContextPath() /memo/list);后悔药开发时打开浏览器开发者工具 → Network 标签看重定向的 Location 响应头立刻知道路径拼错了没。7. 进阶技巧用 Log4j2 记录关键操作日志用updated_at字段做乐观锁防并发覆盖7.1 配置 Log4j2替换System.out.println()让调试有迹可循在src/log4j2.xml中写?xml version1.0 encodingUTF-8? Configuration statusWARN Appenders Console nameConsole targetSYSTEM_OUT PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /Console File nameFile fileNamelogs/memo.log PatternLayout pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /File /Appenders Loggers Root levelinfo AppenderRef refConsole/ AppenderRef refFile/ /Root /Loggers /Configuration在MemoServlet中使用private static final Logger logger LogManager.getLogger(MemoServlet.class); private void updateMemo(HttpServletRequest request, HttpServletResponse response) { int id Integer.parseInt(request.getParameter(id)); String title request.getParameter(title); String content request.getParameter(content); // ✅ 记录谁在什么时候改了哪条备忘 User user (User) request.getSession().getAttribute(user); logger.info(用户 {} (id{}) 更新备忘 id{}标题{}, user.getUsername(), user.getId(), id, title); Memo memo new Memo(); memo.setId(id); memo.setTitle(title); memo.setContent(content); MemoDAO.update(memo); response.sendRedirect(list); }效果logs/memo.log中会实时追加2024-06-15 14:22:33.123 [http-nio-8080-exec-1] INFO com.servlet.MemoServlet - 用户 admin (id1) 更新备忘 id5标题买牛奶7.2 用updated_at实现乐观锁当两人同时编辑同一条备忘时后提交者收到提示修改MemoDAO.update()方法public static int update(Memo memo) { // 1. 先查出当前 updated_at String selectSql SELECT updated_at FROM memos WHERE id ?; Timestamp currentUpdatedAt null; try (Connection conn DBUtil.getConnection(); PreparedStatement selectStmt conn.prepareStatement(selectSql)) { selectStmt.setInt(1, memo.getId()); try (ResultSet rs selectStmt.executeQuery()) { if (rs.next()) { currentUpdatedAt rs.getTimestamp(updated_at); } } } catch (SQLException e) { e.printStackTrace(); return 0; } // 2. 带条件更新只有 updated_at 没变才更新成功 String updateSql UPDATE memos SET title?, content?, updated_atNOW() WHERE id? AND updated_at?; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(updateSql)) { pstmt.setString(1, memo.getTitle()); pstmt.setString(2, memo.getContent()); pstmt.setInt(3, memo.getId()); pstmt.setTimestamp(4, currentUpdatedAt); // ✅ 关键只更新未被他人改过的记录 int rows pstmt.executeUpdate(); if (rows 0) { logger.warn(乐观锁失败备忘 id{} 已被他人修改, memo.getId()); } return rows; } catch (SQLException e) { e.printStackTrace(); return 0; } }验证方法打开两个浏览器标签页都编辑同一条备忘A 先点保存 → 成功B 后点保存 →executeUpdate()返回 0logger.warn输出警告B 页面可弹窗提示“内容已被更新请刷新后重试”。7.3 一个我坚持了 5 年的习惯每次git commit前必跑一次mvn clean compile并手动启动 Tomcat 测试登录流程不是信 CI/CD而是信自己的手指。因为pom.xml里漏了个scopeprovided/scope或者web.xml少了个servlet-mapping这些错误编译器不报但 Tomcat 启动就炸。我见过太多人把ClassNotFoundException归咎于“jar 包冲突”结果是WEB-INF/lib下压根没放mysql-connector-java.jar。所以我的本地工作流永远是改代码 →mvn clean compile→cp target/*.war $TOMCAT/webapps/→sh $TOMCAT/bin/startup.sh→ 打开浏览器点三下注册 → 登录 → 添加备忘 → 查看列表。只要这四步通我就敢推。希望帮到你。本文还有配套的精品资源点击获取
返回列表