
简介本资源是一套面向JavaWeb初学者与课程设计者的个人博客系统全栈学习包聚焦基于SSM框架的博客平台从需求分析、架构设计到编码实现的完整开发流程。资源共19个文件涵盖PPT教学课件、Word开题报告与任务书、PDF论文及技术手册、SQL数据库脚本、PNG项目截图、ZIP源码工程含Blog.zip与基于SSM的源代码.zip以及配套学习URL链接包体大小44.52MB内容组织清晰便于分模块研习与复现。已有761人学习下载适合高校学生完成课程设计、毕业设计或自学JavaWeb实战开发。学习者可直接获取可运行的SSM博客源码、规范化的数据库设计db_blog3.sql、完整的前后端交互逻辑ServletJSPJDBC、安全实践要点防SQL注入/XSS及MVC分层结构示例同时辅以视频指引链接与项目截图显著降低环境搭建与调试门槛。1. 这不是又一个“Hello World”博客模板它是一套能跑通登录、文章发布、评论审核、后台管理全链路的 JavaWeb 实战工程专为想把课设/毕设真正部署到云服务器上、且不愿被 Spring Boot 自动配置黑匣子反向支配的开发者准备你可能已经下载过十几个标着“个人博客系统”的 ZIP 包——解压后发现只有 index.html 和几段 jQuery或者干脆是 IDEA 里跑不起来的空 Maven 项目pom.xml 里连 servlet-api 版本都写错了。这个个人博客系统的设计与实现.zip不同它基于原生 Servlet JSP JDBC 构建无 Spring、无 MyBatis、无前端框架但完整实现了用户注册登录含密码加密、富文本文章发布支持图片上传、分类/标签管理、前台分页展示、后台内容审核、评论开关控制、访客统计基于 sessioncookie 粗粒度计数等 7 类核心业务模块。所有 DAO 层 SQL 手写、所有跳转逻辑显式控制、所有异常捕获落地日志。它不炫技但每行代码你都能在 Tomcat 9.0.83 MySQL 8.0.33 环境下逐行调试它不教设计模式但你在ArticleServlet里能看到命令模式雏形在CommentFilter里能摸到责任链影子。适合两类人一是 JavaWeb 课程设计卡在“登录跳转总 404”的同学二是想甩开 Spring Boot 魔法棒、亲手拧紧每个螺丝钉的转岗工程师。这不是玩具是能塞进阿里云轻量应用服务器、用域名访问的真实可运行系统。2. 从解压到首页可见三步完成本地环境搭建与基础验证2.1 环境清单与版本对齐为什么必须用 Tomcat 9 MySQL 8 而不是最新版项目依赖明确写在README.md第二行“建议运行环境JDK 1.8、Tomcat 9.0.x、MySQL 8.0.x”。这不是保守而是血泪经验。Tomcat 10 会翻车项目使用javax.servlet.*包非jakarta.servlet.*Tomcat 10 默认启用 Jakarta EE 9 命名空间直接启动报NoClassDefFoundError: javax/servlet/Filter。MySQL 5.7 会沉默失败user表中create_time字段定义为DATETIME DEFAULT CURRENT_TIMESTAMPMySQL 5.7 不支持 DATETIME 类型的CURRENT_TIMESTAMP默认值仅支持 TIMESTAMP插入用户时该字段为 NULL后续LoginServlet的last_login_time更新逻辑因空指针中断。JDK 11 会拒绝加载web.xml中jsp-config的taglib声明使用旧式 TLD 路径JDK 11 的java.xml.bind模块移除导致 JSP 编译失败。提示务必核对apache-tomcat-9.0.83/conf/server.xml中Connector port8080的端口未被占用MySQL 用户需提前创建blog_db数据库并授权rootlocalhost全权限。2.2 数据库初始化执行sql/blog_schema.sql的四个关键动作解压后进入sql/目录找到blog_schema.sql。这不是单条 CREATE TABLE 语句而是一个包含 6 张表、3 个存储过程、2 个触发器的完整脚本。执行前请确认-- 在 MySQL 8.0 CLI 中执行注意必须用 root 用户 USE blog_db; SOURCE /path/to/blog_schema.sql;脚本核心动作分解动作SQL 片段示意为什么必须做1. 创建用户表并设默认时间CREATE TABLE user (id INT PRIMARY KEY AUTO_INCREMENT, ... create_time DATETIME DEFAULT NOW());NOW()兼容 MySQL 5.7避免CURRENT_TIMESTAMP兼容性问题2. 插入初始管理员账号INSERT INTO user (username, password, role) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, ADMIN);密码是123456的 MD5非 BCrypt用于首次登录密码明文存于src/main/webapp/WEB-INF/web.xml注释中3. 创建文章分类关联表CREATE TABLE article_category (article_id INT, category_id INT, PRIMARY KEY(article_id, category_id));支持多对多分类避免article表中冗余category_ids字符串字段4. 创建评论审核状态触发器DELIMITER $$ CREATE TRIGGER comment_audit AFTER INSERT ON comment FOR EACH ROW BEGIN IF NEW.status PENDING THEN UPDATE article SET comment_count comment_count 1 WHERE id NEW.article_id; END IF; END$$保证前台文章页显示“已有 X 条评论”时只统计已审核通过的评论执行成功后用SELECT * FROM user;验证 admin 账号存在SELECT COUNT(*) FROM article;应返回 0初始无文章。2.3 项目导入 IDEA 并部署绕过两个经典陷阱将解压目录拖入 IDEA选择 “Open as Project”IDEA 会自动识别为 Maven 项目pom.xml存在。但此时不能直接点击绿色三角运行——因为缺少 Web Facet 配置。正确步骤右键项目根目录 →Open Module Settings→Project→ 确认 Project SDK 为 JDK 1.8左侧选Modules→ 选中模块 →Dependencies标签页 → 点→JARs or directories→ 选择apache-tomcat-9.0.83/lib/下全部 JAR尤其servlet-api.jar、jsp-api.jarFacets→→Web→Web resource directory设为src/main/webappWeb.xml路径设为src/main/webapp/WEB-INF/web.xmlArtifacts→→Web Application: Archive→From modules→ 选当前模块 →Output directory设为target/blog.warRun→Edit Configurations→→Tomcat Server→Local→Deployment→→Artifact→ 选blog:war exploded→Application context填/blog。注意若启动时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver说明mysql-connector-java-8.0.33.jar未放入WEB-INF/lib/。项目未打包该驱动需手动下载并复制进去——这是故意为之逼你理解 JDBC 驱动加载机制。启动成功后浏览器访问http://localhost:8080/blog应看到首页轮播图与文章列表访问http://localhost:8080/blog/admin/login.jsp用admin/123456登录进入后台管理页。3. 后台功能链路拆解从用户登录到文章发布的 5 层调用栈实录3.1 登录流程LoginServlet如何串联 Session、Filter 与密码校验用户在admin/login.jsp输入账号密码表单提交至LoginServletURL 映射/admin/login。该 Servlet 不是简单查库比对而是走了一条带审计痕迹的链路// src/main/java/com/blog/servlet/LoginServlet.java protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); // Step 1: 密码 MD5 加密注意此处是硬编码 salt实际应动态生成 String encryptedPwd DigestUtils.md5Hex(password blog_salt_2023); // Step 2: 调用 DAO 查询非 ORM纯 JDBC User user userDao.findByUsernameAndPassword(username, encryptedPwd); if (user ! null ADMIN.equals(user.getRole())) { // Step 3: 设置 Session 属性非 Cookie HttpSession session req.getSession(); session.setAttribute(admin, user); // 关键属性名为 admin后台页面靠此判断登录态 // Step 4: 记录登录日志写入文件非数据库 LogUtil.writeLog(ADMIN_LOGIN_SUCCESS, username, req.getRemoteAddr()); resp.sendRedirect(req.getContextPath() /admin/index.jsp); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/admin/login.jsp).forward(req, resp); } }关键参数说明DigestUtils.md5Hex()来自commons-codec-1.15.jar项目已引入userDao.findByUsernameAndPassword()内部使用PreparedStatement防止 SQL 注入SQL 为SELECT * FROM user WHERE username? AND password? AND statusACTIVEsession.setAttribute(admin, user)是整个后台权限控制的基石——所有后台 JSP 页面顶部都有% if(session.getAttribute(admin)null) response.sendRedirect(../login.jsp); %。3.2 文章发布富文本编辑器如何与后端图片上传协同工作前台write.jsp使用KindEditor 4.1.11位于js/kindeditor/其图片上传配置指向UploadImageServlet// write.jsp 中 KindEditor 初始化 var editor KindEditor.create(#content, { uploadJson : /blog/UploadImageServlet, // 注意路径含上下文 /blog fileManagerJson : /blog/FileManagerServlet, allowFileManager : true });UploadImageServlet的核心逻辑// src/main/java/com/blog/servlet/UploadImageServlet.java protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // Step 1: 解析 multipart/form-data不用 Apache Commons FileUpload手写解析 String boundary req.getHeader(Content-Type).split(boundary)[1]; BufferedReader reader req.getReader(); String line; StringBuilder content new StringBuilder(); while ((line reader.readLine()) ! null) { if (line.contains(filename)) { String filename line.split(filename\)[1].split(\)[0]; String ext filename.substring(filename.lastIndexOf(.) 1).toLowerCase(); if (!Arrays.asList(jpg, jpeg, png, gif).contains(ext)) { throw new ServletException(不支持的图片格式 ext); } // Step 2: 生成唯一文件名防覆盖 String newFilename System.currentTimeMillis() _ UUID.randomUUID().toString().substring(0, 8) . ext; String savePath req.getServletContext().getRealPath(/upload/) newFilename; // Step 3: 流式写入磁盘非内存缓存 FileOutputStream fos new FileOutputStream(savePath); // ...省略流读取与写入代码 // Step 4: 返回 JSON 给 KindEditor格式严格限定 resp.setContentType(application/json); resp.getWriter().print({\error\:0,\url\:\/upload/ newFilename \}); } } }为什么必须返回{error:0,url:/upload/xxx.jpg}KindEditor 的 JS 会解析该 JSON提取url字段插入img src...。若返回{code:200,data:/upload/xxx.jpg}编辑器将无法识别图片上传按钮永远转圈。3.3 评论审核Filter 如何拦截未审核评论并重定向前台文章页article.jsp的评论表单提交至CommentServlet但真正起作用的是CommentAuditFilter// src/main/java/com/blog/filter/CommentAuditFilter.java public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; String uri request.getRequestURI(); // 仅拦截 /CommentServlet 请求 if (uri.endsWith(/CommentServlet)) { String content request.getParameter(content); // 简单敏感词过滤非正则白名单机制 if (content ! null containsSensitiveWord(content)) { HttpServletResponse response (HttpServletResponse) resp; response.sendRedirect(request.getContextPath() /error/sensitive.jsp?msg评论含敏感词); return; } } chain.doFilter(req, resp); } private boolean containsSensitiveWord(String content) { String[] words {政治, 赌博, 色情, 广告}; for (String word : words) { if (content.contains(word)) return true; } return false; }Filter 在web.xml中的声明顺序至关重要filter filter-nameCommentAuditFilter/filter-name filter-classcom.blog.filter.CommentAuditFilter/filter-class /filter filter-mapping filter-nameCommentAuditFilter/filter-name url-pattern/CommentServlet/url-pattern /filter-mapping !-- 必须放在 CharacterEncodingFilter 之后否则 getParameter() 获取乱码 --4. 避坑指南五个让开发者凌晨三点还在查日志的真实问题4.1 现象首页文章列表为空但数据库article表有 10 条数据原因ArticleDAO.listAll()方法中 SQL 语句写为SELECT * FROM article WHERE statusPUBLISHED而新插入的文章status字段默认为DRAFT草稿未在后台“发布”操作中更新状态。解决登录后台 →文章管理→ 找到对应文章 → 点击发布按钮该按钮触发UpdateStatusServlet执行UPDATE article SET statusPUBLISHED WHERE id?。4.2 现象上传图片后 KindEditor 显示undefined控制台报404原因UploadImageServlet的 URL 映射在web.xml中配置为url-pattern/UploadImageServlet/url-pattern但write.jsp中uploadJson路径写成/UploadImageServlet缺上下文路径/blog。解决修改write.jsp第 32 行uploadJson : %request.getContextPath()%/UploadImageServlet确保路径动态拼接。4.3 现象后台登录成功后跳转index.jsp页面报NullPointerException原因index.jsp第 15 行%((User)session.getAttribute(admin)).getUsername()%中session.getAttribute(admin)返回 null。根本原因是LoginServlet中req.getSession()创建了新 Session但response.sendRedirect()是 302 重定向浏览器发起新请求时未携带原 Session ID因 Cookie 路径不匹配。解决在LoginServlet登录成功后添加session.setMaxInactiveInterval(30*60);并确认web.xml中session-configcookie-confighttp-onlytrue/http-only/cookie-config/session-config未禁用 Cookie。4.4 现象MySQL 8.0 执行blog_schema.sql报错ERROR 1064 (42000)原因脚本开头有SET SQL_MODESTRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO;而 MySQL 8.0 默认 SQL Mode 不含NO_ZERO_DATE且该模式已被弃用。解决删除blog_schema.sql第 1 行SET SQL_MODE...整行或替换为SET SQL_MODE(SELECT REPLACE(sql_mode,NO_ZERO_DATE,));。4.5 现象ArticleServlet处理分页时pageNo参数始终为 1原因article.jsp中分页链接写为a hrefArticleServlet?pageNo${i}但ArticleServlet的doGet()方法未调用req.setCharacterEncoding(UTF-8)当 URL 含中文如?pageNo2时Tomcat 8 默认用 ISO-8859-1 解码数字被误判。解决在ArticleServletdoGet()开头添加req.setCharacterEncoding(UTF-8);并在web.xml中全局配置CharacterEncodingFilter项目已提供需确认 filter-mapping 顺序在最前。5. 跨浏览器兼容性加固让 IE11、Edge Legacy、Chrome 115 都能正确渲染的三个硬核技巧5.1 JSP 输出编码% page contentTypetext/html;charsetUTF-8%不是万能的很多开发者以为在 JSP 顶部加contentType就万事大吉但 IE11 对meta charsetUTF-8的解析极其脆弱。真实场景中header.jsp的title标签内中文显示为方块根源在于响应头缺失Content-Type的charset声明。解决方案在web.xml中强制设置全局响应编码filter filter-nameCharacterEncodingFilter/filter-name filter-classcom.blog.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingCharacterEncodingFilter的doFilter()必须同时设置请求和响应编码req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); // 关键此行决定浏览器解析方式 chain.doFilter(req, resp);为什么resp.setContentType()比 JSP 指令更可靠因为它是 HTTP 响应头层面的强制声明IE11 会优先信任Content-Type头而非 HTML 内的meta标签。5.2 CSS Flex 布局降级用display: table-cell替代flex的实战取舍首页轮播图.banner使用了display: flex但在 IE11 中部分 flex 属性如flex-wrap支持不全导致图片堆叠错位。项目未用 Polyfill而是采用渐进增强策略/* css/style.css */ .banner { display: table; /* IE8 安全兜底 */ width: 100%; table-layout: fixed; } .banner-item { display: table-cell; width: 100%; vertical-align: top; } /* Chrome/Firefox/Edge 新版额外加载 */ supports (display: flex) { .banner { display: flex; } .banner-item { display: block; flex: 0 0 100%; } }验证方法在 IE11 开发者工具中禁用supports规则观察.banner是否仍保持横向排列——若正常则降级生效。5.3 JavaScript API 兼容fetch()替换为XMLHttpRequest的最小化改造后台CategoryServlet的 AJAX 分类删除使用了fetch()但 IE11 不支持。项目提供了js/ajax.js封装层// js/ajax.js function ajax(options) { if (window.fetch) { return fetch(options.url, { method: options.method || GET, headers: {Content-Type: application/json}, body: options.data ? JSON.stringify(options.data) : null }).then(r r.json()); } else { // IE11 fallback return new Promise((resolve, reject) { const xhr new XMLHttpRequest(); xhr.open(options.method || GET, options.url, true); xhr.setRequestHeader(Content-Type, application/json); xhr.onreadystatechange function() { if (xhr.readyState 4) { if (xhr.status 200) { resolve(JSON.parse(xhr.responseText)); } else { reject(xhr.statusText); } } }; xhr.send(options.data ? JSON.stringify(options.data) : null); }); } }调用示例admin/category.jsp// 原 fetch 写法删掉 // fetch(/blog/CategoryServlet?opdeleteidid) // 改为 ajax({url: /blog/CategoryServlet?opdeleteidid, method: POST}) .then(data { if(data.success) location.reload(); });从那以后我每次交付 JavaWeb 项目都强制走一遍 IE11 兼容性 checklist① 检查Content-Type响应头是否含charset② 查看console.log是否有Object doesnt support property or method fetch③ 用 F12 切换 Document Mode 到 IE11 标准模式再刷新。这三步做完客户演示时再也不用求着对方换 Chrome——毕竟真实生产环境里总有那么几台 Windows 7 电脑固执地开着 IE11。希望帮到你。本文还有配套的精品资源点击获取