JavaWeb个人博客系统实战:Servlet+JDBC+MySQL完整实现

发布时间:2026/8/28 19:53:58
JavaWeb个人博客系统实战:Servlet+JDBC+MySQL完整实现 简介JavaWeb是Java企业级Web开发的基础技术体系核心涵盖Servlet容器、JDBC数据库交互与MVC分层架构等关键概念。其原理在于通过HTTP协议驱动请求响应周期由Servlet接收并分发请求JDBC完成关系型数据持久化再经JSP或模板引擎渲染视图。该技术栈具备轻量可控、调试直观、教学友好等工程价值广泛应用于高校课程设计、毕业设计及中小规模后台系统快速原型开发。典型应用场景包括个人博客、企业内网CMS、教学管理系统等需强可追溯性与低学习门槛的项目。本文以真实可运行的JavaWeb个人博客系统为例深入拆解从数据库建模、SQL防注入到Servlet生命周期管理的完整链路覆盖源代码、SQL脚本与报告PDF交付标准。1. 这不是“交差作业”而是一套可落地、能上线、经得起推敲的JavaWeb个人博客系统实战指南你搜“JavaWeb个人博客系统”时页面上堆满标题党——“一键运行”“秒部署”“免配置”点进去却发现数据库脚本缺字段、前端页面全是占位图、Controller里硬编码了管理员密码、连个登录校验都用的是明文比对。我带过六届Java方向毕业设计每年都有学生拿着这类“源码”来问“老师为什么登录总跳404”“为什么文章发布后查不到”“报告里写的MVC三层架构可Service层全空着……”这根本不是教学案例是教学事故。今天这篇不讲虚的只拆解一个真实可用的JavaWeb个人博客系统——它跑在Tomcat 8.5、MySQL 5.7、JDK 1.8环境下从数据库建模到前端响应式布局从SQL防注入到文件上传安全校验每一步都经我手实测三遍以上。核心关键词就五个JavaWeb、个人博客系统、源代码、SQL、报告PDF——它们不是标签而是交付物的硬性标准。如果你正为课程设计发愁或想用一个真实项目补全Web开发知识链这篇就是你的“施工蓝图”。它不教你“怎么应付老师”而是告诉你“怎么做出一个别人真会点开看的博客”。2. 整体架构设计与技术选型逻辑为什么选这套组合而不是Spring Boot全家桶2.1 拒绝“过度设计”课程设计的本质是验证基础能力不是炫技很多同学一上来就想用Spring BootMyBatis PlusRedisVue结果两周过去连数据库连接池都配不对。课程设计的核心目标很明确验证你是否真正理解HTTP请求生命周期、Servlet容器工作原理、JDBC操作规范、MVC分层思想。所以我的技术栈选择非常克制后端框架原生Servlet JSP配合JSTL提示这不是复古而是精准打击。Servlet让你看清doGet()/doPost()如何接收参数、HttpServletRequest对象里到底装了什么JSP让你亲手写c:forEach循环渲染文章列表而不是依赖Vue的v-for自动魔法。当request.setAttribute(articles, list)和request.getRequestDispatcher(/list.jsp).forward(request, response)这两行代码在你脑中形成条件反射时Spring MVC的RequestMapping才真正有意义。持久层JDBC Template自封装轻量版注意没用Hibernate或MyBatis因为课程设计要暴露SQL细节。比如分页查询你得手写LIMIT ? OFFSET ?并理解PreparedStatement如何防止SQL注入文章搜索功能你得自己拼接LIKE CONCAT(%, ?, %)而不是交给框架的SelectProvider。这种“笨功夫”恰恰是企业面试官最看重的底层能力。数据库MySQL 5.7InnoDB引擎提示必须用5.7而非8.0因为课程机房普遍装的是5.7。8.0默认的caching_sha2_password认证插件会让JDBC驱动报错而解决它需要改MySQL配置、重置用户密码——这已超出课程设计范围。我们追求的是“拷贝即用”不是“折腾即止”。前端Bootstrap 4.6 原生JavaScript无jQuery注意放弃Vue/React不是因为它们不好而是因为课程设计要求你掌握DOM操作本质。比如文章编辑页的富文本我用document.execCommand()实现加粗/斜体而不是引入TinyMCE。当你亲手用element.innerHTML markdownToHtml(content)把Markdown转成HTML时你才真正理解前端渲染的边界。这套组合的终极价值在于所有技术点都可追溯、可调试、可解释。你在答辩时被问“为什么用JDBC不用MyBatis”你能指着DBUtil.java里的getConnection()方法说“因为我要控制连接获取时机避免长连接占用这是数据库性能课讲过的。”——这才是课程设计该有的深度。2.2 分层结构必须“看得见摸得着”每个包名都在讲一个故事很多“源码”把所有类塞进com.example.blog一个包里这是灾难。我的包结构严格遵循MVC分层且命名直指职责com.blog.dao → 只放接口ArticleDao、UserDao定义SQL操作契约 com.blog.dao.impl → 只放实现ArticleDaoImpl含具体JDBC代码含try-with-resources com.blog.service → 只放接口ArticleService定义业务规则如“发布前校验标题长度” com.blog.service.impl → 只放实现ArticleServiceImpl调用Dao处理事务不含SQL com.blog.servlet → 只放ServletArticleListServlet、LoginServlet纯粹的请求分发器 com.blog.filter → 只放FilterLoginFilter拦截未登录访问非注解式配置 com.blog.util → 工具类DBUtil连接池管理、MD5Util密码加密、FileUploadUtil安全文件上传提示com.blog.dao.impl包下绝不出现System.out.println()所有日志走log4jcom.blog.servlet里绝不写业务逻辑哪怕一行if判断都不许有——它的唯一使命是调用Service并转发请求。这种“洁癖式分层”会让你在写报告PDF时画架构图不再靠脑补而是直接截图包结构。2.3 安全不是“加个过滤器”从SQL注入到XSS每一处都设防课程设计常被忽略的致命点安全。我见过太多“源码”里写着String sql SELECT * FROM article WHERE title title;——这等于把数据库大门钥匙焊死在门把手上。我的防护是立体的SQL注入所有查询必用PreparedStatement连最简单的登录验证都这样写String sql SELECT * FROM user WHERE username ? AND password ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, username); // 参数化绑定杜绝拼接 ps.setString(2, MD5Util.encode(password));XSS攻击JSP页面所有动态内容必过StringEscapeUtils.escapeHtml4()h1${org.apache.commons.text.StringEscapeUtils.escapeHtml4(article.title)}/h1文件上传禁用.jsp.html后缀白名单校验String ext FilenameUtils.getExtension(fileItem.getName()); if (!Arrays.asList(jpg, jpeg, png, gif).contains(ext.toLowerCase())) { throw new RuntimeException(不支持的文件类型 ext); }密码存储MD5加盐盐值存数据库非明文或简单MD5。这些不是“加分项”是底线。你的报告PDF里如果没写“安全防护措施”答辩时老师一句“你这系统能防SQL注入吗”就能让你卡壳。3. 核心模块实现详解从数据库建模到前后端联调每一步都是踩坑实录3.1 数据库设计5张表撑起完整博客字段设计暗藏玄机很多人以为博客系统只要article一张表。实际要支撑“用户-文章-分类-评论-标签”闭环至少5张表且字段设计必须考虑扩展性表名字段设计意图实操避坑userid(PK),username(UNIQUE),password,email,avatar_url,status(TINYINT)status0表示禁用非删除avatar_url存相对路径方便CDN迁移别用VARCHAR(255)存密码MD5后是32位字符串定长CHAR(32)更省空间categoryid(PK),name,slug(UNIQUE),sort_order(INT)slug用于SEO友好URL如/category/javasort_order控制后台分类排序slug必须小写英文短横线避免/category/Java教程这种非法路径articleid(PK),title,content(LONGTEXT),summary(VARCHAR(500)),user_id(FK),category_id(FK),status(TINYINT),create_time,update_timecontent用LONGTEXT存Markdown源码summary存首段摘要避免全文截取乱码status1已发布2草稿3审核中——别用布尔值未来加状态好扩展commentid(PK),article_id(FK),user_id(FK),content,parent_id(INT,0为根评论),status(TINYINT)parent_id支持二级回复status区分待审核/已通过/垃圾评论别在comment表加username字段关联user表查避免数据冗余tagid(PK),name,slug(UNIQUE)独立标签表通过中间表article_tag关联文章标签名去重用INSERT IGNORE INTO tag...比先查再插快10倍提示article_tag中间表必须有联合唯一索引ALTER TABLE article_tag ADD UNIQUE KEY uk_article_tag (article_id, tag_id);否则同一文章重复打同标签会插入多条记录。SQL脚本blog.sql关键片段-- 创建用户表 CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password CHAR(32) NOT NULL, email VARCHAR(100), avatar_url VARCHAR(200) DEFAULT /static/img/avatar.png, status TINYINT DEFAULT 1 COMMENT 1-启用,0-禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 创建文章表重点全文索引加速搜索 ALTER TABLE article ADD FULLTEXT(title, summary, content);注意FULLTEXT索引是MySQL原生全文检索核心比LIKE %关键词%快10倍以上。但必须用MATCH() AGAINST()语法SELECT * FROM article WHERE MATCH(title,summary,content) AGAINST(JavaWeb IN NATURAL LANGUAGE MODE);3.2 后端核心逻辑Servlet生命周期与事务控制的真实战场3.2.1 登录流程Filter拦截Session管理拒绝“裸奔”LoginServlet只做三件事校验参数、查库比密、设置Session。所有权限控制交给LoginFilter// LoginFilter.java public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // 白名单登录页、静态资源、验证码接口放行 String uri request.getRequestURI(); if (uri.contains(/login.jsp) || uri.contains(/static/) || uri.contains(/captcha)) { chain.doFilter(req, resp); return; } // 检查Session中是否有user User user (User) request.getSession().getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp?msgnot_login); return; } chain.doFilter(req, resp); }实操心得request.getSession().getAttribute(user)返回null时必须用sendRedirect()跳转不能forward()。因为forward是服务器内部跳转浏览器地址栏不变用户刷新会重复提交redirect是客户端重定向地址栏更新符合HTTP语义。3.2.2 文章发布事务控制与富文本安全的双重保障ArticlePublishServlet是事务典型场景插入文章插入中间表article_tag必须原子性。JDBC手动事务代码Connection conn null; PreparedStatement ps1 null; PreparedStatement ps2 null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 插入文章 String sql1 INSERT INTO article(title,content,summary,user_id,category_id) VALUES(?,?,?,?,?); ps1 conn.prepareStatement(sql1, Statement.RETURN_GENERATED_KEYS); ps1.setString(1, title); ps1.setString(2, content); // Markdown源码 ps1.setString(3, summary); ps1.setLong(4, userId); ps1.setLong(5, categoryId); ps1.executeUpdate(); // 获取生成的article_id ResultSet rs ps1.getGeneratedKeys(); long articleId 0; if (rs.next()) { articleId rs.getLong(1); } // 批量插入标签关联 String sql2 INSERT INTO article_tag(article_id, tag_id) VALUES(?, ?); ps2 conn.prepareStatement(sql2); for (Long tagId : tagIds) { ps2.setLong(1, articleId); ps2.setLong(2, tagId); ps2.addBatch(); // 批处理提升性能 } ps2.executeBatch(); conn.commit(); // 显式提交 } catch (Exception e) { if (conn ! null) { try { conn.rollback(); // 出错回滚 } catch (SQLException ex) { ex.printStackTrace(); } } request.setAttribute(msg, 发布失败 e.getMessage()); request.getRequestDispatcher(/publish.jsp).forward(request, response); return; } finally { DBUtil.close(ps1, conn); }提示ps1.getGeneratedKeys()获取自增ID是关键否则无法关联标签。很多“源码”在这里用SELECT LAST_INSERT_ID()但在高并发下可能取错ID。3.3 前端交互实现Bootstrap原生JS拒绝“黑盒框架”3.3.1 响应式文章列表Flex布局卡片式设计list.jsp用Bootstrap 4的d-flex实现瀑布流而非传统浮动div classcontainer mt-4 div classrow c:forEach items${articles} vararticle div classcol-md-6 col-lg-4 mb-4 div classcard h-100 shadow-sm img src${article.coverUrl} classcard-img-top alt${article.title} onerrorthis.src/static/img/default-cover.jpg div classcard-body d-flex flex-column h5 classcard-title${fn:escapeXml(article.title)}/h5 p classcard-text text-muted flex-grow-1${fn:substring(article.summary, 0, 80)}.../p div classmt-auto span classbadge badge-primary${article.category.name}/span small classtext-muted ml-2${article.createTime}/small /div /div div classcard-footer bg-white a href${pageContext.request.contextPath}/article?id${article.id} classbtn btn-sm btn-outline-primary阅读全文/a /div /div /div /c:forEach /div /div注意fn:escapeXml()防止XSSonerror处理封面图缺失flex-grow-1让摘要高度自适应——这些细节决定用户体验。3.3.2 富文本编辑器用contenteditable实现轻量级Markdown放弃第三方编辑器用原生div[contenteditable]实时预览!-- 编辑区 -- div ideditor contenteditabletrue classborder p-3 mb-3 placeholder输入Markdown格式内容.../div !-- 预览区 -- div idpreview classborder p-3 bg-light/div script document.getElementById(editor).addEventListener(input, function() { const md this.innerText; // 简单Markdown解析生产环境用marked.js let html md.replace(/\*\*(.*?)\*\*/g, strong$1/strong); html html.replace(/(.*?)/g, code$1/code); html html.replace(/\n/g, br); document.getElementById(preview).innerHTML html; }); /script实操心得contenteditable的innerText比innerHTML安全避免执行恶意脚本预览区用innerHTML渲染是可控的因为已做过滤。4. 源代码与文档交付如何让“源代码SQL报告PDF”真正成为你的作品集4.1 源代码组织目录即文档结构说明一切交付的ZIP包必须是“开箱即用”结构而非杂乱文件堆blog-system/ ├── src/ # Java源码标准Maven结构 │ └── main/ │ ├── java/com/blog/ # 包结构如前所述 │ └── webapp/ # JSP/静态资源 ├── sql/ # 数据库脚本 │ ├── blog.sql # 建库建表初始化数据 │ └── init-data.sql # 测试用的10条文章3个用户 ├── docs/ # 技术文档 │ ├── design-doc.md # 数据库ER图接口说明 │ └── deployment.md # Tomcat部署步骤含context.xml配置 ├── report/ # 报告PDF见4.3 └── README.md # 一句话启动指南导入SQL→部署到Tomcat→访问http://localhost:8080提示README.md必须包含精确的JDK/Tomcat版本号例如“本项目基于JDK 1.8.0_291、Apache Tomcat 8.5.92、MySQL 5.7.36开发低版本可能因JDBC驱动不兼容报错。”4.2 SQL脚本不只是建表更是数据质量的承诺blog.sql必须包含三部分建库语句指定字符集CREATE DATABASE IF NOT EXISTS blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE blog;建表语句含注释-- 用户表存储注册用户信息 CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;初始化数据非空数据证明系统可运行INSERT INTO user (username, password, email, status) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, adminblog.com, 1); INSERT INTO category (name, slug, sort_order) VALUES (JavaWeb, javaweb, 1), (前端, frontend, 2);注意密码e10adc3949ba59abbe56e057f20f883e是123456的MD5方便测试。但报告PDF中必须强调“生产环境需强制修改默认密码”。4.3 报告PDF撰写用技术语言讲故事而非堆砌截图报告PDF不是Word截图集合而是技术叙事。我推荐四章结构4.3.1 系统需求分析用用例图说话而非文字罗列参与者博主管理员、访客游客核心用例博主登录、发布文章、管理分类、审核评论访客浏览文章、按分类筛选、搜索文章、发表评论需登录提示用draw.io画用例图导出PNG嵌入PDF。文字描述聚焦“为什么需要这个功能”例如“评论审核功能源于博客内容合规要求避免敏感词传播”。4.3.2 数据库设计ER图关键字段说明表ER图展示5张表关系重点标注外键如article.user_id → user.id。补充字段说明表字段类型是否为空默认值说明article.statusTINYINTNOT NULL11-已发布2-草稿3-待审核。此设计支持未来增加“定时发布”状态4.3.3 关键技术实现选3个最硬核的点深挖安全防护实现对比“未防护”与“防护后”的SQL语句展示PreparedStatement如何阻断 OR 11攻击。分页性能优化给出LIMIT 10 OFFSET 1000与WHERE id ? LIMIT 10的执行计划对比EXPLAIN截图。文件上传安全列出白名单后缀、大小限制2MB、存储路径隔离/upload/article/vs/upload/avatar/。4.3.4 系统测试用真实数据说话功能测试表用例输入预期输出实际结果发布空标题文章标题为空提示“标题不能为空”✅搜索不存在关键词“xyzabc”返回“未找到相关文章”✅压力测试用JMeter模拟100并发用户访问首页平均响应时间300ms附截图。提示报告PDF必须包含致谢“感谢XX老师在数据库事务章节的讲解让我理解了commit与rollback的真正意义”。5. 常见问题排查与实操速查那些没人告诉你的“灵异事件”5.1 数据库连接失败90%的问题出在这3个地方现象原因解决方案java.sql.SQLException: Access denied for userMySQL用户权限不足执行GRANT ALL PRIVILEGES ON blog.* TO blog_user% IDENTIFIED BY 123456; FLUSH PRIVILEGES;Communications link failureMySQL服务未启动或端口被占netstat -anoUnknown database blog未执行blog.sql建库在MySQL命令行执行source D:/blog.sql注意路径用正斜杠实操心得在DBUtil.java的getConnection()方法开头加日志System.out.println(尝试连接数据库 url);这能快速定位是URL写错还是服务没起来。5.2 JSP页面中文乱码不是编码问题是传输链路断裂现象数据库存的是“JavaWeb”页面显示“JavaWeb????”。这不是% page contentTypetext/html;charsetUTF-8%没加而是Tomcat的URIEncoding未配置。解决方案修改Tomcat/conf/server.xml在Connector标签内加Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 / !-- 关键 --JSP页面顶部加% page contentTypetext/html;charsetUTF-8 languagejava % meta charsetUTF-8MySQL连接URL加参数jdbc:mysql://localhost:3306/blog?useUnicodetruecharacterEncodingUTF-8提示三者缺一不可。只改JSP编码GET请求参数仍乱码只改URL参数POST请求仍乱码。5.3 文件上传失败别怪Tomcat先查磁盘空间现象上传图片时抛java.io.IOException: No space left on device。原因不是磁盘真满了而是Linux系统/tmp分区Tomcat临时目录满了。排查命令df -h /tmp # 查看/tmp使用率 ls -lh /tmp/ # 查看大文件 # 清理Tomcat临时文件 rm -rf /path/to/tomcat/work/Catalina/localhost/blog/实操心得在FileUploadUtil.java中上传前检查磁盘剩余空间File uploadDir new File(/path/to/upload); if (uploadDir.getUsableSpace() 1024 * 1024 * 100) { // 小于100MB throw new RuntimeException(磁盘空间不足请清理上传目录); }5.4 报告PDF生成失败LaTeX不是必须Markdown才是生产力很多同学用Word写报告导出PDF时格式错乱。推荐极简方案用Typora写report.md支持数学公式、流程图、代码块导出PDF时勾选“导出为PDF含目录”若需页眉页脚在Typora设置→导出→PDF→页眉页脚填入页眉JavaWeb个人博客系统课程设计页脚第${page}页共${total}页提示代码块用java标注语言Typora会自动配色表格用Markdown语法导出PDF对齐完美。6. 最后一点掏心窝子的话这个项目真正的价值不在及格而在“可生长”我见过太多同学交完作业就把源码删了仿佛完成任务就清零。但这个博客系统是你Web开发能力的“活体种子”。它现在能跑在Tomcat上下一步可以把JSP换成Thymeleaf体验模板引擎差异把JDBC换成MyBatis对比SQL管理方式加个Redis缓存热门文章观察QPS提升用Nginx反向代理学负载均衡概念。这些都不是“为了做而做”而是当你在实习面试被问“你做过什么Web项目”时能打开GitHub仓库指着commit记录说“这是我用原生Servlet做的博客第17次commit加了全文检索第23次commit修复了XSS漏洞……”——那一刻你交付的不是一份作业而是一个开发者成长的证据链。所以别急着打包交差。花半小时把README.md里的启动步骤真正在自己电脑上敲一遍把blog.sql里的每一条INSERT对着数据库客户端执行一次把LoginFilter的doFilter方法打个断点单步调试三次。当这些动作变成肌肉记忆课程设计才真正属于你。毕竟技术世界里没有“交差”的项目只有“生长”的作品。本文还有配套的精品资源点击获取