简介:这是一套基于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.x | JDK 7 / 8 | 3.1 | 2.3 | ✅ 默认开启 |
| 8.5.x | JDK 8 | 3.1 | 2.3 | ✅ |
| 9.0.x | JDK 8+ | 4.0 | 2.3 | ✅(需 web.xml 中<el-ignored>false</el-ignored>) |
提示:如果你用的是 Tomcat 8.5.99(非官方编译版),务必检查
lib/servlet-api.jar是否被替换过——某次模拟项目X中,因运维误替换成 Servlet 2.5 的 jar,导致所有${user.name}全部原样输出为字符串,排查耗时 3 小时。
验证方式:新建一个test.jsp放入webapps/ROOT/目录:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head><title>JSP 环境测试</title></head> <body> <h3>当前时间:<%= new java.util.Date() %></h3> <h3>EL 测试:${'Hello' + ' World'}</h3> <h3>PageContext 测试:<%= 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_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?useSSL=false&serverTimezone=Asia/Shanghai在web.xml或 Java 文件里,等于把密码和地址焊死在代码里。正确做法是利用 Tomcat 的context.xml做 JNDI 绑定。
编辑$CATALINA_HOME/conf/context.xml,在<Context>标签内添加:
<Resource name="jdbc/MemoDB" auth="Container" type="javax.sql.DataSource" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/memo_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4" username="root" password="your_password" maxActive="20" minIdle="5" maxWait="10000" validationQuery="SELECT 1" testOnBorrow="true" removeAbandonedOnBorrow="true" logAbandonedOnBorrow="true"/>注意:
&是 XML 转义,不可写成&,否则 Tomcat 启动报错;com.mysql.cj.jdbc.Driver是 MySQL 8.0+ 驱动类名(5.1.x 用com.mysql.jdbc.Driver);validationQuery="SELECT 1"是连接池健康检查语句,必须写,否则空闲连接超时后会假死;removeAbandonedOnBorrow="true"可防连接泄漏,适合教学环境反复重启调试。
接着,在你的 Web 应用WEB-INF/web.xml中声明资源引用:
<resource-ref> <description>Memo Database Connection</description> <res-ref-name>jdbc/MemoDB</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</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 contentType="text/html;charset=UTF-8" %> <% String errorMsg = (String) request.getAttribute("errorMsg"); %> <html> <body> <form action="LoginServlet" method="post"> <input type="text" name="username" placeholder="用户名" required><br> <input type="password" name="password" placeholder="密码" required><br> <button type="submit">登录</button> </form> <% if (errorMsg != null && !errorMsg.isEmpty()) { %> <p style="color:red"><%= errorMsg %></p> <% } %> </body> </html>后端 LoginServlet.java:
@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. 简单空校验 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()是关键,避免用户刷新登录页导致重复 POST;session.setMaxInactiveInterval(1800)显式设超时,Tomcat 默认 30 分钟,但显式写更可控;PreparedStatement替代Statement,哪怕只是教学项目,也要养成防注入习惯。
3.2 备忘列表页:JSTL + EL 驱动的动态渲染
memoList.jsp不写 Java 脚本(<% %>),全用 JSTL 标签 + EL 表达式,既清晰又安全:
<%@ page contentType="text/html;charset=UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <c:if test="${empty sessionScope.userId}"> <c:redirect url="login.jsp"/> </c:if> <html> <head><title>我的备忘录</title></head> <body> <h2>欢迎,${sessionScope.username}!</h2> <a href="addMemo.jsp">+ 新增备忘</a> | <a href="LogoutServlet">退出登录</a> <table border="1"> <tr> <th>标题</th><th>内容摘要</th><th>标签</th><th>状态</th><th>操作</th> </tr> <c:forEach items="${memoList}" var="m"> <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 href="EditServlet?id=${m.id}">编辑</a> | <a href="DeleteServlet?id=${m.id}" onclick="return confirm('确定删除?')">删除</a> </td> </tr> </c:forEach> </table> </body> </html>参数说明:
<c:if test="${empty sessionScope.userId}">检查登录态,未登录强跳转;${fn:substring(...)}需在页面顶部加<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>;onclick="return 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(); List<Memo> 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 contentType="text/html;charset=UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:set var="memo" value="${requestScope.memo}" /> <html> <body> <h2><c:if test="${empty memo}">新增</c:if><c:if test="${not empty memo}">编辑</c:if>备忘</h2> <form action="<c:if test='${empty memo}'>AddMemoServlet</c:if><c:if test='${not empty memo}'>UpdateMemoServlet</c:if>" method="post"> <input type="hidden" name="id" value="${memo.id}"> <label>标题:<input type="text" name="title" value="${memo.title}" required></label><br> <label>内容:<textarea name="content" rows="4" cols="50">${memo.content}</textarea></label><br> <label>标签(英文逗号分隔):<input type="text" name="tags" value="${memo.tags}"></label><br> <label>状态: <input type="radio" name="status" value="0" <c:if test="${memo.status == 0 || empty memo}">checked</c:if>> 待办 <input type="radio" name="status" value="1" <c:if test="${memo.status == 1}">checked</c:if>> 已完成 </label><br> <button type="submit"><c:if test="${empty memo}">提交新增</c:if><c:if test="${not empty memo}">保存修改</c:if></button> </form> </body> </html>EditServlet.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"))报NumberFormatException;findByIdAndUserId()必须加AND user_id = ?条件,防止越权查看他人数据;- 表单中
value="${memo.title}"和checked的 EL 表达式,是回显的核心,新手常漏写导致编辑时字段空白。
4. 避坑指南:JSP + MySQL 备忘系统开发中 5 个高频翻车现场
4.1 现象:中文插入 MySQL 全变成问号(????)
原因:四个环节任一没设 UTF8MB4 —— MySQL 服务端字符集、数据库字符集、表字符集、JDBC URL 参数characterEncoding。常见是只改了数据库,忘了改my.cnf中的[mysqld]段collation-server=utf8mb4_unicode_ci和init-connect='SET NAMES utf8mb4'。
解决:执行SHOW VARIABLES LIKE 'character_set_%';和SHOW VARIABLES LIKE 'collation%';确认全为utf8mb4;JDBC URL 必须含characterEncoding=utf8mb4;建表语句显式写DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_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 href="DeleteServlet?id=123">发的是 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+ 驱动 jar(mysql-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.properties:
user.login=SELECT id, username FROM user WHERE username = ? AND password = ? memo.list=SELECT id, title, content, tags, status, create_time, update_time FROM memo WHERE user_id = ? ORDER BY update_time DESC memo.add=INSERT INTO memo(user_id, title, content, tags, status) VALUES (?, ?, ?, ?, ?) memo.update=UPDATE memo SET title = ?, content = ?, tags = ?, status = ?, update_time = NOW() WHERE id = ? AND user_id = ? memo.delete=DELETE 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 修改无需重新编译 Java;DBA 可直接审阅
.properties文件;同一 SQL 多处复用,改一处全生效。
5.3 用 Log4j2 替代e.printStackTrace(),让错误可追溯
System.out.println()和e.printStackTrace()在生产环境等于盲人摸象。引入log4j2.xml(放在src/main/resources/):
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <File name="FileAppender" fileName="logs/memo-app.log"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </File> </Appenders> <Loggers> <Root level="info"> <AppenderRef ref="FileAppender"/> </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 协议本质、关系型数据库约束力的最短路径。希望帮到你。
本文还有配套的精品资源,点击获取