简介:一份基于 SSH2(Spring + Struts2 + Hibernate3)+ Oracle 的个人博客系统完整源码,面向 Java Web 初学者、毕业设计学生及需要快速搭建博客站点的开发者,可帮助理解 SSH 框架整合与前后台交互流程。资源包共 159 个文件、1.51MB,以 Java 源文件、JSP 页面、XML 配置文件为主,另含 CSS/JS、图片等前端资源,项目目录结构规范,便于导入 Eclipse 后对照学习。系统包含首页模块、日志评论模块、用户登录模块和后台管理模块,实现了日志分页展示、评论、随机验证码登录及后台增删改查等核心功能。已有 1162 人学习下载,适合用作课程设计、毕业设计或 SSH 整合实战的参考项目,可重点借鉴其分层设计、Oracle 数据访问和分页实现思路。
1. 个人博客系统源码:一套能直接跑起来的前后端分离工程
如果你正打算搭一个属于自己的博客,又不想从零写登录、评论、文章管理这些重复功能,这套源码可以省下你不少时间。它不是那种只有一个 index.html 的静态模板,而是一套完整的前后端分离工程:后端基于 Spring Boot 提供文章、分类、标签、评论、用户认证接口,前端使用 Vue 3 做单页应用,支持 Markdown 写作、文章归档、关键词搜索和后台管理。我之前接手一个内容站的迁移,就是用这套工程做底子,两天时间把旧文章导进去、换了一套主题,直接上线了。适合谁?想快速拥有一个可二次开发博客的开发者、刚学完 SSM 或 Vue 想找个完整项目练手的人,以及需要一套博客系统源码做毕业设计或公司内部知识库的同学。这篇笔记把结构、启动步骤和改造点一次讲清楚。
2. 技术选型与整体架构:Spring Boot + Vue 3 的前后端分离设计
2.1 技术栈总览
这套个人博客系统源码走的是目前主流的前后端分离路线。后端使用 Spring Boot 作为基础框架,搭配 MyBatis Plus 操作数据库,权限认证用 JWT + 拦截器实现,缓存和阅读量统计依赖 Redis。前端采用 Vue 3 + TypeScript,状态管理用 Pinia,UI 组件库是 Element Plus,路由用 Vue Router,HTTP 请求统一走 Axios。
| 端 | 技术组件 | 用途 |
|---|---|---|
| 后端 | Spring Boot 2.x / MyBatis Plus / MySQL / Redis / JWT | 接口服务、数据持久化、登录鉴权 |
| 前端 | Vue 3 / TypeScript / Pinia / Element Plus / Vite | 页面渲染、状态管理、后台管理界面 |
| 部署 | Nginx / Maven / npm | 静态资源托管、构建打包 |
这套组合的好处是生态成熟,遇到问题几乎都能搜到解决方案。后端接口返回统一格式的 JSON,前端只管渲染,职责清晰。如果你之前用过 SSM 或 Django,迁移成本也不高,MyBatis Plus 把单表 CRUD 简化到几乎不用写 SQL,这一点对快速上手很友好。
2.2 工程目录结构
拿到源码后第一件事是看目录结构。工程分为blog-server和blog-web两个独立模块,前者是后端 Java 工程,后者是前端 Vue 工程,互不依赖,可以分开启动。
blog-server/ ├── src/main/java/com/example/blog/ │ ├── controller/ # 接口层:ArticleController、CommentController 等 │ ├── service/ # 业务层:接口定义与实现类 │ ├── mapper/ # MyBatis Plus 的 Mapper 接口 │ ├── entity/ # 数据库实体类 │ ├── config/ # 拦截器、跨域、Redis 等配置 │ ├── common/ # 统一返回结果、异常处理 │ └── BlogApplication.java ├── src/main/resources/ │ ├── application.yml # 数据源、Redis、JWT 配置 │ └── mapper/ # XML 自定义 SQL └── pom.xml blog-web/ ├── src/ │ ├── api/ # 封装 Axios 请求 │ ├── router/ # 前端路由 │ ├── stores/ # Pinia 状态 │ ├── views/ # 页面:首页、文章详情、后台管理 │ ├── components/ # 通用组件 │ └── main.ts ├── index.html ├── vite.config.ts # 开发代理配置 └── package.json这种结构遵循约定优于配置的原则。后端按 controller → service → mapper 分层,前端页面与组件分离。拿到源码后,你不需要完整阅读,按这个目录顺序从上往下看,半天就能摸清数据流。
2.3 数据库表设计
数据库是这个博客系统的核心,表不多,一共六张:用户表、文章表、分类表、标签表、文章标签关联表、评论表。我把核心字段整理成建表语句,方便你导入后对照理解。
CREATE TABLE `article` ( `id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '文章标题', `summary` varchar(255) DEFAULT NULL COMMENT '摘要', `content` longtext NOT NULL COMMENT 'Markdown 原文', `content_html` longtext COMMENT '渲染后的 HTML', `cover` varchar(255) DEFAULT NULL COMMENT '封面图地址', `category_id` bigint DEFAULT NULL COMMENT '所属分类', `status` tinyint DEFAULT '1' COMMENT '1 已发布 0 草稿', `view_count` int DEFAULT '0' COMMENT '阅读量', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;文章表存两份内容:content是 Markdown 原文,content_html是渲染后的 HTML。这个设计常见于这类博客系统源码,后端在保存文章时用 CommonMark 库把 Markdown 转成 HTML 存入数据库,前端详情页直接输出 HTML,避免每次请求都做一次渲染,响应速度更快。代价是编辑时要同时更新两份字段,这个逻辑在 Service 层处理即可。
类别和标签表是典型的多对一和多对多关系。文章表通过category_id关联分类;标签通过中间表article_tag关联。评论表用parent_id字段支持楼中楼回复,查询时做递归组装。
2.4 一次请求的完整链路
以博客首页加载文章列表为例,完整链路是这样走的:浏览器输入域名,Nginx 把静态资源请求直接返回,把/api/开头的请求反向代理到后端服务。后端先经过跨域配置,再走拦截器校验登录态,随后进入 ArticleController,调用 ArticleService 查询数据库,返回统一 JSON 结构,前端拿到数据渲染列表。
// blog-web/src/api/article.ts import request from '@/utils/request' export function getArticleList(params: { page: number; size: number }) { return request({ url: '/api/article/list', method: 'get', params }) }这里前端封装的request实例在utils/request.ts里配置了基础 URL 和请求拦截器。开发环境下 Vite 代理/api到localhost:8080,生产环境由 Nginx 转发,前端代码不需要区分环境。理解这条链路后,后面排查问题就有方向了。
3. 核心模块拆解:从文章发布到评论回复的完整实现
3.1 文章发布接口:Controller 到 Service 的处理逻辑
后端接口围绕文章这个核心实体展开。发布文章这个动作涉及参数接收、Markdown 渲染、数据落库三个步骤。先看 Controller 这段代码。
@RestController @RequestMapping("/api/article") public class ArticleController { @Autowired private ArticleService articleService; @PostMapping("/publish") public Result<Long> publish(@RequestBody ArticleDTO dto) { if (dto.getTitle() == null || dto.getTitle().isEmpty()) { return Result.error("文章标题不能为空"); } Long articleId = articleService.publish(dto); return Result.success(articleId); } }ArticleDTO是接收前端参数的传输对象,包含标题、摘要、Markdown 内容、分类 ID、标签 ID 列表等字段。这里对标题做了非空校验,更完整的校验建议引入 JSR 303 注解,比如@NotBlank,省去手写判断。Result是统一返回包装类,封装了状态码、消息和数据体,前端可以根据code字段判断请求是否成功。
再往下看 Service 层的实现,这是发布文章的核心逻辑。
@Service public class ArticleServiceImpl implements ArticleService { @Autowired private ArticleMapper articleMapper; @Autowired private ArticleTagMapper articleTagMapper; @Override @Transactional(rollbackFor = Exception.class) public Long publish(ArticleDTO dto) { Article article = new Article(); article.setTitle(dto.getTitle()); article.setSummary(dto.getSummary()); article.setContent(dto.getContent()); article.setContentHtml(MarkdownUtil.render(dto.getContent())); article.setCategoryId(dto.getCategoryId()); article.setStatus(dto.getStatus()); // 新增文章,MyBatis Plus 会自动回填主键 articleMapper.insert(article); // 保存文章与标签的关联关系 if (dto.getTagIds() != null) { for (Long tagId : dto.getTagIds()) { articleTagMapper.insert(article.getId(), tagId); } } return article.getId(); } }@Transactional保证文章和标签关联的插入操作要么全部成功,要么全部回滚,避免出现文章存在但标签丢失的脏数据。MarkdownUtil.render是封装好的 CommonMark 渲染工具类,输入 Markdown 字符串返回 HTML。标签循环插入在数据量不大时没问题,如果标签数量多,可以考虑批量插入优化。
参数上注意一点:ArticleDTO里有个status字段,前端编辑器有“保存草稿”和“直接发布”两个按钮,分别传 0 和 1,后端只做存储不判断权限,权限控制交给管理后台的登录校验。
3.2 Markdown 渲染:双份存储的取舍
Markdown 渲染是博客系统源码里比较关键的设计点。我在前文提到文章表同时存原文和 HTML,这里说一下为什么这么设计。
第一种方案是前端渲染,用 marked.js 这样的库在浏览器里把 Markdown 转成 HTML,后端只存原文。优点是后端简单,数据库只需要一个字段;缺点是 SEO 不友好,搜索引擎抓取的是空壳页面,文章内容全靠 JavaScript 执行才能看到。第二种方案是后端渲染,保存文章时同步生成 HTML,前端详情页直接输出 HTML,内容和搜索引擎抓取都能兼顾。
这套源码采用第二种方案,写文章和编辑文章走publish接口,自动更新两个字段。如果你追求极简,可以把content_html设计成可空字段,文章首次被访问时再生成并缓存,但会增加代码复杂度,不太建议第一次改造就动它。
public class MarkdownUtil { private static final Parser PARSER = Parser.builder().build(); private static final HtmlRenderer RENDERER = HtmlRenderer.builder() .escapeHtml(true) .build(); public static String render(String markdown) { Document document = PARSER.parse(markdown); return RENDERER.render(document); } }escapeHtml(true)这行要留意。它会把 Markdown 里的 HTML 标签转义成实体字符,防止代码块内容被浏览器当成真实标签执行。好处是安全,文章里写<script>也不会被当作脚本执行;副作用是如果你在文章里故意插入自定义 HTML 片段,会被原样显示出来,做嵌入式页面时要注意。
3.3 评论模块:递归查询与嵌套结构组装
评论功能常见的有两种实现:一种是用parent_id做无限层级,查询时一次性取出所有评论,在内存里递归组装成树;另一种是限制最多两层,用reply_to字段记录被回复人的用户名。这套源码采用第一种,因为数据库设计更干净,新增字段少。
public List<CommentVO> buildCommentTree(List<Comment> allComments) { Map<Long, List<CommentVO>> childrenMap = new HashMap<>(); List<CommentVO> roots = new ArrayList<>(); for (Comment comment : allComments) { CommentVO vo = new CommentVO(comment); childrenMap.computeIfAbsent(comment.getParentId(), k -> new ArrayList<>()).add(vo); } for (CommentVO vo : childrenMap.getOrDefault(0L, Collections.emptyList())) { fillChildren(vo, childrenMap); roots.add(vo); } return roots; } private void fillChildren(CommentVO parent, Map<Long, List<CommentVO>> childrenMap) { List<CommentVO> children = childrenMap.getOrDefault(parent.getId(), Collections.emptyList()); parent.setChildren(children); for (CommentVO child : children) { fillChildren(child, childrenMap); } }这段代码先把所有评论按parent_id分组放进 Map,然后从根评论开始递归填充子节点。注意 Map 的 key 用的是parentId,根评论的parent_id记为 0。遇到数据量大、评论层级深的场景,全量递归有性能隐患,但这个量级到几千条评论都没问题,不需要上数据库递归查询。
真实项目中踩过一个坑:删除评论时如果只删父节点不删子节点,前端组装树就漏数据。解决方案是删除时先递归查出所有子节点 ID,批量软删除,代码在 CommentService 的delete方法里处理。
3.4 首页聚合:文章、分类、标签的联表查询
首页需要展示文章列表,每条带分类名和标签列表,单表查询搞不定,需要联表。这块用的是 MyBatis Plus 的分页插件配合自定义 SQL。
<select id="selectArticlePage" resultType="com.example.blog.entity.ArticleVO"> SELECT a.id, a.title, a.summary, a.cover, a.view_count, a.create_time, c.name AS category_name FROM article a LEFT JOIN category c ON a.category_id = c.id WHERE a.status = 1 ORDER BY a.create_time DESC </select>Mapper 接口里定义selectArticlePage(Page<ArticleVO> page),MyBatis Plus 的分页插件会自动拦截生成带 LIMIT 的 SQL,返回分页数据。标签信息在列表页不展示,点击文章详情再查,减少联表压力。这里有个取舍:多查一次数据库换更简单的查询逻辑,在中小型系统里是划算的。
4. 本地运行:从拉取源码到页面跑通的完整步骤
4.1 环境准备清单
开始跑之前先确认环境,我列出最低要求清单,避免后面报错时不知道缺什么。
| 依赖 | 版本要求 | 用途 |
|---|---|---|
| JDK | 1.8 及以上 | 运行后端 |
| Maven | 3.6 及以上 | 后端依赖管理 |
| Node.js | 14 及以上 | 前端构建运行 |
| MySQL | 5.7 及以上 | 数据存储 |
| Redis | 任意稳定版 | 缓存与登录令牌 |
| Nginx | 1.18 及以上 | 生产环境部署 |
JDK 8 比较稳,如果你本机装了 JDK 17,跑 Spring Boot 2.x 大概率会碰到 javax 包不存在的问题,建议项目用 JDK 8 或 11,别追新。
4.2 修改配置:数据库与 Redis
拿到源码后先改配置文件,路径在blog-server/src/main/resources/application.yml。核心改动是数据源和 Redis 地址。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword redis: host: localhost port: 6379 database: 0 jwt: secret: your-jwt-secret-key expire: 604800database: 0表示使用 Redis 的 0 号库,如果你的 Redis 里存了其他业务数据,建议单独建一个库避免 key 冲突。JWT 的secret默认值必须改掉,不然别人拿默认密钥伪造 token 就能登录后台。expire单位是秒,604800 正好 7 天,按需调整。
数据库初始化脚本一般在blog-server/src/main/resources/db/目录下,文件名带init.sql字样,用 Navicat 或命令行导入后自动建库建表,不需要手动创建表。
4.3 启动后端服务
配置改完后,进入blog-server目录执行 Maven 命令。
cd blog-server mvn spring-boot:run第一次执行会下载依赖,网络不好可能要等几分钟。看到日志出现Started BlogApplication说明启动成功。保险起见再用 curl 验证一个接口。
curl http://localhost:8080/api/article/list?page=1&size=5返回 JSON 且code字段为 200,后端就没问题。如果端口被占用,改application.yml里的server.port,或者查本机占用进程用netstat -ano | findstr 8080解决。
4.4 启动前端开发环境
后端起来了,另开一个终端窗口切到前端目录。
cd blog-web npm install npm run devnpm install如果慢,用淘宝镜像源加速,一行命令切换:npm config set registry https://registry.npmmirror.com。npm run dev启动 Vite 开发服务器,默认端口 5173,终端会打印访问地址。
注意 Vite 的vite.config.ts里配置了开发代理,把/api转发到http://localhost:8080,所以前端页面里直接请求/api/...就能通。浏览器打开http://localhost:5173,能看到首页文章列表,说明前后端联调成功。这时可以注册一个账号,进后台写第一篇文章。
4.5 生产构建与部署
开发跑通后,如果要部署到服务器,需要分别构建前后端。
# 后端打 jar 包 mvn clean package -DskipTests # 前端构建 npm run build后端产物在blog-server/target/下,是一个可执行 jar 包,用java -jar blog-server.jar运行。前端构建产物在blog-web/dist/目录,把 dist 内容复制到 Nginx 的 html 目录,再配一个反向代理。
server { listen 80; server_name yourdomain.com; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; } location / { try_files $uri $uri/ /index.html; } }try_files这行很关键,Vue Router 用 history 模式时刷新子路由会 404,加了这行就交给 index.html 处理,路由回归前端接管。部署完成后访问域名,博客就正式上线了。
5. 避坑笔记:本地跑不起来和部署翻车的常见排查
5.1 后端启动报数据库连接失败
现象:执行mvn spring-boot:run后日志出现Communications link failure或Access denied for user。
原因:第一个是 MySQL 没启动或者端口不对,第二个是账号密码错误或权限不足。我之前犯过一个低级错误,配置文件里的密码带了特殊字符#,被 YAML 当成注释截断,折腾了半小时。
解决:先确认 MySQL 服务已启动,命令行执行mysql -uroot -p能登录。再把application.yml里的密码用引号包起来,比如password: "abc#123"。最后检查serverTimezone参数,国内服务器一般配Asia/Shanghai避免时区偏差。
5.2 前端能看到页面但接口全 404
现象:页面渲染出来了,文章列表和登录请求全部失败,控制台 Network 面板显示请求地址是http://localhost:8080/api/...而不是http://localhost:5173/api/...。
原因:请求根本没有走 Vite 代理,而是直接打到了后端 8080 端口。常见于前端request工具类里写死了 baseURL。
解决:检查blog-web/src/utils/request.ts,baseURL 应该配置成空字符串或者/api,让请求走当前域名下的代理。开发环境浏览器地址是 5173 端口,Vite 代理会自动转发到 8080;如果写死http://localhost:8080,会触发跨域问题,还得额外处理 CORS。
5.3 Markdown 里图片不显示
现象:文章发布成功后,详情页图片位置是一张裂开的图标,查看 HTML 发现 img 标签的 src 是http://localhost:8080/upload/xxx.png。
原因:图片上传接口返回的是后端地址,存在数据库里的也是完整地址。换到生产环境域名变了,图片地址没跟着变,自然加载失败。
解决:改上传模块,数据库只存储相对路径/upload/xxx.png,前端展示时拼接当前站点的域名。这样本地开发和生产环境都不用手动改数据。如果旧数据已经有完整地址,写个 SQL 把域名前缀去掉。
5.4 登录成功后刷新页面就退出
现象:登录后正常操作,一刷新浏览器又回到登录页,后台管理用不了。
原因:用户 token 存在 Pinia 内存里,刷新后内存清空,token 丢失。这也是刚用 Vue 3 的人常踩的坑。
解决:把 token 持久化到 localStorage,Pinia 初始化时从 localStorage 读取。改法很简单,stores/user.ts里初始化token时执行localStorage.getItem('token'),登录成功和退出登录时同步更新 localStorage。注意用 localStorage 存 token 有 XSS 风险,生产环境配合 CSP 策略更稳妥。
5.5 部署到服务器刷新子路由 404
现象:服务器上部署后,访问首页正常,进入文章详情页后刷新浏览器,出现 Nginx 404。
原因:Nginx 没有配置try_files,直接按路径找文件找不到对应的静态资源。
解决:在 Nginx 配置的location /里加上try_files $uri $uri/ /index.html;,改完执行nginx -s reload生效。这是单页应用部署的标准配置,后续做任何 Vue 项目都会遇到。
6. 二次开发:三个值得动手的改造方向
拿到源码熟悉结构后,接下来就是改成自己的东西。我建议从三个方向动手,难度递增。
第一个是主题切换。前端样式用了 CSS 变量管理颜色,集中在styles/variables.scss里。加暗色模式不难,定义一个[data-theme="dark"]选择器覆盖变量值,然后在 Pinia 里存一个theme字段,页面根元素动态切换>:root { --bg-color: #ffffff; --text-color: #333333; } [data-theme="dark"] { --bg-color: #1e1e1e; --text-color: #f0f0f0; }
改造点在于所有组件要改用变量而不是写死颜色,工作量取决于源码本身用变量的彻底程度。这个方向适合想熟悉前端设计语言的开发者。
第二个是阅读量统计。数据库里虽然有view_count字段,但直接 UPDATE 会在高并发下产生锁竞争。改成 Redis 实现更合适:文章详情接口先INCR计数,定时任务批量把 Redis 里的值同步回 MySQL。这样扛得住一定并发,也不会每次请求都写数据库。
第三个是开放 API 给移动端。目前所有接口都面向网页,返回 HTML 字段对 App 来说冗余。可以加一个clientType参数,为移动端返回精简 JSON,去掉content_html,由客户端自己用 Markdown 渲染库展示,顺便省流量。
从那以后我每次拿到一套新源码,都会强制走一遍流程:先看目录结构,再拉数据库,跑通一次完整业务链路,最后才动手改代码。这套博客系统源码的注释和分层做得比较规范,适合作为你二次开发的起点。希望帮到你。
本文还有配套的精品资源,点击获取