ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SpringBoot的简化博客系统:从需求到部署的完整实践

基于SpringBoot的简化博客系统:从需求到部署的完整实践 手上这套基于SpringBoot的简化博客系统是我在实际项目里真正跑通的一段实践。标题编号11687可以理解为某个项目代号但对我来说它更像一次刻意收敛需求边界的修炼不做商城、不做社交、不做后台权限树只做一个人写文章、读者看文章的最小闭环。这个决定在后期几乎挽救了整个项目周期——我把省下来的时间全部投入到了代码结构、数据库设计和真实部署上最终交付的系统不仅能在本地跑通也能稳定部署在云服务器上供日常访问。如果你正准备做类似的Spring Boot毕业设计或者刚学完框架想找一个能讲清楚来龙去脉的练手项目这篇内容应该能帮到你。我会从需求边界、架构设计、数据库表结构、核心功能实现一直聊到部署上线和踩坑记录整个过程尽量还原一名开发者的真实思考路径而不是只丢出一堆代码片段。1. 为什么是精简博客系统需求边界与技术选型的先决思考1.1 需求梳理一个博客系统的核心闭环是什么很多同学拿到博客系统题目后的第一反应是功能堆叠用户角色要分管理员和普通用户要加友链、要加留言板、要加收藏、要加阅读排行。这其实是给自己挖坑。需求一旦膨胀论文要写的内容也会跟着膨胀而答辩现场面试官最常问的恰恰是这个表为什么这么设计这个功能到底有没有人用功能越多破绽越多。我最终确定的核心闭环是六件事用户的注册与登录文章的发布、编辑、下线文章列表的分页浏览与按分类、标签检索文章详情页的展示与上一篇/下一篇导航针对文章的评论功能系统操作日志的记录这套闭环覆盖了一个博客站点的全部高频路径。用户登录后可以写文章、改文章未登录用户可以浏览文章和评论但不能发表管理员可以从后台看到操作日志。这个边界既满足了课程设计/毕设对完整性的要求又把代码量控制在一个两个周末能写完的范围内。1.2 技术选型的心路与依据技术栈的选定我给出了三个硬性约束稳定、文档多、我能讲明白。最终落地的组合是Spring Boot 2.7.x MyBatis-Plus 3.5.x MySQL 5.7 Redis可选 Thymeleaf Bootstrap。这里逐个说理由。Spring Boot版本我刻意没有追新。2.7.x是Spring Boot 2.x的最后一个稳定主线基于javax命名空间网上能搜到的案例最多遇到问题大概率能直接搜到答案。如果你选3.x会遇到jakarta命名空间的迁移问题不少旧教程直接失效排查成本会高很多。持久层我选MyBatis-Plus而不是Spring Data JPA原因很简单MP能够直接生成单表CRUD的基础方法省掉大量重复的XML映射同时对复杂查询保留了原生SQL的逃生舱。博客系统最核心的文章检索涉及多表关联和条件拼接MP的Wrapper机制能写得很清晰这在论文里也好画图说明。前端我选择Thymeleaf服务端渲染而不是做彻底的前后端分离。这不是技术退化而是出于项目规模考量。博客系统页面以展示为主服务端渲染天然对搜索引擎友好也不需要额外维护一套Node.js前端工程和跨域配置。辅助组件上我引入了Hutool工具库处理日期、随机数、对象转换等琐碎逻辑、JWT用于登录状态管理、BCrypt用于密码加密。Redis我把它设计成可选增强项部署在生产时不启动也不影响主流程这样能避免为了用Redis而用Redis的尴尬。1.3 从11687这个编号想到的毕设文档怎么配套这个编号让我联想到毕设管理系统的常见编号规则。如果你是在做毕业设计一定要意识到系统代码只是成果的一半论文、开题报告、任务书这些文档占比同样重要。我的建议是每写完一个模块立刻把对应文档段落补上而不是等项目结束再统一写。比如数据库设计章节就在建表完成后立刻写包括ER图、表结构说明和索引依据核心功能章节则边写代码边截屏记录。这样到最后只需要整理润色而不是对着空白的Word文档发呆。2. 系统架构与目录划分让代码结构一眼能看懂2.1 三层架构在Spring Boot项目里的落点我采用了经典的Controller-Service-Mapper三层架构外加一套统一的返回体与全局异常处理。很多人觉得三层架构老生常谈但在博客系统这种规模的项目里三层架构恰恰能最大化减少认知负担Controller层只做参数接收和响应包装Service层只管业务规则Mapper层只负责数据访问。页面模板通过Controller返回视图名称由Thymeleaf渲染。项目的包结构如下com.example.blog ├── controller # 视图控制器与接口控制器 │ ├── admin # 后台管理相关 │ └── web # 前台页面相关 ├── service # 业务接口与实现 ├── mapper # MyBatis-Plus数据访问接口 ├── entity # 数据库实体类 ├── dto # 数据传输对象 ├── vo # 视图对象 ├── config # 配置类WebMvc、拦截器、Redis等 ├── common # 统一结果包装、全局异常、常量 └── utils # JWT、MD5等工具类Controller层和Service层的接口返回值我统一用一个Result 对象包装。结构是code、message、data三个字段。code为200时代表正常其他值由全局异常处理器映射为对应的提示信息。这样前端模板和Ajax请求都能用同一套规范解析返回内容。2.2 我的统一响应与全局异常处理设计Result类的核心代码如下Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.setCode(code); r.setMessage(message); return r; } }全局异常处理类使用RestControllerAdvice拦截业务异常与系统异常。业务异常直接返回友好提示系统异常则记录日志后返回服务器开小差了这类兜底文案避免把堆栈信息直接暴露给用户。在Controller返回视图时我使用ModelAndView或Model传参而在处理Ajax请求时直接返回Result对象。Thymeleaf模板通过th:each、th:text等标签渲染数据动态部分通过th:if判断登录状态来显示不同的导航栏。2.3 配置文件里的关键可调项application.yml是最需要细心对待的文件。我拆成三份application-dev.yml、application-prod.yml和公共的application.yml。公共部分只写应用名、端口、JSON序列化规则开发环境配置本地数据库连接、SQL日志输出生产环境配置服务器数据库地址、关闭SQL日志、配置Redis地址。关键配置片段spring: datasource: url: jdbc:mysql://localhost:3306/blog_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: xxx driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里两个细节需要特别说明。第一serverTimezone必须设置为Asia/Shanghai否则连接MySQL 8时会出现8小时时差问题第二逻辑删除字段统一命名为deletedMyBatis-Plus会在所有单表操作中自动追加deleted0条件。这个机制让文章下线不再物理删除数据对后续做回收站或草稿箱非常有用。3. 数据库设计七张表撑起一个博客站点的落地方案3.1 表结构与字段设计思路数据库我命名为blog_db设计时先画ER图再落成建表语句。七张表分别是sys_user用户表article文章表category分类表tag标签表article_tag文章与标签的中间表comment评论表sys_oper_log操作日志表sys_user表的核心字段有id、username、password、nickname、avatar、email、role、status、create_time、update_time、deleted。password字段存的是BCrypt加密后的密文长度需要设成100而不是32这个问题我后面专门踩过坑。article表是系统的核心。字段包括id、user_id、category_id、title、summary、content、cover_image、status1草稿、2已发布、3已下线、view_count、comment_count、create_time、update_time、deleted。正文采用MEDIUMTEXT类型因为用户写的Markdown原文和渲染后的HTML可能都比较长摘要字段summary则是从正文中截取的纯文本用于列表页展示。category表很简单id、name、create_time。一个分类下可以有多篇文章文章与分类是多对一关系。tag表是id、name文章与标签是多对多关系通过article_tag中间表关联。中间表只有article_id和tag_id两个字段联合主键避免重复记录。comment表设计时考虑了层级关系。字段有id、article_id、user_id、parent_id、content、create_time、deleted。parent_id为0表示顶层评论非0表示对某条评论的回复。这里没有做无限级回复树两层结构足够博客场景使用。sys_oper_log表记录操作行为字段有id、username、operation、method、params、ip、create_time。用AOP切面记录Controller中标注了Log注解的方法调用。3.2 关系设计与查询路径数据库关系设计可以归纳为三句话用户与文章是一对多article表中的user_id指向sys_user表分类与文章是一对多article表中的category_id指向category表文章与标签是多对多通过article_tag中间表解耦查询文章详情时通过article表左连接user和category表获取作者名与分类名通过中间表关联tag表获取标签列表。这个查询在MyBatis-Plus中通过自定义XML实现因为涉及多张表的关联和字段映射。3.3 索引与字段约束让查询不会变慢的细节我建索引的依据是WHERE子句里最常出现的字段。文章表对category_id和status建了联合索引对create_time建了排序索引。评论表对article_id建了索引这样按文章查评论就能走索引避免全表扫描。操作日志表对create_time建了索引方便按时间范围排查。字符编码统一使用utf8mb4而不是utf8。原因很简单utf8mb4才是完整的UTF-8实现能存储emoji和生僻字。如果你的博客文章里出现emoji而数据库是utf8插入时会直接报错。有一个字段约束容易被忽略article表的content用MEDIUMTEXT但summary必须用VARCHAR并限制长度否则列表页加载大量文章时会拖慢响应。4. 核心功能实现登录鉴权、文章发布、分类标签聚合与评论4.1 登录功能Session方案到JWT Token的演进登录模块我最初打算用传统Session但考虑到将来可能把管理接口暴露给小程序或移动端最终采用了JWT Token方案。用户登录成功后服务端签发一个有效期2小时的Token客户端在请求头中携带后端通过拦截器校验。密码加密使用BCrypt。我引入了Spring Security中的BCryptPasswordEncoder类不需要引入完整的Spring Security框架只使用它的加密工具。BCrypt加密的特点是每次生成的密文不同验证时却始终能比对成功这能有效防止彩虹表攻击。用户表初始化时需要一个管理员账号。我在项目启动时使用ApplicationRunner检测sys_user表是否为空若为空就创建一个默认管理员admin密码使用BCrypt加密后存入数据库。这样系统首次启动就能直接登录不需要手动执行SQL脚本。生成Token的工具类核心逻辑public class JwtUtil { private static final String SECRET your-secret-key; private static final long EXPIRE_TIME 2 * 60 * 60 * 1000; public static String createToken(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }登录成功后返回的Token保存在浏览器的localStorage中。导航栏通过JavaScript读取Token来判断是否显示写文章入口。这个方案避免每次刷新页面都要向后端请求用户状态。在WebMvc配置类中注册一个登录拦截器拦截路径为/admin/**。拦截器对放行条件做两层判断先从请求头取Token解析失败则重定向到登录页。后台管理相关的Controller都放在/admin路径下前台展示页面全部放行。4.2 文章发布与管理Markdown与富文本的取舍写文章涉及内容编辑器的选型。我调研后选择了wangEditor作为富文本编辑器原因有两个它支持图片上传到后端存储中文文档友好不需要掌握额外语法。同时我在后端预留了summernote等编辑器的切换空间编辑器只是前端渲染工具核心是存储结构。文章发布流程是这样的前端表单提交标题、分类、标签ID列表、摘要、正文HTML后端Controller接收DTO校验标题非空且不超过100字Service层处理标签如果标签不存在则自动创建然后维护中间表设置文章的初始状态为草稿通过则改为已发布同时更新分类下的文章计数返回Result包装的保存成功信息文章正文我采用直接存储HTML的方式编辑器输出什么就存什么。出于安全考虑Service层在存储之前会用Hutool的HtmlUtil过滤掉script标签和事件属性防止XSS攻击。这个问题会在后面的安全章节详细展开。列表页的文章摘要不是人工填写的长篇大论我在Service层封装了一个纯文本截取的方法把HTML转成纯文本按80个字符截取补上省略号。这样既能保证列表页美观又不需要作者额外维护摘要字段。分页查询是文章列表最核心的功能。MyBatis-Plus的Page对象配合LambdaQueryWrapper可以动态拼装条件PageArticleVO page articleMapper.selectPage(new Page(current, size), new LambdaQueryWrapperArticle() .eq(Article::getStatus, 2) .eq(categoryId ! null, Article::getCategoryId, categoryId) .like(keyword ! null, Article::getTitle, keyword) .orderByDesc(Article::getCreateTime));eq方法传入的condition参数为false时自动忽略该条件这是MP最实用的特性能让代码非常整洁。4.3 文章列表、分类标签聚合与详情页的渲染文章详情页是信息密度最大的页面需要展示作者、发布时间、浏览量、分类、标签列表、正文内容、评论列表以及上一篇和下一篇的导航。我自定义了一个ArticleDetailVO来承载这些数据避免前端模板直接处理多表关联。查询逻辑分为两步第一步根据文章主键查询文章基础信息连表获取作者昵称与分类名通过中间表获取标签列表第二步根据当前文章的create_time分别查询上一篇文章和下一篇文章同样连表取标题这里需要特别注意上一篇/下一篇的实现方式。通常人们会按id±1来写正确做法是按创建时间比较。删除过文章后id会不连续按时间排序更符合实际体验。SQL条件可以写成-- 上一篇 SELECT id, title FROM article WHERE status 2 AND create_time #{createTime} ORDER BY create_time DESC LIMIT 1 -- 下一篇 SELECT id, title FROM article WHERE status 2 AND create_time #{createTime} ORDER BY create_time ASC LIMIT 1浏览量我采用先读Redis、定期刷库的策略。每次访问详情页时执行redisTemplate.opsForValue().increment同时先把浏览量暂存到Java内存Map中每5分钟批量更新一次article表。上线初期没有Redis时也能退化为直接update代码里做好是否启用的开关即可。4.4 评论模块层级结构与敏感词过滤的基础思路评论算是这个系统里最需要理清楚父子关系的部分。表结构上parent_id逻辑删除字段design解决了两层评论的存储问题但查询时需要额外的逻辑来组织层级。我在查询文章评论时直接查全部然后在内存中按parent_id进行分组顶层评论按时间升序排列子评论紧跟在所属父评论下方。评论的发布接口放在登录拦截的路径之外前端先把Token放在请求头中后端通过拦截器里的用户信息确定评论作者。服务端做好评论内容的长度校验最多500字同时做了简单的敏感词过滤。敏感词过滤我用的是DFA算法加前缀树的思路实现了大约200个基础敏感词的替换屏蔽。代码放在一个SensitiveWordUtil工具类中核心方法是把敏感词库构建成Trie树遍历评论内容时发现匹配就替换成*号。这个方案相比正则匹配性能高很多尤其当词库扩大到几千条时差异更明显。5. 性能优化与安全加固我实测过的坑和建议方案5.1 文章详情页的性能优化文章详情页的正文内容通常是几百KB级别的HTML如果每次访问都从MySQL读取数据库压力会很直接地反映在响应时间上。我的优化思路是增加一层Redis缓存。缓存Key设计为article:detail:{id}启动时加载状态正常的文章信息到缓存。访问详情页时先查缓存缓存不存在再查数据库并回填。文章更新时先更新数据库再删除对应缓存。这样一个读多写少的场景就能把数据库压力打下来。静态资源方面图片上传到本地磁盘后通过nginx直接访问不经过Tomcat容器为Tomcat省下带宽。封面图在生成摘要时压缩为固定尺寸通过Java的ImageIO实现等比缩放在保持可读性的前提下减小体积。5.2 常见安全隐患与修复博客系统因为包含登录和内容发布功能是网络安全演练的常见目标。我对照OWASP Top 10逐项排查过其中三个问题最实际。第一个是SQL注入。MyBatis-Plus默认使用预编译的#{}占位符这是安全的。但如果你在XML里手写了${}拼接就存在注入风险。我的排查方案是全局搜索XML和注解SQL把${}全部替换为#{}动态排序字段则先做白名单判断。第二个是XSS攻击。用户在编辑器里写文章时可能故意插入script标签如果直接存储并通过th:utext输出浏览器就会执行脚本。我的处理方式是存储前使用Jsoup清理不安全的标签前端输出正文时仍使用th:utext因为Markdown转换后需要渲染HTML但存储时已经过白名单过滤script、iframe、object等标签都会被移除。第三个是CSRF攻击。因为使用了JWT Token我在前端每次发起POST请求时都会带一个自定义请求头杜绝了表单跨站提交的可能。如果完全依赖Cookie就需要引入Spring Security的CSRF Token机制。JWT密钥的管理也要注意。SECRET字符串不能硬编码在代码里我把它放到了application.yml中生产环境使用单独的环境变量注入。密钥长度至少32位否则签名算法容易暴力破解。5.3 我实测踩过的坑从报错到解决第一个坑BCrypt密码长度导致数据库字段不够用。BCrypt加密后的字符串长度约60个字符如果这张表一开始就设计成password varchar(32)登录时无论如何都会报错。解决方法是把密码字段改成varchar(100)。不少人在建表初期就把密码设计成32位源于常见的MD5思维抢着记一下。第二个坑MySQL 5.7和8.0的驱动类名不同。MySQL 8的驱动类名是com.mysql.cj.jdbc.Driver需要在URL里加serverTimezone参数。MySQL 5.7使用com.mysql.jdbc.Driver就可以。我用的是MySQL 8一开始按旧教程配置启动直接报Loading class失败。第三个坑Thymeleaf模板中的日期格式化。如果实体中的create_time是LocalDateTime类型直接输出会显示成类似2024-03-26T10:15:30的格式。需要在模板中使用格式化表达式span th:text${#temporals.format(article.createTime, yyyy-MM-dd HH:mm)}/span如果不想在模板里处理也可以在实体上使用JsonFormat配合DateTimeFormat注解。两者选一种即可同时使用反而会产生序列化冲突。第四个坑事务失效问题。我在文章发布方法上标注了Transactional但Service内部有一个方法调用了同类中的另一个方法比如发布后调用更新标签查询这个内部调用绕过了代理事务没有生效导致部分数据写入成功而部分失败。排查了半天解决方案是把内部调用拆到独立的Service Bean中或者让调用点与被调用点分离。6. 部署上线与运行维护从本地跑到服务器全过程的经验6.1 打包与外部化配置开发完成后的打包我选用Maven的package命令跳过测试直接生成可执行jar。命令如下mvn clean package -DskipTests生产环境部署不使用默认的application.yml而是外部指定配置文件。好处是服务器上的数据库密码、Redis地址不用写进jar包给运维交接时也更安全。java -jar blog-system.jar --spring.profiles.activeprod生产配置里还需要设置JVM参数nohup java -Xms256m -Xmx512m -XX:HeapDumpOnOutOfMemoryError -jar blog-system.jar \ --spring.config.location/opt/blog/config/application-prod.yml \ /opt/blog/logs/startup.log 21 -Xms与-Xmx设置为256m和512m对博客这种中小规模应用足够。HeapDumpOnOutOfMemoryError参数能让系统OOM时自动导出堆快照方便事后排查内存泄漏。6.2 服务器环境准备服务器环境我使用宝塔面板来降低运维操作门槛但服务进程改用systemd管理保证崩掉后能自动拉起。systemd服务单元文件的配置如下[Unit] DescriptionBlog System Afternetwork.target mysql.service redis.service [Service] Userwww WorkingDirectory/opt/blog ExecStart/usr/bin/java -Xms256m -Xmx512m -jar /opt/blog/release/blog-system.jar Restartalways RestartSec5 StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target配置完成后执行systemctl daemon-reload和systemctl enable blog服务就能开机自启。之后所有日志通过journalctl -u blog -f查看比原来nohup追加到log文件的方式方便很多。MySQL和Redis分别部署在同一台服务器上。MySQL需要创建一个专用的blog_user而不是使用root密码通过环境变量注入到Spring配置中。Redis可以不对外开放端口仅监听127.0.0.1外网无法直接访问。6.3 上线后要关注的日常巡检项上线一周里我养成了几个固定巡检动作。第一是用df -h盯着磁盘空间因为图片上传和日志文件都会增长超过80%就得清理。第二是看MySQL慢查询日志把执行超过2秒的SQL捞出来优化实际运行中最常见的慢查询是按文章标题模糊搜索因为LIKE %keyword%无法走索引这个是预期内的代价。第三是每天凌晨自动备份数据库。0 2 * * * mysqldump -u blog_user -pxxx blog_db | gzip /data/backup/blog_$(date \%Y\%m\%d).sql.gz备份文件保留30天即可。上线初期我遇到了一个有意思的问题MySQL连接数飙到上限。排查后发现问题不在并发访问量而是HikariCP连接池的积压配置过高导致每次操作都占着连接不放。解决方案是将maximum-pool-size从默认的10调小到8同时降低connection-timeout和idle-timeout让空闲连接及时归还。调整后连接数恢复平稳。这轮从需求梳理到上线运维做完我最深的体会是精简从来不等于简陋。项目边界收敛之后每一行代码的职责都变得清晰每一个模块都能在论文里讲透部署时的排查链路也更短。如果你手上也有一个博客系统要做我建议你也先把不做哪些功能定下来再谈怎么做。功能越少你才有精力把登录安全、数据一致性和部署运维这些真正值钱的东西做扎实遇到不懂的问题也能追到根因而不是停留在能跑就行的层面。最后分享一个实用经验在开发过程中我每做完一个功能都会先在浏览器里完整走一遍用户路径再从控制台看一次SQL日志。这两个动作用不了几分钟但能过滤掉大量开发环境正常、联调就崩的尴尬。系统的代码量不大但这种自我检查的习惯是比代码本身更值得保留下来的资产。
返回列表