
简介这是一份面向Java Web初学者与课程设计者的JSP动态网站实战源码基于JSPMySQL实现完整的个人日记管理功能涵盖用户登录、日记增删改查、分类管理及个人中心等核心模块适用于高校Web开发实训、毕业设计参考或Servlet/JSP技术入门实践。压缩包共90个文件包含18个Java后端逻辑类如Servlet控制器与DAO层、8个JSP页面登录页、日记列表、编辑表单等视图、27个HTML静态模板与21张PNG界面素材辅以6份Markdown说明文档含中英文README整体仅256KB轻量易读。目前已有223人学习下载资源结构清晰采用近似MVC分层组织含JDBC数据库连接、Session会话管理、表单验证与基础SQL操作等关键实现细节可直接部署运行是理解Java Web基础架构与前后端交互流程的典型教学案例。1. JSP个人日记本为什么一个“过时”的技术栈反而成了理解Web底层最干净的教科书你可能第一眼看到“JSP MySQL”就下意识划走——这不就是十年前高校实验课的标配连IDEA都默认不带JSP支持了Tomcat 10干脆把JSP API移出核心包。但恰恰是这份看似落伍的jsp_diary源码藏着现代Web框架里早已被封装到黑匣子深处的原始脉搏HTTP请求如何一帧一帧流经容器、Java代码怎样在HTML里“呼吸”、session到底存在哪、SQL查询怎么从字符串变成数据库里的真实行记录。它没用Spring Boot自动装配没用MyBatis动态代理所有链路都赤裸裸摊开在.jsp、.java和.sql文件里。某高校实验室曾用它做Web原理课实验学生第一次亲手改完login.jsp里那行request.getParameter(pwd)再在UserServlet.java里打断点看到密码明文进来的那一刻才真正信了“表单提交不是魔法”。这不是怀旧而是把Web开发从“调API”拉回“造轮子”的临界点——当你能徒手复现一次登录态校验全过程Vue的v-model、React的useState、甚至Next.js的Server Actions就再也不是玄学。2. JSP执行机制从.jsp文件到_login_jsp.classTomcat到底做了什么JSP从来不是“直接运行”的脚本而是一套编译时生成Servlet的约定。这份日记本源码里所有.jsp文件如login.jsp、diary_list.jsp在首次访问时都会被Tomcat的Jasper引擎翻译成标准Java Servlet类再编译成字节码。这个过程暴露了Web容器最原始的契约HTTP请求必须落地为Java对象响应必须包装成HTTP报文。2.1 JSP编译流程三步拆解login.jsp的“变形记”以login.jsp为例其核心逻辑是接收表单参数、调用DAO验证用户、跳转到主页或返回错误。Tomcat处理它的真实路径如下解析阶段Jasper读取login.jsp识别% %中的Java代码块、% %中的表达式、% page %指令将静态HTML内容转为out.write()调用动态Java代码原样保留生成Servlet源码输出类似org.apache.jsp.login_jsp.java的Java文件结构为public final class login_jsp extends org.apache.jasper.runtime.HttpJspBase { public void _jspService(HttpServletRequest request, HttpServletResponse response) { // 1. 静态HTML转为out.write() out.write(htmlbody); // 2. 表达式转为out.print() out.print( request.getParameter(msg) ); // 3. 脚本片段原样嵌入 String username request.getParameter(username); if (username ! null !username.trim().isEmpty()) { // ... DAO调用逻辑 } } }编译与加载javac编译该Java文件为login_jsp.class由JasperLoader加载进JVM后续请求直接调用_jspService()方法。提示你可以在Tomcat的work/Catalina/localhost/jsp_diary/org/apache/jsp/目录下找到这些生成的.java和.class文件——这是理解JSP本质的唯一实证入口。删掉它下次访问login.jsp会触发重新编译。2.2page指令背后的容器契约isThreadSafe与session的真相login.jsp顶部常有这行% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 sessiontrue isThreadSafefalse %sessiontrue并非“开启session”而是声明当前JSP需要访问HttpSession对象。Tomcat据此在_jspService()开头插入HttpSession session request.getSession(true);若设为false则session变量不可用编译报错isThreadSafefalse是关键陷阱它告诉容器此JSP生成的Servlet不是线程安全的容器会为每个请求创建独立实例实际是通过同步_jspService方法实现。而设为true默认值时容器复用同一实例但要求开发者自行保证成员变量线程安全——这正是早期JSP项目高并发下数据错乱的根源。2.3 EL表达式与JSTL为什么${user.name}比% user.getName() %更安全日记本源码中diary_list.jsp大量使用${diary.title}而非% diary.getTitle() %。这不是语法糖而是两套完全不同的执行模型% %在_jspService()中直接调用Java方法异常会中断整个页面渲染如diary为null时抛NullPointerException${}由EL Resolver按作用域page→request→session→application逐层查找自动处理null值输出空字符串且支持.操作符链式调用${user.profile.avatar}。JSTL标签库如c:if test${not empty msg}进一步将控制逻辑从Java代码中剥离其本质是TagHandler接口实现类在_jspService()中被编译为new IfTag().doStartTag()调用——这正是MVC中View层“去逻辑化”的雏形。3. JDBC直连MySQL没有ORM遮蔽的数据库交互全链路这份源码未使用Hibernate或MyBatis所有数据库操作都基于原生JDBC。这意味着每一步——从驱动加载、连接获取、SQL拼接、参数绑定到结果集遍历——都必须手动编码。这种“笨办法”恰恰是理解数据持久层本质的必经之路。3.1DBUtil.java一个50行工具类道尽连接池之前的生存法则src/utils/DBUtil.java是整个项目的数据库心脏其核心逻辑只有三段public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/diary_db?useSSLfalseserverTimezoneUTC; private static final String USER root; private static final String PASSWORD 123456; public static Connection getConnection() throws SQLException { // 1. 显式加载驱动JDBC 4.0后可省略但源码保留 try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL Driver not found, e); } // 2. 每次请求新建Connection无连接池 return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { // 3. 严格按rs→ps→conn顺序关闭ResultSet依赖StatementStatement依赖Connection 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参数深意useSSLfalse关闭SSL开发环境简化serverTimezoneUTC强制时区对齐避免java.sql.SQLException: The server time zone value XXX is unrecognized这类经典翻车无连接池的代价每次getConnection()都建立TCP新连接MySQL默认最大连接数151高并发下直接Too many connections——这正是后来Druid/HikariCP诞生的原始驱动力。3.2UserDAO.javaPreparedStatement防注入的底层实现用户登录验证逻辑在src/dao/UserDAO.java中public User findUserByUsernameAndPassword(String username, String password) { String sql SELECT id, username, email FROM users WHERE username ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); // 参数1绑定username ps.setString(2, password); // 参数2绑定password try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return new User(rs.getInt(id), rs.getString(username), rs.getString(email)); } } } catch (SQLException e) { e.printStackTrace(); } return null; }?占位符的本质PreparedStatement将SQL语句发送给MySQL服务端预编译setString()只传输参数值服务端绝不会将参数值拼进SQL字符串。即使username传入 OR 11最终执行的仍是WHERE username ?问号处的值被当作纯数据处理对比Statement的灾难若用Statement stmt conn.createStatement(); stmt.executeQuery(SELECT ... WHERE username username )恶意输入会直接拼成WHERE username OR 11绕过所有校验。3.3 事务边界DiaryDAO.java中save()方法的手动控制日记保存需同时写入diary主表和diary_tag关联表源码用Connection.setAutoCommit(false)显式管理事务public boolean save(Diary diary, ListString tags) { String sql1 INSERT INTO diary(title, content, create_time) VALUES(?, ?, ?); String sql2 INSERT INTO diary_tag(diary_id, tag_name) VALUES(?, ?); try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); // 关闭自动提交 try (PreparedStatement ps1 conn.prepareStatement(sql1, Statement.RETURN_GENERATED_KEYS)) { ps1.setString(1, diary.getTitle()); ps1.setString(2, diary.getContent()); ps1.setTimestamp(3, new Timestamp(diary.getCreateTime().getTime())); int affected ps1.executeUpdate(); if (affected 0) throw new SQLException(Insert diary failed); // 获取自增主键 try (ResultSet rs ps1.getGeneratedKeys()) { if (rs.next()) { int diaryId rs.getInt(1); // 批量插入标签 try (PreparedStatement ps2 conn.prepareStatement(sql2)) { for (String tag : tags) { ps2.setInt(1, diaryId); ps2.setString(2, tag); ps2.addBatch(); } ps2.executeBatch(); } } } conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { conn.rollback(); // 任一环节失败则回滚 throw e; } } catch (SQLException e) { e.printStackTrace(); return false; } }RETURN_GENERATED_KEYS的妙用ps1.executeUpdate()后通过ps1.getGeneratedKeys()获取刚插入行的自增ID这是跨表关联的基石addBatch()的性能逻辑将多条INSERT合并为单次网络请求避免N次往返——但注意MySQL默认rewriteBatchedStatementsfalse需在JDBC URL中显式添加rewriteBatchedStatementstrue才能真正生效。4. Session会话管理用户状态如何在无状态HTTP上“粘住”HTTP协议天生无状态而日记本需要记住“谁正在写日记”。源码用HttpSession实现但它的运作远非session.setAttribute(user, user)这么简单——背后是Cookie、内存存储、超时策略的精密配合。4.1 Session ID的传递从JSESSIONIDCookie到URL重写用户登录成功后LoginServlet.java中HttpSession session request.getSession(); // 创建或获取session session.setAttribute(user, user); // 存储用户对象 response.sendRedirect(main.jsp); // 重定向到主页此时Tomcat做了三件事生成唯一sessionId如2F7C9E3A1B5D8F0C4E6A9B2D7F0C4E6A将sessionId写入响应头Set-Cookie: JSESSIONID2F7C9E3A...; Path/; HttpOnly在服务器内存中创建StandardSession对象以sessionId为key存储user等属性。注意若客户端禁用CookieTomcat会自动启用URL重写——在response.sendRedirect(main.jsp)生成的Location头中URL变为main.jsp;jsessionid2F7C9E3A...。你可在浏览器开发者工具Network面板中观察Set-Cookie头是否存在。4.2 Session存储位置内存、文件、数据库的三种实现源码默认使用Tomcat内置的StandardManager将Session对象存于JVM堆内存。但生产环境必须考虑集群部署问题若用户请求被负载均衡分发到不同Tomcat节点内存Session无法共享解决方案对比方案实现方式优点缺点PersistentManagerSession序列化到本地磁盘文件进程重启不丢失磁盘IO瓶颈不支持集群RedisStore需扩展Session存入Redis高性能、天然支持集群需额外部署Redis增加运维复杂度JDBCStoreSession存入MySQL表利用现有数据库数据库成为单点瓶颈日记本源码未涉及这些但web.xml中session-config配置已埋下伏笔session-config session-timeout30/session-timeout !-- 单位分钟 -- cookie-http-onlytrue/cookie-http-only !-- 防XSS窃取 -- tracking-modeCOOKIE/tracking-mode !-- 可选URL、COOKIE、SSL -- /session-config4.3 Session失效的四种触发条件Session并非永生以下任一情况即失效超时session-timeout30/session-timeout配置后30分钟内无请求则销毁主动失效调用session.invalidate()如LogoutServlet.java中容器重启内存Session全部丢失除非配置了持久化JVM崩溃进程退出内存清空。提示session.getAttribute(user)返回null时不能简单认为“用户未登录”必须结合request.getRequestedSessionId()是否为空、request.isRequestedSessionIdValid()是否为true综合判断——这是很多新手排查登录态丢失的盲区。5. 常见问题排查十个血泪经验总结出的JSPMySQL踩坑清单这份源码虽小但覆盖了Java Web开发的典型故障场景。以下是某开发者在部署调试过程中记录的真实问题按“现象→原因→解决”结构整理每一条都对应源码中的具体文件和配置。5.1 现象访问login.jsp报错HTTP Status 500 – java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver原因MySQL 8.0驱动类名从com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver而DBUtil.java中Class.forName()硬编码了旧类名或mysql-connector-java.jar未放入WEB-INF/lib/目录。解决修改DBUtil.java第12行Class.forName(com.mysql.cj.jdbc.Driver);下载mysql-connector-java-8.0.33.jar复制到WEB-INF/lib/重启Tomcat热部署不生效因驱动需在容器启动时加载。5.2 现象登录成功后跳转main.jsp页面显示java.lang.NullPointerException在% session.getAttribute(user).getUsername() %原因LoginServlet.java中session.setAttribute(user, user)执行前user对象为null如数据库查无此人导致存入null值后续main.jsp直接调用getUsername()抛NPE。解决在LoginServlet.java中增加判空if (user ! null) { session.setAttribute(user, user); response.sendRedirect(main.jsp); } else { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); }main.jsp中用EL表达式${empty user}替代直接调用。5.3 现象中文日记内容存入MySQL后显示为???乱码原因三处编码未统一① JSP页面% page pageEncodingUTF-8 %② MySQL连接URL缺少characterEncodingutf8③ MySQL数据库/表字符集非utf8mb4。解决DBUtil.java中URL改为jdbc:mysql://localhost:3306/diary_db?useSSLfalseserverTimezoneUTCcharacterEncodingutf8MySQL执行ALTER DATABASE diary_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE diary CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Tomcat的conf/server.xml中Connector标签添加URIEncodingUTF-8。5.4 现象diary_list.jsp中c:forEach items${diaryList} vardiary不渲染任何日记原因JSTL标签库缺失。WEB-INF/lib/中无jstl.jar和standard.jarJSP 2.0需jakarta.servlet.jsp.jstl-api和jakarta.servlet.jsp.jstl。解决下载jakarta.servlet.jsp.jstl-api-2.0.0.jar和jakarta.servlet.jsp.jstl-2.0.0.jar复制到WEB-INF/lib/diary_list.jsp顶部添加% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %Tomcat 10需改为http://xmlns.jcp.org/jsp/jstl/core。5.5 现象修改web.xml中welcome-file-list为welcome-fileindex.jsp/welcome-file但访问http://localhost:8080/jsp_diary/仍显示404原因index.jsp不存在或web.xml未被Tomcat识别文件名大小写错误、XML格式非法、未放在WEB-INF/目录下。解决确认index.jsp位于Web应用根目录与login.jsp同级用XML校验工具检查web.xml是否符合DTD/XSD规范如web-app根标签的xmlns属性查看Tomcat启动日志搜索Deploying web application directory确认应用是否成功部署。6. 设计取舍反思当MVC被拆成三个物理文件夹我们失去了什么又得到了什么这份日记本源码的目录结构极简WEB-INF/下放web.xml和lib/Java类按功能散落在src/dao/、src/servlet/、src/utils/JSP页面平铺在Web根目录。它没有Spring MVC的Controller注解没有MyBatis的Select甚至没有Maven的pom.xml——所有依赖、配置、路由都靠手工维护。这种“返祖式”设计表面是技术落后实则是刻意为之的认知锚点。6.1 物理分层即逻辑分层src/目录下的MVC三原色打开src/目录你能清晰看到MVC的物理映射Model数据模型src/entity/User.java、src/entity/Diary.java——纯粹的POJO无任何框架注解字段与数据库表一一对应View视图login.jsp、diary_list.jsp——仅含HTML和EL/JSTL零Java脚本% %被严格禁止Controller控制器src/servlet/LoginServlet.java、src/servlet/DiaryServlet.java——接收请求、调用DAO、设置请求属性、转发跳转。这种分离不是靠框架约定而是靠开发者自觉遵守的目录约束。某导师曾让学生故意把DAO代码写进JSP结果diary_list.jsp体积暴涨到800行修改一个SQL要同时改JSP和Servlet——三天后全班自发重构为标准三层。物理隔离倒逼逻辑清晰这比任何注解都管用。6.2web.xml一份被遗忘的中央路由注册表现代框架用WebServlet(/login)或RequestMapping(/login)将路由分散到每个类上而本源码的web.xml是唯一的路由中心servlet servlet-nameLoginServlet/servlet-name servlet-classsrc.servlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping优势全局路由一目了然安全策略如security-constraint可集中配置劣势新增一个Servlet需手动改三处servlet、servlet-mapping、welcome-file易遗漏现实启示Spring Boot的SpringBootApplication本质是自动生成web.xml的替代品但当application.properties里server.servlet.context-path/api和spring.mvc.servlet.path/v1冲突时你依然得回到web.xml思维去理解容器行为。6.3 为什么不用Filter做统一编码因为request.setCharacterEncoding()必须在getParameter()之前src/filter/EncodingFilter.java实现了UTF-8编码过滤器但它的doFilter()方法必须这样写public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; req.setCharacterEncoding(UTF-8); // 必须在getParameter()前调用 resp.setCharacterEncoding(UTF-8); chain.doFilter(req, resp); }关键细节request.setCharacterEncoding()只对POST请求的请求体Body生效对GET请求的URL参数无效需在server.xml中配置URIEncoding血泪教训某开发者把req.setCharacterEncoding(UTF-8)放在chain.doFilter()之后结果所有中文参数仍是ISO-8859-1乱码——因为getParameter()在Filter链中已被前置Filter或Servlet调用过编码已固化。从那以后我每次写Filter都强制走一遍getParameterNames()打印所有参数名确认编码正确后再继续。这份源码里EncodingFilter的存在不是为了炫技而是把“请求生命周期中编码设置的窗口期”这个抽象概念钉死在doFilter()方法的第3行。希望帮到你。本文还有配套的精品资源点击获取