ARTICLE DETAIL

资讯详情

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

基于Spring Boot的个人博客系统开发实战:从表设计到部署全攻略

基于Spring Boot的个人博客系统开发实战:从表设计到部署全攻略 作为一个前后端都写过不少项目的老程序员我这两年接到的博客系统相关的咨询一直没有断过。很多人问的第一句通常是“现在都2025年了还有必要自己写博客系统吗用WordPress或Hexo不香吗” 我的答案往往是如果你想深入理解一个Web应用从零到部署的全过程想掌握Spring Boot在实际业务中的套路那么自己动手做一个博客系统依然是性价比最高的练手项目没有之一。这篇博客不是给你贴一段完整源码就完事而是把我在开发“基于Spring Boot的个人博客系统”过程中从技术选型、数据库设计、核心模块实现到上线部署踩过的坑整个决策链路都摊开来讲。项目配套了完整源码、SQL脚本和开发文档你可以直接拿去用也可以照着思路自己重写一版。无论你是刚学完Spring Boot基础、需要项目经历加持的应届生还是想给团队搭建一个轻量内容平台的Java工程师这篇文章都值得你花十分钟读完。1. 为什么我最终选了Spring Boot而不是别的方案先聊点题外话。动笔之前我其实纠结了很久到底是用热门的Vue3 Spring Boot前后端分离架构还是用Spring Boot Thymeleaf服务端渲染身边不少朋友都劝我上前后端分离理由是“现在主流”“简历好看”。但我最后选了Spring Boot Thymeleaf的组合。原因有三点每一条都是实际考量SEO对个人博客至关重要。博客内容需要被搜索引擎收录服务端渲染返回完整HTML爬虫直接就能抓到正文。而前后端分离架构下搜索引擎虽然现在能抓取SPA页面但效果依然不如直出HTML稳定。开发效率高维护成本低。一个人做全栈项目最怕的是前后端接口联调时来回扯皮。服务端渲染模式下后端一次性把页面渲染好前端的工作量大幅减少几乎不需要单独维护一套API接口文档。架构足够轻。博客的核心是内容发布和展示没有复杂交互。为了一个博客硬上Redis缓存、消息队列、微服务拆分属于典型的过度设计。简单可靠的单体应用才是个人项目的最优解。技术栈定下来之后是这样的技术组件选型版本用途说明Spring Boot2.7.14项目主框架注意没有用3.x下面细说Thymeleaf3.0.15服务端模板引擎渲染页面MyBatis-Plus3.5.2ORM框架简化数据库操作MySQL8.0主数据库存储业务数据Druid1.2.20数据库连接池附带监控功能Lombok1.18.24消除样板代码validation2.x后端参数校验springdoc-openapi2.1.0接口文档生成对标Swagger关于Spring Boot版本这里单独提醒一句。Spring Boot 3.x已经把javax包替换为jakartaMaven坐标也改了一些老插件需要额外适配。如果你只是做学习项目建议直接上2.7.x资料多、坑少。如果目标是新项目落地或者找工作可以冲3.x但要有心理准备遇到问题搜到的答案可能因为包名差异跑不通。2. 数据库表设计博客系统的地基六张表就够了很多新手会犯一个典型错误一上来就在user表里塞一堆字段觉得反正表是私有的随便加。这样做后期会带来一连串麻烦。我的做法是先画出博客系统的数据关系再落库。个人博客系统的核心实体其实就六个文章、分类、标签、评论、用户后台管理员、网站配置。对应到数据库里我设计了六张核心表2.1 文章表和用户表文章表article是核心中的核心字段不能敷衍。我最终的建表语句经过了好几轮调整最终定稿的关键字段如下CREATE TABLE article ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 文章ID, title varchar(100) NOT NULL COMMENT 文章标题, summary varchar(255) DEFAULT NULL COMMENT 文章摘要列表页展示用, content longtext NOT NULL COMMENT 文章正文Markdown格式, cover_image varchar(255) DEFAULT NULL COMMENT 封面图URL, category_id bigint(20) DEFAULT NULL COMMENT 所属分类ID, author_id bigint(20) NOT NULL COMMENT 作者ID关联user表, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态0草稿1已发布2回收站, view_count int(11) NOT NULL DEFAULT 0 COMMENT 浏览量, like_count int(11) NOT NULL DEFAULT 0 COMMENT 点赞数, comment_count int(11) NOT NULL DEFAULT 0 COMMENT 评论数冗余字段避免联表查询, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_category_id (category_id), KEY idx_status_create_time (status, create_time) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT文章表;这里有两个设计细节值得展开一个是comment_count这个冗余字段一开始我没有加后来发现列表页要展示每篇文章的评论数量如果不冗余就得每条文章跑一次COUNT查询当文章数量上来后N1问题非常明显。加了冗余字段更新评论数时多写一次就好对博客这种低频写入场景完全值得。另一个是索引设计。idx_status_create_time这个联合索引是专门为列表页“展示已发布文章并按发布时间倒序”这个高频查询设计的。MySQL的索引最左前缀原则意味着单独按status筛选也能走这个索引但按status create_time排序时可以避免回表后的额外排序实测在十万级数据量下性能差异非常明显。用户表user就没那么复杂了CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码BCrypt加密, nickname varchar(50) DEFAULT NULL COMMENT 昵称, email varchar(100) DEFAULT NULL COMMENT 邮箱, avatar varchar(255) DEFAULT NULL COMMENT 头像URL, role tinyint(4) NOT NULL DEFAULT 0 COMMENT 角色0普通用户1管理员, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT用户表;用户名加唯一索引接口层再做一次唯一性校验双重保障。加密方式没有用MD5直接上BCrypt虽然加盐逻辑稍复杂但抗彩虹表攻击的能力强得多。2.2 分类、标签和多对多关系分类和标签这两个概念经常被混淆。我的理解是分类是树形结构的一般只有一层或两层强调内容的归属比如“Java”“数据库”“随笔”标签是扁平化的强调内容的交叉描述比如“Spring Boot”“优化”“踩坑”。一篇文章属于一个分类但可以有多个标签。为了支持文章和标签的多对多关系我建了关联表article_tagCREATE TABLE article_tag ( id bigint(20) NOT NULL AUTO_INCREMENT, article_id bigint(20) NOT NULL, tag_id bigint(20) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_article_tag (article_id,tag_id), KEY idx_tag_id (tag_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT文章-标签关联表;唯一键uk_article_tag的作用是防止同一篇文章重复添加同一个标签。我最初设计时没有加这个唯一约束结果测试环境下用脚本批量导入数据时出现了大量重复关联记录标签统计页面的数据直接没法看。教训就是任何一张关联表复合唯一约束都是标配。分类表category本身很简单CREATE TABLE category ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 分类名称, slug varchar(50) DEFAULT NULL COMMENT URL访问别名, sort int(11) NOT NULL DEFAULT 0 COMMENT 排序权重越大越靠前, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT分类表;2.3 评论表和网站配置表评论表comment用自关联来支持楼中楼回复CREATE TABLE comment ( id bigint(20) NOT NULL AUTO_INCREMENT, article_id bigint(20) NOT NULL COMMENT 文章ID, user_id bigint(20) DEFAULT NULL COMMENT 评论用户ID未登录则为NULL, nickname varchar(50) DEFAULT NULL COMMENT 游客昵称, email varchar(100) DEFAULT NULL COMMENT 游客邮箱, content varchar(500) NOT NULL COMMENT 评论内容, parent_id bigint(20) DEFAULT NULL COMMENT 父评论IDNULL为根评论, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_article_id (article_id), KEY idx_parent_id (parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT评论表;这个表设计最需要注意的一点是不要用递归查询去取所有层级的回复树那在数据量稍大时性能会很差。我采用的是“平铺展示 前端缩进”的方案查询某个文章的全部评论时一次SELECT把该文章所有评论拉出来在Java层用parent_id组装成树形结构返回给页面。对个人博客一天几条评论的量级这个方案简单可靠。网站配置表site_config是后期加上的存博客标题、副标题、页脚信息、ICP备案号等。用key-value结构CREATE TABLE site_config ( id bigint(20) NOT NULL AUTO_INCREMENT, config_key varchar(50) NOT NULL COMMENT 配置键, config_value varchar(500) DEFAULT NULL COMMENT 配置值, remark varchar(100) DEFAULT NULL COMMENT 备注说明, PRIMARY KEY (id), UNIQUE KEY uk_config_key (config_key) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT网站配置表;业务代码里写一个工具类启动时把全表加载到内存Map里之后读取配置零数据库开销。2.4 数据库字符集与导入导出字符集我统一用了utf8mb4。utf8mb4是utf8的超集能存emoji表情和生僻字。如果用了utf8用户在评论里发一个表情符号程序会直接报错。这个问题在真实环境里出现过一次所以建库时务必确认CREATE DATABASE blog_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;项目配套的SQL文件里我加了不少测试数据包括十几篇示例文章、几个分类和标签。有测试数据的好处是项目启动后页面不至于一片空白你可以直接看到效果也方便调试分页、标签聚合等功能。3. 核心功能模块拆解内容管理这套逻辑一次吃透技术栈和表结构定好之后就到了写代码的阶段。项目整体分层是标准的Controller-Service-Mapper三层架构包结构如下com.blog ├── controller # 控制器接收请求、参数校验、返回视图或数据 ├── service # 业务逻辑层处理核心业务 │ └── impl ├── mapper # MyBatis-Plus的Mapper接口 ├── entity # 数据库实体类 ├── dto # 数据传输对象接收前端参数 ├── vo # 视图对象返回页面渲染数据 ├── config # 配置类如WebMvc、拦截器、加密组件 ├── common # 通用工具类、异常处理、统一返回结果 └── BlogApplication.java # 启动类3.1 文章发布的完整链路发布文章是博客的基础功能接口设计看似简单实际涉及不少细节。前端表单提交的字段包括标题、摘要、正文、分类ID、标签ID列表、封面图URL、状态草稿/发布。后端接收后做什么PostMapping(/admin/article/save) public R save(RequestBody Valid ArticleDTO dto) { return articleService.saveOrUpdateArticle(dto); }Controller层很薄真正的逻辑在Service里。saveOrUpdateArticle方法里我做了这样几件事参数校验。标题必填且不超过100字符正文必填分类ID必须存在。保存或更新文章主表信息。重建文章和标签的关联关系。先删除原有的article_tag记录再插入新的。这个操作虽然多了一次删除和插入但逻辑清晰避免复杂的增量比对。毕竟一篇文章的标签通常不会多到影响性能。维护评论计数、标签计数等冗余数据。新增标签时标签表的引用计数加一。用MyBatis-Plus写这块业务时最方便的是它的LambdaUpdateWrapper和saveOrUpdate方法。比如更新浏览量articleMapper.update(null, new LambdaUpdateWrapperArticle() .eq(Article::getId, id) .setSql(view_count view_count 1));这样写是数据库层面的原子自增比“先查出来、1、再写回去”的三步操作安全得多不会有并发问题。3.2 列表页分页和分类归档博客首页的文章列表、分类页、标签页本质上都是条件查询加分页。MyBatis-Plus提供了一个分页插件PaginationInnerInterceptor配置如下Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }配置好之后分页查询一行代码搞定PageArticleVO page articleMapper.selectPage(new Page(current, size), new LambdaQueryWrapperArticle() .eq(Article::getStatus, 1) .eq(categoryId ! null, Article::getCategoryId, categoryId) .eq(tagId ! null, Article::getTagId, tagId) .orderByDesc(Article::getCreateTime));这里有两个小坑必须提一下。第一个是分类页和标签页的查询如果直接查article表就会发现tag_id这个字段根本不存在因为标签关系在关联表里。这时候我的做法是先根据tag_id从article_tag表查出符合条件的article_id集合再用IN语句去查文章表。对博客系统这个量级一条IN查询的性能完全够用。另一个坑是文章正文是longtext列表页查询如果不加限制会把正文全部查出来浪费网络IO和内存。我的处理是查列表时用select指定只查需要的列不要正文。ArticleVO selectArticleSummary(Param(id) Long id);手写Mapper XML时明确列出需要的字段select idselectArticleSummary resultTypecom.blog.vo.ArticleVO SELECT a.id, a.title, a.summary, a.cover_image, a.view_count, a.like_count, a.comment_count, a.create_time, c.name AS category_name FROM article a LEFT JOIN category c ON a.category_id c.id WHERE a.id #{id} /select3.3 Markdown编辑与渲染Markdown是博客系统的标配纯文本的textarea早就被淘汰了。编辑器我选了Markdown Editor和highlight.js的组合。存储时直接把Markdown原文存库展示时在服务端渲染成HTML然后返回给Thymeleaf模板。这一步最要注意的就是安全。直接把用户提交的Markdown转成HTML然后原样输出会有XSS攻击风险。比如用户在正文里写一段script标签渲染时会被浏览器执行产生安全问题。我的方案是在渲染完成后用Jsoup对HTML做白名单过滤public String mdToHtml(String markdown) { Markdown4jProcessor processor new Markdown4jProcessor(); String html processor.process(markdown); return Jsoup.clean(html, Safelist.relaxed()); }Safelist.relaxed()允许常见的标题、列表、链接、图片等标签但会过滤掉script、iframe、object等危险标签。链接属性中的javascript:协议也会被清理。这样既保留了Markdown的排版能力又堵住了XSS的入口。3.4 评论模块与拦截器评论模块我设计成支持游客评论和管理员回复。游客评论时无需注册填写昵称和邮箱即可但后端要加一层防刷机制。我的做法是使用Session存储最近一次评论时间两次评论间隔不能小于30秒一个人一天对同一篇文章的评论数限制在10条以内。用AOP在Service层做拦截不影响Controller的代码简洁度。管理员回复用户评论时需要先确认当前登录用户是管理员。登录拦截器用Spring Boot的HandlerInterceptor实现public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(SessionConstants.USER_KEY); if (user null) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\请先登录\}); return false; } return true; } }然后在WebMvcConfig中注册拦截器只拦截/admin/**路径放行首页、文章详情、评论等公开接口。3.5 接口文档springdoc-openapi的接入项目配套的文档里我接入了springdoc-openapi它能在项目运行期间自动生成符合OpenAPI 3.0规范的接口文档并且提供一个可视化的Swagger UI页面。这个对团队协作和后期维护很有用别人接你的后端接口时不用瞎猜参数格式。不过要提醒的是个人博客这种项目接口文档不需要追求全自动化。更务实的做法是把核心接口文章保存、评论提交、用户登录的入参、出参、异常码写清楚其余的交给代码注释。文档是给活人看的清晰比完整重要。4. 实战中那些坑我替你先踩了一遍这部分是最想分享的。理论写起来容易真正落地时遇到的问题往往让人抓狂。我按照时间顺序记录了几个印象最深刻的坑。4.1 MyBatis-Plus分页插件不生效第一次写完分页代码怎么测都发现分页不生效——查询结果永远是全量数据没有LIMIT语句。检查了半天发现是分页插件的Configuration配置类没被Spring Boot扫描到。我的配置类放在com.blog.config包下而启动类BlogApplication默认扫描的是com.blog包如果启动类放在com.blog下扫描范围没问题。但当时我把配置类放到了com.blog.system.config这个子包下不在默认扫描范围内导致MybatisPlusInterceptor这个Bean根本没有注入容器。解决办法是在启动类上加ComponentScan(com.blog)强制指定扫描路径或者把配置类放到启动类的子包下。大家自己搭项目时如果遇到某个配置不生效第一反应先确认配置类是否被Spring扫描到了这是排查Spring Boot问题的最快路径。4.2 Thymeleaf页面引用静态资源404Thymeleaf模板引擎会解析th:href、th:src等属性但静态资源JS、CSS、图片分两种一种放在resources/static目录下一种放在resources/templates下。后者是由Thymeleaf解析的模板文件不能直接作为静态资源访问。我踩的坑是把一些CSS文件放进templates/css/目录引用时写th:href{/css/style.css}结果页面加载了N次都是404。原因在于Spring Boot默认只把classpath:/static/、classpath:/public/、classpath:/resources/、classpath:/META-INF/resources/这4个目录映射为静态资源所在目录。templates目录本身被Thymeleaf占用不参与静态资源映射。把所有CSS和JS统一放到src/main/resources/static/下问题迎刃而解。4.3 文章详情页的“上一篇/下一篇”这个功能看起来简单但第一次实现时我写的是“查所有文章在内存里找到当前文章的前后两条”。这种写法在数据量小的时候没问题但一旦文章超过几百篇每次详情页请求都会把全部文章ID和标题捞出来显然不合理。正确的SQL应该是通过create_time和id做条件查询-- 上一篇 SELECT id, title FROM article WHERE status 1 AND (create_time #{createTime} OR (create_time #{createTime} AND id #{id})) ORDER BY create_time DESC, id DESC LIMIT 1; -- 下一篇 SELECT id, title FROM article WHERE status 1 AND (create_time #{createTime} OR (create_time #{createTime} AND id #{id})) ORDER BY create_time ASC, id ASC LIMIT 1;注意到create_time相同的场景了吗如果两篇文章在同一秒发布只按create_time排序会有歧义所以必须加上id作为第二排序条件。4.4 数据库连接耗尽问题项目本地跑得好好的上线后运行两天突然报Communications link failure查看日志发现是Too many connections。分析了一圈问题出在Druid连接池的maxActive配置。默认情况下maxActive8而我又没有配置minIdle和validationQuery导致数据库在高并发场景下连接池被占满新请求拿不到连接。修复方案是调整连接池参数spring: datasource: druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 validation-query: SELECT 1 test-while-idle: truemaxWait60000表示请求连接的最大等待时间为60秒避免无限等待。test-while-idle定期检查空闲连接的有效性防止数据库关闭空闲连接后应用还在使用坏连接。这些都调好之后上线一个多月再没出过连接相关的问题。4.5 图片上传与访问路径博客文章免不了要插图。项目里我实现了简单的本地存储上传上传的图片保存到服务器/data/blog/images/目录访问时通过Spring Boot的静态资源映射规则暴露出来Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceHandler(file: imageUploadPath); } }这里有个细节要注意imageUploadPath必须是绝对路径并且以/结尾。如果配置成相对路径项目启动目录一变图片就全挂了。生产环境建议把图片目录放到磁盘独立分区不要把图片放到resources/static下因为每次重新部署都可能被覆盖。5. 构建、打包、部署从本地到云服务器5.1 打包方式选择Spring Boot官方支持两种打包方式jar和war。jar包方式Spring Boot内嵌TomcatJava环境即可运行部署最简单。war包方式需要把war包丢到外部Tomcat的webapps目录适合已有运维体系的传统部署场景。个人博客我选的是jar包。mvn clean package -Dmaven.test.skiptrue打包完成后在target目录下会生成blog-system-1.0.0.jar这个包包含了项目所有依赖。运行只需要一条命令java -jar blog-system-1.0.0.jar --spring.profiles.activeprod生产环境的配置放在application-prod.yml里里面配的是线上数据库地址、用户名密码、文件上传路径等。5.2 用Nginx做反向代理生产环境不能让用户直接访问应用端口比如8080最好在前面挡一层Nginx。一是因为HTTP默认端口是80用户访问时不需要输入端口号二是因为Nginx可以对静态资源做缓存减轻应用服务器压力。server { listen 80; server_name yourdomain.com; # 静态资源缓存提高加载速度 location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2?)$ { expires 30d; add_header Cache-Control public, no-transform; access_log off; } # 动态请求转发给Spring Boot location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }proxy_set_header Host $host这句很重要。如果不加后端拿到的请求Host是127.0.0.1:8080如果项目里用到了绝对地址生成比如邮件中的链接、跳转URL用户会跳转到错误地址。5.3 自动化部署脚本手敲命令部署太累我写了一个简单的部署脚本deploy.sh放在服务器上#!/bin/bash # 博客系统部署脚本 APP_NAMEblog-system-1.0.0.jar APP_PATH/opt/blog BACKUP_PATH/opt/blog/backup echo 停止旧进程 PID$(ps -ef | grep ${APP_NAME} | grep -v grep | awk {print $2}) if [ -n $PID ]; then kill -9 $PID echo 已停止进程: $PID fi echo 备份旧版本 if [ -f $APP_PATH/$APP_NAME ]; then cp $APP_PATH/$APP_NAME $BACKUP_PATH/${APP_NAME}.$(date %Y%m%d%H%M%S) echo 备份完成 fi echo 上传新版本并启动 cp /tmp/blog-system.jar $APP_PATH/$APP_NAME cd $APP_PATH nohup java -jar $APP_NAME --spring.profiles.activeprod /dev/null 21 echo 部署完成配合Git Hook或者手动把本地target目录下的jar包上传到服务器的/tmp目录然后执行脚本一分钟就能完成一次上线。对于个人博客来说这样的部署链路已经够用了。6. 项目文档与二次开发思路配套的开发文档我写了大概40多页涵盖以下内容环境搭建步骤、数据库初始化脚本说明、项目结构说明、核心接口文档、部署方案。文档里没有废话每部分都是按照“拿来就能跑”的标准写的。如果你拿到这套源码想做一些二次开发我建议按以下优先级顺序来接入第三方登录。目前系统只支持本地用户名密码登录。用OAuth 2.0接入GitHub或Gitee登录让访客可以免注册评论可以显著提高互动率。增加文章搜索功能。目前搜索依赖SQL的LIKE %keyword%数据量大了之后效率低。可以引入Elasticsearch或使用MySQL全文索引做一个轻量版的站内搜索。接入对象存储。把本地图片存储替换成阿里云OSS或MinIO解决服务器存储扩容问题。做数据统计。基于ECharts展示文章的浏览量趋势、来源渠道、热门文章Top10等。每个人的技术路线不同但有一条我很确定与其去网上找那种几十年前老掉牙的SSH博客系统Demo不如好好研究一套基于Spring Boot、设计合理、代码整洁、文档齐全的现代版本。这个项目做完之后你对Spring Boot的理解会有一个质的飞跃——不再只是“会写CRUD”而是真正理解了一个Web应用从设计到上线的完整链路。最后再分享一个小技巧。项目上线后我给自己配了一个“一键发布”的小工具用GitHub Actions监听仓库的main分支代码push后自动构建、打包、上传服务器、重启服务。从此以后我只需要在本地写内容、提交代码剩下的都交给流水线。对个人项目来说这大概就是“技术改变生活”的实感了。
返回列表