简介:这份资源是面向计算机相关专业在校学生与JavaWeb初学者的一套个人博客系统课程设计完整方案,可用于期末大作业、课程设计或毕业设计参考。项目基于JavaWeb技术栈实现,涵盖用户登录注册、文章发布与管理、头像修改、收藏与评论等典型博客功能,代码经过实际运行测试,答辩评审平均分达到96分。压缩包共128个文件,约12.38MB,包含17个java源文件、17个class编译文件、13个css样式、12个js脚本以及sql建库脚本、pdf报告文档和md说明文件,另附jar依赖与图片字体等静态资源,结构完整、层次清晰。目前已有560人学习下载。读者可据此获得一套可直接运行的博客系统源码、数据库脚本与配套文档,既能对照学习JavaWeb分层开发与数据库操作思路,也便于在此基础上修改扩展功能,适合作为课设、毕设或项目立项演示的参考模板。
1. 个人博客系统:从课程设计到能写进简历的完整交付物
每年期末,总有一批 JavaWeb 课程设计的选题落在「个人博客系统」上。原因很直接:它覆盖了用户登录注册、文章增删改查、分类标签、评论互动、分页搜索这几块 Web 开发的核心动作,又不像电商那样牵扯订单、库存、支付等复杂状态机。但真正动手时,多数人卡在同一个地方——代码能跑,但说不清为什么这么分层;SQL 建了表,但字段类型和索引全凭感觉;文档和报告更是最后两天拼凑出来的。这篇笔记就围绕「个人博客系统 + 源代码 + 文档说明 + SQL + 报告 PDF」这套完整交付物,把从环境搭建、数据库设计、分层实现到文档撰写的路径拆开讲。适合正在做 JavaWeb 课程设计的学生,也适合想用一个完整案例复习 Servlet/JSP/Spring Boot 的开发者。下面说的每一步,都是我带过几届课设后觉得最省事、也最经得起答辩追问的做法。
2. 技术选型与工程骨架:Servlet/JSP 还是 Spring Boot
2.1 两种路线的取舍判断
课程设计最常见的两种技术栈:一种是纯 Servlet + JSP + JDBC,另一种是 Spring Boot + MyBatis + Thymeleaf。选哪个不取决于哪个更「高级」,而取决于你的验收标准和答辩老师关注什么。
如果课程大纲只讲到 Servlet 和 JSP,用纯 Servlet 路线反而更稳。老师能看到你手写doGet/doPost、手动管理HttpSession、自己封装 JDBC 工具类,这些是课程知识点的直接体现。缺点是代码量大,一个博客系统的 CRUD 写下来,DAO 层加 Servlet 层轻松超过两千行。
如果课程允许或鼓励用框架,Spring Boot 路线能把开发时间压缩到三分之一。spring-boot-starter-web加mybatis-spring-boot-starter,再配一个application.yml,项目骨架就起来了。但要注意:答辩时老师很可能追问「自动配置帮你做了什么」,如果答不上来,反而扣分。
我一般建议:课程大纲覆盖到哪,就用哪条路线。想稳拿分选 Servlet/JSP,想省时间且能讲清原理选 Spring Boot。两条路线的数据库设计、SQL 脚本、文档结构是通用的,后面几章的内容对两者都适用。
2.2 用 Maven 搭出可运行的工程骨架
不管选哪条路线,工程结构都建议按分层来组织。下面是一个 Maven 项目的pom.xml核心依赖配置,以 Spring Boot 路线为例:
<!-- pom.xml 核心依赖,Spring Boot 2.7.x + MyBatis + MySQL --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <!-- Web 层:内嵌 Tomcat + Spring MVC --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 持久层:MyBatis 与 Spring Boot 整合 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <!-- MySQL 驱动,注意 8.x 版本驱动类名带 cj --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- 模板引擎,用于服务端渲染页面 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies>这段配置里三个参数值得说明。spring-boot-starter-parent的版本决定了整个依赖树的版本对齐,课设环境用 2.7.x 比 3.x 稳,因为 3.x 要求 JDK 17 且部分教程还没跟上。MyBatis starter 的版本要和 Spring Boot 版本匹配,2.3.x 对应 Boot 2.7。MySQL 驱动 8.0.33 是 8.x 系列里兼容性较好的一个,驱动类名是com.mysql.cj.jdbc.Driver,不是老教程里的com.mysql.jdbc.Driver,写错会直接报ClassNotFoundException。
对应的application.yml数据源配置:
# application.yml 数据源与 MyBatis 配置 spring: datasource: url: jdbc:mysql://localhost:3306/blog_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml # XML 映射文件位置 type-aliases-package: com.example.blog.entity # 实体类别名包 configuration: map-underscore-to-camel-case: true # 下划线字段自动映射驼峰属性serverTimezone=Asia/Shanghai这个参数不加,MySQL 8 会报时区错误。map-underscore-to-camel-case打开后,数据库的create_time能自动映射到实体类的createTime,省掉大量resultMap配置。这两个是新手最容易漏、又最容易排查半天的点。
2.3 包结构怎么分才经得起追问
分层不是形式主义,答辩时老师看包结构就能判断你是否理解 MVC。推荐这样分:
com.example.blog ├── controller // 接收请求,参数校验,返回视图或 JSON ├── service // 业务逻辑,事务边界 │ └── impl ├── mapper // 数据访问接口,对应 XML ├── entity // 数据库实体,与表一一对应 ├── dto // 数据传输对象,用于接收前端参数 ├── vo // 视图对象,用于返回给页面 └── config // 拦截器、跨域等配置entity和dto分开是关键。很多课设把前端传的参数直接塞进实体类,结果注册接口把id、createTime也暴露出去,被老师问「用户能不能自己传 id 覆盖」就答不上来。dto只包含前端该传的字段,entity只对应数据库表,职责清晰。
3. 数据库设计:五张表撑起一个博客系统
3.1 表结构与字段类型选择
个人博客系统的核心表不多,五张就够:用户表、文章表、分类表、标签表、评论表。下面给出建表 SQL,以 MySQL 8 为例:
-- 用户表:存储登录信息与基本资料 CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键', `username` VARCHAR(50) NOT NULL COMMENT '登录名', `password` VARCHAR(100) NOT NULL COMMENT '密码,存加密后值', `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称', `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 文章表:博客核心内容 CREATE TABLE `article` ( `id` INT NOT NULL AUTO_INCREMENT, `title` VARCHAR(200) NOT NULL COMMENT '标题', `content` TEXT NOT NULL COMMENT '正文,支持较长内容', `user_id` INT NOT NULL COMMENT '作者ID', `category_id` INT DEFAULT NULL COMMENT '分类ID', `view_count` INT DEFAULT 0 COMMENT '浏览量', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_category_id` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章表'; -- 分类表 CREATE TABLE `category` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL COMMENT '分类名', PRIMARY KEY (`id`), UNIQUE KEY `uk_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分类表'; -- 标签表 CREATE TABLE `tag` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_tag_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签表'; -- 文章标签关联表:多对多 CREATE TABLE `article_tag` ( `article_id` INT NOT NULL, `tag_id` INT NOT NULL, PRIMARY KEY (`article_id`, `tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签关联表'; -- 评论表 CREATE TABLE `comment` ( `id` INT NOT NULL AUTO_INCREMENT, `article_id` INT NOT NULL COMMENT '所属文章', `user_id` INT NOT NULL COMMENT '评论人', `content` VARCHAR(500) NOT NULL COMMENT '评论内容', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_article_id` (`article_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表';几个字段选择需要解释。content用TEXT而不是VARCHAR(5000),因为博客正文可能超过 VARCHAR 的实用长度,TEXT 类型在 InnoDB 里存储更合理。password给到 100 长度,是因为如果做 MD5 或 BCrypt 加密,密文长度会超过原始密码,留足空间。article表的user_id和category_id都建了普通索引,因为列表页按作者或分类筛选是高频查询。article_tag用联合主键而不是自增 id,因为关联表本身不需要独立标识,联合主键天然防重复。
3.2 索引与查询性能的取舍
课设数据量小,索引的效果看不出来,但答辩时老师可能问「如果文章上万篇,列表页怎么优化」。这时候能说出索引设计思路就是加分项。
文章列表页最常见的查询是「按创建时间倒序分页」。如果create_time没有索引,MySQL 需要全表扫描再排序。加一个索引:
-- 为文章列表的时间倒序查询加索引 ALTER TABLE `article` ADD INDEX `idx_create_time` (`create_time`);但要注意,如果查询条件是「先按分类筛选再按时间排序」,单列索引idx_create_time帮助有限,因为 MySQL 会先走idx_category_id过滤再排序。更优的做法是建联合索引:
-- 联合索引:分类 + 创建时间,覆盖「分类下按时间倒序」的查询 ALTER TABLE `article` ADD INDEX `idx_category_time` (`category_id`, `create_time`);联合索引遵循最左前缀原则,category_id在前、create_time在后,正好匹配「where category_id = ? order by create_time desc」这种查询模式。这个点在文档里写清楚,比单纯贴建表语句更能体现设计能力。
3.3 初始化数据与 SQL 脚本组织
交付物里的 SQL 文件建议分成两个:schema.sql放建表语句,data.sql放初始化数据。初始化数据至少包含一个管理员账号和几条示例文章,方便验收时直接登录演示。
-- data.sql 初始化数据 INSERT INTO `user` (`username`, `password`, `nickname`, `email`) VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', '管理员', 'admin@example.com'); -- 上面密码是 123456 的 MD5 值,演示用,实际项目应加盐 INSERT INTO `category` (`name`) VALUES ('技术笔记'), ('生活随笔'), ('读书摘要'); INSERT INTO `article` (`title`, `content`, `user_id`, `category_id`) VALUES ('第一篇博客', '这是初始化示例文章内容。', 1, 1), ('Servlet 学习笔记', '记录 Servlet 生命周期与请求处理流程。', 1, 1);密码存 MD5 值而不是明文,这是文档里必须体现的安全意识。虽然 MD5 本身已不够安全,但课设层面能说出「不存明文、用哈希」就已经到位。如果想让文档更完整,可以补一句「生产环境应使用 BCrypt 加盐哈希」。
4. 核心功能实现:登录、文章 CRUD 与分页
4.1 登录与 Session 管理
登录功能是博客系统的入口,也是答辩必看的地方。核心逻辑是:接收用户名密码,查库比对,成功则写入 Session,失败返回错误提示。
// UserController.java 登录接口(Spring Boot 路线) @PostMapping("/login") public String login(@RequestParam String username, @RequestParam String password, HttpSession session, Model model) { // 对前端传来的明文密码做 MD5,与库中密文比对 String md5Pwd = DigestUtils.md5DigestAsHex(password.getBytes()); User user = userService.findByUsernameAndPassword(username, md5Pwd); if (user == null) { model.addAttribute("error", "用户名或密码错误"); return "login"; // 返回登录页,携带错误信息 } // 登录成功,用户信息写入 Session,后续拦截器据此判断登录态 session.setAttribute("loginUser", user); return "redirect:/article/list"; }这段代码有三个关键点。第一,密码比对在服务端做 MD5,前端传的是明文,但传输层如果没上 HTTPS,明文在网络中仍可见,课设层面至少要在文档里提到这一点。第二,Session 里存的是整个User对象,后续页面可以直接取loginUser.nickname显示。第三,登录失败返回同一个视图并带error属性,而不是抛异常,这样页面能友好提示。
配套的登录拦截器:
// LoginInterceptor.java 拦截未登录请求 public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); if (session.getAttribute("loginUser") == null) { // 未登录,重定向到登录页 response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; // 已登录,放行 } }拦截器注册时排除登录页、注册页和静态资源路径,否则登录页本身也会被拦截,形成死循环。这个坑几乎每届都有人踩。
4.2 文章发布与富文本处理
文章发布涉及表单提交和内容存储。如果只是纯文本,<textarea>提交即可。如果想支持富文本,课设层面常用的是引入一个轻量编辑器,提交 HTML 内容。
// ArticleController.java 发布文章 @PostMapping("/article/save") public String saveArticle(@ModelAttribute ArticleDTO dto, HttpSession session) { User loginUser = (User) session.getAttribute("loginUser"); Article article = new Article(); article.setTitle(dto.getTitle()); article.setContent(dto.getContent()); article.setUserId(loginUser.getId()); article.setCategoryId(dto.getCategoryId()); articleService.save(article); // service 层处理事务与标签关联 return "redirect:/article/list"; }ArticleDTO只包含title、content、categoryId三个字段,userId从 Session 取而不是前端传,防止越权。articleService.save里如果涉及标签关联,需要在一个事务里完成文章插入和关联表插入,否则可能出现文章存了但标签没存的不一致状态。
// ArticleServiceImpl.java 保存文章并关联标签 @Transactional public void save(Article article) { articleMapper.insert(article); // 插入后 article.id 由主键回填 if (article.getTagIds() != null) { for (Integer tagId : article.getTagIds()) { articleTagMapper.insert(article.getId(), tagId); } } }@Transactional保证两步操作要么都成功要么都回滚。MyBatis 的useGeneratedKeys="true"配置能让插入后自动回填主键到实体,这样关联表才能拿到article.getId()。
4.3 分页查询与列表展示
分页是列表页的核心。课设层面有两种做法:手动LIMIT offset, size加总数查询,或用 PageHelper 插件。手动方式更能体现对 SQL 的理解,推荐在文档里写清楚。
-- 分页查询:查当前页数据 SELECT a.id, a.title, a.view_count, a.create_time, u.nickname, c.name AS category_name FROM article a LEFT JOIN user u ON a.user_id = u.id LEFT JOIN category c ON a.category_id = c.id ORDER BY a.create_time DESC LIMIT #{offset}, #{pageSize}; -- 查总数,用于计算总页数 SELECT COUNT(*) FROM article;offset的计算方式是(pageNum - 1) * pageSize。LEFT JOIN保证即使作者或分类被删,文章仍能显示。Service 层把这两个查询结果组装成一个分页对象:
// PageResult.java 分页结果封装 public class PageResult<T> { private List<T> list; // 当前页数据 private int total; // 总记录数 private int pageNum; // 当前页码 private int pageSize; // 每页条数 private int totalPages; // 总页数,由 total 和 pageSize 计算 public PageResult(List<T> list, int total, int pageNum, int pageSize) { this.list = list; this.total = total; this.pageNum = pageNum; this.pageSize = pageSize; this.totalPages = (total + pageSize - 1) / pageSize; // 向上取整 } // getter/setter 省略 }总页数用(total + pageSize - 1) / pageSize向上取整,避免最后一页数据被截断。这个公式在文档里写出来,比直接调插件更能说明你理解分页原理。
5. 避坑与排查:课设里最容易翻车的五个点
5.1 中文乱码:从数据库到页面的全链路排查
现象:文章标题存进数据库变成问号,或者页面显示乱码。
原因:乱码可能出现在三个环节——数据库字符集、连接 URL 字符集、页面编码。MySQL 8 默认字符集是utf8mb4,但建库时如果没指定,可能继承成latin1。连接 URL 没加characterEncoding=utf8也会导致传输乱码。JSP 页面没写<%@ page contentType="text/html;charset=UTF-8" %>同样会乱。
解决:建库时显式指定CREATE DATABASE blog_db DEFAULT CHARSET utf8mb4;,连接 URL 加useUnicode=true&characterEncoding=utf8,JSP 页面头部声明 UTF-8。三处都对齐后,乱码基本消失。如果还有问题,用SHOW VARIABLES LIKE 'character%';检查数据库实际字符集。
5.2 静态资源 404:拦截器把 CSS 和 JS 也拦了
现象:登录页能打开,但样式全丢,F12 看到 CSS 和 JS 请求返回 404 或重定向到登录页。
原因:登录拦截器配置时只写了拦截路径/**,没排除静态资源路径。浏览器请求/css/style.css时被拦截器判定为未登录,重定向到登录页,导致样式加载失败。
解决:注册拦截器时用excludePathPatterns排除静态资源目录:
// WebConfig.java 拦截器注册,排除静态资源与登录注册页 registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/register", "/css/**", "/js/**", "/images/**");Spring Boot 默认静态资源放在src/main/resources/static/下,对应 URL 路径就是/css/**这种。排除后样式正常加载。
5.3 事务不生效:方法内部调用绕过了代理
现象:文章保存时标签关联失败,但文章本身存进去了,数据不一致。
原因:在同一个 Service 类里,一个非事务方法直接调用了本类的事务方法,Spring AOP 代理没生效,@Transactional形同虚设。
解决:把事务方法拆到另一个 Service 类,或者通过注入自身代理调用。课设层面最简单的做法是确保@Transactional标注的方法是被 Controller 直接调用的入口方法,不在类内部嵌套调用。
5.4 分页最后一页数据重复或丢失
现象:翻到最后一页,数据条数和预期不符,或者和上一页有重复。
原因:offset计算错误,或者排序字段有相同值导致 MySQL 返回顺序不稳定。
解决:确认offset = (pageNum - 1) * pageSize。排序时如果create_time有相同值,加一个次级排序字段ORDER BY create_time DESC, id DESC,保证顺序确定。
5.5 密码明文存储与 SQL 注入
现象:数据库里能看到明文密码;登录框输入' OR '1'='1能绕过验证。
原因:密码没加密直接存;SQL 用字符串拼接而不是预编译参数。
解决:密码存 MD5 或 BCrypt 哈希值。所有 SQL 用#{}占位符(MyBatis)或PreparedStatement,不要用${}拼接用户输入。这两点在文档的「安全设计」小节里写清楚,答辩时是明确的加分项。
6. 文档与报告:让交付物经得起翻看
6.1 文档说明该写什么
课程设计交付物里的「文档说明」不是 README 的复制粘贴,而是一份能让老师快速理解项目结构、运行方式、功能清单的说明文件。建议包含这几块:项目简介(一段话说清做了什么)、技术栈(列出框架和版本)、环境要求(JDK、MySQL、Maven 版本)、部署步骤(从导入 SQL 到启动项目的完整命令)、功能清单(按模块列出已实现功能)、目录结构说明(每个包负责什么)。
部署步骤要写到「复制粘贴就能跑」的程度:
# 1. 创建数据库并导入 SQL mysql -u root -p -e "CREATE DATABASE blog_db DEFAULT CHARSET utf8mb4;" mysql -u root -p blog_db < schema.sql mysql -u root -p blog_db < data.sql # 2. 修改 application.yml 中的数据库密码 # 3. Maven 构建并启动 mvn clean package -DskipTests java -jar target/blog-0.0.1-SNAPSHOT.jar # 4. 浏览器访问 # http://localhost:8080/login 默认账号 admin / 123456这段命令里-DskipTests是跳过测试加速构建,课设项目通常没有测试用例,加上能省时间。默认账号密码写在文档里,方便验收。
6.2 报告 PDF 的结构与写法
报告 PDF 通常有固定模板,但内容质量差距很大。常见结构是:需求分析、系统设计、数据库设计、功能实现、测试、总结。其中「系统设计」和「数据库设计」是老师重点看的部分。
系统设计里画一张分层架构图(用文字描述或表格代替图形),说明 Controller、Service、Mapper 各层职责。数据库设计里放 E-R 关系描述和表结构表格:
| 表名 | 字段数 | 主键 | 外键/索引 | 说明 |
|---|---|---|---|---|
| user | 6 | id | uk_username | 用户信息 |
| article | 8 | id | idx_user_id, idx_category_id | 文章内容 |
| category | 2 | id | uk_name | 分类 |
| tag | 2 | id | uk_tag_name | 标签 |
| article_tag | 2 | (article_id, tag_id) | 联合主键 | 多对多关联 |
| comment | 5 | id | idx_article_id | 评论 |
功能实现部分不要贴大段代码,而是用「流程描述 + 关键代码片段」的方式。比如登录功能,写清楚「接收参数 → 加密比对 → 写入 Session → 重定向」,再附上核心方法签名即可。测试部分列出测试用例和预期结果,比如「输入错误密码,预期提示用户名或密码错误,实际结果一致」。
6.3 答辩时最容易被追问的三个问题
第一个:「你的项目里哪些地方用了事务,为什么?」答文章保存时文章表和关联表要一起成功或一起失败,所以用@Transactional。
第二个:「密码为什么存哈希,用的什么算法?」答避免明文泄露,课设用 MD5,生产环境应换 BCrypt 加盐。
第三个:「如果文章量很大,列表页怎么优化?」答加联合索引(category_id, create_time),分页用LIMIT,避免全表扫描。
这三个问题提前准备好,答辩基本稳。我自己的习惯是:文档写完后再通读一遍,把每个「为什么这么设计」的地方都标出来,这些就是老师可能追问的点。希望帮到你。
本文还有配套的精品资源,点击获取