ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JavaWeb+MySQL日记系统实战:JSP到数据库的完整链路

JavaWeb+MySQL日记系统实战:JSP到数据库的完整链路 大概在三年前我第一次完整跑通一个 JavaWeb 项目时用的就是 JSP Servlet MySQL Bootstrap 这套组合。今天想认真拆解一下这个经典课设/毕设项目——基于 JavaWeb 和 MySQL 的日记系统。它麻雀虽小五脏俱全用户注册登录、日记的新增编辑删除、数据库设计、前端页面美化恰好把 JavaWeb 从浏览器到数据库的整条数据链路串了一遍。这篇博客会覆盖完整的环境搭建、建库建表脚本、核心代码思路、页面排版细节以及我在实际运行中踩过的各种坑适合正在学 JavaWeb 的同学、准备课程设计的朋友以及那些不想一上来就扎进 Spring Boot 注解海洋、想先把底层 Web 原理摸透的人。1. 项目概述与需求拆解日记系统到底要解决什么1.1 为什么需要一个在线日记系统先聊聊业务背景。很多人写日记的习惯是用 Word 文档、txt 或者手机备忘录但这类方式有个共同问题数据太分散时间一长就乱了。换设备想看之前的日记要么靠同步盘要么手动转存时间久了基本就找不到。日记这个场景的核心需求其实很明确私密、能分类、能随时回溯历史记录。这个项目就是在 Web 端实现了一个轻量级日记管理工具解决记录、查询、编辑、删除这四件高频操作。从技术角度看这又是一个非常标准的 JavaWeb CRUD 项目用户注册时把账号密码写入 MySQL登录成功后把用户信息放进 Session写日记就是一次 INSERT列表页就是一个带排序的 SELECT。全程没有中间件、没有分布式、没有缓存有的就是最纯粹的 Web 应用数据流转。也正因为链路短、知识密度高它特别适合作为理解和掌握 JavaWeb 主线的入门实战项目。我在帮人改代码的过程中发现凡是能把这个项目里数据流讲清楚的人后续学框架普遍顺畅得多。1.2 功能清单与边界控制做课程设计或者毕业设计最忌讳一上来就什么都想做。这个日记系统我建议把功能控制在下面这个范围内模块功能点说明优先级用户模块注册用户名唯一性校验、密码加密存储高用户模块登录Servlet 处理请求、Session 保存登录态高用户模块退出登录销毁 Session 并跳转登录页高日记模块新增日记标题、内容、天气、心情、配图高日记模块日记列表按时间倒序、分页展示高日记模块日记详情根据 ID 查询单条完整记录高日记模块编辑与删除必须校验当前用户是作者本人高检索模块关键词搜索标题或内容模糊匹配中边界也提前划好不做复杂的好友关系、不做评论点赞、不做消息推送就把自己定位成一个个人工具型应用。数据权限同样简化每个用户只能操作自己创建的日记通过 SQL 条件WHERE user_id ?来实现不做细粒度的角色体系。这个设计既保证了项目的完整性又不至于把工程量撑到一个人做不完的程度。2. 技术选型与工具链jspservletmysql这套组合凭什么还能打2.1 Servlet 与 JSP 在 Web 开发里的真实定位先纠正一个被很多人误解的点Servlet 并不过时它只是被 Spring MVC 封装了。Spring MVC 的前端控制器 DispatcherServlet 本身就是一个 Servlet这一点在任何一本框架源码分析的书里都能看到。所以在还没有建立请求如何进入 Java 程序、响应如何写回浏览器这个底层认知之前直接上手 Spring Boot遇到各种自动配置异常会很难定位。JSP 的本质也是一个 Servlet容器会把 JSP 文件编译成 Java 类再执行。在这个项目里JSP 负责视图层渲染Servlet 负责接收请求、调用业务逻辑、决定跳转到哪个页面JDBC 负责和 MySQL 打交道。三层各司其职就是标准的 MVC 思想。很多网上笔记喜欢把 JSPServlet 说成老技术但对教学项目而言把这一套手工流程跑通后面的框架学习就是查漏补缺而不是从零开始。2.2 为什么前端用 Bootstrap 而不是 Vue 或 React如果这是一个纯后端练习项目前端用原生 HTMLCSS 也说得过去。但课设和毕设答辩时页面观感真的很影响第一印象。Bootstrap 的优势在于现成的 CSS 类和组件、响应式栅格系统、不需要 Node.js 环境、不需要 npm install下载本地文件或者直接引用 CDN 就能用。对传统 JavaWeb 项目来说引入成本几乎为零。选择 Bootstrap 还有一个隐形好处它是响应式的。日记列表在电脑上显示三列在手机浏览器上自动堆叠成一列这个能力是 Bootstrap 底层栅格系统已经处理好的不需要额外写媒体查询。对不熟悉前端工程化的 Java 学习者来说这是性价比最高的选型。Vue 和 React 本身没有问题但它们需要 Node 环境、构建工具、组件库配合复杂度会明显高出这个项目的定位。2.3 MySQL 与 JDBC 驱动选型的版本细节数据库这块选 MySQL 基本没争议免费、跨平台、资料多JDBC 驱动足够稳定。真正容易踩坑的是驱动版本和类名对应关系。8.0 版本的 mysql-connector-java驱动类名是com.mysql.cj.jdbc.Driver而 5.x 版本是com.mysql.jdbc.Driver名字差一个cj。很多新手复制了旧资料里的驱动类名连 5.7 的 MySQL 却报 ClassNotFoundException就是因为驱动和类名不匹配。建议统一用 8.0.x 的驱动连接串里再带上useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8这几个参数能避免大量连接层面的兼容性问题。2.4 项目包结构与分层设计一个规范的 Web 项目包结构第一天就要理清楚后面才不会越来越乱。我常用的分层方式是这样的src ├─ com.diary.entity // 实体类User、Diary ├─ com.diary.dao // 数据访问层UserDao、DiaryDao ├─ com.diary.service // 业务逻辑层UserService、DiaryService ├─ com.diary.servlet // 控制层登录、注册、日记相关 Servlet ├─ com.diary.filter // 过滤器编码过滤器、登录过滤器 ├─ com.diary.util // 工具类DBUtil、MD5Util web ├─ index.jsp ├─ login.jsp ├─ register.jsp ├─ diary │ ├─ list.jsp │ ├─ add.jsp │ ├─ edit.jsp │ └─ detail.jsp └─ static ├─ css ├─ js └─ upload核心原则就是分层Servlet 不直接写 JDBC 代码DAO 层不出现 HttpServletRequest业务逻辑尽量放在 Service 层。这个习惯坚持下来以后如果想把 DAO 换成 MyBatis只需要动 DAO 层其他层完全不受影响这也是我说这套结构能打的原因——它尊重了职责分离的底线。3. 环境搭建与数据库设计从IDEA配置到MySQL建表3.1 IDEA 创建并运行 JavaWeb 项目的完整配置idea 运行 javaweb 项目配置是很多人卡住的第一关我按步骤拆一遍。第一步IDEA 里新建 Project选 Java Empty Project 就行不要用 Java Enterprise 模板否则会把 Maven 和 Spring 相关的插件全部带进来对理解底层流程反而是干扰。第二步选中模块右键Add Framework Support勾选 Web ApplicationIDEA 会自动生成 web 目录和 web.xml。第三步配置 TomcatRun → Edit Configurations → 加号 → Tomcat Server → Local在 Application Server 里选择本地解压好的 Tomcat 路径。第四步也是最关键的一步Deployment 选项卡里点加号把当前项目的 Artifact 添加进去Application context 建议设为/或/diary。这一步漏掉的话启动 Tomcat 后访问任何页面都会 404。还有两个细节值得注意IDEA 里的 Project SDK 建议选 JDK8 或 JDK11要和 Tomcat 版本的兼容性对应运行前检查一下 8080 端口是否被占用如果被占可以把 Tomcat 的 HTTP port 改成 8081或者到 conf/server.xml 里统一修改。3.2 MySQL 安装与连接绕不开的几个坑MySQL 安装教程到处都是但真正容易出问题的集中在这几个位置。如果用的是免安装版也就是 zip 解压版需要在 MySQL 目录下手动创建 my.ini里面至少要包含 basedir 和 datadir 两个配置项并且 datadir 对应的 data 目录必须通过初始化命令生成。初始化命令是mysqld --initialize-insecure这样 root 账号初始密码为空方便本地调试如果用了--initialize系统会生成一个临时随机密码存放在 data 目录下的 .err 日志文件里很多人找不到密码就是因为没看这个文件。启动服务的时候在管理员命令行执行net start mysql之前先确认服务存在。如果提示服务名无效需要先执行mysqld --install mysql注册 Windows 服务。至于最常见的net start mysql 服务无法启动绝大多数原因是 my.ini 里的路径写错或者 data 目录没有成功初始化。排查方法很直接打开 data 目录下的 .err 日志看最后几行报错信息比在网上盲目搜索高效得多。JDBC 连接阶段还有一个高频报错是 SSL connection error。解决方法是连接串加参数jdbc:mysql://localhost:3306/diary?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8。8.0 驱动默认开启 SSL本地调试用useSSLfalse关掉即可serverTimezone 不指定的情况下会报 CST 时区相关的异常这也是必带的。3.3 数据库表设计与建表 SQL数据库叫 diary核心两张表用户表、日记表。CREATE DATABASE IF NOT EXISTS diary DEFAULT CHARACTER SET utf8mb4; USE diary; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(50) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_diary ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, title VARCHAR(100) NOT NULL, content TEXT NOT NULL, weather VARCHAR(20) DEFAULT , mood VARCHAR(20) DEFAULT , image_path VARCHAR(200) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_user_time (user_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个设计点说明一下。user_id 上建索引因为列表查询永远带WHERE user_id ?数据量上来以后没有索引就是全表扫描。create_time 和 update_time 用 DATETIME 加 DEFAULT 值减少业务代码负担。内容字段用 TEXT 而不是 VARCHAR因为 VARCHAR 在 MySQL 的行长度限制下不适合存长文本。字符集统一 utf8mb4兼容 emoji避免用户输入特殊字符时入库报错。3.4 JDBC 工具类的封装数据访问层依赖一个统一的连接工具类这里给出基础版本public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/diary?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USERNAME root; private static final String PASSWORD root; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }教学项目里用 DriverManager 完全够用不需要一开始就上连接池。每个 DAO 方法结束之后必须关闭连接否则时间一长就会触发 MySQL 的 max_connections 上限出现 Too many connections 错误这个错误在演示现场非常尴尬。等系统需要更稳定并发的时候再换成 Druid 或 HikariCP 不迟。4. 核心功能实现注册登录、日记增删改查的完整链路4.1 注册与登录密码怎么存才不算白干注册功能是第一个入口也是最容易写出安全漏洞的地方。最忌讳的是明文密码入库。这里用 MD5 做一次摘要再存虽然 MD5 现在已经不是最推荐的加密方案但作为教学项目比明文强太多了——至少日志泄露或者数据库被拖走时密码不会直接暴露。想更安全一些可以加盐例如MD5(password username)让相同密码产生不同的摘要值。登录逻辑分三步接收参数、按用户名查用户、比对密码摘要。比对通过后把 user 对象放入 Session再重定向跳转到首页这里用重定向而不是转发是为了避免刷新页面时重复提交表单。注册时同样需要处理用户名唯一的场景先按 username 查一次存在就返回错误提示否则执行 INSERT。Session 的用途不只是保存登录态后面的列表页、详情页都会通过 Session 里的 user_id 来过滤数据所以权限控制的起点其实是在登录成功那一刻。4.2 登录拦截过滤器让未登录用户无处可去有了登录功能必须配套一个登录过滤器否则用户直接在地址栏输入/diary/list.jsp就能绕过登录看到其他用户的日记列表。这是最典型、也最容易在演示时翻车的安全缺口。WebFilter(/diary/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; Object user req.getSession().getAttribute(user); if (user null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }用 WebFilter 注解可以省去 web.xml 里的配置。过滤路径设为/diary/*覆盖所有日记相关页面和 Servlet。这里有个很容易忽略的细节login.jsp 本身不能放在被过滤的路径下否则过滤器会一直拦截登录页形成死循环跳转。4.3 新增、列表、编辑、删除核心 CRUD 的编码思路新增日记的 Servlet 逻辑很简单核心是接收参数、组装对象、调用 DAO 插入。唯一要留意的是创建时间不要信任前端传来的字符串直接交给数据库的 DEFAULT CURRENT_TIMESTAMP 生成避免用户伪造时间。内容字段做一下长度校验防止提交超大文本拖垮数据库。列表查询是最能体现mysql 排序价值的场景。日记列表天然需要按时间倒序让最新内容排在最前面public ListDiary findByUserId(int userId, int offset, int size) { String sql SELECT id, title, weather, mood, image_path, create_time FROM t_diary WHERE user_id ? ORDER BY create_time DESC, id DESC LIMIT ?, ?; // 执行查询并封装为 ListDiary }ORDER BY create_time DESC是倒序再加id DESC作为次级排序防止同一秒写入多条数据时出现顺序抖动。LIMIT 的两个参数第一个是起始偏移量第二个是每页条数分页的核心就是这条 SQL。编辑和删除的权限校验要特别重视update 语句的 WHERE 条件带上AND user_id ?删除同理写成DELETE FROM t_diary WHERE id ? AND user_id ?。这样即使有人手动篡改请求里的 id 参数也改不到别人的数据。这是数据隔离最后一道闸门绝对不能漏。4.4 事务处理什么时候必须用、怎么用单表单语句的 CRUD 不需要事务但出现两件事必须同时成功或同时失败的场景时事务就是刚需。比如删除日记的同时更新用户的统计字段再比如注册时插入用户记录、同时初始化用户的个性化配置表都属于典型的双写场景。MySQL 事务处理的标准写法是Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 操作1执行删除 // 操作2执行统计更新 conn.commit(); // 全部成功才提交 } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) {} } throw new RuntimeException(事务执行失败, e); } finally { DBUtil.close(null, null, conn); }核心就是三句话关闭自动提交、全部执行后 commit、任何一环异常就 rollback。这里有一个非常容易踩的坑同一个事务里的所有操作必须使用同一个 Connection。如果每个 DAO 方法内部都单独 getConnection 再 close事务就完全失效了。很多同学写了事务代码数据还是不一致根源就在这里——每个方法各拿各的连接自动提交还是开着commit 和 rollback 根本管不到别的连接。5. 前端页面与 Bootstrap美化jsp页面的排版与交互细节5.1 页面公共部分的抽取与复用JSP 项目最怕的是页面之间大量复制粘贴头部导航、CSS 引入、JS 引入每个文件都重复一遍后期想改一个类名就要翻遍所有页面。解决办法是把公共部分抽成独立的 JSP 文件用静态 include 引进来% include file/common/header.jsp %被引入的 header.jsp 里放 DOCTYPE、html 标签、Bootstrap 的 CSS 链接、导航栏 HTML。新增页面时只需要引 header.jsp 和 footer.jsp主体只写本页独有内容。这里建议把公共文件放在 web 目录下的/common子目录并始终用绝对路径引用避免不同层级页面里的相对路径计算错误。5.2 Bootstrap 栅格与卡片组件的实际运用日记列表页适合用 Bootstrap 的 card 组件做三列网格展示div classcontainer mt-4 div classrow c:forEach items${diaryList} vardiary div classcol-md-4 mb-3 div classcard div classcard-body h5 classcard-title${diary.title}/h5 p classcard-text text-truncate${diary.content}/p a hrefdetail?id${diary.id} classbtn btn-primary btn-sm查看详情/a /div /div /div /c:forEach /div /divcol-md-4是栅格系统的精髓中等宽度屏幕以上一行放三列每列占 4/12窄屏上自动堆叠为单列。text-truncate类可以让超长内容自动省略不需要写字符串截断逻辑。登录页的表单同样可以利用 Bootstrap 的 form-group、form-control、btn 系列类组合起来页面立刻有正规感。配合 HTML5 的 required、minlength 属性不写 JS 也能实现基础校验。5.3 删除确认用模态框提升完成度原生 confirm 弹窗功能上没有问题但观感太工程风。Bootstrap 的 modal 组件可以实现更好的交互点击删除按钮弹出带有日记标题的确认框点击确认才真正提交删除请求。具体做法是在列表页放一个 modal根据当前行数据设置弹窗里隐藏字段的值确认按钮绑定 click 事件跳转到删除 Servlet。这一步做完项目的完成度会有肉眼可见的提升这也是我坚持推荐 Bootstrap 的原因——组件都给你了剩下的只是拼装逻辑。5.4 页面加载完成后自动刷新一次有些场景需要页面加载后自动刷新一次比如登录跳转后希望列表页重新拉取数据或者编辑保存后希望详情页展示最新内容。这个需求对应实际开发里常见的jsp 页面让加载完后刷新一次。最简单的是在 JSP 页面加 meta 标签meta http-equivrefresh content0页面一加载就立刻刷新。另一种方式是 JSwindow.addEventListener(load, function () { location.reload(); });两种方式适用场景不同meta 刷新简单直接适合无条件刷新JS 方式可以加条件判断比如只有请求参数带 refresh1 时才刷新灵活性更高。演示项目里建议用 JS 方式后续扩展时不用改 HTML 结构。5.5 图片上传与坐标定位这个项目里的配图功能涉及两类图片问题上传和定位。上传流程是前端表单设置enctypemultipart/form-data后端 Servlet 用 Part 接口接收文件保存到 web 目录下的 upload 文件夹把相对路径写入数据库的 image_path 字段。关键细节有三个文件名必须重命名否则多人上传同名文件会互相覆盖建议用时间戳加随机数上传目录要保证可写不同环境下部署时最容易忽略页面显示时用相对路径不要硬编码本地磁盘绝对路径。jsp 图片如何对坐标定位这个问题在实际项目里通常这样处理如果是简单的位置标记在图片的父容器上加position: relative标记元素用position: absolute并设置 left 和 top 的百分比值如果要精确到像素就得结合图片容器实际尺寸换算课程设计阶段用百分比方案完全够用。5.6 中文乱码的三处排查位置JSP 项目里中文乱码是一个逃不掉的话题排查顺序基本固定。第一处是 JSP 文件本身% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %必须写全。第二处是请求参数Servlet 里要在所有 getParameter 之前执行request.setCharacterEncoding(UTF-8)对 GET 请求Tomcat 8 之后默认按 UTF-8 处理8 之前的版本还需要在 server.xml 里配置 URIEncoding。第三处是数据库JDBC URL 带characterEncodingutf8表结构统一 utf8mb4。三处全部对齐中文才能畅通无阻任何一处遗漏都可能出现前端显示正常但数据库里是问号或者反过来。6. 常见问题与踩坑记录从MySQL服务启动失败到404排查6.1 MySQL 服务无法启动的完整排查链路这个问题值得单独拿出来讲。现象是执行net start mysql提示服务无法启动或者服务启动后立刻停止。按下面顺序排查命中率极高打开 data 目录下的 .err 日志看最后几行报错信息这是最直接的线索。检查 my.ini 里 basedir 和 datadir 是否使用绝对路径Windows 下注意路径分隔符和末尾的反斜杠。确认 data 目录是否初始化成功没有 data 目录时先执行mysqld --initialize-insecure。确认 3306 端口没有被其他进程占用用netstat -ano | findstr :3306查看。确认服务注册信息路径不对时先执行mysqld --remove mysql再重新mysqld --install。90% 以上的情况出在前三步。日志文件永远是排查的第一老师比在网上漫无目的地搜索有效得多。6.2 JDBC 连接报错的典型场景JDBC 连接报错里除了 SSL connection error还有三个高频问题。Access denied for user 表示用户名或密码错误连本地 root 都进不去时检查 MySQL 服务运行时用的是哪个配置文件和数据目录。Unknown database 表示连接串里的库名不存在先登录 MySQL 执行 CREATE DATABASE。Communications link failure 通常是数据库服务没启动或者防火墙拦截了 3306 端口本地调试可以先临时关闭防火墙验证。6.3 404 与 500 的定位思路访问 JSP 页面 404不要急着怀疑代码按顺序排查IDEA 控制台 Tomcat 是否启动成功出现 Failed to start component 说明启动阶段就失败了。访问路径和 web 目录下的真实路径是否一致注意大小写和 .jsp 后缀。Deployment 里的 Artifact 是否添加Application context 是否和访问 URL 前缀一致。页面 500 的话把 Tomcat 控制台的异常信息往上翻找最底部的 Caused by大部分集中在 SQL 语法、空指针、ClassNotFoundException 三类。我改过很多项目发现 500 错误的定位能力直接决定了你能不能独立完成课设——耐心读异常栈比反复刷新页面有用得多。6.4 跑通之后还能怎么升级功能全部跑通之后如果时间和精力允许按以下顺序优化性价比最高引入 Druid 连接池替换 DriverManager解决并发连接数不足的问题。全部 SQL 改用 PreparedStatement 参数化统一防 SQL 注入。密码存储从纯 MD5 升级为加盐 MD5 或 BCrypt。把 DAO 从接口加实现类的结构整理好为以后替换 MyBatis 做准备。引入 Log4j 或 SLF4J把关键操作记录下来方便后期排查。任何一项做完都能在答辩时拿出实际的东西证明你不只是跑通了流程而是理解了项目的性能瓶颈和数据安全。最后说一点个人体会。这个项目我不仅当年写过后来帮人改过的版本也不下十次。最大的感受是很多同学能跑通流程但对数据流转没有形成完整的闭环认知。登录为什么用 Session、删除为什么要带 user_id、列表顺序为什么不对——这些细节才是真正值钱的东西。把这套 JSP Servlet MySQL Bootstrap 的土办法玩明白再去看 Spring Boot你会发现它不过是把 Servlet 的模板化和参数封装做了升级底层还是请求、处理、响应、持久化这一套。先把地基打稳框架只是新瓶装旧酒。后面如果你们在做类似的项目遇到具体报错或者拿不准的设计点欢迎随时带着问题来交流。
返回列表