不知道有多少人和我一样,学 Spring Boot 的时候最头疼的不是语法,而是“学完不知道能做什么”。看官方文档太零散,跟着视频敲又容易卡在环境上,想找一套能完整跑起来、还能看到全部源码的教程,往往是逛遍各大社区都拼不齐。所以当我决定自己动手整理一套 Spring Boot 教程网站时,定的第一个目标就很朴素:每一篇教程都必须附带可运行的完整源码,点开就能看,拉下来就能跑。这篇文章就把我从零搭建这个教程网站的全过程拆开讲,从技术选型、功能设计到实际踩过的坑,一次性说清楚。
教程网站和普通博客最大的区别,在于它要承担“教”的任务。博客可以想到哪写到哪,教程必须有一条清晰的学习路径:先搭环境、再写接口、然后连数据库、最后做完整项目。我用 Spring Boot 做了这个站点的后端,前端页面用模板引擎直接渲染,所以整个项目不需要额外启动前端服务,对初学者来说也更容易看懂。源码全部放在一个独立的仓库里,按教程章节编号归档,配套的内容管理系统负责维护教程正文和示例代码,站点的核心数据都落在数据库里,方便后续扩展。
这篇文章适合的人群很明确:打算用 Spring Boot 做内容型网站的人、想给团队搭建内部技术文档库的人、以及那些“跟着教程学但总想看看完整工程长什么样”的初学者。我会把项目结构、表设计、关键功能的实现思路、以及真实的排错过程都记录下来,如果你正准备做类似的东西,直接照着思路去抄作业就好。
1. 为什么是“教程网站”而不是一堆 Markdown 文件
1.1 教程网站的定位:内容、导航与学习路径的乘积
在动手之前我先想清楚了一件事:教程网站的核心资产是内容,但内容的组织形式决定了学习效率。如果只是把几十篇 Markdown 文件堆在仓库里,读者看完第 3 篇就不知道第 4 篇在哪,这种体验和十年前下载一个资料包没什么区别。
所以我给这个项目定了三个基础能力。第一是 结构化目录,所有教程按模块分组,比如基础入门、数据访问、Web 开发、安全认证、部署运维,每个模块下再按编号排列具体章节;第二是 进度感,每篇文章都有“上一篇/下一篇”的导航,让读者知道自己学到哪里;第三是 内容与代码的一致性,每章教程都关联一个源码快照,标注清楚这一章对应的代码版本和运行方式。
这三个能力听起来简单,但用纯静态方案实现会很难维护。如果靠手写 HTML 维护导航,内容一多就乱;如果用静态站点生成器,又需要额外搭建构建链路。用 Spring Boot 做后端的好处是,导航、标签、代码关联都可以通过数据库动态生成,改一篇教程的内容,其他页面自动同步更新。
1.2 “附源码”三个字的含金量
很多教程站只贴关键代码片段,但片段和完整工程之间隔着太多信息:pom.xml 里有什么依赖、application.yml 里配了什么参数、代码文件的目录结构长什么样。这些内容如果不展示,新手照葫芦画瓢都画不出来。所以“附源码”不是把代码贴进文章里,而是给每一章配一个可运行的完整工程快照。
我的做法是,在教程的数据库记录里增加一个 source_code_ref 字段,存的是当前章节对应的源码目录名,比如 chapter03-web-mvc。站点的源码管理页面会读取仓库里的目录结构,列出所有章节源码的入口说明和启动方式。读者点进某一章,先看正文了解思路,再打开对应的源码目录对照实现,理解和动手两条线同时推进,学习效率会高很多。
1.3 版本编号 50319 的含义
标题里的 50319,是我给这个教程网站项目打的第一个正式版本的归档编号。很多做开源内容项目的人习惯直接用日期当编号,因为教学内容不像软件发版那样有明确的语义化版本节奏,用日期快照的方式反而清晰:5 月 3 日整理的 19 号构建,对应的是某个时间点的完整内容状态。
后来我把这个思路沿用到源码管理上,每个章节目录都带编号前缀,教程正文里引用代码时也写明“本章代码位于 chapter03 目录”。这样读者下载源码后能按图索骥,不会出现“文章说在 A 目录、代码实际在 B 目录”的困惑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型:为什么是 Spring Boot + Thymeleaf + MyBatis-Plus
2.1 技术栈的选择逻辑与取舍
教程网站本身不复杂,但我想让它承担“教学示例”的职责——读者看这个项目,不应只学会某个业务功能,还能理解一个 Spring Boot 项目的完整生命周期。因此技术选型遵循一个原则:主流、轻量、易于讲解。
最终敲定的方案是:
- Spring Boot 2.7.x:稳定且资料多,绝大多数生产环境仍在使用 2.x 系列;
- Thymeleaf:服务端渲染模板引擎,天然支持 SEO,无需单独拆前端项目;
- MyBatis-Plus:兼顾 SQL 可控性和开发效率,学习曲线比 JPA 更平滑;
- H2 数据库(开发)/ MySQL(生产):开发阶段零配置起步,部署阶段无缝切换;
- Spring Security:用于管理员登录认证,暂时不做复杂的权限模型。
有朋友问过我为什么不用前后端分离,前端用 Vue 或 React。我的回答是:教程网站的读者大部分是后端初学者,让他们同时维护两套工程、处理跨域问题、学习构建配置,会把注意力从“Spring Boot 核心知识”上挪走。用 Thymeleaf 做服务端渲染,所有页面和后端代码在一个工程里,跑起来就是一个完整的站,这对新手太友好了。
2.2 项目目录结构:按业务功能而非技术层次划分
Spring Boot 项目最常见的目录风格是按技术层次分包:controller、service、mapper、entity。但在这个教程网站项目里,我换了一种组织方式——按业务模块分目录,每个模块内部再分层次。
这样设计的原因很简单:教程网站的内容是模块化的,数据库访问、安全配置、公共组件这些属于“系统支撑”,而教程管理、用户登录、源码浏览这些属于“业务功能”。如果所有 Controller 都堆在同一个包下,后续迭代时定位代码会越来越费劲。按业务模块组织后,每个功能域自成体系,模块之间通过接口交互,也方便以后拆分微服务。
实际的 src/main/java 目录结构大概是这样的:
text复制com.example.tutorialsite
├── common // 全局异常处理、通用返回结构、工具类
├── config // 安全配置、Web 配置、MyBatis-Plus 配置
├── module
│ ├── auth // 注册、登录、管理员会话
│ ├── tutorial // 教程模块、章节管理、内容渲染
│ ├── source // 源码快照浏览、仓库目录解析
│ └── search // 简单关键词搜索
└── TutorialSiteApplication.java
这种结构的直观好处是:提到“教程模块”,所有相关代码都在 module/tutorial 下面,新接手的人一分钟就能定位到需要改的文件。
2.3 数据库设计:四张核心表支撑全部功能
教程网站的数据模型不复杂,但每张表的字段设计都要考虑内容型产品的特殊性。我建了四张核心表,外加一张管理员表。
第一张是 教程分类表,用于支撑首页的模块化导航。字段包括分类名称、排序号、封面描述、状态。排序号非常重要,教程的学习路径靠它决定,不用每次调整顺序都改代码。
第二张是 教程内容表,存储正文的核心表。除了常规的标题、所属分类、正文内容之外,我额外加了 content_format 字段来标识正文是 Markdown 还是 HTML。起初全部设计成 HTML,但后来发现直接写 HTML 喂给模板引擎太难受,改成 Markdown 后用后端渲染,写起来顺手得多。
第三张是 代码示例表,用于实现“正文与代码分离”的核心设计。每篇教程可以关联多条代码记录,每条记录包含代码标题、代码体、语言类型、所属章节。页面展示时正文是一块区域,代码区由这张表动态加载,每个代码块右上角带“复制”“查看完整源码”的操作按钮。
第四张是 用户足迹表,记录读者的学习进度。游客可以浏览和搜索教程,登录用户可以标记“学到第几章”,下次打开时首页会提示“继续学习”。这个功能虽然简单,但对教程网站的用户留存帮助很大,读者不再需要自己记看到哪了。
管理员表就不细说了,存账号、密码(BCrypt 加密)、最后登录时间和 IP,方便排查异常登录。
3. 核心功能模块:从零实现教程发布、代码展示和学习进度
3.1 教程内容的录入与 Markdown 渲染链路
教程内容是网站的核心资产,录入和渲染的链路必须顺畅。我的数据表里存的是 Markdown 原始文本,渲染发生在后端,用了一个轻量的 Markdown 解析库,把文本转成 HTML 后再交给 Thymeleaf 模板输出。
之所以不在前端渲染,是因为要同时解决两个问题:一是 代码高亮,二是在教程正文里嵌入调用代码示例表的占位符。这些逻辑在后端处理起来更直接,代码示例表的记录通过占位符和正文关联,后端解析正文时发现 [code:chapter03-01] 这样的标记,就从数据库查出对应的代码记录并拼进渲染结果。
实现链路大致是:
- 管理员在后台表单里选择栏目、填写标题、粘贴 Markdown 正文;
- 提交后后端保存原始 Markdown 到
tutorial_content表; - 读者访问详情页时,后端读取正文,解析占位符,渲染 Markdown 为 HTML;
- Thymeleaf 模板输出完整页面,代码区域的渲染单独交给一个自定义方言函数处理。
这个设计让录入人员完全不需要懂前端代码,只管写 Markdown,页面呈现效果由模板统一控制。
3.2 登录注册与管理员会话的轻量实现
教程网站不需要复杂的权限体系,游客可读、管理员可维护就够用。我用 Spring Security 实现了基于 Session 的登录认证,但并没有引入它那套繁琐的过滤器链配置,而是采用最直接的方式:配置一个登录接口,校验通过后把用户信息放入 Session,再通过拦截器对 /admin/** 路径做访问控制。
管理员注册的入口在部署时默认关闭,通过配置文件里的开关控制,避免网站上线后任何人都能注册成管理员。第一次启动时系统会自动创建一个初始管理员账号,密码是随机生成的并打印在启动日志里,管理员登录后必须在后台修改密码。
为什么不用 JWT?教程网站是典型的服务端渲染应用,页面刷新和跳转频繁,Session 方案天然契合模板渲染模式。JWT 更适合前后端分离的接口型项目,在这里反而增加复杂度,而且 Session 失效和退出的处理也更直观。
3.3 代码示例的展示、复制与源码跳转
代码展示这个功能虽然不起眼,却是教程网站体验的胜负手。如果你看过那种代码贴在正文里、缩进全乱、还没法复制的教程,就知道一个好的代码展示组件有多重要。
我的实现方案是:代码示例表里的每条记录在页面上渲染为一个独立的代码卡片,卡片标题栏显示文件名或代码块名,右上角提供“复制代码”和“查看完整源码”两个按钮。复制功能前端用剪贴板 API 实现,完整源码跳转则指向该章节对应的源码目录页面。
代码高亮用的是 Prism.js,配合自定义主题,行号开启,长代码区域设置最大高度并出现滚动条。关键一点是:高亮必须在前端完成,后端只负责输出被转义的代码文本。因为代码里经常出现 <、>、& 这些字符,直接输出会破坏 HTML 结构,后端渲染时必须先对代码体做 HTML 转义,前端再交给 Prism 处理,这样才能保证代码内容原样展示。
3.4 搜索功能:从 LIKE 查询到中文分词
教程网站的搜索功能开始我用的是最简单的 SQL LIKE 查询,搜索关键词匹配标题和正文摘要,结果按时间倒序排列。对内容量不大的站来说,这条方案完全够用,但有一个问题让我很别扭:中文分词。用户搜“springboot 教程”,如果内容里写的是“Spring Boot 教程”,带空格和不带空格匹配结果完全不一样。
后来我引入了分词逻辑,把搜索关键词按空格、标点拆分成多个词,每个词单独做 LIKE 匹配,所有词必须同时命中的结果才展示。比如搜“springboot 数据库”,会拆成“springboot”和“数据库”两个词,标题或正文同时包含这两个词的教程才会出现在结果列表里。
这只是一个轻量方案,够用但谈不上精深。如果内容量超过几千篇,我会换成全文检索引擎,引入索引和分词器,但在项目的当前阶段,轻量方案的可维护性明显更优。
4. 源码管理:让每一篇教程都能对应可运行的工程
4.1 单仓库多模块还是每个章节独立工程
这是搭建过程中比较纠结的一个设计决策。教程网站的源码有两种组织方式:一种是单仓库多模块,所有章节代码在一个工程里,通过 Maven 模块划分;另一种是每个章节一个独立项目,目录彼此隔离。
我的取舍是:每章一个可独立运行的 Spring Boot 工程,统一放在一个仓库的 chapters 目录下。第一感觉大家可能会觉得这样工程数量多、启动麻烦,但对学习者来说,独立工程的好处非常实在:每个章节的工程只包含当前章节涉及的代码,没有无关类的干扰,初学者能聚焦在当章的知识点上,项目复杂度随学习进度递增。
统一放在一个仓库里,则方便做版本管理和批量更新。目录大概是这样:
text复制tutorial-site-sources
├── chapter01-hello-world
├── chapter02-di-container
├── chapter03-web-mvc
├── chapter04-data-jpa
├── chapter05-mybatis-plus
├── chapter06-security
└── chapter07-deploy
每个工程内部是完整的 Maven 结构,自带 README,说明运行方式、需要修改的配置项和验证方式。我要求每章源码必须能独立启动,数据库用 H2 内存模式或提供初始化脚本,最大程度降低读者的运行门槛。
4.2 让源码和教程保持同步的检查机制
内容型项目最怕“文章和代码对不上”。最常见的场景是:文章里说 UserController 里有 3 个接口,但读者下载的源码里只有 2 个,或者接口路径完全变掉了。为了杜绝这种问题,我建了一个简单的章节源码索引表,记录每章源码的目录名、核心类列表、接口清单和验证方式。
文章发布前,我会按索引表的记录逐项核对源码,确认接口清单与正文描述一致。这个流程没有自动化,靠的是发布检查清单,但非常有效。每次代码更新后,同时更新索引表和受影响的教程页面,避免文章内容滞后。
4.3 源码下载与在线浏览的取舍
原计划做 ZIP 打包下载,用一个接口把章节源码目录压缩后返回给用户。这个功能本身不难,用 ZipOutputStream 遍历目录文件就能实现。但我后来犹豫了:直接打包下载,读者拿到的是一堆文件,没有版本管理的上下文,遇到问题也不知道当前版本对应哪个提交。
最后我采用“在线浏览 + Git 仓库入口”的方案:每个章节的源码页面展示核心文件列表,点击文件可以查看源码内容,同时在页面顶部提供“前往仓库查看完整工程”的链接。如果想下载,直接从 Git 仓库 clone 或打包即可,既能追踪版本变更,也不给服务器增加文件压缩的压力。
5. 实测阶段踩过的坑:路径、编码与静态资源
5.1 静态资源 404:Web 配置的路径陷阱
这个坑几乎每个 Spring Boot 新手都会踩。我在本地测试时发现,页面能正常访问,但页面里引用的 CSS 和 JS 文件全部 404。排查了一圈,问题出在静态资源路径的配置上。
我在 application.yml 里设置了自定义的 Spring MVC 路径匹配规则,又在配置类里加了 addResourceHandlers 方法,两个配置叠加后冲突了,导致默认的 classpath:/static/ 资源映射失效。解决办法是,保留自定义拦截器配置,同时显式声明静态资源的映射路径:
java复制@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:/static/assets/");
}
这里要提醒的是:凡是自定义了 WebMvcConfigurer,最好把静态资源的映射也一并写清楚,不要依赖默认配置。否则模板渲染得再好,样式加载不出来,体验直接崩。
5.2 数据库初始化时的时区与编码问题
教程网站的初始化数据包含了全部教程内容,使用 SQL 脚本在首次启动时导入。有一回我在一个没有配置时区参数的 MySQL 实例上部署,发现启动后系统日志里时间全部差了 8 个小时。查下来是 JDBC 连接串里没加 serverTimezone=Asia/Shanghai,连接器使用了默认时区导致的。
另一个问题是字符集。教程正文里有大量的中文和代码符号,如果数据库表的默认字符集不是 utf8mb4,部分生僻字或emoji 会直接变成乱码。我的处理方式是,在初始化脚本里对每张表显式声明:
sql复制CREATE TABLE tutorial_content (
...
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
建议所有涉及内容存储的表都统一用 utf8mb4,不要嫌麻烦,后面省掉一堆乱码相关的工单。
5.3 教程正文里的特殊字符:转义与安全过滤
这个坑和代码展示直接相关。早期版本里,教程正文中嵌入的代码块和普通文本一样存入数据库,渲染时统一输出到页面。后来我发现,当教程中讲解“如何在 HTML 里嵌入脚本”这类主题时,正文中的脚本标签会被浏览器直接解析,存在 XSS 隐患。
我的修复方案分两层:存储时原样保留,渲染时统一过滤。后端渲染 Markdown 时对原始文本先做 HTML 转义,再执行 Markdown 解析,这样正文里的 <script> 只会以文本形式展示,不会被执行。需要展示代码时,代码示例表的数据单独走转义逻辑,双重保险。实操中建议始终遵循一个原则:所有用户输入和数据库读取的内容,输出到页面之前都必须经过转义,不要信任任何一层的输入。
5.4 本地一切正常、部署就出问题的环境差异排查
这是在本项目实测阶段最折磨我的一次排错。本地开发用的 H2 数据库,一切正常;部署到服务器换成 MySQL 5.7 后,教程列表页直接 500。日志里报的是 SQL 语法错误,仔细一看,原来我有一张表的字段名叫做 describe,在 H2 里不是保留字,在 MySQL 里却是保留字,查询时直接报错。
排查思路其实很老套:先把 SQL 单独拿出来在 MySQL 客户端执行,确认是语法层面还是数据层面的问题,再去翻代码里的查询语句和实体映射。最后把字段重命名为 description_text,同步修改实体类和 XML 映射,问题解决。这个坑告诉我们,表字段命名时要提前规避数据库保留字,不要用 describe、order(想做排序却用了保留字)、group 这类词做列名。
6. 部署上线与运营一个教程网站的后续思路
6.1 打包与部署:从 Jar 到反向代理
教程站点的部署方式非常传统:Maven 打包成可执行 Jar 包,放到服务器上通过 nohup java -jar 启动。前面再加上 Nginx 做反向代理,静态资源由 Nginx 直接托管,动态请求转发给 Spring Boot 的 8080 端口。
如果后续访问量上来了,可以把 Session 存储迁移到 Redis,多实例部署时保持会话同步,就不会出现用户在一台服务器上登录、刷新后被另一台服务器踢下线的问题。在项目初期,单实例加 Nginx 足够稳定,不建议一上来就引入过多中间件。
6.2 数据备份与内容更新的工作流
教程网站的身体是代码,灵魂是内容。内容更新的频率往往高于代码发版,所以我单独把数据库备份和代码备份分开处理。数据库每天凌晨自动备份一次,保留最近 7 天的备份文件;代码仓库本身有版本管理,发版时打 Tag 归档。
如果需要在服务器上快速修改教程内容,而不想改代码重新打包,可以直接用后台编辑器改数据库。生产环境建议在后台管理界面增加“编辑预览”功能,保存前先预览渲染效果,避免格式问题直接暴露给读者。
6.3 后续扩展:评论互动、学习路线与统计看板
这个项目的第一个版本可以正常工作之后,我把它作为一个持续迭代的实验田,列了一张后续改进清单。首先是评论互动,让读者可以在章节下留言提问,管理员能回复,形成学习社区的氛围;其次是学习路线图,根据表结构里的分类和编号,前端生成一条可视化的进阶路线,读者能看到每个模块的知识点和前置依赖关系;最后是访问统计看板,记录每篇教程的浏览量、搜索关键词来源、读者学习进度分布,帮助自己判断哪些内容需要重点补充、哪些章节的跳出率异常。
这些功能单拆出来都不难,难的是一开始就把技术架构和数据模型设计得足够灵活,不要每加一个功能就要推翻表结构。我的体会是:做内容类的 Spring Boot 项目,前期花时间理清数据模型和内容流转链路,收益远远大于后期返工修补。
7. 写在最后:这个项目带来的三点实战体会
如果把这次搭建教程网站的经历浓缩成几句话,我想分享三点。
第一,教程网站的“教程”二字是数据模型设计出来的。它不只是文章列表,而是有分类、有排序、有代码关联、有学习进度的内容系统。数据库设计时多想一步,后端的灵活度会高很多。
第二,源码和正文一体化,才是教程网站的护城河。互联网上不缺内容,缺的是能跑起来的代码和内容之间的一一对应关系。维护好这份对应关系,比写十篇文章更难,也更有价值。
第三,任何项目都值得打一个清晰的版本快照。50319 这个编号提醒我,站在一个具体的时间点回头看,项目当时是什么状态、哪些地方不成熟、后来怎么改的,都是宝贵的经验记录。做这类长期维护的项目,建议从第一天就把版本管理意识立起来,让时间和迭代成为项目的资产,而不是负担。
如果你正准备动手做一个类似的教程型网站,欢迎把这套设计思路拿去改造。遇到拿不准的技术选型,优先考虑“主流技术栈 + 最简单的实现方案”,先把内容跑起来,再逐步迭代。毕竟对教程网站来说,让读者顺利学完一章,比把所有功能都做得花哨重要得多。
