
简介基于JSP和MySQL实现的个人记事备忘系统源码面向Java Web初学者、课程设计或毕业设计场景。系统以在线记事管理为核心支持笔记的新增、修改、删除与查询前端通过JSP生成动态页面后端由Servlet处理请求并使用JDBC访问MySQL完成数据持久化整体技术链路完整便于学习Java Web基础开发。压缩包共374个文件约27.51MB除9个JSP页面和24个Java类外还包含26个HTML页面、86个JS脚本、28个CSS样式表以及大量PNG、GIF等图片资源另有XML配置、JAR库、属性文件与使用文档适合导入Tomcat与MySQL环境后直接运行分析。目前已有109人学习/下载。资源梳理了JSP语法、Servlet生命周期、MVC模式、SQL防注入、事务处理等要点源码目录清晰可帮助读者快速理解记事类项目的分层结构与部署方式也便于二次开发或答辩演示。建议结合源码阅读JSP与Servlet的请求流转重点关注数据库连接与预编译SQL的编写方式便于快速迁移到同类项目中。1. 为什么现在还要用JSPMySQL做一个个人记事备忘系统如果你打开招聘网站Java Web 岗位的要求里早就看不到 JSP 的身影了前后端分离和 Spring Boot 是绝对主流。但真到落地场景——课程设计、毕业设计、公司内部工具、临时管理后台你反而会发现 JSP 和 MySQL 的组合依然有生命力它没有 Node 模块的版本地狱不要求 Docker 编排一台 2G 内存的云服务器加一个 Tomcat 就能跑起来。个人记事备忘系统是这个组合里最典型的 CRUD 蓝图用户注册登录、记事增删改查、按时间检索、分页展示如果把这张图吃透换成订单管理、留言板、简单 CMS 只是改表名的事。这篇文章要做的就是把这套从 JSP 到 MySQL 的完整链路讲清楚包括表结构怎么设计、JDBC 怎么写才不容易踩坑、源码打包成 War 部署到 Tomcat 的每一步以及几个只有写多了才会注意到的细节。适合正在做课设的在校生也适合需要快速搭内部小工具的工程师。2. 数据模型先行个人记事系统的表结构与JDBC连接设计写 JSP 项目最忌讳上来就写页面。页面只是数据的投影先把表建对后面至少省一半的返工时间。个人记事系统虽然简单但涉及用户体系表结构至少要两张表再加上索引、字符集、时间字段的处理方式这是整个系统的地基。2.1 用户表与记事表的字段设计先定义一个最小但完整的 schema。用户表存账号信息记事表存用户的内容条目两表通过 user_id 关联。字段类型直接决定后续查询体验和扩展空间。CREATE TABLE t_user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password CHAR(64) NOT NULL COMMENT SHA-256散列后的密文, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_note ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, title VARCHAR(100) NOT NULL DEFAULT , content TEXT, is_deleted TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_create (user_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明password 字段用 CHAR(64) 而不是 VARCHAR(32)原因是 SHA-256 输出的十六进制正好是 64 字符长度固定检索时对索引更友好。明文密码是绝对红线后续服务端要做散列后入库。is_deleted 采用软删除标记而不是物理 DELETE个人系统虽然无所谓但养成这个习惯后迁移到企业项目不用改思路。所有列表查询默认加上is_deleted0。联合索引idx_user_create专门服务“某个用户的记事按时间倒序”这个最频繁的查询场景避免 Using filesort。提示MySQL 8.0 默认字符集已是 utf8mb4但如果你用的 5.7 或更早版本建库时务必显式指定否则 emoji 存不进去报错信息是不明不白的 Incorrect string value。2.1.1 为什么不用外键和存储过程很多人建表时习惯顺手加上 FOREIGN KEY个人项目里我从不建议这么做。外键约束在删除父表记录时要先检查子表加了 ON DELETE RESTRICT 之后改数据非常别扭更关键的是业务层已经通过 user_id 强制关联外键带来的性能损耗在个人记事这种量级毫无必要。存储过程也一样JSP 技术栈的核心是 Java把业务逻辑写在存储过程里会让调试变得困难你在 Navicat 里测试没问题一调 Java 就报错定位问题要两边来回查还不如用 JDBC 预编译语句来得直白。另外提一个容易被忽略的点表名和字段名尽量统一用下划线命名Java 实体类用驼峰。MyBatis 有自动映射但这里的 JDBC 方案是手写 Bean反正都要手动 set不如让数据库列名和实体字段名建立清晰可记忆的对应关系。比如 t_note 表的 create_time对应 Note 类的 createTime一行代码就能读出来不用写注释。2.2 JDBC连接池的选择DBCP vs C3P0 vs HikariCPJSP 项目最常见的初学者错误是每次请求都DriverManager.getConnection()用完不关。MySQL 默认最多 151 个连接一旦在线用户超过这个数新连接直接报 Too many connections。解决办法是用连接池。老牌选择是 DBCPApache Commons 系和 C3P0Hibernate 时代常用但 2024 年我建议直接用 HikariCPSpring Boot 2.x 默认内置就是它性能在同类池里是公认的第一梯队而且 jar 包只有一个体积约 100KB特别适合 JSP 项目的手动 lib 管理模式。在 WEB-INF/lib 下放好 hikaricp-x.y.z.jar 和 slf4j-api 后写一个配置类。import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; public class DbPool { private static HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/note_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai); config.setUsername(root); config.setPassword(your_password); config.setDriverClassName(com.mysql.cj.jdbc.Driver); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setConnectionTimeout(30000); dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }参数说明useUnicodetruecharacterEncodingutf8保证中文字符写入读取不乱码。这是 JSP 中文乱码问题的最常见根因之一。useSSLfalse本地开发不会有 SSL 证书不关掉会刷一大堆 warning 日志。serverTimezoneAsia/ShanghaiMySQL 8.x 的驱动强制要求指定时区不写会报 CST 时区识别异常。我一般把池大小设成 20个人记事系统并发不会很高5 个核心连接保底足够了。如果以后要接短信服务或者爬虫任务再按需调大不必一开始就堆性能。2.3 字符集与时区两个看不见的坑JSP 页面顶部要写% page contentTypetext/html;charsetUTF-8 languagejava %Servlet 里request.setCharacterEncoding(UTF-8)数据库连接串里指定characterEncodingutf8三个地方一个都不能少。少了任何一个你可能会遇到“页面乱码但数据库没问题”“数据库乱码但页面正常”这类诡异现象本质就是编码链路在某一环断裂了。时区的坑则藏得更深。如果数据库的 create_time 用的是 DEFAULT CURRENT_TIMESTAMP而 JVM 默认时区和 MySQL 会话时区不一致查出来的时间会和实际相差 8 小时。解决方法是统一走 Asia/Shanghai即连接串里设置 serverTimezone同时 Tomcat 启动参数加-Duser.timezoneGMT08:00。两者一致后时间字段就不要在 Java 里做任何手动拼接了直接用new Date()传给setTimestamp()即可。3. 从登录到CRUDJSPServlet核心实现数据模型就绪后进入业务层。JSP 项目的经典结构是 Model 1 或者轻量 Model 2这里是后者Servlet 接收请求、调用 DAO、分发到 JSP 页面展示。不用框架用原生 Servlet 把流程走通你会对 HTTP 请求生命周期有一个更扎实的体感。3.1 登录与Session拦截登录是所有业务页面的入口。用户提交用户名和密码Servlet 校验通过后把用户对象放进 Session然后重定向到主页。这里的关键是密码的校验逻辑数据库里存的是 SHA-256 散列值前端的明文密码在 Servlet 里先用 MessageDigest 转成散列再拿去查库不能直接拼 SQL 字符串。WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String rawPassword request.getParameter(password); // SHA-256 散列加固定盐避免彩虹表直接命中 String hashed DigestUtil.sha256(rawPassword note_app_salt); User user UserDao.findByUsernameAndPassword(username, hashed); if (user ! null) { request.getSession().setAttribute(currentUser, user); response.sendRedirect(request.getContextPath() /note/list); } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }逻辑说明散列时加盐是一个值得养成的习惯即使是个人项目也不要裸存 SHA-256。盐值可以硬编码在服务端进阶做法是每个用户随机生成盐。登录成功后使用sendRedirect而不是forward这样可以避免表单重复提交用户刷新页面时不会弹出“确认重新提交表单”的提示。getRequestDispatcher(/login.jsp).forward()是转发给 JSP 渲染错误信息注意这里是服务端跳转地址栏不会变。再配一个简单的拦截过滤器防止未登录用户直接访问列表页WebFilter(/note/*) public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) res; if (request.getSession().getAttribute(currentUser) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, res); } }JSP 内置对象这里用上了三个request、response、session。JSP 页面里还可以用${sessionScope.currentUser.username}这种 EL 表达式直接取用户名减少% %脚本片段的使用。3.2 记事的增删改查记事本的核心是 NoteDao它承担所有 SQL 交互。增、删、改、查各一个方法注意删除这里执行的是 UPDATE 软删除。public class NoteDao { // 查询某用户的所有记事按创建时间倒序 public ListNote findByUserId(int userId) throws SQLException { String sql SELECT id, title, content, create_time, update_time FROM t_note WHERE user_id? AND is_deleted0 ORDER BY create_time DESC; ListNote list new ArrayList(); try (Connection conn DbPool.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, userId); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Note note new Note(); note.setId(rs.getInt(id)); note.setTitle(rs.getString(title)); note.setContent(rs.getString(content)); note.setCreateTime(rs.getTimestamp(create_time)); note.setUpdateTime(rs.getTimestamp(update_time)); list.add(note); } } } return list; } // 新增记事 public boolean insert(Note note) throws SQLException { String sql INSERT INTO t_note(user_id, title, content) VALUES(?,?,?); try (Connection conn DbPool.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, note.getUserId()); ps.setString(2, note.getTitle()); ps.setString(3, note.getContent()); return ps.executeUpdate() 0; } } // 软删除 public boolean softDelete(int id, int userId) throws SQLException { String sql UPDATE t_note SET is_deleted1 WHERE id? AND user_id?; try (Connection conn DbPool.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); ps.setInt(2, userId); return ps.executeUpdate() 0; } } }方法SQL特征参数说明findByUserId条件查询 排序WHERE 中的 user_id 用?占位避免 SQL 注入insert无返回值关注影响行数只插入 user_id、title、contentcreate_time 交给数据库默认值softDeleteUPDATE 而非 DELETE同时用 id 和 user_id 双重条件防止横向越权删别人数据PreparedStatement的?占位符机制是 JSP 项目里最值得强调的点。字符串拼接 SQL 的最大问题不在性能而在于用户输入里带着单引号时 SQL 直接被截断篡改比如 content 里存一句 OR 11拼接后整个条件恒真数据就泄露了。预编译语句把参数通过协议传给数据库参数永远只是参数不参与 SQL 解析这是成本最低的防注入手段。3.3 JSP页面与EL表达式JSP 页面的核心职责是展示不要在页面里写大段 Java 脚本片段。列表页用 JSTL 标签遍历集合% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table c:forEach varnote items${noteList} tr td${note.title}/td td${note.createTime}/td tda href${pageContext.request.contextPath}/note/edit?id${note.id}编辑/a/td /tr /c:forEach /table${note.title}对应的是 Note 类的 getTitle() 方法EL 表达式自动调用 getter。如果页面显示的是空字符串而不是 null检查实体类有没有写 getter这是一个很容易被忽略的低级错误。JSP 编译成 Servlet 的过程对开发者是透明的但你要理解这个映射关系调 bug 时才会往这个方向想。pageContext.request.contextPath是动态获取项目部署路径的写法。比如项目部署名为 note_app这个值就是/note_app所有链接都要加上这个前缀否则直接把项目改名部署后资源路径全部 404。4. 源码打包与部署从项目目录到可运行War包源码写完后要交付最常见的形式是打包成 War 文件。标题里“源码打包”四个字实际操作起来包括三个层面项目目录结构规范化、构建产物验证、以及部署到 Tomcat 的具体操作。这里用 Maven 构建最常见但也要考虑纯手工 javac 编译的备选方案。4.1 项目目录结构与Maven打包一个标准的 JSPServlet 项目即使不用 Spring 全家桶目录也要遵守 Servlet 规范。Maven 项目下的结构是note-app/ ├── pom.xml ├── src/main/java # Java 源码 │ ├── com/note/servlet/ # Servlet 类 │ ├── com/note/dao/ # 数据访问层 │ ├── com/note/model/ # 实体类 │ └── com/note/util/ # 工具类 ├── src/main/resources/ # 配置文件 ├── src/main/webapp/ # Web 根目录 │ ├── WEB-INF/ │ │ ├── web.xml # 部署描述符 │ │ └── lib/ # 第三方 jar 包 │ ├── login.jsp │ ├── noteList.jsp │ └── css/ └── target/pom.xml 里要按 Servlet 版本匹配依赖。Tomcat 9 对应 Servlet 4.0Tomcat 10 是 Jakarta EE 9 的命名空间变化包名从 javax.servlet 改成 jakarta.servlet。个人项目建议锁定 Tomcat 9 javax.servlet因为网上搜到的资料几乎都是 javax 前缀的遇到问题好查。dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version4.0.3/version /dependency /dependenciesscopeprovided表示 Servlet 容器自带这些类Tomcat 的 lib 目录里已经有 servlet-api.jar打包时不需要再包含进去否则可能出现类冲突。打包命令是mvn clean package产物在 target/note-app.war。如果不用 Maven可以手工javac编译后把 class 文件放 WEB-INF/classes再把整个 webapp 目录用jar cvf打成 War但依赖的 jar 包要手动核清楚容易遗漏建议还是 Maven。4.2 Tomcat部署与数据初始化把 War 包拷到 Tomcat 的 webapps 目录下启动 Tomcat 后它会自动解压部署。用命令行启动时观察两个关键日志。# Linux 或 macOS 启动 catalina.sh run # Windows 启动 catalina.bat run # 部署后访问地址 # http://localhost:8080/note-app/catalina.sh run是前台模式日志直接输出到终端看错误最方便。确认无异常后可以改用startup.sh后台运行。首次部署前手动在 MySQL 里建库建表或者用 JDBC 执行 schema.sql。我习惯把建表语句放在项目的 resources 目录里上线前手动执行一次不会写自动化迁移脚本因为这种体量的项目引入 Flyway 属于过度设计。数据库初始化命令mysql -uroot -p src/main/resources/schema.sql4.3 部署后验证与常见报错排查部署完要验证的不只是首页能打开而是三个关键链路都通登录链路页面能进、数据链路增删改查生效、静态资源链路CSS 加载正常。错误现象日志关键字原因与修复访问 404Catalina: Deploying web application archive访问地址少了项目上下文路径检查 /note-app 前缀JSP 改了不生效无明显错误IDEA 里热部署没开清 target 目录重新构建或浏览器缓存CtrlF5 强刷ClassNotFoundExceptionClassNotFound缺 jar 包检查 WEB-INF/lib 或 Maven 依赖是否引入数据库连接失败Access denied for user连接串里用户名密码错误或 MySQL 未配置远程访问中文乱码无页面 charset、请求编码、连接串 characterEncoding 三处逐一排查提示JSP 首次请求会编译成 class 文件Tomcat 默认开启 JSP 修改后的自动重编译但如果服务器的 current time 比文件修改时间早会出现“改了不生效”的诡异问题执行ant或重新部署前用touch同步一下文件时间即可。5. 让个人记事系统真正好用的三个进阶技巧前面的内容覆盖了基本 CRUD但一个“能用”和“好用”的记事系统之间有明显的体验鸿沟。这里有三个投入产出比最高的优化点按优先级排序依次是全文搜索、数据导出、连接池监控。5.1 标题搜索与LIKE优化记事多了之后按标题过滤是刚需。列表页加一个搜索输入框DAO 里加一个模糊查询方法public ListNote searchByTitle(int userId, String keyword) throws SQLException { String sql SELECT id, title, content, create_time FROM t_note WHERE user_id? AND is_deleted0 AND title LIKE ? ORDER BY create_time DESC LIMIT 100; try (Connection conn DbPool.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, userId); ps.setString(2, % keyword %); // 执行查询 } }LIKE 前导通配符%keyword%走不了索引全表扫描。个人数据量在几千条时没有问题但如果哪天导入了一万条以上你会发现这个查询开始变慢。两个方向一是只用keyword%前缀匹配来支持索引二是引入全文索引或搜索引擎但这对 JSP 项目来说过重了。接受前导通配符的性能损耗加上 LIMIT 100 防止结果集过大是性价比最高的取舍。5.2 记事内容导出为 ExcelJSP 页面无法直接生成 Excel要用 Servlet 动态输出。常见做法是借助 Apache POI 生成.xlsx文件然后通过 response 的输出流推送给浏览器。WebServlet(/note/export) public class ExportServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType(application/vnd.ms-excel); response.setHeader(Content-Disposition, attachment; filenamenotes_ System.currentTimeMillis() .xlsx); try (Workbook workbook new XSSFWorkbook()) { Sheet sheet workbook.createSheet(记事); // 创建表头行、遍历数据写入单元格 workbook.write(response.getOutputStream()); } } }字段含义Content-Disposition里的attachment告诉浏览器这是下载行为而非页面跳转filename指定下载后的文件名。POI 的 XSSFWorkbook 对应 2007 版本格式HSSFWorkbook 是老的 .xls 格式新项目直接用 XSSF 就行。导出操作会占用较多内存如果导出数据量超过一万行建议先在前端点击时做二次确认按钮防止多次重复点击把 Tomcat 内存打满。5.3 连接池监控与慢查询日志HikariCP 提供了一个轻量的监控指标入口HikariDataSource暴露了getHikariPoolMXBean()方法可以拿到当前活跃连接数、空闲连接数、等待获取连接的线程数。做一个管理页面定时刷新这些数据能直观看到连接池是否有泄漏。MySQL 端开启慢查询日志是另一个廉价有效的监控手段# 在 MySQL 控制台执行 SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SHOW VARIABLES LIKE slow_query_log%;long_query_time1表示超过 1 秒的 SQL 会被记录到日志文件。个人记事系统的数据量很难跑出慢查询但本地开发时把阈值设低一点偶尔能看到某个 SQL 走了全表扫描拿出来 EXPLAIN 分析很快能养成看执行计划的习惯。这个小习惯会在你转向任何其他数据库时持续产生收益。本文还有配套的精品资源点击获取