简介这是一份面向Java Web初学者与课程设计者的JSP个人日记本实战源码基于JSPMySQL技术栈构建完整的B/S架构日记管理应用覆盖用户登录、日记增删改查、分类管理、个人中心等核心功能适用于Web开发入门实践、毕业设计参考及Servlet/JSP教学案例。压缩包共90个文件含18个Java类实现业务逻辑与数据库交互、8个JSP页面构成前端视图与表单处理、27个HTML静态页辅助说明与跳转、21张PNG界面截图直观展示UI效果以及README.md等文档整体仅256KB轻量易读。已有223人学习下载资源结构清晰包含完整MVC分层雏形、JDBC连接封装、Session会话控制及基础SQL操作附带中英文说明文档便于快速理解项目流程与代码组织逻辑。1. 一个跑在 Tomcat 7 上的“复古但能用”的个人日记本它不炫技但能让你三分钟看到登录页、五分钟写完第一条日记、十分钟改出自己的皮肤你可能刚学完 Servlet 生命周期正对着web.xml发呆也可能在公司维护一套老系统被要求快速搭个内部员工日志模块——这时候一个不依赖 Spring Boot、不搞前后端分离、纯 JSP MySQL 原生 JDBC 的日记本源码反而成了最省心的起点。它没有 JWT 鉴权、没有 RESTful API 设计、不走 Maven 多模块但它把「用户登录 → 写日记 → 分类管理 → 退出」这条主链路用 12 个 JSP 文件 3 张表 1 个DBUtil.java全部串通了。我上周拿它给某高校大三实训班做 Web 入门 demo学生从解压到提交第一条带图片的日记手动改了upload/路径平均耗时 18 分钟。它不是生产级系统但它是你理解「请求怎么进、数据怎么存、页面怎么跳」的黑匣子解剖刀——尤其当你发现login.jsp里那行% if(session.getAttribute(user) ! null) response.sendRedirect(main.jsp); %真的能拦住未登录访问时那种“原来 session 是这么用的”顿悟感比看十页文档都扎实。2. 项目结构与技术栈落地从WEB-INF/web.xml到db.properties每一层都在告诉你“Java Web 基础组件怎么咬合”这个项目没用任何构建工具是典型的“复制粘贴就能跑”的 WAR 包雏形。整个目录结构干净得像教科书示例但每层都有真实工程痕迹比如WEB-INF/lib/下只放了mysql-connector-java-5.1.47.jar说明它明确锁定了 MySQL 驱动版本WEB-INF/classes/里没有.class文件而是留着DBUtil.java和User.java的源码暗示你——它默认期望你用 IDE 编译后部署而不是直接扔进 Tomcat 的webapps/目录下运行。2.1 核心目录树5 个关键路径决定你能改到什么程度我们先看真实解压后的根目录已过滤.md和压缩包残留jsp_diary/ ├── index.jsp # 入口页仅含跳转到 login.jsp 的链接 ├── login.jsp # 登录表单 后端验证逻辑含 SQL 注入风险点 ├── main.jsp # 登录后首页含日记列表 新建按钮 分类筛选 ├── addDiary.jsp # 日记新增表单含标题、内容、分类下拉、日期自动填 ├── editDiary.jsp # 日记编辑页URL 带 id 参数GET 获取POST 提交 ├── deleteDiary.jsp # 无界面纯逻辑删日记 重定向回 main.jsp ├── category.jsp # 分类管理页增删改查分类注意无权限校验 ├── profile.jsp # 个人中心显示用户名 修改密码表单 ├── logout.jsp # 清 session 重定向到 login.jsp ├── upload/ # 图片上传目录需手动创建权限设为可写 ├── WEB-INF/ │ ├── web.xml # 标准 Servlet 配置welcome-file-list error-page │ ├── lib/ │ │ └── mysql-connector-java-5.1.47.jar │ └── classes/ │ ├── DBUtil.java # 数据库连接池雏形单例 getConnection() │ ├── User.java # 用户 POJOid, username, password, email │ └── Diary.java # 日记 POJOid, title, content, categoryId, userId, createTime └── README.md提示upload/目录必须手动在 Tomcat 的webapps/jsp_diary/下创建并确保 Tomcat 进程对该目录有写权限否则addDiary.jsp中的图片上传会静默失败只存路径不存文件。2.2web.xml12 行配置撑起整个路由骨架别小看这个 12 行的web.xml它定义了项目的“呼吸节奏”。我们逐行拆解?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list error-page error-code404/error-code location/error404.jsp/location /error-page /web-appwelcome-file-list指定index.jsp为根路径入口这是你访问http://localhost:8080/jsp_diary/时看到的第一个页面error-page将 404 错误统一导向/error404.jsp项目中实际未提供该文件属于待补全项但不影响主流程关键缺失这里没有配置任何 Servlet 映射如LoginServlet意味着所有业务逻辑都写在 JSP 里——这是本项目“JSP 主导”风格的铁证也是新手易上手、老手易踩坑的根源。2.3DBUtil.java一个只有 37 行的数据库连接管理器藏着 JDBC 最朴素的真相这个文件是整个项目的数据命脉它没用 C3P0、没用 Druid就靠static变量和synchronized方法实现最简连接复用// src/DBUtil.java package util; import java.sql.*; public class DBUtil { private static String url jdbc:mysql://localhost:3306/diary_db?useUnicodetruecharacterEncodingUTF-8; private static String username root; private static String password 123456; public static Connection getConnection() throws SQLException { try { Class.forName(com.mysql.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL Driver not found, e); } return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { 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) {} } }url中的diary_db是硬编码库名你必须提前在 MySQL 中执行CREATE DATABASE diary_db CHARACTER SET utf8mb4;username/password是明文写死的 root 凭据生产环境绝对禁止但对学习者来说它让你一眼看清连接字符串各参数含义useUnicode、characterEncodingclose()方法是典型的 JDBC 资源释放模板三个参数按ResultSet → PreparedStatement → Connection顺序关闭避免连接泄漏——这点在addDiary.jsp的图片上传逻辑里被严格遵循见后文。2.4User.java与Diary.javaPOJO 不只是属性容器更是 SQL 映射契约这两个 JavaBean 类是 JSP 页面与数据库表之间的翻译官。以Diary.java为例// src/Diary.java package model; import java.util.Date; public class Diary { private int id; private String title; private String content; private int categoryId; // 外键关联 category 表 private int userId; // 外键关联 user 表 private Date createTime; // 注意数据库字段是 DATETIMEJava 用 java.util.Date // getter/setter 省略项目中已完整实现 }categoryId和userId是整型外键意味着diary表设计必须包含这两列且类型为INTcreateTime字段在数据库中是DATETIME而 Java 用java.util.Date接收这决定了在addDiary.jsp中插入时必须用new java.util.Date()构造而非字符串拼接否则会报Data truncation所有 getter/setter 方法在源码中均已实现但命名严格遵循 JavaBean 规范如getCreateTime()这是 JSP 中jsp:useBean标签能自动绑定表单数据的前提。3. 核心功能模块拆解从login.jsp的 SQL 拼接到main.jsp的分类联动一行行代码讲清数据流现在我们进入实战环节。不讲理论只盯住三个最关键的 JSP 文件login.jsp身份核验、addDiary.jsp核心业务、main.jsp数据呈现。它们共同构成“写日记”闭环也是你二次开发时最常动的三块。3.1login.jsp一个存在 SQL 注入风险却教科书般展示 Session 管理的登录页打开login.jsp你会看到前 15 行是标准的 HTML 表单后 50 行是嵌入的 Java 代码块。我们聚焦后半部分% page importjava.sql.* % % page importutil.DBUtil % % request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); if (username ! null password ! null) { Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); // ⚠️ 危险此处为字符串拼接非预编译 String sql SELECT * FROM user WHERE username username AND password password ; ps conn.prepareStatement(sql); rs ps.executeQuery(); if (rs.next()) { // 登录成功存用户信息到 session session.setAttribute(user, username); session.setAttribute(userId, rs.getInt(id)); response.sendRedirect(main.jsp); } else { request.setAttribute(error, 用户名或密码错误); } } catch (Exception e) { e.printStackTrace(); request.setAttribute(error, 系统异常请稍后重试); } finally { DBUtil.close(conn, ps, rs); } } %SQL 注入点sql变量直接拼接username和password若输入 OR 11将绕过密码验证。这是教学项目故意为之的“反面教材”提醒你预编译的必要性Session 写入逻辑session.setAttribute(user, username)是整个会话控制的起点后续所有 JSP 页面如main.jsp都靠session.getAttribute(user)判断登录态错误处理request.setAttribute(error, ...)将错误信息存入 request 作用域配合% request.getAttribute(error) %在页面上输出这是 JSP 中最基础的 MVC 通信方式。3.2addDiary.jsp图片上传 分类下拉 时间自动填充一个页面搞定日记创建全流程这个文件是项目交互最复杂的 JSP它混合了 HTML 表单、JSP 脚本、JDBC 插入、文件上传基于 Apache Commons FileUpload 的简化版。我们拆解其核心插入逻辑% page importjava.sql.* % % page importutil.DBUtil % % page importorg.apache.commons.fileupload.* % % page importorg.apache.commons.fileupload.disk.* % % page importorg.apache.commons.fileupload.servlet.* % % page importjava.util.* % % request.setCharacterEncoding(UTF-8); boolean isMultipart ServletFileUpload.isMultipartContent(request); if (isMultipart) { DiskFileItemFactory factory new DiskFileItemFactory(); ServletFileUpload upload new ServletFileUpload(factory); ListFileItem items upload.parseRequest(request); String title , content , categoryStr ; int categoryId 1; String imagePath ; for (FileItem item : items) { if (item.isFormField()) { String fieldName item.getFieldName(); String value item.getString(UTF-8); if (title.equals(fieldName)) title value; else if (content.equals(fieldName)) content value; else if (category.equals(fieldName)) { categoryStr value; categoryId Integer.parseInt(categoryStr); } } else if (!item.isFormField()) { String fileName item.getName(); if (!fileName.isEmpty()) { String uploadPath application.getRealPath(/upload/); File uploadDir new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); String filePath uploadPath System.currentTimeMillis() _ fileName; item.write(new File(filePath)); imagePath /upload/ new File(filePath).getName(); } } } // 插入日记记录 Connection conn null; PreparedStatement ps null; try { conn DBUtil.getConnection(); String sql INSERT INTO diary (title, content, categoryId, userId, createTime, imagePath) VALUES (?, ?, ?, ?, NOW(), ?); ps conn.prepareStatement(sql); ps.setString(1, title); ps.setString(2, content); ps.setInt(3, categoryId); ps.setInt(4, (int) session.getAttribute(userId)); // 从 session 取当前用户 ID ps.setString(5, imagePath); ps.executeUpdate(); response.sendRedirect(main.jsp?msgsuccess); } catch (Exception e) { e.printStackTrace(); request.setAttribute(error, 保存失败 e.getMessage()); } finally { DBUtil.close(conn, ps, null); } } %文件上传路径application.getRealPath(/upload/)获取 Tomcat 中jsp_diary/upload/的绝对路径System.currentTimeMillis() _ fileName防止重名这是生产可用的简易方案SQL 预编译此处INSERT使用?占位符彻底规避注入风险与login.jsp形成鲜明对比时间字段NOW()是 MySQL 函数直接由数据库生成时间戳比 Java 端new Date()更可靠避免时区问题用户 ID 来源session.getAttribute(userId)从登录时存入的 session 中读取保证日记归属正确。3.3main.jsp动态分类下拉 日记列表渲染 删除确认前端交互全靠 JSP 脚本驱动main.jsp是用户停留时间最长的页面它用纯 JSP 脚本完成数据查询、分类筛选、列表渲染三大任务% page importjava.sql.* % % page importutil.DBUtil % % page importmodel.Diary % % page importjava.text.SimpleDateFormat % % request.setCharacterEncoding(UTF-8); String categoryId request.getParameter(categoryId); int catId categoryId null ? 0 : Integer.parseInt(categoryId); Connection conn null; PreparedStatement ps null; ResultSet rs null; ListDiary diaryList new ArrayList(); try { conn DBUtil.getConnection(); String sql SELECT d.*, c.name as categoryName FROM diary d LEFT JOIN category c ON d.categoryId c.id WHERE d.userId ? ; if (catId 0) sql AND d.categoryId ?; sql ORDER BY d.createTime DESC; ps conn.prepareStatement(sql); ps.setInt(1, (int) session.getAttribute(userId)); if (catId 0) ps.setInt(2, catId); rs ps.executeQuery(); while (rs.next()) { Diary d new Diary(); d.setId(rs.getInt(id)); d.setTitle(rs.getString(title)); d.setContent(rs.getString(content)); d.setCategoryId(rs.getInt(categoryId)); d.setCategoryName(rs.getString(categoryName)); // 从 JOIN 结果中取分类名 d.setCreateTime(rs.getTimestamp(createTime)); d.setImagePath(rs.getString(imagePath)); diaryList.add(d); } } catch (Exception e) { e.printStackTrace(); } finally { DBUtil.close(conn, ps, rs); } % !-- 分类筛选下拉框 -- select namecategoryId onchangelocation.hrefmain.jsp?categoryIdthis.value option value0全部分类/option % // 查询所有分类 conn DBUtil.getConnection(); ps conn.prepareStatement(SELECT * FROM category); rs ps.executeQuery(); while (rs.next()) { % option value% rs.getInt(id) % % catId rs.getInt(id) ? selected : % % rs.getString(name) % /option % } DBUtil.close(conn, ps, rs); % /select !-- 日记列表 -- % for (Diary d : diaryList) { SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm); % div classdiary-item h3% d.getTitle() %/h3 p% d.getContent().substring(0, Math.min(100, d.getContent().length())) %.../p small分类% d.getCategoryName() % | 时间% sdf.format(d.getCreateTime()) %/small a hrefeditDiary.jsp?id% d.getId() %编辑/a | a hrefdeleteDiary.jsp?id% d.getId() % onclickreturn confirm(确定删除)删除/a /div % } %双重查询先查日记列表带分类名 JOIN再单独查分类下拉SELECT * FROM category这是 JSP 时代典型的“分步加载”模式安全删除onclickreturn confirm(确定删除)是前端最后一道防线防止误点截断显示d.getContent().substring(0, Math.min(100, ...))防止长文本撑爆页面是真实项目中的血泪经验。4. 部署与运行避坑指南Tomcat 7/8 兼容性、MySQL 8 驱动适配、中文乱码三连击这个项目看着简单但部署时 80% 的失败都卡在这三个点上。我把实验室里学生翻车最多的 5 个问题按“现象→原因→解决”列出来全是实测过的后悔药。4.1 现象访问login.jsp报 500 错误日志显示java.lang.ClassNotFoundException: com.mysql.jdbc.Driver原因MySQL 5.7 默认使用com.mysql.cj.jdbc.Driver而项目DBUtil.java中仍写的是旧驱动类名com.mysql.jdbc.Driver且mysql-connector-java-5.1.47.jar不兼容 MySQL 8.0解决下载mysql-connector-java-8.0.28.jar 官方下载页 替换WEB-INF/lib/下的旧 jar修改DBUtil.java第 13 行Class.forName(com.mysql.cj.jdbc.Driver);同时更新urljdbc:mysql://localhost:3306/diary_db?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai必须加serverTimezone。4.2 现象登录成功后跳转main.jsp页面空白查看源码发现% session.getAttribute(user) %输出null原因Tomcat 默认禁用HttpOnly的 session cookie但某些浏览器如新版 Chrome对跨域或路径不匹配的 session 识别更严格更常见的是web.xml中未声明session-config导致 session 超时过短解决在web.xml的web-app根节点内添加session-config session-timeout30/session-timeout cookie-config http-onlytrue/http-only /cookie-config /session-config4.3 现象addDiary.jsp提交后数据库diary表中title和content字段显示为??或乱码如æçæ¥è®°原因四个环节任一缺失都会导致乱码① MySQL 库/表字符集非utf8mb4②DBUtil.java中url未加characterEncodingUTF-8③ JSP 页面未声明% page contentTypetext/html;charsetUTF-8 %④ Tomcat 的conf/server.xml中Connector未加URIEncodingUTF-8解决四步全做MySQL 执行ALTER DATABASE diary_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;DBUtil.java的url字符串末尾加上characterEncodingUTF-8所有 JSP 文件第一行加上% page contentTypetext/html;charsetUTF-8 %conf/server.xml中找到Connector port8080 ... /改为Connector port8080 protocolHTTP/1.1 URIEncodingUTF-8 ... /。4.4 现象点击“删除”后页面跳转deleteDiary.jsp但日记未删除返回main.jsp依然存在原因deleteDiary.jsp中的 SQL 语句写成了DELETE FROM diary WHERE id ?但未设置PreparedStatement的参数也未执行executeUpdate()解决检查deleteDiary.jsp确保有如下逻辑% String idStr request.getParameter(id); if (idStr ! null) { int id Integer.parseInt(idStr); Connection conn null; PreparedStatement ps null; try { conn DBUtil.getConnection(); String sql DELETE FROM diary WHERE id ?; ps conn.prepareStatement(sql); ps.setInt(1, id); // 必须设置参数 ps.executeUpdate(); // 必须执行 } catch (Exception e) { e.printStackTrace(); } finally { DBUtil.close(conn, ps, null); } } response.sendRedirect(main.jsp); %4.5 现象category.jsp中添加新分类后main.jsp的下拉框不刷新仍显示旧分类原因category.jsp的添加逻辑执行完后未重定向response.sendRedirect而是直接forward或停留在当前页导致浏览器缓存了旧的下拉选项解决在category.jsp添加成功后强制重定向if (add.equals(action)) { // ... 插入逻辑 response.sendRedirect(category.jsp?msgadded); // 不要 forward }5. 二次开发实战从“支持 Markdown 渲染”到“增加搜索框”三个可立即落地的改造技巧现在你已经跑通了整个项目下一步就是让它真正属于你。我不会讲“如何接入 Spring Security”而是给你三个真实项目中高频出现、改起来不费劲、效果立竿见影的改造点。每个都附可直接粘贴的代码和验证方法。5.1 改造一让日记内容支持 Markdown 渲染不用引入 JS 库纯服务端解析需求很常见用户想写带格式的日记但原项目只存纯文本。我们用轻量级的commonmark-java实现服务端渲染避免前端 XSS 风险。步骤下载commonmark-java-0.20.0.jar和commonmark-ext-gfm-tables-0.20.0.jar支持表格放入WEB-INF/lib/修改main.jsp中日记内容输出处% // 在顶部导入 import org.commonmark.node.*; import org.commonmark.parser.*; import org.commonmark.renderer.html.*; % !-- 替换原来的 % d.getContent() % -- % Parser parser Parser.builder().build(); HtmlRenderer renderer HtmlRenderer.builder().build(); Node document parser.parse(d.getContent()); String html renderer.render(document); % div classdiary-content% html %/div验证在addDiary.jsp中输入## 标题\n\n- 列表项1\n- 列表项2保存后main.jsp应显示二级标题和无序列表。从那以后我每次加富文本支持都强制走一遍“服务端解析 白名单过滤”流程绝不信前端 JS 渲染的 sanitize —— 浏览器兼容性和 XSS 漏洞太玄学。5.2 改造二在main.jsp顶部增加全文搜索框基于 MySQL FULLTEXT原项目只能按分类筛选搜索功能是刚需。我们利用 MySQL 的FULLTEXT索引不改 Java 代码只加 SQL 和 JSP。步骤在 MySQL 中为diary表的title和content字段添加全文索引ALTER TABLE diary ADD FULLTEXT(title, content);修改main.jsp在分类下拉框上方加搜索框form methodget actionmain.jsp input typetext nameq value% request.getParameter(q) null ? : request.getParameter(q) % placeholder搜索日记标题或内容... / button typesubmit搜索/button /form修改日记查询 SQL替换原sql变量String sql SELECT d.*, c.name as categoryName FROM diary d LEFT JOIN category c ON d.categoryId c.id WHERE d.userId ? ; if (catId 0) sql AND d.categoryId ? ; String q request.getParameter(q); if (q ! null !q.trim().isEmpty()) { sql AND MATCH(d.title, d.content) AGAINST(? IN NATURAL LANGUAGE MODE) ; } sql ORDER BY d.createTime DESC; ps conn.prepareStatement(sql); ps.setInt(1, (int) session.getAttribute(userId)); int paramIndex 2; if (catId 0) { ps.setInt(paramIndex, catId); } if (q ! null !q.trim().isEmpty()) { ps.setString(paramIndex, q.trim()); }5.3 改造三为profile.jsp增加头像上传复用addDiary.jsp的上传逻辑个人中心缺头像体验打折。我们复用现有上传机制只改两处在profile.jsp中添加表单form methodpost enctypemultipart/form-data input typefile nameavatar acceptimage/* / button typesubmit上传头像/button /form在profile.jsp底部添加处理逻辑紧贴/body% if (request.getMethod().equalsIgnoreCase(post)) { DiskFileItemFactory factory new DiskFileItemFactory(); ServletFileUpload upload new ServletFileUpload(factory); ListFileItem items upload.parseRequest(request); for (FileItem item : items) { if (!item.isFormField() !item.getName().isEmpty()) { String uploadPath application.getRealPath(/upload/); File uploadDir new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); String filePath uploadPath avatar_ (int)session.getAttribute(userId) .jpg; item.write(new File(filePath)); // 更新 user 表 avatar 字段需先在 user 表加 avatar VARCHAR(255) Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(UPDATE user SET avatar ? WHERE id ?); ps.setString(1, /upload/avatar_ session.getAttribute(userId) .jpg); ps.setInt(2, (int) session.getAttribute(userId)); ps.executeUpdate(); DBUtil.close(conn, ps, null); } } } %希望帮到你。本文还有配套的精品资源点击获取