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

文章详情

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

JSP+MySQL个人备忘系统:教学环境下的轻量Web开发实践

JSP+MySQL个人备忘系统:教学环境下的轻量Web开发实践 简介这是一套基于JSP与MySQL开发的个人记事备忘系统完整源码面向Java Web初学者及课程设计实践者解决日常轻量级笔记在线管理需求适用于高校Web开发实训、毕业设计参考或Servlet/JSP技术栈入门项目复现。资源共374个文件涵盖24个Java后端类含NoteServlet、NoteDAO等核心逻辑、9个JSP页面如note.jsp、index.jsp等视图层、86个JS脚本与28个CSS样式文件支撑前端交互与界面呈现另有46个GIF/16个JPG/PNG图片资源用于UI美化整体压缩包大小为27.51MB。已有109人学习下载资源结构清晰包含标准MVC分层目录如WEB-INF配置、src业务逻辑、static静态资源并预置Bootstrap、UEditor等成熟前端组件便于快速部署至Tomcat运行调试同时提供数据库建表SQL与基础安全防护示例是理解JDBC连接、CRUD实现、前后端协同及Web应用工程化组织的典型教学案例。1. 为什么一个“个人记事备忘系统”还值得用 JSP MySQL 重做一遍不是所有需求都该上 Spring Boot 或 Vue 前后端分离——当你要在一台老式教学服务器、某高校机房的 Windows Server 2012 环境里给大一学生交一个能本地部署、不依赖 npm install、不报 node-gyp 错误、连 Tomcat 8.5 都算新版本的 Web 作业时“基于 JSP 和 MySQL 实现的个人记事备忘系统”就不是怀旧而是精准卡点。它解决的是零构建工具链、单文件可导入数据库、JSP 页面即改即看、Servlet 层逻辑肉眼可 debug 的真实交付场景。这个系统不追求高并发但要求每个增删改查操作都能在 3 秒内响应不强调 UI 动效但必须让“新增一条带日期和标签的备忘”这件事在 IE11 下也能点完保存不丢数据。它适合三类人刚学完 Servlet 生命周期的学生、需要快速搭内部轻量管理页的行政岗同事、以及想拿源码反向吃透 MVC 分层边界的自学开发者。别被“JSP 过时”带偏——当你面对的是没有 Docker 权限、不能开 3307 端口、连 Maven 镜像源都要手动换阿里云的封闭环境时这套组合反而是最稳的“后悔药”。2. 从零搭起Tomcat MySQL JSP 环境的最小闭环验证2.1 确认 JDK 与 Tomcat 版本兼容性血泪经验别跳过这步很多翻车始于 JDK 版本错配。JSP 编译依赖javax.servletAPI而不同 Tomcat 版本绑定的 Servlet 规范版本不同Tomcat 版本推荐 JDK 版本Servlet 规范JSP 规范是否支持 EL 表达式${}8.0.xJDK 7 / 83.12.3✅ 默认开启8.5.xJDK 83.12.3✅9.0.xJDK 84.02.3✅需 web.xml 中el-ignoredfalse/el-ignored提示如果你用的是 Tomcat 8.5.99非官方编译版务必检查lib/servlet-api.jar是否被替换过——某次模拟项目X中因运维误替换成 Servlet 2.5 的 jar导致所有${user.name}全部原样输出为字符串排查耗时 3 小时。验证方式新建一个test.jsp放入webapps/ROOT/目录% page contentTypetext/html;charsetUTF-8 languagejava % html headtitleJSP 环境测试/title/head body h3当前时间% new java.util.Date() %/h3 h3EL 测试${Hello World}/h3 h3PageContext 测试% pageContext.getServletContext().getServerInfo() %/h3 /body /html访问http://localhost:8080/test.jsp若看到时间、拼接字符串和Apache Tomcat/8.5.xx字样说明基础环境通了。2.2 MySQL 数据库初始化建库、建表、设字符集三步到位备忘系统核心表只有两张user用户登录和memo记事主体。关键不是字段多而是字符集和排序规则必须显式声明否则中文插入变问号是高频翻车点。执行以下 SQL推荐用 MySQL 5.7避免 8.0 默认 strict mode 导致datetime插入空字符串失败-- 1. 创建数据库强制 UTF8MB4支持 emoji也兼容中文 CREATE DATABASE IF NOT EXISTS memo_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 2. 切库 USE memo_db; -- 3. 用户表id, username, password明文仅用于教学演示生产必须加盐哈希 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; -- 4. 备忘表含标题、内容、状态0未完成/1已完成、标签逗号分隔简化设计、创建/更新时间 CREATE TABLE memo ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, title VARCHAR(100) NOT NULL, content TEXT, status TINYINT DEFAULT 0 COMMENT 0:待办,1:完成, tags VARCHAR(200) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;参数说明utf8mb4_unicode_ci比utf8mb4_general_ci更准尤其对中文排序ON DELETE CASCADE是关键删用户时自动清空其所有备忘避免外键残留update_time的ON UPDATE CURRENT_TIMESTAMP让每次UPDATE自动刷新省去 Java 层手动赋值。验证插入一条测试数据INSERT INTO user(username, password) VALUES (test, 123456); INSERT INTO memo(user_id, title, content, tags) VALUES (1, 明天交作业, Java Web 实验报告, 学习,紧急);然后用SELECT * FROM memo;查看中文是否正常显示。2.3 配置 JDBC 连接池用 Tomcat 自带的 JNDI 而非硬编码 URL硬写jdbc:mysql://localhost:3306/memo_db?useSSLfalseserverTimezoneAsia/Shanghai在web.xml或 Java 文件里等于把密码和地址焊死在代码里。正确做法是利用 Tomcat 的context.xml做 JNDI 绑定。编辑$CATALINA_HOME/conf/context.xml在Context标签内添加Resource namejdbc/MemoDB authContainer typejavax.sql.DataSource factoryorg.apache.tomcat.jdbc.pool.DataSourceFactory driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/memo_db?useSSLfalseamp;serverTimezoneAsia/Shanghaiamp;characterEncodingutf8mb4 usernameroot passwordyour_password maxActive20 minIdle5 maxWait10000 validationQuerySELECT 1 testOnBorrowtrue removeAbandonedOnBorrowtrue logAbandonedOnBorrowtrue/注意amp;是 XML 转义不可写成否则 Tomcat 启动报错com.mysql.cj.jdbc.Driver是 MySQL 8.0 驱动类名5.1.x 用com.mysql.jdbc.DrivervalidationQuerySELECT 1是连接池健康检查语句必须写否则空闲连接超时后会假死removeAbandonedOnBorrowtrue可防连接泄漏适合教学环境反复重启调试。接着在你的 Web 应用WEB-INF/web.xml中声明资源引用resource-ref descriptionMemo Database Connection/description res-ref-namejdbc/MemoDB/res-ref-name res-typejavax.sql.DataSource/res-type res-authContainer/res-auth /resource-ref最后在 Java 代码中通过 JNDI 获取连接例如在DBUtil.java工具类中public class DBUtil { private static DataSource dataSource; static { try { Context initCtx new InitialContext(); Context envCtx (Context) initCtx.lookup(java:comp/env); dataSource (DataSource) envCtx.lookup(jdbc/MemoDB); } catch (Exception e) { throw new RuntimeException(JNDI DataSource lookup failed, e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }这样做的好处换数据库只需改context.xml无需动一行 Java 代码密码不进 Git连接池参数如maxActive可独立调优。3. 核心功能落地登录、列表、增删改查的 JSP Servlet 实现链3.1 登录模块Session 管理与防暴力尝试的朴素实现不要小看登录——它是整个系统的信任起点。这里不用框架纯靠HttpSession和request.getParameter()构建最小可行链。前端 login.jsp精简关键部分% page contentTypetext/html;charsetUTF-8 % % String errorMsg (String) request.getAttribute(errorMsg); % html body form actionLoginServlet methodpost input typetext nameusername placeholder用户名 requiredbr input typepassword namepassword placeholder密码 requiredbr button typesubmit登录/button /form % if (errorMsg ! null !errorMsg.isEmpty()) { % p stylecolor:red% errorMsg %/p % } % /body /html后端 LoginServlet.javaWebServlet(/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. 简单空校验 if (username null || username.trim().isEmpty() || password null || password.trim().isEmpty()) { request.setAttribute(errorMsg, 用户名或密码不能为空); request.getRequestDispatcher(login.jsp).forward(request, response); return; } // 2. 查询用户使用 PreparedStatement 防 SQL 注入 String sql SELECT id, username FROM user WHERE username ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); // 生产环境应为 BCrypt.checkpw(password, hashedPwd) ResultSet rs ps.executeQuery(); if (rs.next()) { // 3. 登录成功存用户 ID 和用户名到 Session HttpSession session request.getSession(true); session.setMaxInactiveInterval(1800); // 30 分钟无操作失效 session.setAttribute(userId, rs.getInt(id)); session.setAttribute(username, rs.getString(username)); response.sendRedirect(memoList.jsp); // 重定向防 F5 刷新重复提交 } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } } catch (SQLException e) { e.printStackTrace(); request.setAttribute(errorMsg, 系统繁忙请稍后再试); request.getRequestDispatcher(login.jsp).forward(request, response); } } }逻辑说明response.sendRedirect()是关键避免用户刷新登录页导致重复 POSTsession.setMaxInactiveInterval(1800)显式设超时Tomcat 默认 30 分钟但显式写更可控PreparedStatement替代Statement哪怕只是教学项目也要养成防注入习惯。3.2 备忘列表页JSTL EL 驱动的动态渲染memoList.jsp不写 Java 脚本% %全用 JSTL 标签 EL 表达式既清晰又安全% page contentTypetext/html;charsetUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % c:if test${empty sessionScope.userId} c:redirect urllogin.jsp/ /c:if html headtitle我的备忘录/title/head body h2欢迎${sessionScope.username}/h2 a hrefaddMemo.jsp 新增备忘/a | a hrefLogoutServlet退出登录/a table border1 tr th标题/thth内容摘要/thth标签/thth状态/thth操作/th /tr c:forEach items${memoList} varm tr td${m.title}/td td${fn:substring(m.content, 0, 30)}${fn:length(m.content) 30 ? ... : }/td td${m.tags}/td td c:choose c:when test${m.status 0}⏳ 待办/c:when c:otherwise✅ 已完成/c:otherwise /c:choose /td td a hrefEditServlet?id${m.id}编辑/a | a hrefDeleteServlet?id${m.id} onclickreturn confirm(确定删除)删除/a /td /tr /c:forEach /table /body /html参数说明c:if test${empty sessionScope.userId}检查登录态未登录强跳转${fn:substring(...)}需在页面顶部加% taglib prefixfn urihttp://java.sun.com/jsp/jstl/functions %onclickreturn confirm(...)是前端轻量确认不能替代后端权限校验但教学足够所有${memoList}数据由MemoListServlet查询后request.setAttribute(memoList, list)注入。MemoListServlet核心查询逻辑带用户隔离// 查询当前用户的所有备忘按更新时间倒序 String sql SELECT id, title, content, tags, status, create_time, update_time FROM memo WHERE user_id ? ORDER BY update_time DESC; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, userId); // 从 session 取出的 userId ResultSet rs ps.executeQuery(); ListMemo list new ArrayList(); while (rs.next()) { Memo m new Memo(); m.setId(rs.getInt(id)); m.setTitle(rs.getString(title)); m.setContent(rs.getString(content)); m.setTags(rs.getString(tags)); m.setStatus(rs.getInt(status)); m.setCreateTime(rs.getTimestamp(create_time)); m.setUpdateTime(rs.getTimestamp(update_time)); list.add(m); } request.setAttribute(memoList, list); request.getRequestDispatcher(memoList.jsp).forward(request, response); }3.3 新增与编辑表单回显与状态保持的细节处理addMemo.jsp和editMemo.jsp共享大部分 HTML 结构差异只在初始值和提交路径。关键技巧是用同一个 JSP 文件通过request.getAttribute(memo)是否为空来判断是新增还是编辑。addMemo.jsp实际是memoForm.jsp% page contentTypetext/html;charsetUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % c:set varmemo value${requestScope.memo} / html body h2c:if test${empty memo}新增/c:ifc:if test${not empty memo}编辑/c:if备忘/h2 form actionc:if test${empty memo}AddMemoServlet/c:ifc:if test${not empty memo}UpdateMemoServlet/c:if methodpost input typehidden nameid value${memo.id} label标题input typetext nametitle value${memo.title} required/labelbr label内容textarea namecontent rows4 cols50${memo.content}/textarea/labelbr label标签英文逗号分隔input typetext nametags value${memo.tags}/labelbr label状态 input typeradio namestatus value0 c:if test${memo.status 0 || empty memo}checked/c:if 待办 input typeradio namestatus value1 c:if test${memo.status 1}checked/c:if 已完成 /labelbr button typesubmitc:if test${empty memo}提交新增/c:ifc:if test${not empty memo}保存修改/c:if/button /form /body /htmlEditServlet.java只负责查数据并转发WebServlet(/EditServlet) public class EditServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Integer id getIntParam(request, id); if (id null) { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } // 查询该条备忘且校验属于当前用户 Memo memo MemoDao.findByIdAndUserId(id, getUserIdFromSession(request)); if (memo null) { request.setAttribute(errorMsg, 找不到该备忘或无权访问); request.getRequestDispatcher(memoList.jsp).forward(request, response); return; } request.setAttribute(memo, memo); request.getRequestDispatcher(memoForm.jsp).forward(request, response); } }避坑点getIntParam()是自定义工具方法避免Integer.parseInt(request.getParameter(id))报NumberFormatExceptionfindByIdAndUserId()必须加AND user_id ?条件防止越权查看他人数据表单中value${memo.title}和checked的 EL 表达式是回显的核心新手常漏写导致编辑时字段空白。4. 避坑指南JSP MySQL 备忘系统开发中 5 个高频翻车现场4.1 现象中文插入 MySQL 全变成问号????原因四个环节任一没设 UTF8MB4 —— MySQL 服务端字符集、数据库字符集、表字符集、JDBC URL 参数characterEncoding。常见是只改了数据库忘了改my.cnf中的[mysqld]段collation-serverutf8mb4_unicode_ci和init-connectSET NAMES utf8mb4。解决执行SHOW VARIABLES LIKE character_set_%;和SHOW VARIABLES LIKE collation%;确认全为utf8mb4JDBC URL 必须含characterEncodingutf8mb4建表语句显式写DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci。4.2 现象登录后跳转memoList.jsp页面报HTTP Status 404原因response.sendRedirect(memoList.jsp)是客户端重定向路径是相对于 Web 应用根目录即http://host:port/appName/memoList.jsp但如果memoList.jsp放在WEB-INF/下出于安全考虑则无法被外部直接访问。解决JSP 页面必须放在webapps/yourApp/目录下如webapps/memo/memoList.jsp而非WEB-INF内WEB-INF只放web.xml、classes/、lib/。4.3 现象点击“删除”后页面空白或 404但数据库记录没删原因DeleteServlet中用了doGet()但前端a hrefDeleteServlet?id123发的是 GET 请求而你写了doPost()方法或者没写WebServlet(/DeleteServlet)导致 URL 映射失败。解决删除操作必须用doGet()因为是链接触发确保WebServlet注解路径与href中路径完全一致删除前务必加WHERE id ? AND user_id ?双条件校验。4.4 现象c:forEach遍历memoList时页面报javax.el.PropertyNotFoundException原因Memo类中 getter 方法命名不规范如字段叫createTime但 getter 写成getCreate_time()下划线或getCreatetime()少_EL 表达式${m.createTime}就找不到对应方法。解决严格遵循 JavaBean 规范字段xxxYyy→ gettergetXxxYyy()用 IDE 自动生成 getter/setter勿手敲检查Memo.java中所有字段的 getter 名称是否与 EL 中引用名完全匹配大小写敏感。4.5 现象Tomcat 启动时报ClassNotFoundException: com.mysql.cj.jdbc.Driver原因MySQL 8.0 驱动 jarmysql-connector-java-8.0.xx.jar没放进$CATALINA_HOME/lib/而是只放在了应用的WEB-INF/lib/下。Tomcat 8.5 的 JNDI 数据源初始化发生在应用加载前驱动必须在 Tomcat 级 classpath。解决将mysql-connector-java-8.0.33.jar复制到$CATALINA_HOME/lib/删除WEB-INF/lib/下同名 jar避免冲突重启 Tomcat。5. 进阶技巧让这个“老派”系统真正好维护的 3 个实操习惯5.1 用 Filter 统一处理登录拦截告别每个 Servlet 里写 session 判断与其在MemoListServlet、AddMemoServlet、EditServlet里都写if (session.getAttribute(userId) null) { ... }不如用一个LoginFilter全局拦截WebFilter(urlPatterns {/memoList.jsp, /addMemo.jsp, /EditServlet, /DeleteServlet, /UpdateMemoServlet}) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); if (session null || session.getAttribute(userId) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }价值点URL 模式用urlPatterns显式列出受保护资源比/*更精准避免拦截静态资源request.getContextPath()保证路径可移植部署在/memo或/下都有效后续加新功能如/export.jsp只需往urlPatterns里加一项逻辑零侵入。5.2 用 Properties 文件管理 SQL把硬编码 SQL 从 Java 中剥离新建src/sql.propertiesuser.loginSELECT id, username FROM user WHERE username ? AND password ? memo.listSELECT id, title, content, tags, status, create_time, update_time FROM memo WHERE user_id ? ORDER BY update_time DESC memo.addINSERT INTO memo(user_id, title, content, tags, status) VALUES (?, ?, ?, ?, ?) memo.updateUPDATE memo SET title ?, content ?, tags ?, status ?, update_time NOW() WHERE id ? AND user_id ? memo.deleteDELETE FROM memo WHERE id ? AND user_id ?在 DAO 类中读取private static final Properties SQL_PROPS new Properties(); static { try (InputStream is MemoDao.class.getClassLoader() .getResourceAsStream(sql.properties)) { SQL_PROPS.load(is); } catch (IOException e) { throw new RuntimeException(Failed to load sql.properties, e); } } // 使用时 String sql SQL_PROPS.getProperty(memo.list); PreparedStatement ps conn.prepareStatement(sql);好处SQL 修改无需重新编译 JavaDBA 可直接审阅.properties文件同一 SQL 多处复用改一处全生效。5.3 用 Log4j2 替代e.printStackTrace()让错误可追溯System.out.println()和e.printStackTrace()在生产环境等于盲人摸象。引入log4j2.xml放在src/main/resources/?xml version1.0 encodingUTF-8? Configuration statusWARN Appenders File nameFileAppender fileNamelogs/memo-app.log PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /File /Appenders Loggers Root levelinfo AppenderRef refFileAppender/ /Root /Loggers /Configuration在 Servlet 中private static final Logger logger LogManager.getLogger(LoginServlet.class); // 替换掉所有 e.printStackTrace(); } catch (SQLException e) { logger.error(Login failed for user: {}, username, e); request.setAttribute(errorMsg, 系统繁忙请稍后再试); request.getRequestDispatcher(login.jsp).forward(request, response); }效果所有异常堆栈、用户操作上下文如username都落盘到logs/memo-app.log半夜出问题不用登服务器tail -f直接查日志文件即可定位。我带过的某高校实训班学生交作业前必做三件事git diff确认没提交context.xml里的密码、SELECT语句跑一遍确认中文不乱码、用 Chrome DevTools Network 标签看所有请求状态码是不是 200。这些不是教条是多年踩坑后长出来的肌肉记忆。JSP MySQL 这套组合不是技术债而是你理解 Web 容器生命周期、HTTP 协议本质、关系型数据库约束力的最短路径。希望帮到你。本文还有配套的精品资源点击获取
返回列表