ARTICLE DETAIL

资讯详情

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

Spring Boot个人博客系统实战:从技术选型到部署踩坑

Spring Boot个人博客系统实战:从技术选型到部署踩坑 说实话个人博客系统是我见过最适合拿来练手的Spring Boot项目没有之一。它不像电商系统那样动辄十几个模块也不像管理后台那样全是重复的增删改查——它刚好卡在“业务逻辑足够完整”和“代码量不至于劝退”之间的黄金位置。这篇博文基于我最近整理的一个springboot个人博客系统项目编号11677来写从技术选型、核心功能实现到部署上线把踩过的坑和总结出来的经验一次性说清楚。适合正在做Java毕设、准备Spring Boot面试、或者单纯想给自己搭一个写作站点的同学。1. 为什么个人博客系统是最值得做的Spring Boot练手项目1.1 功能边界麻雀虽小五脏俱全很多人做项目喜欢追求大而全上来就是“XX管理系统”模块列表拉出来几十个最后能写完的没几个。个人博客系统的妙处在于它天然具备一个完整业务系统该有的核心要素但每个要素的复杂度又不至于失控。我当时定这个springboot个人博客系统的功能边界时只保留了四块文章管理发布、编辑、删除、分页列表、分类与标签一对多、多对多关系、评论基础的用户留言、站点信息关于我、友情链接。这几块拼起来覆盖了几乎所有Java后端面试里会被问到的场景RESTful接口设计、数据库表关系设计、MyBatis关联查询、参数校验、异常处理、日志记录。更关键的是这个系统能让你真正感知到“一个请求从浏览器到数据库再回来”的完整过程。比如你在前端点一下“发布文章”后端Controller接收请求、Service做业务判断、Mapper操作数据库最后返回JSON给前端渲染——这条链路搞清楚了Spring Boot对你就不再是黑盒。1.2 它覆盖了Spring Boot面试高频考点的七成我经常跟准备面试的朋友说简历上写“个人博客系统”并不丢人丢人的是只会照着教程敲一遍追问几句就答不上来。实际上一个认真做的博客系统能串起来的面试考点非常多Spring Boot自动装配原理为什么加一个spring-boot-starter-web依赖内嵌Tomcat就能跑起来这背后是EnableAutoConfiguration和META-INF/spring.factories的机制。Spring Boot配置体系application.yml里的数据源、端口、日志级别这些配置如何被绑定到ConfigurationProperties实体类上。MyBatis整合SqlSessionFactory的创建、Mapper接口的动态代理注册、MapperScan的作用。MVC分层思想Controller只做参数接收和响应包装Service只做业务逻辑Mapper只做数据访问。事务管理当一篇文章同时插入文章表、标签关联表、分类统计时Transactional的生效规则和回滚场景。换句话说把这个项目吃透Spring Boot相关的基础面试题你基本都能聊。反过来说如果你能用简洁的代码让这个系统跑起来并且能讲清楚每个注解的含义面试官通常不会刁难你。1.3 适合谁来做以及一个合理的迭代顺序我给这个项目规划的学习路径是先照着已有代码比如11677这套跑通然后手写核心模块最后尝试重构。具体分三步走照葫芦画瓢阶段把项目导入IDE启动用Postman调用文章列表接口理解请求进来之后发生了什么。脱稿重写阶段把文章模块的Controller、Service、Mapper删掉自己重新写一遍写不出来就看答案但一定要写。扩展创新阶段加一个别人没做的功能比如全文搜索、图片上传、静态资源加速。这也是这个项目在毕设里拿高分的关键。2. 动手前必须定下来的几件事技术选型与项目骨架2.1 后端技术栈守住Spring Boot这个核心很多新手纠结要不要上Spring Cloud、要不要用Dubbo我的态度很明确个人博客系统完全不需要微服务硬上只会给自己找麻烦。服务拆分的收益来自独立部署和独立扩容的需求而博客系统单机部署绰绰有余。我最终定的技术栈是这样的组件选型理由开发框架Spring Boot 2.7.x稳定兼容性好教程资料多持久层MyBatis复杂SQL可控面试常问数据库MySQL 8.0个人部署成本低功能完备模板/前端Thymeleaf或Vue二选一各有侧重构建工具Maven主流比Gradle更普及缓存Spring Cache Caffeine可选热点文章列表提速需要说明的是Spring Boot版本不要追新。我在后文会专门讲Spring Boot版本太高带来的依赖冲突问题这里先记住2.7.x这个系列目前是最稳的折中选择。2.2 持久层是选MyBatis还是Spring Data JPA这个问题几乎每个做博客项目的人都会纠结。我的答案是如果目标是面试或毕设优先选MyBatis。原因很现实——国内Java开发岗位面试中MyBatis的出现频率远高于JPA而且MyBatis的XML文件能让你直观地看到SQL排查问题时思维负担更小。用MyBatis时你只需要做三件事写一个BlogMapper接口定义方法。在resources/mapper目录下写对应的XML文件在里面写SQL。在启动类上加上MapperScan(com.example.blog.mapper)或者在每个Mapper接口上标注Mapper。Mapper public interface ArticleMapper { ListArticleVO selectPublishedArticles(Param(offset) int offset, Param(limit) int limit); ArticleVO selectArticleDetail(Param(id) Long id); int insertArticle(Article article); }对应的XML片段select idselectPublishedArticles resultTypecom.example.blog.vo.ArticleVO SELECT a.id, a.title, a.summary, a.create_time, c.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 LIMIT #{offset}, #{limit} /selectSQL写在XML里的好处是一旦前端页面数据不对你可以直接把这句SQL复制到Navicat里执行很快就能定位是SQL写错了还是后端参数传错了。2.3 前端方案Thymeleaf还是前后端分离如果时间紧张我建议用Thymeleaf服务端渲染。原因很简单不需要单独部署前端不用担心跨域Spring Boot打包成jar直接跑。Thymeleaf表达式和HTML混写对只写过Java的同学来说上手极快。但如果你本身会Vue或者毕设要求前后端分离那就用Vue Spring Boot的方案。这个方案的坑主要在后期的“打包合并”——Vue构建出来的dist目录怎么放进Spring Boot后文会有专门一节讲。无论选哪种博客系统的前端核心页面就四类首页文章列表、文章详情页、分类/标签归档页、后台管理页。把这四类页面打通系统就算完成八成。2.4 项目骨架分包分对了后面少走一半弯路包结构的设计直接影响后续开发的节奏。我习惯按“模块分包”而不是“技术层分包”。所谓的模块分包就是先按功能切成controller、service、mapper、domain、dto、vo几个包然后每个模块文章、分类、评论的代码归拢到对应包下而不是把所有Controller堆在一起。com.example.blog ├── BlogApplication.java ├── common // 统一返回结果、异常处理、工具类 ├── config // Web配置、拦截器、资源映射 ├── controller │ ├── ArticleController.java │ ├── CategoryController.java │ └── CommentController.java ├── service │ ├── ArticleService.java │ └── impl │ └── ArticleServiceImpl.java ├── mapper // MyBatis的Mapper接口 ├── domain // 实体类 ├── dto // 入参对象 └── vo // 返回给前端的视图对象这个结构里有两个容易被忽略的细节。第一domain里的实体和vo对象一定要分开不要把数据库表字段直接暴露给前端。比如数据库里文章的content字段可能是很长的Markdown原文但列表页只需要一个summary摘要用VO隔离能避免接口数据臃肿。第二common包里放一个ResultT统一返回类接口统一返回{code, message, data}结构前端处理逻辑会非常清爽。3. 从零跑通核心链路文章的写、存、读、渲染3.1 数据库设计四张表建模别急着加字段个人博客系统的数据库设计重点在“关系”。我当时设计了四张核心表表名关键字段说明articleid, title, summary, content, category_id, status, create_time, update_time文章主表content存Markdown原文categoryid, category_name, category_desc分类表tagid, tag_name标签表article_tagarticle_id, tag_id文章与标签的多对多关联表设计的时候有一个最重要的原则content字段尽量存Markdown原文不要存渲染后的HTML。HTML体积大、难以修改、还有XSS注入风险。Markdown原文保存后后端可以随时改变渲染规则比如换一个更美观的代码高亮主题不用动数据库。分类和标签的区别也要想清楚分类是树状结构一篇文章通常只有一个分类比如“Java”标签是扁平结构一篇文章可以打多个标签比如“Spring Boot”“MyBatis”“踩坑”。如果这两点混了后续查询归档页会非常别扭。评论表虽然在核心功能里但我建议第一版不要做“评论回复评论”的嵌套功能先做一层简单评论。嵌套评论涉及到递归查询和前端树形渲染复杂度会瞬间上来放到第二版再说。3.2 发布文章接口三层架构的真实写法拿最关键的“发布文章”功能举例它不像很多人想的只是insert一条记录。真实业务里发布一篇文章至少要做四件事插入文章主记录、更新或校验分类存在、批量绑定文章标签、清理首页列表缓存。Controller层我一般不写业务逻辑只做参数校验和结果包装RestController RequestMapping(/api/article) public class ArticleController { Resource private ArticleService articleService; PostMapping(/publish) public ResultLong publish(RequestBody Valid ArticlePublishDTO dto) { Long articleId articleService.publish(dto); return Result.success(articleId); } }Service层负责事务和业务规则。发布方法必须加上Transactional因为往article_tag表批量插入时如果中间某一条失败而主文章已经插入成功会留下脏数据。这里有个经验事务在Service层具体是ArticleServiceImpl的实现方法上加不是加在Controller上。Transactional(rollbackFor Exception.class) public Long publish(ArticlePublishDTO dto) { Article article new Article(); BeanUtils.copyProperties(dto, article); article.setStatus(1); articleMapper.insertArticle(article); if (dto.getTagIds() ! null !dto.getTagIds().isEmpty()) { articleTagMapper.batchInsert(article.getId(), dto.getTagIds()); } return article.getId(); }rollbackFor Exception.class这个属性非常关键。Spring里Transactional默认只在RuntimeException时回滚如果你的代码捕获了异常并包装成自定义业务异常BusinessException而它是Exception的子类而非RuntimeException事务就不会回滚。所以统一显式指定rollbackFor Exception.class能避免很多隐性bug。3.3 Markdown存储与渲染后端只存原文渲染交给前端我做这个springboot个人博客系统的时候Markdown的存储和渲染踩过一个大坑一开始想在服务端用commonmark-java把Markdown转成HTML再存进数据库结果后发现两个问题——一是HTML体积膨胀二是想改渲染主题时必须重新渲染所有旧文章。正确的做法是数据库只存Markdown原文渲染动作放在前端或者后端接口层。如果使用Thymeleaf后端可以用commonmark把content转成HTML再塞进Model但这只是“展示时转换”不落库dependency groupIdorg.commonmark/groupId artifactIdcommonmark/artifactId version0.21.0/version /dependencypublic String renderMarkdown(String markdownContent) { Parser parser Parser.builder().build(); HtmlRenderer renderer HtmlRenderer.builder().build(); return renderer.render(parser.parse(markdownContent)); }如果是前后端分离方案建议把这个渲染直接放在前端做。Vue生态里有成熟好用的Markdown编辑器组件比如md-editor-v3它能让用户写Markdown的同时实时预览效果体验比纯后端渲染好得多。3.4 首页文章列表与分页LIMIT offset的隐患列表分页是每个人都会写的功能但容易被忽视的是“深分页”问题。比如用户直接翻到第100页LIMIT 2000, 10这种写法会让MySQL扫描前2010条记录再丢弃前2000条数据量大时非常慢。博客系统数据量级一般不大但为了养成好习惯我建议在SQL层面处理传入上一页最后一条记录的create_time作为游标用WHERE create_time #{lastCreateTime} ORDER BY create_time DESC LIMIT 10来替代LIMIT offset, size。再加一个create_time和id的联合索引翻页会非常顺滑。如果不想改SQL至少也要做两件事分页参数做上限约束pageSize最大50热点列表加缓存。4. 给博客加两个实用功能中文分词搜索和图片上传4.1 为什么接HanLP一个普通LIKE查询不够用博客系统如果文章多了搜索功能就成了刚需。最初我用的是MySQL的LIKE %关键词%结果一搜“Spring Boot自动装配”这种长词匹配不完整搜“Spring”搜不到“Spring Boot”搜“自动装配”又匹配不到体验很差。问题出在中文分词。MySQL内置的分词对中文支持太弱而HanLP是Java生态里成熟的中文自然语言处理库分词能力专门为中文优化过。在Spring Boot项目里用它只需要加一个依赖dependency groupIdcom.hankcs/groupId artifactIdhanlp/artifactId versionportable-1.8.4/version /dependency然后调用它的分词APIListTerm termList HanLP.segment(Spring Boot自动装配原理); // 输出[Spring/nx, Boot/nx, 自动/d, 装配/v, 原理/n]这里要注意HanLP默认的分词结果里面有词性标注你可能只关心词本身。在做搜索时我会把用户输入的查询词切分成若干关键词然后用这些关键词去组装SQL查询条件// 伪代码示意 ListString keywords HanLP.segment(query).stream() .map(term - term.word) .toList(); // 将keywords拼成 title LIKE %kw1% OR title LIKE %kw2% OR content LIKE ...实际做的时候SQL必须动态拼接而且一定不能直接字符串拼接要用script标签配合foreach处理参数避免SQL注入。4.2 MinIO接入文件上传选型的一个务实方案博客写久了图片附件就是刚需。图片怎么存我见过最蠢的方案是把图片base64编码直接塞进数据库字段一张图片能把数据库撑爆。还有人是把文件存到本地磁盘的某个目录这个方案单机没问题但后续迁移服务器时图片全没了。更稳的方案是引入对象存储。如果不想用云厂商的OSS需要付费完全可以在本地用MinIO它兼容S3协议而且Docker一键就能跑起来docker run -d \ --name minio \ -p 9000:9000 \ -p 9001:9001 \ -e MINIO_ROOT_USERadmin \ -e MINIO_ROOT_PASSWORDadmin123456 \ minio/minio:latest server /data --console-address :9001Spring Boot集成MinIO也是加依赖、写配置、封装工具类的流程。注意新版MinIO SDK的API变化如果你是照旧博客抄代码经常会出现bucketExists等方法的编译错误。4.3 搜索和上传功能的四个实践细节这部分我踩过的坑比较多挑四个最典型的分享第一个坑HanLP分词后拼SQL关键词个数不可控。用户输入一整句话可能被切出十几个词语如果全部拼进SQL会导致索引完全失效查询极慢。我的处理是只取前两个在标题中能命中的关键词其余词放content里做模糊匹配。第二个坑上传文件类型过滤。接口不能只检查文件后缀名。.jpg后缀的文件内容可能是恶意脚本。规范的做法是检查文件的Content-Type并用工具类解码文件头比如JPEG的文件头是FF D8 FF来校验真实格式。第三个坑MinIO返回的链接是后端接口地址不是MinIO的9000端口。前端拿到的图片URL应该是/api/file/preview/{objectName}后端在预览接口里设置响应头为图片MIME类型并把文件流写到Response里。这样既隐藏了内部存储细节也方便做权限控制。第四个坑搜索接口必须做超时控制。HanLP第一次加载词典会初始化比较慢接口可能白屏几秒。我的做法是在应用启动后用一个PostConstruct方法预热加载一次HanLP把词典初始化放在启动阶段而不是第一次请求时。5. Vue打包后放进Spring Boot前后端一体化部署方案5.1 为什么要一体化部署而不是分开部署现在很多教程让你前端部署到Nginx、后端部署到服务器中间还要配CORS跨域。但个人博客这种低流量项目完全没必要引入Nginx。Vue构建后的dist目录本质上就是一堆静态文件Spring Boot本身就是支持静态资源服务的把前端打包产物放进src/main/resources/static目录整个项目一个jar就能跑部署成本最低。具体做法是在Vue项目的根目录执行npm run build之后把生成的dist目录下的index.html和static或assets目录复制到Spring Boot项目的src/main/resources/static下。如果你希望后端和前端分开维护可以配置Maven插件在构建时自动完成复制plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-resources-plugin/artifactId version3.3.0/version executions execution idcopy-vue-dist/id phaseprepare-package/phase goals goalcopy-resources/goal /goals configuration outputDirectory${project.build.outputDirectory}/static/outputDirectory resources resource directory${project.basedir}/../blog-frontend/dist/directory /resource /resources /configuration /execution /executions /plugin配置好了之后执行mvn clean packageVue前端产物会被自动打进jar包里发布时只需要一个java -jar blog.jar不用再折腾Nginx。5.2 路由的历史模式问题刷新404的根因前后端分离项目最典型的部署坑是首页能打开但直接在浏览器地址栏进入/article/123就报404。这个问题的根因在Vue Router的history模式——它依赖后端把所有非静态文件请求都转发到index.html由前端路由接管。Spring Boot默认没有这个转发规则所以需要手动加一个Controller或者实现WebMvcConfigurer处理请求转发。我推荐加一个简单的“路径回退”配置但只回退非静态资源路径Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/**) .addResourceLocations(classpath:/static/); } Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{path:[^\\.]*}) .forwardTo(/index.html); } }这里面有个细节{path:[^\\.]*}这个正则表达式是关键。它只对“不含点号的路径”做转发像/api/article/1、/static/js/app.js这些含点号或以/api开头的请求不会被误转发。如果写得过于宽松比如/**会导致后端接口也被拦截到index.html那才是灾难现场。如果你想避开这个坑最简单粗暴的办法是Vue Router改成hash模式。hash模式的URL长这样/#/article/123整个地址永远是index.html#xxx不存在服务器端路由问题。代价是URL不够美观但对个人博客来说完全够用。我的建议是第一次做一体化部署先用hash模式跑通后再挑战history模式。5.3 跨域问题其实可以不用配置CORS前后端分离开发时Vue跑在localhost:5173Spring Boot跑在localhost:8080两者端口不同前端请求后端必然遇到跨域。很多教程会教你在Spring Boot里加CrossOrigin或者配置CorsFilter。但如果最终采用一体化部署开发期的跨域配置在部署后就成了隐患——一旦配置不当反而可能拦截正常请求。更干净的做法是开发阶段利用Vue的代理能力解决跨域部署阶段不做任何CORS配置// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样在开发时前端请求/api/article会由Vite代理转发到后端浏览器里不产生跨域部署后前端静态文件和后端接口同源更不需要CORS。这个方案比起在Spring Boot里加CorsFilter逻辑上要干净得多。6. 版本和依赖引发的几个棘手问题踩坑实录6.1 Spring Boot版本太高导致的依赖冲突我刚开始做这个springboot个人博客系统时图新鲜选了Spring Boot 3.2.x结果第一天就炸了。Spring Boot 3.0之后Java最低要求17同时整个Spring框架迁移到了Jakarta EE命名空间javax.servlet变成了jakarta.servlet。这意味着网上大量基于2.x写的教程代码直接复制过来会看到一片红色报错。更麻烦的是第三方库的兼容性。比如MyBatis的starter在3.x版本下需要换用新的依赖坐标旧坐标mybatis-spring-boot-starter在部分版本下不生效一些代码生成工具也不支持Jakarta命名空间。折腾了一个晚上后我果断回退到Spring Boot 2.7.18。这个版本兼容Java 8/11/17主流教程资料几乎都基于它第三方组件的兼容问题最少。经历这一轮之后我的体会是技术选型时“稳定”比“新”更重要。尤其做毕设或生产项目新特性带来的收益远小于兼容性踩坑的代价。除非你有明确理由必须用Spring Boot 3.x比如需要虚拟线程newVirtualThreadPerTaskExecutor否则2.7.x永远是最稳的选择。6.2 MyBatis的Mapper扫描问题启动报错还是运行时才报错MyBatis集成Spring Boot后最容易踩的坑有两个一个是在启动类上漏写MapperScan结果是启动时直接报“Invalid bound statement (not found)”错误另一个是XML文件没有被打包到classes目录下。第二个坑特别隐蔽。项目用IDEA启动时resources/mapper/*.xml通常能被正常加载但用mvn package打jar包后如果pom.xml没有显式配置resources资源拷贝XML文件可能不会进jar包部署到服务器后所有Mapper方法全部报错。解决方法是确保pom.xml里配置了资源过滤和拷贝resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory /resource /resources这个配置时刻提醒我们Maven打包和IDE运行是两码事本地跑得好好的并不代表部署到Linux服务器上也能跑起来。每次改动涉及资源文件后都应该用clean package复现一次完整的构建流程。6.3 端口配置和IDEA编辑配置的坑博客系统默认端口是8080但如果你电脑上已经跑了别的服务端口冲突会导致启动失败。这个问题排查起来不算难但新手容易慌。我一般用三条命令定位# 查看8080端口被谁占用Linux/macOS lsof -i:8080 # Windows下是这个 netstat -ano | findstr 8080 # 查看占用进程的详细信息Windows tasklist | findstr 上一命令得到的PID如果是干净的机器直接改application.yml里的端口即可server: port: 8080 servlet: context-path: /在IDEA里还有个容易让人困惑的地方如果你配置了“编辑配置”里的环境变量或者VM选项比如-Dserver.port8081那么它的优先级会比application.yml高。遇到“我明明改了yml端口为什么没生效”的问题先检查是不是IDEA的Run Configuration里写了server.port参数。6.4 一次完整排查链路文章列表500错误的定位过程我分享一个真实发生的排查过程借此演示遇到问题时该怎么一步步缩小范围。现象前端调用GET /api/article/list?page1返回500错误日志里有一大堆异常堆栈。第一步先看日志中最关键的异常类型。那次报的是org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.example.blog.mapper.ArticleMapper.selectPublishedArticles。第二步凭经验判断这不是SQL语法错误而是MyBatis没找到Mapper接口对应的XML。于是先检查ArticleMapper.java上有没有Mapper注解再看启动类有没有MapperScan。第三步发现注解都有于是怀疑XML文件没被加载。打开target/classes/mapper目录发现里面根本没有ArticleMapper.xml。到这里问题定位到Maven构建没有拷贝resources文件。第四步在pom.xml里补上resources配置重新mvn clean package再启动请求返回200。整个过程不到15分钟但如果不懂“接口 → XML → SQL语句”这条链路看到Invalid bound statement很容易懵。这个案例说明Spring Boot项目的排错本质上就是对“谁调用了谁、配置有没有生效、资源有没有打包”这三件事的逐一验证。7. 收尾一些我反复踩坑后沉淀下来的习惯项目做到后面我发现真正拉开差距的不是用了多少框架而是有没有形成一套稳定的开发习惯。在这套springboot个人博客系统上沉淀下来几个值得分享的习惯第一个习惯每写一个接口先用Postman把返回JSON格式确定下来再写前端页面。不要前端后端并行开发然后靠联调去碰运气。接口返回结构统一用{code, message, data}前端处理逻辑能简练很多。第二个习惯SQL日志一定要在开发阶段打开。在application.yml里配置mybatis.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl每个SQL在执行时都会打印到控制台。这样前端页面数据不对时直接对比SQL结果和接口返回值立刻知道问题在后端还是前端。第三个习惯敏感配置和本地配置分开。数据库密码、MinIO密钥这些不要硬编码在application.yml里用ConfigurationProperties配合application-local.yml和application-prod.yml做环境隔离。本地开发用本地配置部署时用--spring.profiles.activeprod指定生产配置。第四个习惯写完代码之后用mvn clean package打一次完整的包而不是只点IDEA的绿色运行按钮。这一步能提前暴露资源拷贝、依赖冲突、打包路径等问题避免等部署到服务器才手忙脚乱。最后说点实际的。个人博客系统这个项目看起来简单但它牵涉的技术点覆盖面非常广从Spring Boot自动装配、MyBatis映射、事务管理到前端构建、资源打包、对象存储几乎没有一处是“死记硬背”能解决的全都要靠亲手搭建一遍才能理解。这也是为什么我一直建议新手别急着去模仿那些复杂的中台系统先把这个博客系统打磨到能上线、能被别人访问、能稳定运行几个月你收获的东西远比多写几个“管理系统”要扎实得多。
返回列表