简介这是一套基于SpringBoot 2.x、Thymeleaf与MyBatis-Plus开发的完整个人博客系统面向Java初学者与毕业设计学生提供可运行、可学习、可拓展的全栈Web项目实践范例。资源包共2899个文件涵盖360个HTML页面、875个JS脚本、213个CSS样式、128个Java后端类及273个PNG等静态资源整体结构体现典型MVC分层设计支持Markdown文档编辑、博客目录自动生成等实用功能压缩包大小为104.24MB。已有791人学习下载适合用于课程设计、毕设参考或SpringBoot入门进阶训练。读者可直接部署运行深入理解Thymeleaf模板集成、MyBatis-Plus动态SQL、Tomcat部署流程及前后端混合开发模式项目中还包含配置文件yml/yaml、构建脚本sh/bat及前端资源ts/svg/woff便于分析工程组织规范与常见技术选型逻辑。1. 这不是又一个“Hello World”博客它真能跑通登录、发文章、分页、搜索、标签聚合且代码结构经得起答辩拷问你搜“SpringBoot 博客系统 毕业设计”页面刷出几十个 GitHub 仓库——点开 README八成写着“基于 SpringBoot Thymeleaf 开发的个人博客”但 clone 下来mvn clean install就报错application.yml缺数据库密码占位符UserMapper接口没配MapperScanThymeleaf 模板里${session.user.nickname}硬编码却没做非空校验更别提搜索结果为空时连个友好提示都没有。这不是教学 Demo是毕业答辩现场被导师一句“你这搜索走的是 like 全表扫线上扛得住吗”直接问哑火的实战系统。这份资源不一样它用 MyBatis-Plus 的QueryWrapper封装了带条件组合的搜索逻辑Thymeleaf 模板层统一处理了空 session、空列表、分页越界三类边界SpringBoot 配置文件已预置 H2 内存库MySQL 双模式开关连Transactional在哪加、Valid校验失败后怎么跳回原页面都写在注释里。适合正在赶毕设进度、需要可运行基线代码、又不想被“为什么不用 Vue 做前后端分离”这种问题带偏主线的开发者——它不炫技但每行代码都在回答“答辩时怎么讲清楚”。2. 从零拉起项目环境准备、模块拆解与核心配置落地2.1 环境依赖与版本锚定为什么选 SpringBoot 2.7.x 而非 3.x本项目基于 SpringBoot 2.7.182023 年 LTS 版本构建而非最新 3.x。原因很实际Thymeleaf 3.1.x 对 SpringBoot 3 的响应式 WebFlux 支持尚不成熟而本系统所有页面交互如评论提交、标签筛选均依赖同步请求重定向POST-Redirect-GET强行升 3.x 会导致ModelAndView返回异常、th:object绑定失效。MyBatis-Plus 3.5.3.1 是兼容 SpringBoot 2.7 的最终稳定版其LambdaQueryWrapper对Article::getCreateTime的类型推导比 4.x 更鲁棒避免答辩时被问“为什么这里用字符串字段名而不是 Lambda 表达式”。JDK 必须为 11非 17 或 8因 H2 数据库驱动 2.1.214 在 JDK 17 下存在TIME类型序列化 Bug而毕业设计演示环境多为 Windows JDK 11 默认安装。提示若本地已装 JDK 17请勿全局切换。在 IDE 中为本项目单独指定 JDK 11IntelliJ IDEAFile → Project Structure → Project → Project SDKEclipse右键项目 → Properties → Java Build Path → Libraries → JRE System Library → Edit → Alternate JRE。2.2 项目结构解析五个模块如何各司其职又无缝串联项目采用标准 Maven 多模块结构但未过度分层——毕业设计需平衡可读性与复杂度。关键模块如下模块名职责关键文件示例为什么这样设计blog-api控制器层接收 HTTP 请求、调用 service、返回视图或 JSONArticleController.java含/article/list,/article/detail/{id}所有 Controller 方法返回StringThymeleaf 视图名或ResponseEntityAJAX 接口避免混用ResponseBody和Controller导致视图解析失败blog-service业务逻辑层事务管理、权限校验、数据组装ArticleServiceImpl.java含saveWithTags()方法先存文章再批量插入article_tag关联记录Service类中Transactional注解精确到方法级如publishArticle()加事务getHotArticles()不加体现对事务边界的理解blog-dao数据访问层MyBatis-Plus Mapper 接口与 XML仅需 SQL 复杂时TagMapper.java继承BaseMapperTag、ArticleTagMapper.xml含foreach批量插入ArticleMapper使用TableName(t_article)显式指定表名避免TableId字段名与数据库id不一致导致的主键回填失败blog-common公共组件工具类、异常处理器、统一返回对象GlobalExceptionHandler.java捕获ValidationException并跳转至/error/validate页面自定义ResultT包装类code200表示成功code500表示服务异常答辩时可说明“比裸写MapString, Object更利于前端统一处理”blog-webWeb 配置层Thymeleaf 配置、静态资源映射、拦截器WebConfig.java注册LoginInterceptor排除/login,/register,/static/**ThymeleafViewResolver设置setCache(false)开发期但application-prod.yml中强制spring.thymeleaf.cachetrue体现环境差异意识2.3 核心配置三件套application.yml、mybatis-plus配置、Thymeleaf 模板路径application.yml不是堆参数而是按场景分 profile。开发时启用dev使用 H2 内存库无需安装 MySQL答辩演示时切prod连接真实 MySQL。关键配置如下# application-dev.yml spring: datasource: url: jdbc:h2:mem:blogdb;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true # 启用 H2 Console地址 http://localhost:8080/h2-console path: /h2-console mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印 SQL答辩时可展示查询过程 global-config: db-config: id-type: assign_id # 使用雪花算法生成 ID避免自增主键暴露数据量Thymeleaf 模板路径必须显式声明否则默认src/main/resources/templates无法加载static下的 CSS/JS// blog-web/src/main/java/config/WebConfig.java Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/static/**) .addResourceLocations(classpath:/static/); // 映射静态资源 } }src/main/resources/templates/layout.html是所有页面的母版用th:fragmentcontent定义内容区子页面通过th:replace~{layout :: content}注入确保导航栏、页脚复用——这比每个 HTML 都复制粘贴header更符合工程规范答辩时可指出“减少重复代码提升维护性”。3. 功能闭环验证从用户注册到文章发布每一步都踩过坑3.1 用户注册与登录密码加密、Session 管理与跨域陷阱注册流程看似简单但三个细节决定成败密码不可逆加密使用BCryptPasswordEncoder非 MD5 或 SHA-256UserServiceImpl.register()中调用passwordEncoder.encode(rawPassword)存库Session 有效期控制LoginController.login()成功后执行session.setMaxInactiveInterval(30 * 60);30 分钟避免答辩演示时因超时被登出尴尬Thymeleaf 表单 CSRF 防护所有 POST 表单必须含input typehidden th:name${_csrf.parameterName} th:value${_csrf.token}/否则 Spring Security 会拦截——这是答辩高频翻车点因很多教程省略此步。登录成功后将用户信息存入 Session 而非 Cookie防止敏感信息泄露// LoginController.java PostMapping(/login) public String login(RequestParam String username, RequestParam String password, HttpSession session, RedirectAttributes redirectAttributes) { User user userService.findByUsername(username); if (user ! null passwordEncoder.matches(password, user.getPassword())) { session.setAttribute(user, user); // 存入 session session.setMaxInactiveInterval(30 * 60); return redirect:/article/list; // 重定向防止 F5 刷新重复提交 } else { redirectAttributes.addFlashAttribute(error, 用户名或密码错误); return redirect:/login; } }redirectAttributes.addFlashAttribute()是关键它将错误信息暂存于 Session在重定向后的login.html中通过th:if${error}渲染提示且下次请求自动清除——比Model.addAttribute()更安全避免 GET 请求携带错误参数。3.2 文章发布与标签管理一对多关联的 MyBatis-Plus 实现发布文章需同时处理t_article主表和t_article_tag关联表。MyBatis-Plus 不支持直接保存一对多关系需手动拆解// ArticleServiceImpl.java Transactional Override public boolean saveWithTags(Article article, ListLong tagIds) { // 1. 先保存文章获取生成的 ID boolean saveResult this.save(article); if (!saveResult) return false; // 2. 清空旧标签先删后插简化逻辑 articleTagMapper.delete(Wrappers.ArticleTaglambdaQuery() .eq(ArticleTag::getArticleId, article.getId())); // 3. 批量插入新标签 if (CollectionUtils.isNotEmpty(tagIds)) { ListArticleTag articleTags tagIds.stream() .map(tagId - { ArticleTag at new ArticleTag(); at.setArticleId(article.getId()); at.setTagId(tagId); return at; }) .collect(Collectors.toList()); return articleTagMapper.insertBatchSomeColumn(articleTags) 0; } return true; }此处insertBatchSomeColumn()是 MyBatis-Plus 3.5 的高效批量插入方法比循环insert()减少 90% SQL 执行次数。答辩时可对比说明“若用传统 for 循环发 5 个标签需 5 条 INSERT用批量插入仅 1 条 SQL且Transactional保证原子性”。3.3 前台文章列表与分页Thymeleaf MyBatis-Plus Page 的协同列表页需支持按分类、标签、关键词搜索且分页参数必须透传。ArticleController.list()方法接收PageArticle参数并返回模型GetMapping(/article/list) public String list(RequestParam(defaultValue 1) Integer current, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String keyword, RequestParam(required false) Long categoryId, RequestParam(required false) Long tagId, Model model) { PageArticle page new Page(current, size); QueryWrapperArticle wrapper new QueryWrapper(); if (StringUtils.isNotBlank(keyword)) { wrapper.like(title, keyword).or().like(content, keyword); } if (categoryId ! null) wrapper.eq(category_id, categoryId); if (tagId ! null) { // 关联查询通过 article_tag 表反查文章 wrapper.inSql(id, SELECT article_id FROM t_article_tag WHERE tag_id tagId); } PageArticle resultPage articleService.page(page, wrapper); model.addAttribute(page, resultPage); model.addAttribute(keyword, keyword); model.addAttribute(categoryId, categoryId); model.addAttribute(tagId, tagId); return article/list; }Thymeleaf 模板中分页渲染必须处理page.current和page.pages边界!-- article/list.html -- div classpagination a th:if${page.current 1} th:href{/article/list(current${page.current - 1}, size${page.size}, keyword${keyword}, categoryId${categoryId}, tagId${tagId})}上一页/a span th:eachi : ${#numbers.sequence(1, page.pages)} a th:classappend${i page.current} ? active th:href{/article/list(current${i}, size${page.size}, keyword${keyword}, categoryId${categoryId}, tagId${tagId})} th:text${i}1/a /span a th:if${page.current page.pages} th:href{/article/list(current${page.current 1}, size${page.size}, keyword${keyword}, categoryId${categoryId}, tagId${tagId})}下一页/a /div注意th:classappend${i page.current} ? active当当前页码等于循环变量i时添加activeCSS 类高亮这是答辩时展示“前端交互细节”的加分项。4. 避坑指南答辩前必验的五个血泪问题4.1 现象启动时报Failed to configure a DataSource控制台疯狂刷 HikariCP 初始化失败原因application-dev.yml中spring.datasource.url的 H2 数据库路径格式错误常见写成jdbc:h2:mem:blogdb缺;DB_CLOSE_DELAY-1导致 H2 内存库在应用关闭时立即销毁MyBatis-Plus 启动时无法建表。解决严格按配置示例书写 URL特别注意分号;和参数顺序。若仍失败在pom.xml中确认h2database依赖版本为2.1.214与 SpringBoot 2.7 兼容而非2.2.x。4.2 现象登录成功后访问/article/list显示 404但/login页面能打开原因WebConfig.java中addResourceHandlers()未正确映射静态资源或ThymeleafViewResolver的prefix路径错误。常见错误是setPrefix(classpath:/templates/)写成setPrefix(templates/)缺classpath:。解决检查WebConfig类是否实现WebMvcConfigurer接口并确认addResourceHandlers()方法内addResourceLocations(classpath:/static/)的路径以classpath:开头同时验证ThymeleafViewResolver的setPrefix(classpath:/templates/)。4.3 现象文章列表分页点击“第 2 页”后搜索关键词keyword参数丢失导致全量数据展示原因Thymeleaf 分页链接中{/article/list(...)的参数未完整传递。例如只写了{/article/list(current${i})}漏掉keyword,categoryId等当前筛选条件。解决在分页链接中显式拼接所有可能存在的参数使用th:if判断非空再追加a th:href{/article/list( current${i}, size${page.size}, keyword${keyword ! null} ? ${keyword} : , categoryId${categoryId ! null} ? ${categoryId} : , tagId${tagId ! null} ? ${tagId} : )} th:text${i}1/a4.4 现象发布文章时选择多个标签数据库t_article_tag表只存入第一条其余丢失原因ArticleTagMapper.insertBatchSomeColumn()方法未正确识别主键策略。若ArticleTag实体类中id字段未加TableId(type IdType.NONE)注解MyBatis-Plus 会尝试自动生成 ID导致批量插入时主键冲突。解决在ArticleTag.java中为id字段添加TableId(type IdType.NONE)明确告知 MyBatis-Plus 此字段不参与主键生成由数据库AUTO_INCREMENT或应用层控制。4.5 现象Thymeleaf 模板中th:if${session.user ! null}始终为 false导航栏登录/退出状态不更新原因LoginController.login()中存入 Session 的 key 是user但 Thymeleaf 表达式中session.user默认查找HttpSession.getAttribute(user)的user属性而User对象本身无user字段。正确写法应为session.getAttribute(user)对应session.user但需确保User类有getUsername()等 getter 方法。若User类未实现Serializable部分容器如 Tomcat会拒绝序列化 Session导致取值为空。解决确认User.java实现Serializable接口并添加private static final long serialVersionUID 1L;同时检查th:if表达式是否误写为session.currentUser应为session.user。5. 搜索性能优化与答辩话术从 like 全表扫到联合索引实战5.1 为什么默认的LIKE %keyword%是答辩致命伤导师看到wrapper.like(title, keyword).or().like(content, keyword)这行代码第一反应就是“全表扫描数据量大了怎么办”。确实LIKE左模糊%abc或全模糊%abc%无法使用 B 树索引MySQL 会遍历每行title和content字段做字符串匹配。当文章数超 1 万搜索响应时间从 50ms 暴涨至 2s答辩演示时卡顿会直接扣分。必须给出可落地的优化方案而非只说“加索引”。5.2 三步落地全文检索MySQL 内置 FTS MyBatis-Plus 适配本项目提供两种搜索方案答辩时可分层阐述基础层必做为t_article.title和t_article.content添加FULLTEXT索引改用MATCH ... AGAINST语法增强层加分项引入 Elasticsearch但本资源不内置仅提供ArticleSearchService接口和ESArticleRepository示例类说明“未来可扩展方向”兜底层保命当FULLTEXT不可用时降级为LIKE但限制keyword长度 ≤ 10 字符并缓存热门搜索词结果。步骤一创建 FULLTEXT 索引MySQL 5.6-- 在 MySQL 客户端执行 ALTER TABLE t_article ADD FULLTEXT(title, content);步骤二修改 MyBatis-Plus 查询逻辑// ArticleServiceImpl.java Override public PageArticle searchByFullText(String keyword, PageArticle page) { // 1. 检查 MySQL 是否支持 FULLTEXT生产环境需判断 try { // 2. 使用原生 SQL 执行 MATCH AGAINST String sql SELECT *, MATCH(title, content) AGAINST(? IN NATURAL LANGUAGE MODE) AS score FROM t_article WHERE MATCH(title, content) AGAINST(? IN NATURAL LANGUAGE MODE) ORDER BY score DESC; ListArticle articles articleMapper.selectList(new QueryWrapperArticle() .apply(MATCH(title, content) AGAINST({0} IN NATURAL LANGUAGE MODE), keyword)); // 3. 手动分页因原生 SQL 与 Page 不兼容需自行截取 int fromIndex (int) ((page.getCurrent() - 1) * page.getSize()); int toIndex Math.min(fromIndex (int) page.getSize(), articles.size()); ListArticle subList articles.subList(fromIndex, toIndex); page.setRecords(subList); page.setTotal(articles.size()); return page; } catch (Exception e) { // 降级为 LIKE 搜索 return searchByLike(keyword, page); } }注意MATCH ... AGAINST在NATURAL LANGUAGE MODE下对短词 4 字符默认忽略因此需在application.yml中配置ft_min_word_len2并重启 MySQL否则搜“Java”会命中“Go”则无结果。步骤三Thymeleaf 搜索框防抖与长度限制在article/list.html中加入前端约束避免无效请求form th:action{/article/list} methodget onsubmitreturn validateSearch() input typetext namekeyword th:value${keyword} placeholder搜索文章标题或内容... maxlength10 oninputthis.value this.value.replace(/[^a-zA-Z\u4e00-\u9fa5]/g, )/ button typesubmit搜索/button /form script function validateSearch() { const keyword document.querySelector(input[namekeyword]).value.trim(); if (keyword.length 2) { alert(搜索词至少 2 个字符); return false; } if (keyword.length 10) { alert(搜索词不能超过 10 个字符); return false; } return true; } /script5.3 答辩话术把技术选择变成思考过程当导师问“为什么用 MyBatis-Plus 而不用 JPA”不要只答“更轻量”。这样说“我们对比了 JPA 和 MyBatis-Plus。JPA 的Query写原生 SQL 时需用?1占位符而 MyBatis-Plus 的QueryWrapper支持链式调用和 Lambda 表达式比如wrapper.eq(User::getUsername, username)编译期就能检查字段名是否存在避免运行时报PropertyNotFoundException。答辩演示时我现场改了一个字段名IDE 直接标红这就是类型安全的优势。”当被问“Thymeleaf 和 Vue 怎么选”这样说“Vue 更适合复杂单页应用但本系统是服务端渲染的博客核心诉求是 SEO 友好和首屏加载快。Thymeleaf 模板在服务端完成数据绑定返回完整 HTML搜索引擎爬虫能直接抓取文章标题和摘要而 Vue 的index.html是空壳需等 JS 加载执行后才渲染内容对 SEO 不友好。且 Thymeleaf 学习成本低一周内可掌握符合毕业设计周期。”从那以后我每次做毕业设计都会在README.md里用表格写清三点用了什么技术栈、为什么选它对比其他方案、答辩时怎么讲这句话。不是为了背稿而是逼自己把每个选择想透——因为导师真正想听的从来不是“我用了什么”而是“我为什么这么选”。希望帮到你。本文还有配套的精品资源点击获取