
简介这套基于 JSP 的共享笔记系统毕业设计资料面向高校计算机相关专业学生用于完成课程设计或毕业设计选题。系统围绕笔记管理与共享展开除标签搜索外还提供用户管理、笔记公开、文本共享数据存储模块并通过权限控制机制细化访问粒度适合学习 JSP/Servlet、Hibernate、权限设计及前后端整合的开发者参考。压缩包共 414 个文件涵盖 Java/JSP 源码、配置文件、HTML/CSS/JS 页面资源、数据库 SQL 脚本以及设计报告文档其中 19 个 Java 类与 18 个 JSP 页面清晰呈现业务分层jar 包保证环境可运行数据库脚本便于快速还原初始数据整体约 25.85MB。已有 274 人浏览学习。资料内附完整项目代码、数据库及配套报告代码注释与目录结构相对完整可直接导入 IDE 运行验证也可按模块拆解学习为毕业设计答辩和二次开发提供落地参考。1. 为什么共享笔记系统还值得用 JSP 做一次在很多毕业设计清单里基于 JSP 的共享笔记系统常常被当成一个“凑数的老技术”但真把这个题目接住的人会发现共享笔记的难点不在写笔记而在“共享”谁能看、谁能改、分享链接有效期多长、以及多人同时打开时页面会不会串数据。用 JSP Servlet MySQL 实现这套逻辑正好能把权限控制和数据访问都放在服务端做前面再配 JSP 页面做简单的模板渲染整条链路短、依赖少答辩时反而容易讲清楚。这篇文章会按“数据建模 → Servlet 流程 → JSP 编译与页面细节 → 权限验证”的顺序把一份能跑通的方案拆给你看覆盖从建表到部署验证的完整闭环。2. 共享笔记的数据建模用户、笔记、共享权限怎么在 MySQL 里落地2.1 选型Eclipse Tomcat MySQL 的取舍理由做共享笔记系统的常见组合是 Eclipse / IDEA 配 Tomcat 8.5数据库用 MySQL 5.7JDK 用 1.8连接池用 c3p0 或 DBCP2。为什么不用 Spring Boot不是不能用而是毕业设计场景里JSP Servlet 这种“请求进来 → Servet 处理 → 转发到 JSP 渲染”的模式每一步都能在答辩时切到源码讲清楚不会像框架封装那样被追问底层时卡住。共享笔记的核心诉求是“多用户数据隔离 受控共享”所以表结构必须先定明白。很多同学直接把笔记表里加一个shared_with字段用逗号分隔用户 ID这是最常见的错误。后续要做过期时间、读写权限、查看谁分享了这篇笔记都会变得极其痛苦。正确做法是用独立的共享关系表。2.2 note 表与 share 表的设计字段、约束与索引用户表user没什么特殊关键是笔记表note和共享表share要拆开。笔记表只存笔记内容和归属人是否公开用is_public控制共享表记录“某篇笔记被共享给了谁、权限是什么、token 是什么、什么时候过期”。SQL 建表如下。CREATE TABLE user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password_md5 CHAR(32) NOT NULL, avatar VARCHAR(128), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE note ( note_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, title VARCHAR(128) NOT NULL, content TEXT, is_public TINYINT DEFAULT 0, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_note_user FOREIGN KEY (user_id) REFERENCES user(user_id) ); CREATE TABLE share ( share_id INT PRIMARY KEY AUTO_INCREMENT, note_id INT NOT NULL, owner_id INT NOT NULL, target_user_id INT, permission ENUM(read, write) DEFAULT read, share_token VARCHAR(32) NOT NULL, expires_at DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_share_note FOREIGN KEY (note_id) REFERENCES note(note_id), CONSTRAINT fk_share_owner FOREIGN KEY (owner_id) REFERENCES user(user_id) ); CREATE INDEX idx_share_token ON share(share_token); CREATE INDEX idx_note_user ON note(user_id);几个容易被忽略的参数说明share_token用 UUID 去掉横线后取前 32 位作为公开链接的唯一凭证expires_at允许为空为空表示永久有效permission用 ENUM 而不是 TINYINT是为了在代码里直接映射枚举避免数字代表含义写死。owner_id虽然在note.user_id里已经存在但共享表里冗余一份是为了方便做“我共享出去的所有笔记”这类反向查询不用每次 join note 表。索引加在share_token上因为共享链接访问是最频繁的查询路径。2.3 DAO 层封装用连接池管理共享访问的连接每个请求都要拿数据库连接的场景直接DriverManager.getConnection会频繁建立 TCP 连接Tomcat 下并发一高页面就卡。常见的做法是用 c3p0配置文件c3p0-config.xml里给连接池定最大连接数、超时时间。下面的DBUtil是所有 DAO 的入口。import com.mchange.v2.c3p0.ComboPooledDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class DBUtil { private static DataSource ds new ComboPooledDataSource(mysql); public static Connection getConnection() throws SQLException { return ds.getConnection(); } }ComboPooledDataSource(mysql)会从c3p0-config.xml中读取名为mysql的配置。连接池的三个关键参数initialPoolSize设置启动时建几个连接maxPoolSize决定峰值连接数checkoutTimeout控制拿不到连接时等多久。共享笔记系统里共享链接可能被同时打开很多次maxPoolSize建议设到 30checkoutTimeout设 3000 毫秒超过 3 秒直接报“连接获取超时”总比页面挂死强。NoteDAO 中addNote方法是最基础也最典型的写法注意is_public参数必须从布尔型转成 intPreparedStatement 的索引从 1 开始顺序与 SQL 里的?完全一致。public int addNote(Note note) { String sql INSERT INTO note(user_id, title, content, is_public) VALUES(?,?,?,?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setInt(1, note.getUserId()); ps.setString(2, note.getTitle()); ps.setString(3, note.getContent()); ps.setInt(4, note.isPublic() ? 1 : 0); ps.executeUpdate(); ResultSet rs ps.getGeneratedKeys(); return rs.next() ? rs.getInt(1) : -1; } catch (SQLException e) { throw new RuntimeException(保存笔记失败, e); } }这里用try-with-resources让连接、语句、结果集在方法结束后自动关闭避免连接泄漏。RETURN_GENERATED_KEYS让 MySQL 自增主键可以立刻回读后续做共享关系时直接拿这个noteId插入 share 表。3. 核心流程的 Servlet 实现编辑笔记、共享链接与 JSP 页面数据回显3.1 MVC 分工JSP、Servlet、JavaBean 各自的边界共享笔记系统里Servlet 只做三件事解析请求参数、调用 DAO、把结果放进 request 或 session。JSP 页面负责用 EL 表达式和 JSTL 标签把数据渲染成 HTML不要在 JSP 里写try-catch、不要做数据库操作、不要用% %包一大段业务代码。常见的问题是同学把查询用户信息的代码直接写进 JSP 页面顶部页面一多代码重复率极高改一个表名字段得把所有页面翻一遍。下面的 ShareServlet 处理“把笔记共享给指定用户”的请求。WebServlet(/share/add) public class ShareAddServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); HttpSession session req.getSession(); Integer userId (Integer) session.getAttribute(userId); if (userId null) { resp.sendRedirect(/login.jsp); return; } int noteId Integer.parseInt(req.getParameter(noteId)); String targetUsername req.getParameter(targetUsername); String permission req.getParameter(permission); String expireDays req.getParameter(expireDays); UserDAO userDAO new UserDAO(); Integer targetUserId userDAO.findIdByUsername(targetUsername); if (targetUserId null) { req.setAttribute(message, 目标用户不存在); req.getRequestDispatcher(/shareAdd.jsp).forward(req, resp); return; } NoteDAO noteDAO new NoteDAO(); Note note noteDAO.findById(noteId); if (note null || note.getUserId() ! userId) { resp.sendError(403, 只能共享自己的笔记); return; } String token UUID.randomUUID().toString().replace(-, ); ShareDAO shareDAO new ShareDAO(); shareDAO.addShare(noteId, userId, targetUserId, permission, token, Integer.parseInt(expireDays)); resp.sendRedirect(/note/view?id noteId); } }参数说明targetUsername是一次性输入的用户名后端再转成target_user_id前端不要传用户 ID否则别人改一下表单值就能把笔记共享给任意用户。expireDays以字符串接收在 DAO 层使用DATE_ADD(NOW(), INTERVAL ? DAY)生成过期时间。注意note.getUserId() ! userId是比较两个 int 的基本类型这里的细节是userId从 session 取出后可能是 null必须先判空再拆箱否则会抛 NullPointerException。3.2 noteForm.jsp 中编辑笔记的防越权控制编辑页面的 JSP 核心逻辑是只有笔记作者能看到“保存”按钮被共享的有写权限用户也能看到但只读用户只能看到内容。这里把当前登录用户 ID 从 session 中取出与笔记的user_id对比页面里用 JSTL 条件标签控制按钮显隐。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % div classnote-header h2${note.title}/h2 span classmeta最后更新${note.updateTime}/span c:if test${sessionScope.userId note.userId} a href/note/edit?id${note.noteId} classbtn-edit编辑/a /c:if c:if test${sessionScope.userId ! note.userId noteEditable} a href/note/edit?id${note.noteId} classbtn-edit协同编辑/a /c:if /divnoteEditable是 Servlet 根据 share 表的 permission 字段放入 request 的布尔值页面只读取结果不自己查表。这里最容易踩的坑用${sessionScope.userId ! note.userId}判断权限但这个写法在两者类型不一致时会永远为 true。正确做法是在 Servlet 里把userId转成 int再setAttribute到 request 中JSP 里只对比 request 属性不要在页面上做类型转换。3.3 共享链接的实现逻辑token 是无登录入口共享链接的定位是“无需登录拿到链接就能看”。所以校验不能依赖 session必须靠share_token。用户点击类似/share/view?tokenabc123的地址时ShareViewServlet 拿着 token 查 share 表查不到或已过期就返回 404否则把 note 内容放进 request 后转发到sharedNote.jsp。WebServlet(/share/view) public class ShareViewServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String token req.getParameter(token); if (token null || token.trim().isEmpty()) { resp.sendError(404); return; } ShareDAO shareDAO new ShareDAO(); SharedNote shared shareDAO.findByToken(token); if (shared null) { resp.sendError(404, 共享链接不存在); return; } if (shared.getExpiresAt() ! null shared.getExpiresAt().before(new Date())) { resp.sendError(404, 共享链接已过期); return; } req.setAttribute(shared, shared); req.getRequestDispatcher(/sharedNote.jsp).forward(req, resp); } }findByToken的 SQL 要 join note 表和 user 表一次性把笔记内容和作者名都查出来不要在 JSP 里再写一个 DAO 调用。过期时间判断放在数据库查询之后用 Java 的Date.before()比较注意expires_at为 null 时表示永久有效必须先判空。sharedNote.jsp页面顶部要显示作者、共享时间、过期时间同时提供一个只读标记隐藏所有表单按钮。这里不校验登录状态意味着任何人都能通过链接访问所以 token 的随机性就特别重要用 UUID 而不是自增 ID 能有效防遍历。3.4 JSP 页面中正确 import JSONArray 处理共享成员列表共享笔记系统里作者需要看到“谁有权限访问这篇笔记”。这个列表可以通过 JSP 页面顶部用% page importnet.sf.json.JSONArray %引入 JSON 工具类然后直接把 List 对象转成 JSON 字符串输出到隐藏域前端 JS 再解析。这种写法虽然不太优雅但在 JSP 为主的技术栈里是最直接的。% page importjava.util.List % % page importnet.sf.json.JSONArray % % page importcom.example.vo.SharedUser % % Integer currentUserId (Integer) session.getAttribute(userId); ListSharedUser sharedUsers new ShareDAO() .listSharedUsers((Integer) request.getAttribute(noteId), currentUserId); JSONArray jsonArray JSONArray.fromObject(sharedUsers); request.setAttribute(sharedUsersJson, jsonArray.toString()); % script const sharedUsers JSON.parse(${sharedUsersJson}); sharedUsers.forEach(function (item) { console.log(item.username - item.permission); }); /scriptJSONArray.fromObject会把 JavaBean 里的 getter 方法自动映射成 JSON 字段所以SharedUser类必须有getUsername()、getPermission()这些标准 getter否则输出会是空的{}。listSharedUsers方法里要 join share 表和 user 表按share.create_time倒序取username、permission、expires_at三个字段。注意 JSP 页面里写% %片段时page import必须在页面最顶部并且这段脚本不要放在 HTML body 中间否则会跟表格渲染交叉导致页面变形。json-lib 这个库依赖 commons-beanutils 和 commons-langTomcat 的 lib 目录和 WEB-INF/lib 都放一份最稳妥。4. 排除 JSP 编译与页面交互的两个高频问题4.1 JSP 编译后的 class 文件保存在哪里以及怎么查看JSP 不是直接运行的Tomcat 首次访问 jsp 文件时会先将其翻译成 Java Servlet 源码再编译成 class 文件。源码和 class 都存放在 Tomcat 的work目录下具体路径是work/Catalina/localhost/你的工程名/org/apache/jsp例如web项目配置tomcat后查看jsp编译后的java类时看到的是noteForm_jsp.java和noteForm_jsp.class。查找这个文件可以解决很多诡异问题页面报“变量找不到”直接打开noteForm_jsp.java看_jspService方法里到底生成了什么代码就知道是 EL 表达式写错还是 request 属性没传进来。修改 JSP 后 Tomcat 是否自动重新编译取决于web.xml里 JspServlet 的配置。开发阶段把development参数保持为true并设置checkInterval为 1 秒这样每次请求都会检查 JSP 是否有修改发现问题即时生效。上线前再把development改为false并把trimSpaces设为true可以去掉 JSP 指令和标签之间的多余空格减小响应体积。servlet servlet-namejsp/servlet-name servlet-classorg.apache.jasper.servlet.JspServlet/servlet-class init-param param-namedevelopment/param-name param-valuetrue/param-value /init-param init-param param-namecheckInterval/param-name param-value1/param-value /init-param init-param param-nametrimSpaces/param-name param-valuetrue/param-value /init-param /servlet如果修改 JSP 后刷新页面还是旧内容优先去work目录里找对应 jsp 文件的时间戳。时间戳没变说明 Tomcat 没有感知到文件变化重启 Tomcat 清一次 work 目录即可时间戳变了但页面还是旧内容那就是浏览器缓存加一次 CtrlF5 强制刷新。4.2 屏蔽 JSP 离开页面提示在个人信息展示页面里处理未保存编辑共享笔记系统中个人信息展示页面profileView.jsp会展示用户最近编辑的笔记列表点击后跳转到笔记编辑页。一个典型场景是用户在编辑页写了半篇笔记没有保存就点击其他链接或关闭标签页此时需要弹窗提示“离开后未保存内容将丢失”。用beforeunload事件能做到但问题在于表单正常提交时也会触发这个事件用户会看到一个多余的“确定要离开吗”的确认框。解决办法是给表单设置一个“已提交标记”。let formDirty false; let formSubmitted false; document.querySelector(#noteForm).addEventListener(input, function () { formDirty true; }); document.querySelector(#noteForm).addEventListener(submit, function () { formSubmitted true; }); window.addEventListener(beforeunload, function (event) { if (formSubmitted || !formDirty) { return; } event.preventDefault(); event.returnValue ; });formDirty在有输入事件时置为 true表示内容被修改过。formSubmitted在 submit 事件里置为 true表示用户已经主动提交保存。beforeunload里如果发现已经提交直接 return 不弹窗。这里有个细节event.returnValue 是一个历史遗留的标准写法Chrome 和 Firefox 都要求设置该属性才会弹窗只调用preventDefault()在某些版本上无效。在个人信息展示页面里点击“编辑”按钮是 location.href 跳转同样会触发beforeunload如果这篇笔记本身没有改动formDirty为 false不会打扰用户。4.3 JSP 页面里的集合为 null 时页面 500 的排查思路共享笔记系统中sharedUserList这类从 Servlet 传入的数据在 DAO 层可能返回 null 而不是空集合。比如某篇笔记还没有共享给任何人listSharedUsers返回了 nullJSP 里用c:forEach items${sharedUserList}遍历时JSTL 会抛javax.servlet.jsp.JspException导致整个页面 500。这个问题在共享列表为空时必现属于开发阶段不查数据就发现不了的典型坑。修复习惯是双保险DAO 层尽量返回Collections.emptyList()而不是 nullJSP 页面上用empty判断做一个兜底。c:choose c:when test${empty sharedUserList} p classno-data还没有共享给任何人点击右上角“共享”按钮添加协作者。/p /c:when c:otherwise ul c:forEach items${sharedUserList} varsu li${su.username}${su.permission}/li /c:forEach /ul /c:otherwise /c:choose${empty sharedUserList}对 null 和空列表都返回 true所以页面逻辑在两种情况下都会走到“暂无数据”分支。使用c:choose比c:if更适合这种二选一场景避免在标签里写反向条件。这类问题在 Tomcat 控制台通常只会显示一行javax.servlet.jsp.JspException: /sharedList.jsp(12,12), 打开对应 JSP 源码第 12 行的 items 属性排查对应 request 属性名是否拼写正确、Servlet 是否真的执行了setAttribute这两处不一致是最高发的原因而不是 JSTL 本身的问题。5. 用只读权限过滤器验证共享边界沉淀成可复测的步骤共享笔记的权限控制如果只靠 JSP 里隐藏按钮相当于只做了“视觉蒙层”别人直接 POST 请求form action/note/edit就能绕过。需要把权限判断集中在过滤器或 Serlvet 入口让 JSP 页面只管展示权限校验统一在服务端做。下面这个PermissionFilter拦截所有/note/edit和/note/delete请求先判断当前用户是不是笔记作者不是再看 share 表是否有 write 权限两者都不满足就返回 403。WebFilter(/note/*) public class PermissionFilter implements Filter { 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); Integer loginUserId (session null) ? null : (Integer) session.getAttribute(userId); if (loginUserId null) { response.sendRedirect(/login.jsp); return; } String uri request.getRequestURI(); if (uri.endsWith(/edit) || uri.endsWith(/delete)) { String noteIdParam request.getParameter(id); if (noteIdParam null) { response.sendError(400); return; } int noteId Integer.parseInt(noteIdParam); NoteDAO noteDAO new NoteDAO(); Note note noteDAO.findById(noteId); if (note null) { response.sendError(404); return; } boolean isOwner note.getUserId() loginUserId; boolean canWrite false; if (!isOwner) { ShareDAO shareDAO new ShareDAO(); canWrite shareDAO.hasPermission(noteId, loginUserId, write); } if (!isOwner !canWrite) { response.sendError(403, 无编辑权限); return; } } chain.doFilter(request, response); } }hasPermission的 SQL 要注意 expire 条件WHERE note_id? AND target_user_id? AND permissionwrite AND (expires_at IS NULL OR expires_at NOW())这样过期的写权限不会被放行。过滤器里用了request.getSession(false)第二个参数 false 表示 session 不存在时不自动创建避免静态资源访问也生成一堆无用 session。验证这套权限是否生效按照下面的操作顺序走一遍即可以笔记作者身份登录创建一篇笔记并共享给 test2 用户权限选择“只读”用普通浏览器未登录状态打开共享链接/share/view?tokentoken必须能看到笔记内容且没有编辑按钮再用 test2 账号登录系统直接访问/note/edit?id该笔记ID预期返回 403 状态码回到作者账号把共享权限改为“可编辑”test2 再次访问/note/edit?id该ID预期能正常加载编辑表单。最后用 Navicat 或命令行执行UPDATE share SET expires_at 2020-01-01 00:00:00 WHERE note_id该ID;刷新共享链接页面确认返回“共享链接已过期”然后把过期时间改回未来时间链接立刻恢复可用。这套流程能在答辩现场用两个浏览器加一条 SQL 完整演示权限模型比空讲设计图更有说服力。本文还有配套的精品资源点击获取