简介:这是一份基于Java Web的博客系统课程设计完整资源包,面向计算机相关专业学生、Java初学者及需要完成课程设计的开发者,解决选题难、缺少可运行示例与配套说明的痛点。项目采用MVC开发模式,基于Eclipse/MyEclipse集成开发环境,运行在Tomcat服务器与MySQL数据库之上,覆盖三种用户角色:管理员可进行博主管理与在线统计,普通用户可注册登录、浏览与发表文章,博主可管理博文、类别、评论及修改密码,功能层次清晰,适合学习Servlet/JSP开发、JDBC操作及分层架构设计。资源共74个文件,压缩包仅802KB,包含21个JSP页面、13个Java源码文件及对应class文件、4个SQL数据库脚本,外加课程设计报告DOC文档、图片素材与项目配置文件,源码目录与WebRoot结构完整,导入IDE即可运行。已有1323人学习下载,既能用于日常练手,也可作为课程设计、毕业设计的参照模板,数据库脚本还原后即可体验完整流程。
1. 基于 Java Web 的博客系统:课设题目为什么卡在“闭环”
课程设计里真正翻车的往往不是代码量,而是项目没有闭环。基于 Java Web 的博客系统是 Java Web 课设里的常客:登录注册、发文、分类、评论,功能看着不多,但要把源码、数据库脚本和课程设计报告三样东西凑齐,并能相互印证,才是大多数人丢分的位置。这类以压缩包交付的题目,通常的预期是:导入数据库脚本能初始化,源码能部署,报告里的 ER 图和表结构能与代码对得上。需要这条路径的人有三类:第一次完整接触 Servlet/JSP 的学生,被拉去验收工程质量的工程师,以及负责指导的老师。后面按通常做法展开:先定需求边界,再落数据库表设计和初始化数据,接着写核心 Java Web 代码,最后讲部署验证和报告收口。工程师看建表和外键取舍,学生直接照搬能跑通的部分,互不耽误。
2. 博客系统的需求边界与 Java Web 选型
课设和技术采购一样,第一步不是写代码,而是把“做完”定义清楚。这个题目的功能边界其实很固定,把它拆开再看技术选型,后面写代码和写报告都会顺畅很多。
2.1 先定义最小功能集:登录注册、文章、分类、评论
我一般会把博客系统砍成两个面:面向浏览者的展示面,面向维护者的管理面。课程设计最少得有用户模块、文章模块、分类模块、评论模块,再加一个后台的简单管理。用户模块是头歌这类在线实验平台上最常见的一关:一个能注册、能登录、登录后能看到自己昵称、退出后不能继续访问后台的完整流程。登录注册界面的验收点不是页面漂亮,而是会话存在哪里、退出后 cookie 和 session 怎么清理。
在定功能时我会直接写成一张表格,这张表后面也会原样挪进课设报告的需求分析章节:
| 模块 | 功能点 | 验收标准 | 优先级 |
|---|---|---|---|
| 用户模块 | 注册、登录、退出、修改昵称 | 未登录访问后台被拦截 | 必做 |
| 文章模块 | 发表、编辑、删除、分页列表 | 标题、正文、发布时间完整显示 | 必做 |
| 分类模块 | 分类维护、按分类筛选文章 | 删除分类时提示文章数 | 建议做 |
| 标签模块 | 打标签、按标签聚合 | 一篇文章可对多个标签 | 选做 |
| 评论模块 | 发表评论、一级回复 | 文章详情页能看到评论列表 | 建议做 |
这段表格能同时回答“数据库里要有哪些表”和“代码要写哪些接口”两个问题。注意不要把“用户管理后台”写成“用户能自己给自己加管理员权限”,这在课设答辩里会被追问权限提升漏洞。
2.2 为什么用 Servlet/JSP + MyBatis 而不是一上来就 Spring Boot
很多人在选型这一步就直接跑偏。Spring Boot 用内嵌 Tomcat 启动方便,但它在 Servlet 之上封装了一层,课设报告里要讲请求生命周期、过滤器、会话保持这些原理时,Spring Boot 会把容器和请求链路藏得太深,反而不容易写出有深度的内容。更实际的问题是 JSP 在 Spring Boot 里不是默认技术栈,要么改依赖要么换模板引擎,对一个课程设计来说配置成本高于收益。
我习惯的方案是 Maven 打包成 war,用 Servlet + JSP + MyBatis 分层。数据访问层用 MyBatis 而不是裸 JDBC,能少写大量预编译和结果集映射代码,同时在报告里解释 SQL 也比解释 Hibernate 的级联逻辑容易。
<dependencies> <!-- Servlet API 由 Tomcat 提供,作用域必须为 provided --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency> </dependencies>这段配置里的关键参数有两个。第一,javax.servlet-api的scope必须写成provided,意思是编译时用 Servlet API,但打包时不把 Tomcat 自带的东西塞进 war,避免出现NoSuchMethodError之类的容器冲突。第二,mysql-connector-j的 groupId 是com.mysql而不是旧的mysql,MySQL 8 之后官方改了坐标,照抄旧教程会把驱动依赖打不进去。
2.3 运行环境与版本兼容矩阵
版本选型是本项目最容易出现“我代码没问题,就是跑不起来”的根源。Servlet、Tomcat、JDK、MySQL 四者的对应关系必须提前定好,否则会浪费大量时间在环境问题上。
| 组件 | 推荐版本 | 注意点 |
|---|---|---|
| JDK | 8 或 11 | 对应 Tomcat 9.x,使用 javax.servlet 包 |
| JDK | 17 或 21 | 对应 Tomcat 10.1+,代码 import 要改成 jakarta.servlet |
| Tomcat | 9.0.x | 课设最稳,网上资料最多 |
| MySQL | 8.0.x | 字符集用 utf8mb4 |
| MyBatis | 3.5.x | mapper XML 和注解二选一,别混用 |
| Navicat / IDEA Database | 任意 | 用来导入脚本和画 ER 图 |
如果电脑装的是 JDK 21,直接配合 Tomcat 10.1,代码里把javax.servlet.http.*替换成jakarta.servlet.http.*,逻辑完全一样。这个点值得在报告里写一句,说明你意识到不同版本的包名分水岭,比通篇写“JDK 21 环境配置教程”更有技术含量。
3. 数据库设计:博客系统的六张表与初始化脚本
数据库设计是这类题目的评分重头戏。博客系统的表结构不需要复杂,但关系要完整,字段类型要经得起追问。下面这套六张表方案是我常用的,能覆盖必做功能,又不至于把时间耗在过度设计上。
3.1 从需求到表:六张表的字段视角
需求表里的用户模块、文章模块、分类模块、标签模块、评论模块,正好对应六张表。核心关系是:用户一对多文章,文章多对一分类,文章和标签通过中间表多对多,评论挂在文章下面。
-- 先删从表,再删主表,避免外键报错 DROP TABLE IF EXISTS article_tag; DROP TABLE IF EXISTS comment; DROP TABLE IF EXISTS article; DROP TABLE IF EXISTS tag; DROP TABLE IF EXISTS category; DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID', `username` VARCHAR(50) NOT NULL COMMENT '登录名', `password` CHAR(64) NOT NULL COMMENT '密码散列值 sha256(salt+password)', `salt` CHAR(16) NOT NULL COMMENT '用户随机盐', `nickname` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '昵称', `avatar` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '头像URL', `role` TINYINT NOT NULL DEFAULT 1 COMMENT '角色:1普通用户,2管理员', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `category` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `name` VARCHAR(50) NOT NULL COMMENT '分类名称', `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值,越小越靠前', UNIQUE KEY `uk_category_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章分类表'; CREATE TABLE `tag` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `name` VARCHAR(50) NOT NULL, UNIQUE KEY `uk_tag_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签表'; CREATE TABLE `article` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `user_id` INT NOT NULL COMMENT '作者ID', `category_id` INT NOT NULL COMMENT '分类ID', `title` VARCHAR(100) NOT NULL COMMENT '标题', `summary` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '摘要', `content` LONGTEXT NOT NULL COMMENT '正文', `view_count` INT NOT NULL DEFAULT 0 COMMENT '浏览量', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY `idx_article_user` (`user_id`), KEY `idx_article_category` (`category_id`), CONSTRAINT `fk_article_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`), CONSTRAINT `fk_article_category` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ) 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`), KEY `idx_tag_id` (`tag_id`), CONSTRAINT `fk_at_article` FOREIGN KEY (`article_id`) REFERENCES `article` (`id`), CONSTRAINT `fk_at_tag` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签关联表'; CREATE TABLE `comment` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `article_id` INT NOT NULL, `user_id` INT NOT NULL, `content` VARCHAR(500) NOT NULL COMMENT '评论内容', `parent_id` INT DEFAULT NULL COMMENT '父评论ID,为空表示一级评论', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY `idx_comment_article` (`article_id`), CONSTRAINT `fk_comment_article` FOREIGN KEY (`article_id`) REFERENCES `article` (`id`), CONSTRAINT `fk_comment_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表';这套 DDL 里有几个参数不是随便写的。password字段用CHAR(64)而不是VARCHAR(64),因为 SHA-256 输出定长 64 位十六进制字符,定长类型在索引比较时开销更小。content用LONGTEXT是因为博客正文可能含有 Markdown 源码或 HTML 片段,TEXT类型上限 64KB 不够用。分类名和标签名都加了唯一索引,这是为了防止同一条后台数据以两种写法存在库里。
3.2 外键、索引与冗余字段:课设数据库的取舍
外键在生产环境里我基本不建,因为大流量下的插入和删除会被外键校验拖慢,而且拆库拆表后外键会变成维护负担。但课设项目我建议保留物理外键,原因很直接:答辩时能被问“数据完整性怎么保证”,你可以理直气壮回答“数据库层面有外键约束,应用层还有事务控制”,这是最容易讲清楚的方案。
冗余字段这个点体现出设计边界。文章表里没有冗余作者名和分类名,而是只存user_id和category_id,原因是分类名和昵称都可能被修改,冗余会让改名字时需要同步多张表。但view_count这种统计字段是冗余在文章表里的,它频繁被读取,每次去 count 关联表代价更高。冗余字段的正确用法是:读多写少的字段可以先冗余,写多读少的一定不要碰。标签查询的常见写法是SELECT t.name FROM tag t JOIN article_tag at ON t.id = at.tag_id WHERE at.article_id = ?,中间表不存任何业务字段,只存两边的 ID。
3.3 用 Navicat 导入 DDL 并初始化管理员账号
拿到脚本后,在 Navicat 建一个blog_db数据库,字符集选utf8mb4,排序规则选utf8mb4_general_ci,然后直接运行上面的 DDL。执行顺序是固定的:如果脚本里已经有数据,必须按从表到主表的顺序删表,否则外键约束会拦住DROP TABLE语句。
初始化管理员账号时,密码一定不能明文存。常见的做法是生成随机盐,把sha256(盐 + 密码)这个散列值存进数据库。注册接口里的密码加密工具类至少要有这个逻辑:
public static String sha256(String input) throws NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(input.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : hash) { sb.append(String.format("%02x", b)); } return sb.toString(); } // 注册时:salt = 随机16位字符串,password = sha256(salt + 明文密码) // 登录时:取用户salt,重新计算sha256(salt + 输入密码),和库中password比对String.format("%02x", b)的作用是把每个字节转成两位小写十六进制,保证长度正好是 64。登录校验时注意不能只比对密码,同一个用户每次登录都要用自己那条记录里的 salt 重新计算,而不是把整个表按用户名查出来后直接拿库里的 password 字段做对比变量。初始化管理员的 SQL 语句里的散列值,需要用上面的工具类先生成,再粘进 INSERT 语句,不要直接在 SQL 里写明文密码。
3.4 被追问最多的三个设计问题
答辩时数据库这块的问题基本集中在三处。第一,为什么文章和标签要多一张中间表而不是把标签存成逗号分隔的字符串?因为按标签聚合文章时,字符串字段要LIKE '%标签%'扫描,无法走索引,中间表可以直接 join。第二,为什么用户密码要加盐?因为两个人密码相同时散列值也不同,攻击者无法用彩虹表反查。第三,为什么评论表要留parent_id?这比只做一层评论多了一个字段的成本,却保留了以后扩展二级回复的空间,数据库设计要有可演进性。这三个问题在报告里写清楚,比堆功能更能体现设计能力。
4. Java Web 核心代码:会话、分页与安全过滤
项目题干里写了源码,那源码这部分就得经得起看。课设最常见的丢分点是:所有业务逻辑堆在 Servlet 里、SQL 写死在 Java 字符串里、登录拦截拿 session 判断后没有白名单。这一章把这三个点全部处理干净。
4.1 工程结构与三个关键文件
代码分层按 controller、service、mapper、entity、filter 五层来分,controller 里只做参数接收和视图跳转,业务逻辑放 service,数据库访问放 mapper。工程目录结构如下:
src/main/java/com/course/blog ├── controller/ # Servlet,负责 HTTP 请求入口 ├── service/ # 业务接口和实现 ├── mapper/ # MyBatis 的 Mapper 接口 ├── entity/ # User、Article、Comment 等实体 ├── filter/ # 登录过滤器和编码过滤器 └── util/ # 加密、分页等工具类 src/main/resources └── mybatis-config.xml src/main/webapp ├── WEB-INF/jsp/ # 页面文件 ├── static/ # css、js、图片 └── index.jspfilter 包要放两个类:字符编码过滤器,统一处理 POST 请求的 UTF-8;登录过滤器,处理会话校验。字符编码过滤器只需要三行代码,但漏掉它中文乱码会让整个课设看起来极不专业。
4.2 登录会话与权限控制:SessionFilter 实用写法
登录模块的常见误用是只在前端隐藏“写文章”按钮,后端所有 Servlet 都不校验会话,或者校验了但把静态资源和登录接口也拦了,导致登录页的 CSS 加载不出来,形成“登录框出现了但没有样式”的尴尬局面。下面这个过滤器是课设里够用的解决方式:
@WebFilter("/*") public class LoginFilter implements Filter { private static final List<String> WHITE_LIST = Arrays.asList( "/login.jsp", "/register.jsp", "/api/login", "/api/register", "/static/", "/article/list", "/article/detail" ); @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 白名单直接放行,注意用 contains 而不是 equals,兼容 /static/css/style.css for (String prefix : WHITE_LIST) { if (uri.contains(prefix)) { chain.doFilter(request, response); return; } } Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }这个过滤器有两个参数值得注意。/static/的匹配用contains,可以兼容/static/css/style.css这种多级路径;request.getContextPath()是必须的前缀,项目部署到 Tomcat 时访问路径里会带工程名,写死重定向 URL 会导致 404。登录成功后应该在 session 里放User对象而不是只放用户名,这样 JSP 里直接取loginUser.nickname就能展示,省去页面里再向数据库查询用户信息的多余 SQL。
4.3 文章列表分页:PageBean 与 LIMIT 偏移量
分页查询是博客系统的必做项,也是分页 bug 的重灾区。我一般用 PageBean 封装分页参数,SQL 只关心两个值:offset和pageSize,前端负责传pageNum。
public class PageBean<T> { private int pageNum; // 当前页码,从 1 开始 private int pageSize; // 每页条数 private long total; // 总记录数 private List<T> rows; // 当前页数据 } // Service 层计算偏移量 int offset = (pageNum - 1) * pageSize;对应的 MyBatis mapper XML 里是这条 SQL:
<select id="selectArticlePage" resultType="Article"> SELECT id, title, summary, view_count, create_time FROM article ORDER BY id DESC LIMIT #{offset}, #{pageSize} </select>分页参数表给出来,方便对照:
| 参数 | 含义 | 示例 |
|---|---|---|
| pageNum | 第几页,从 1 开始 | 2 |
| pageSize | 每页条数,建议 5 或 10 | 10 |
| offset | 偏移量,(pageNum - 1) * pageSize | 10 |
| total | 总记录数,用 COUNT(*) 查询 | 25 |
pageNum 从 0 开始传是分页最常见的错误,会导致第一页数据永远少十条。另外必须注意ORDER BY id DESC不要去掉,否则数据更新后翻页会出现同一篇文章出现在两页里的情况。如果想要热门文章置顶,就改成ORDER BY view_count DESC, id DESC,多字段排序是课设里能写进报告的一个亮点。
4.4 XSS 过滤:编辑器与展示之间的一层保险
填了富文本编辑器的项目,必须处理 XSS 注入。一个最简单的防守手段是展示层做 HTML 转义,把用户输入的内容里的尖括号、引号变成实体字符。Java 里的替换工具类如下:
public static String cleanContent(String input) { if (input == null) { return ""; } return input.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); }注意替换顺序:必须先把&替换成&,否则第二次替换产生的&&会被重复加工。如果博客系统需要支持 Markdown 渲染,只做转义会把代码块和链接也转义掉,这时需要在渲染层把白名单允许的标签还原,比如只允许<code>、<pre>、<a>这类安全标签,其余全部转义。JSP 页面里显示文章标题时也应该用<c:out value="${article.title}"/>而不是${article.title},JSTL 这个标签会默认转义文本内容,是页面层最省事的保险。
5. 在 Tomcat 上跑通并写进课设报告
这一章解决两个收尾问题:环境跑不起来时最快怎么定位,报告里怎么写才能让代码和数据库“对得上”。
5.1 部署参数与常见报错对照表
Maven 项目打包部署的标准流程是:mvn clean package生成 war 包,把 war 复制到 Tomcat 的 webapps 目录,启动 Tomcat 后自动解压部署。命令行方式运行可以看实时日志:
mvn clean package cp target/blog.war /path/to/tomcat/webapps/ /path/to/tomcat/bin/startup.sh /path/to/tomcat/bin/catalina.sh runcatalina.sh run会把日志输出到控制台而不是写进 catalina.out,排查启动报错时信息更直观。部署最常遇到的四类问题整理成对照表:
| 报错/现象 | 原因 | 修复 |
|---|---|---|
| ClassNotFoundException: com.mysql.cj.jdbc.Driver | 数据库驱动 jar 没进 WEB-INF/lib | 确认 MySQL connector 依赖是 compile 作用域 |
| Communications link failure | 数据库连接 URL 或端口错误 | 检查 jdbc:mysql://localhost:3306/blog_db |
| 中文全变问号 | 连接 URL 缺编码参数、页面编码不一致 | URL 末尾加 useUnicode=true&characterEncoding=UTF-8 |
| 登录成功后一直跳回登录页 | session 里没放 loginUser 或过滤器白名单写死 | 检查登录 Servlet 的 setAttribute 代码 |
连接 URL 这个参数值得多说一句:characterEncoding=UTF-8是 MySQL 连接层的字符集声明,它管的是 Java 程序往 MySQL 发送数据的编码,和数据库建库时的utf8mb4是两层东西,两个都要对才不会乱码。
5.2 报告别贴假 ER 图:用这组证据链收口
报告里数据库设计章节最常见的扣分点,是截图里的 ER 图和实际建表 SQL 对不上。避免这个问题的做法是在 Navicat 的模型页面,直接根据六张表生成关系图,再导出成图片插入报告。注意导出的模型要和 DDL 脚本是同一次改动的产物,不要在写完报告后又改了表结构却不重新截图。
报告里建议放三样东西:数据字典表、ER 图、测试用例表。数据字典用表格列出字段类型和说明,测试用例表按功能列步骤,格式是“输入什么、预期结果、实际结果”。答辩的时候拿出 Navicat,把 DDL 脚本从头到尾重新执行一遍能成功,再打开后台页面演示一遍发文和评论,比解释十页架构图都更能说明问题。
本文还有配套的精品资源,点击获取