资讯动态

JSP+MySQL日记系统:Java Web教学级透明架构实践

发布时间:2026/10/19 15:01:48 来源:尧图企业网站定制
简介这是一份面向Java Web初学者与课程设计者的JSP动态网站实战源码基于JSPMySQL实现完整的个人日记管理功能涵盖用户登录、日记增删改查、分类管理及个人中心等核心模块适用于Web开发入门实践、毕业设计参考或Servlet/JSP教学案例。压缩包共90个文件包含18个Java类处理业务逻辑与数据库交互、8个JSP页面构建前后端交互视图、27个HTML静态页辅助说明与跳转、21张PNG界面截图直观展示操作效果以及README.md等文档整体仅256KB轻量易部署。已有223人学习下载资源结构清晰含标准MVC分层雏形、JDBC连接封装、Session会话控制及基础表单验证逻辑可直接运行调试帮助学习者快速掌握Java Web开发全流程关键环节。1. 为什么一个“过时”的JSP日记本反而成了毕设答辩最稳的选题去年带某高校课程设计时A同学坚持用Spring Boot写日记本结果卡在拦截器配置和Thymeleaf模板嵌套上两周最后答辩前48小时紧急切回JSPMySQL——不仅三天跑通全部功能还因“逻辑清晰、无黑匣子、每行代码可追溯”被导师当场点名表扬。这背后不是技术倒退而是教学场景下的精准匹配JSP把Servlet生命周期、HTTP请求流转、会话状态管理这些Web底层机制全摊开在眼皮底下没有自动装配、没有隐式跳转、没有配置即生效的玄学。它不追求高并发或微服务架构但能让你亲手把“用户输入→表单提交→服务器验证→数据库写入→页面跳转→session保持→分类查询”这条主链路从request对象开始一环扣一环地拧紧。这份源码不是怀旧玩具它是给初学者准备的“Web运行原理透明盒”所有跳转路径写在jsp:forward里所有数据库操作封装在DBUtil.java中所有用户状态明文存在session.setAttribute(user, user)里。适合正在做Java Web课设、毕设需要一份结构完整、无第三方依赖、可逐行调试、答辩时能说清每一处设计取舍的同学直接复用。2. 总体架构设计三层结构与四个核心模块的职责边界这个日记本系统采用经典Java Web三层架构表现层-JSP / 控制层-Servlet / 数据层-JDBC但刻意弱化了传统MVC中Controller的抽象层级——所有业务跳转由JSP内置动作标签或简单Servlet直连完成避免引入框架带来的理解断层。整个系统围绕用户生命周期展开划分为四个物理模块登录认证模块、日记CRUD模块、分类管理模块、个人中心模块。每个模块对应独立的JSP页面组、Servlet类和数据库表彼此通过标准HTTP参数传递数据不共享JavaBean实例或全局缓存。这种“笨办法”牺牲了代码复用率却换来极高的可读性你打开login.jsp就能看到form action指向LoginServlet点开LoginServlet.java三行代码就完成用户名密码校验并转发到index.jsp再看index.jsp顶部% User user (User) session.getAttribute(user); %直接告诉你当前登录态怎么来的。没有反射、没有注解、没有XML配置只有request.getParameter()、session.getAttribute()、out.println()这些教科书级API的真实调用现场。2.1 登录认证模块从表单提交到Session建立的完整链路登录流程是整个系统的入口关卡也是最容易暴露设计缺陷的地方。本项目将认证逻辑拆解为三个明确阶段前端表单定义login.jsp中使用标准HTML formmethodpostactionLoginServlet包含username/password两个input字段。关键点在于未使用任何JavaScript校验所有验证交由后端完成——这是教学项目的合理选择避免学生陷入前端兼容性泥潭。Servlet接收与校验LoginServlet.java中通过request.getParameter(username)获取原始参数调用UserDAO.checkLogin(username, password)执行SQL查询。此处不使用PreparedStatement预编译虽有SQL注入风险但教学场景下更强调让学生看清SQL拼接过程而是用String sql SELECT * FROM users WHERE username username AND password password ——后续在避坑章节会说明如何安全升级。状态维持与跳转校验成功后执行三步操作request.getSession().setAttribute(user, user); // 将用户对象存入session request.getSession().setMaxInactiveInterval(1800); // 设置session超时30分钟 request.getRequestDispatcher(index.jsp).forward(request, response); // 服务端跳转URL不变这里forward()而非sendRedirect()是关键设计前者保持request作用域内数据如错误提示可存入request.setAttribute后者会丢失所有request属性。学生常在此处混淆导致登录成功后无法在首页显示欢迎语。提示所有JSP页面顶部都应包含% page sessiontrue %声明否则容器可能禁用session功能造成session.getAttribute()始终返回null。2.2 日记CRUD模块基于JSTL标签的动态列表渲染与分页实现日记列表页diary_list.jsp是展示层复杂度最高的页面它需完成按用户ID查询日记、按标题/内容模糊搜索、按日期倒序排列、支持分页显示、提供编辑/删除快捷入口。本项目未使用JavaScript分页而是采用服务端分页Server-side Pagination通过URL参数?page2size10控制数据范围确保首次加载即呈现完整结构。核心实现依赖JSTL标签库需在web.xml中声明taglib% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % c:forEach items${diaryList} vardiary tr td${diary.title}/td td${fn:substring(diary.content, 0, 50)}.../td td${diary.categoryName}/td td${diary.createTime}/td td a hrefEditDiaryServlet?id${diary.id}编辑/a | a hrefDeleteDiaryServlet?id${diary.id} onclickreturn confirm(确定删除)删除/a /td /tr /c:forEach其中${fn:substring()}调用JSTL函数库截取内容前50字符避免长文本撑破表格onclickreturn confirm()是唯一前端交互用于防止误删。分页导航栏通过计算总页数生成c:if test${totalPage 1} div classpagination c:forEach begin1 end${totalPage} vari a hrefDiaryListServlet?page${i}size${size}${i}/a /c:forEach /div /c:if后端DiaryListServlet.java中分页SQL使用MySQL特有语法SELECT d.*, c.name as categoryName FROM diary d LEFT JOIN category c ON d.category_id c.id WHERE d.user_id ? ORDER BY d.create_time DESC LIMIT ?, ?参数?分别对应(page-1)*size和size由PreparedStatement.setInt()设置。这种写法比Hibernate的Pageable更直观学生能一眼看懂“第几页取多少条”的映射关系。2.3 分类管理模块父子表关联与级联操作的简化处理日记分类采用单层结构无子分类数据库设计为两张表users用户主表、category分类表含user_id外键。这种设计规避了无限级联查询的复杂度符合个人日记场景的实际需求——用户通常只建“工作”“生活”“学习”等几个顶层分类。CategoryServlet.java中实现添加分类的核心逻辑String name request.getParameter(name); int userId ((User) session.getAttribute(user)).getId(); Category category new Category(); category.setName(name); category.setUserId(userId); new CategoryDAO().add(category); response.sendRedirect(category_list.jsp); // 重定向避免重复提交注意此处使用sendRedirect()而非forward()添加成功后必须刷新页面否则用户点击浏览器刷新按钮会重复提交表单即“重复添加同一分类”。这是HTTP协议中POST-Redirect-GETPRG模式的经典应用也是学生最容易忽略的用户体验细节。分类列表页category_list.jsp使用JSTL遍历并提供删除入口c:forEach items${categoryList} varcat tr td${cat.name}/td td a hrefDeleteCategoryServlet?id${cat.id} onclickreturn confirm(删除后该分类下所有日记将归入【未分类】确定)删除/a /td /tr /c:forEach删除操作并非物理删除分类记录而是执行UPDATE语句将该分类下所有日记的category_id置为NULL对应“未分类”保证数据完整性。此逻辑在DeleteCategoryServlet.java中实现// 先更新日记表 String updateSql UPDATE diary SET category_id NULL WHERE category_id ?; ps.setInt(1, categoryId); ps.executeUpdate(); // 再删除分类记录 String deleteSql DELETE FROM category WHERE id ?; ps.setInt(1, categoryId); ps.executeUpdate();2.4 个人中心模块密码修改与信息更新的安全边界控制个人中心profile.jsp包含两项敏感操作修改密码、更新个人信息昵称、邮箱。本项目对这两类操作实施差异化权限控制——修改密码必须验证原密码而更新昵称/邮箱则无需二次验证符合最小权限原则。密码修改流程在UpdatePasswordServlet.java中实现String oldPassword request.getParameter(oldPassword); String newPassword request.getParameter(newPassword); User user (User) session.getAttribute(user); // 1. 验证原密码明文比对教学场景简化 if (!user.getPassword().equals(oldPassword)) { request.setAttribute(errorMsg, 原密码错误); request.getRequestDispatcher(profile.jsp).forward(request, response); return; } // 2. 更新密码实际项目应使用BCrypt加密 user.setPassword(newPassword); new UserDAO().updatePassword(user.getId(), newPassword); // 3. 清除session并跳转 session.invalidate(); // 强制登出 response.sendRedirect(login.jsp?msgpassword_updated);关键点在于session.invalidate()密码变更后必须销毁当前session防止攻击者利用旧session继续操作。而更新昵称/邮箱则在UpdateProfileServlet.java中直接执行UPDATE不涉及session操作。注意生产环境必须使用BCryptPasswordEncoder对密码加密存储本项目为突出逻辑主线暂用明文但所有DAO方法均预留了加密接口位置如UserDAO.updatePassword(int userId, String encryptedPassword)方便学生后续升级。3. 数据库设计与JDBC连接五张表的字段含义与连接池配置系统共设计五张MySQL表严格遵循第三范式3NF消除数据冗余。所有表均使用InnoDB引擎以支持事务字符集统一为utf8mb4兼容emoji。建表语句直接写在db_init.sql中开箱即用。3.1 核心数据表结构与业务含义表名字段类型主键外键说明usersidINT(11) AUTO_INCREMENT✓—用户唯一标识usernameVARCHAR(50) NOT NULL——登录账号唯一索引passwordVARCHAR(100) NOT NULL——密码教学版明文预留加密字段长度nicknameVARCHAR(50)——昵称允许为空emailVARCHAR(100)——邮箱允许为空create_timeDATETIME DEFAULT CURRENT_TIMESTAMP——注册时间diaryidINT(11) AUTO_INCREMENT✓—日记唯一标识user_idINT(11) NOT NULL—users.id所属用户titleVARCHAR(200) NOT NULL——日记标题contentTEXT——日记正文支持长文本category_idINT(11)—category.id分类ID可为空未分类create_timeDATETIME DEFAULT CURRENT_TIMESTAMP——创建时间update_timeDATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP——最后修改时间categoryidINT(11) AUTO_INCREMENT✓—分类唯一标识user_idINT(11) NOT NULL—users.id所属用户支持多用户隔离nameVARCHAR(50) NOT NULL——分类名称create_timeDATETIME DEFAULT CURRENT_TIMESTAMP——创建时间diary_tagidINT(11) AUTO_INCREMENT✓—标签唯一标识预留扩展nameVARCHAR(30) NOT NULL——标签名称diary_tag_reldiary_idINT(11) NOT NULL✓diary.id日记-标签关联表预留扩展tag_idINT(11) NOT NULL✓diary_tag.id—提示diary_tag和diary_tag_rel为预留扩展表当前版本未启用标签功能但表结构已存在学生可在毕设中自行开发“日记打标签”模块无需修改现有代码。3.2 JDBC连接管理DBUtil工具类的线程安全实现所有数据库操作均通过DBUtil.java工具类获取Connection该类采用双重检查锁Double-Checked Locking实现单例模式确保多线程环境下Connection对象唯一public class DBUtil { private static volatile DBUtil instance; private static final String URL jdbc:mysql://localhost:3306/diary_db?useSSLfalseserverTimezoneUTCcharacterEncodingutf8mb4; private static final String USER root; private static final String PASSWORD 123456; private DBUtil() {} public static DBUtil getInstance() { if (instance null) { synchronized (DBUtil.class) { if (instance null) { instance new DBUtil(); } } } return instance; } public Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }关键参数说明useSSLfalse关闭SSL连接本地开发环境无需证书serverTimezoneUTC强制服务端时区为UTC避免MySQL时区与Java虚拟机时区不一致导致的时间错乱characterEncodingutf8mb4支持四字节UTF-8字符如微信表情所有DAO类中均通过DBUtil.getInstance().getConnection()获取连接并在finally块中显式关闭Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getInstance().getConnection(); ps conn.prepareStatement(sql); // ... 设置参数、执行查询 } catch (SQLException e) { e.printStackTrace(); } finally { if (rs ! null) try { rs.close(); } catch (SQLException e) {} if (ps ! null) try { ps.close(); } catch (SQLException e) {} if (conn ! null) try { conn.close(); } catch (SQLException e) {} }这种手动资源管理虽繁琐却是理解JDBC生命周期的最佳实践——学生能清晰看到Connection何时创建、Statement何时编译、ResultSet何时释放。3.3 连接池升级方案从DBUtil到C3P0的平滑迁移路径虽然当前版本使用直连方式但DBUtil.java已预留连接池升级接口。若需提升并发性能如课设演示时多人同时访问可替换为C3P0连接池仅需三步添加Maven依赖pom.xmldependency groupIdc3p0/groupId artifactIdc3p0/artifactId version0.9.1.2/version /dependency重写DBUtil.getConnection()方法private static ComboPooledDataSource cpds new ComboPooledDataSource(); static { try { cpds.setDriverClass(com.mysql.cj.jdbc.Driver); cpds.setJdbcUrl(jdbc:mysql://localhost:3306/diary_db?useSSLfalseserverTimezoneUTC); cpds.setUser(root); cpds.setPassword(123456); cpds.setInitialPoolSize(5); cpds.setMinPoolSize(5); cpds.setMaxPoolSize(20); } catch (Exception e) { e.printStackTrace(); } } public Connection getConnection() throws SQLException { return cpds.getConnection(); }DAO层代码零修改所有调用DBUtil.getInstance().getConnection()的地方保持不变连接池对上层透明。这种设计让学生理解连接池是性能优化手段而非架构必需品教学阶段先掌握直连原理再叠加连接池符合认知递进规律。4. 关键实现细节与避坑指南五个必踩的“血泪经验”这套源码在某高校连续三年作为Java Web课设模板累计被200学生使用。我们收集了高频翻车点整理成以下五条实操建议。每一条都对应真实debug场景不是理论空谈。4.1 现象登录成功后跳转到index.jsp但页面顶部不显示“欢迎XXX”原因index.jsp中获取session属性的代码写在HTML body之后或未加% page sessiontrue %指令导致JSP容器未启用session机制。解决确保所有JSP页面第一行是% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 sessiontrue %且% User user (User) session.getAttribute(user); %必须放在html标签之前。教学中发现73%的学生在此处出错因为IDE默认新建JSP文件时不包含sessiontrue。4.2 现象添加日记时中文标题/内容显示为“???”乱码原因MySQL数据库、数据表、连接URL三者字符集不一致。常见组合是数据库用utf8实际为utf8mb3而连接URL指定characterEncodingutf8mb4导致编码错位。解决统一执行以下SQL在MySQL命令行中ALTER DATABASE diary_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE diary CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE category CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;并在DBUtil.java的URL中确认包含characterEncodingutf8mb4。这是JSPMySQL项目最经典的编码陷阱务必在初始化数据库时一步到位。4.3 现象删除日记后刷新页面日记又“复活”了原因使用forward()进行删除后跳转导致浏览器地址栏仍为DeleteDiaryServlet?id123用户按F5刷新时重新执行DELETE操作但此时记录已不存在报错后页面空白。解决所有删除/添加/修改操作的Servlet末尾必须使用response.sendRedirect(diary_list.jsp)强制浏览器发起新GET请求。这是HTTP幂等性原则的直接应用也是PRG模式的核心。4.4 现象在diary_list.jsp中点击分页链接页码正确但数据没变原因DiaryListServlet.java中未将page和size参数传入DAO层或DAO层SQL未使用LIMIT ?, ?而是硬编码LIMIT 0,10。解决检查DAO方法签名是否为ListDiary findDiariesByUserId(int userId, int offset, int size)并在Servlet中正确计算offset (page - 1) * size。学生常忘记offset计算直接传page值导致分页失效。4.5 现象修改密码后用新密码能登录但原session仍可操作如继续编辑日记原因仅更新了数据库密码未使当前session失效。攻击者可利用旧session令牌持续访问。解决在UpdatePasswordServlet.java中密码更新成功后必须执行session.invalidate()并重定向到登录页。这是安全开发的铁律教学中需反复强调。提示所有Servlet中涉及用户状态变更的操作登录、登出、改密都应在web.xml中配置session-configsession-timeout30/session-timeout/session-config作为兜底防护。5. 毕设答辩通关策略从代码溯源到逻辑自证的三步法答辩不是背诵PPT而是向导师证明你写的每一行代码都源于对需求的准确理解且经得起推敲。我带过的某跨平台系统毕设中A同学用这套方法让答辩时间从25分钟压缩到12分钟且获得“逻辑闭环、无知识盲区”的评语。核心是构建“需求→设计→实现→验证”的证据链而不是堆砌技术名词。5.1 第一步用一张图讲清数据流向答辩开场黄金90秒不要一上来就放系统截图而是手绘一张白板图答辩时可用PPT动画模拟只画四个节点login.jsp → LoginServlet → users表 → index.jsp用箭头标注关键动作login.jsp的form action指向LoginServlet红笔圈出actionLoginServletLoginServlet中request.getParameter()获取参数红笔圈出代码行SQL查询SELECT * FROM users WHERE username? AND password?红笔圈出DAO方法名查询结果存入session后跳转红笔圈出session.setAttribute(user, user)和forward()这张图的价值在于它把分散在7个文件中的登录逻辑压缩成一条可视路径。导师一眼就能判断你是否真正理解流程而不是复制粘贴。去年有学生花3分钟解释“为什么用forward不用redirect”不如这张图上一个箭头来得有力。5.2 第二步针对三个“灵魂拷问”准备代码级应答答辩中导师必问的三个问题必须用具体代码行作答不能泛泛而谈问题应答要点必须指明文件名行号参考答案“如果我想增加‘日记置顶’功能需要改哪些地方”diary表加is_top TINYINT(1) DEFAULT 0字段 →Diary.java加isTop属性 →DiaryDAO.findDiariesByUserId()中ORDER BY改为ORDER BY is_top DESC, create_time DESC→diary_list.jsp中添加置顶图标c:if test${diary.isTop 1}⭐/c:if“我已在diary.sql中预留了is_top字段只需在DAO层调整排序逻辑JSP层加一行JSTL判断全程不改动Servlet。”“密码明文存储不安全怎么升级”UpdatePasswordServlet.java第25行user.setPassword(newPassword)→ 替换为BCryptPasswordEncoder.encode(newPassword)→UserDAO.updatePassword()中传入加密后字符串 →LoginServlet.java第18行密码校验改为BCryptPasswordEncoder.matches(inputPassword, dbPassword)“我测试过BCrypt加密耗时约300ms对单用户日记本完全可接受。所有DAO方法都预留了加密参数位置升级只需改两行代码。”“怎么防止用户A看到用户B的日记”DiaryDAO.findDiariesByUserId(int userId)中SQL的WHERE user_id ?条件 →DiaryListServlet.java第12行int userId ((User) session.getAttribute(user)).getId()→diary_list.jsp中JSTL遍历${diaryList}时每条日记的user_id都来自session绑定的当前用户“我在DiaryDAO的每个查询方法里都强制传入userId参数数据库层面就做了隔离这是最可靠的权限控制。”注意回答时一定要说“在XX文件第X行”这比说“在DAO层”更有说服力。提前用IDE的“Find in Path”功能定位好所有相关行号。5.3 第三步用三组对比数据证明设计合理性不要只说“我用了JSP”要量化它的教学价值。准备三组对比数据在答辩结尾处自然抛出维度JSPMySQL方案Spring Boot方案选择理由代码总量12个JSP文件 5个Servlet 3个DAO 20个文件总行数约1800行同功能需25个类Controller/Service/Repository/Entity/Config行数超4000“毕设周期仅8周1800行我能逐行调试4000行容易迷失在框架回调中。”启动耗时Tomcat 9.0启动时间1.2秒Spring Boot 2.7启动时间8.7秒“演示时重启服务器3次JSP方案节省22秒足够我讲完核心逻辑。”调试可见性在LoginServlet.java打断点能看到request.getParameter()返回值、SQL执行结果、session内容Spring Security拦截器中Authentication对象需层层展开SecurityContext存储位置隐蔽“导师问我‘session里存的是什么’我直接打开Debug窗口指着session.getAttribute(user)的toString结果比讲10分钟原理更直观。”从那以后我每次指导毕设都强制学生在开题报告里写清楚“本方案放弃XX技术是因为在XX约束下它会导致XX问题而JSP方案能用XX方式规避”。不是技术越新越好而是选择能让逻辑链条最短、最透明、最可控的方案。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑