
简介这是一套基于Spring Boot开发的完整个人博客系统源码面向Java初学者、课程设计学生及毕业设计开发者解决从零搭建功能完备博客系统的实践难题。资源包含544个文件涵盖96个Java业务逻辑类、116个MyBatis映射XML、56个Thymeleaf前端模板HTML、24个JS交互脚本及16个CSS样式文件辅以MySQL建表SQL、配置properties与README说明包体大小9.86MB结构清晰、分层明确便于理解MVC架构与前后端协作流程。已有1948人学习下载适合作为Spring Boot项目实战范例。读者可直接导入IDEAJDK1.8MySQL 8.0运行快速掌握用户端博客展示、归档、友链、关于与管理端文章发布、评论审核、分类标签、文件上传、系统设置双模块开发要点并通过预览中的Controller、VoExample、工具类如DateKit、TaleUtils深入学习RESTful接口设计与通用组件封装实践。1. 为什么一个“基于 Spring Boot 的个人博客系统”仍是 Java 后端新人最值得动手的练手项目不是因为功能多炫酷而是它精准卡在「够用、可控、可延展」的黄金交点上它不依赖第三方 SaaS 服务所有核心链路——用户注册登录、文章 CRUD、分类标签管理、富文本编辑、静态资源托管、数据库增删改查——全部由你亲手串联它天然适配 Spring Boot 四层架构Controller-Service-Repository-Entity让你在写PostMapping的同时自然理解分层职责边界它对数据库的要求明确且典型MySQL 或 H2既需要建表设计用户表、文章表、分类表、评论表又必须处理一对多用户→文章、多对多文章↔标签等真实关系更重要的是它不涉及分布式事务或高并发压测但会真实暴露Transactional失效、N1 查询、JSON 序列化循环引用、文件上传路径配置错误等高频问题。如果你刚学完 MyBatis 和 Thymeleaf这个项目就是你把零散知识点焊成肌肉记忆的第一块钢板——它不教你“怎么成为架构师”但它能让你清楚说出“我写的每行代码落在哪一层、调了哪个 SQL、返回了什么 JSON”。2. 从零初始化用 Spring Initializr 搭建符合四层架构规范的脚手架2.1 选型依据为什么放弃 JPA 而坚持 MyBatis-Plus MySQL 原生组合Spring Boot 官方推荐 JPA但对个人博客这类以查询为主、需精细控制 SQL 的场景MyBatis-Plus 更具实操优势可调试性SQL 直接写在 XML 或Select注解中出错时能立刻定位到具体语句而非陷入 Hibernate 的org.hibernate.exception.GenericJDBCException黑盒灵活性博客首页需按发布时间倒序、按分类筛选、按关键词模糊搜索这些动态条件用 MyBatis 的if标签组合清晰直观JPA Criteria API 则易写成嵌套冗长的CriteriaBuilder链式调用性能感知MyBatis-Plus 的PageT分页插件底层仍走LIMIT offset, size而某些 JPA 实现如早期 Hibernate在offset过大时会全表扫描新人难以察觉。提示不要用 H2 内存数据库做最终部署。它适合单元测试启动快、无依赖但无法模拟 MySQL 的GROUP BY排序行为、TEXT字段索引限制、datetime时区处理等真实约束建议开发阶段即用 Docker 启动 MySQL 8.0 容器。2.2 初始化工程通过命令行快速生成标准目录结构执行以下命令需提前安装spring-boot-clispring init \ --dependenciesweb,thymeleaf,mybatis-plus-boot-starter,mysql,validation,lombok,devtools \ --package-namecom.example.blog \ --namepersonal-blog-system \ --typemaven-project \ --java-version17 \ personal-blog-system该命令生成的目录严格遵循 Spring Boot 目录规范src/main/java/com/example/blog/下自动创建controller/、service/、mapper/、entity/、config/子包src/main/resources/包含application.yml非 propertiesYAML 支持层级缩进更易读和mapper/文件夹存放 XML 映射文件src/main/resources/static/托管 CSS/JStemplates/托管 Thymeleaf HTML 模板。2.2.1 关键配置项解析application.yml 中必须显式声明的 3 个参数spring: datasource: url: jdbc:mysql://localhost:3306/blog_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false # 开发阶段禁用模板缓存修改 HTML 立即生效 suffix: .html mybatis-plus: mapper-locations: classpath:mapper/**/*.xml # 指向 XML 文件位置避免扫描失败 configuration: map-underscore-to-camel-case: true # 自动将 user_name → userName省去 Results 映射serverTimezoneAsia/Shanghai是 MySQL 8.0 强制要求否则datetime字段插入为0000-00-00 00:00:00map-underscore-to-camel-case: true让 MyBatis-Plus 自动转换下划线命名字段如create_time→createTime避免在每个Select中手动ASmapper-locations必须精确到**/*.xml若写成mapper/*.xml则子包如mapper/article/ArticleMapper.xml无法被加载。2.3 数据库建表用 DDL 脚本定义博客核心实体关系在 MySQL 中执行以下建表语句已适配 MySQL 8.0-- 用户表 CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, -- BCrypt 加密后长度约 60 email VARCHAR(100), avatar_url VARCHAR(255), status TINYINT DEFAULT 1, -- 1:启用, 0:禁用 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 分类表 CREATE TABLE category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, description VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 文章表 CREATE TABLE article ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, content LONGTEXT NOT NULL, -- 存储富文本 HTML summary VARCHAR(500), -- 文章摘要用于列表页预览 user_id BIGINT NOT NULL, category_id BIGINT, status TINYINT DEFAULT 1, -- 1:发布, 0:草稿, -1:删除 view_count INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE SET NULL ); -- 标签表 CREATE TABLE tag ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE ); -- 文章-标签关联表多对多 CREATE TABLE article_tag ( article_id BIGINT NOT NULL, tag_id BIGINT NOT NULL, PRIMARY KEY (article_id, tag_id), FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE );注意article.content使用LONGTEXT而非TEXT因富文本编辑器如 TinyMCE生成的 HTML 可能超 65535 字节ON DELETE CASCADE确保删除文章时自动清理关联的article_tag记录避免外键残留。3. 核心功能落地实现文章发布与分类检索的完整闭环3.1 Controller 层定义 RESTful 接口并校验请求参数RestController RequestMapping(/api/articles) RequiredArgsConstructor public class ArticleController { private final ArticleService articleService; // 发布新文章POST /api/articles PostMapping public ResultArticle createArticle(Valid RequestBody ArticleCreateDTO dto) { Article saved articleService.create(dto); return Result.success(saved); } // 按分类 ID 查询文章列表GET /api/articles?categoryId1page1size10 GetMapping public ResultPageArticle listByCategory( RequestParam(required false) Long categoryId, RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { PageArticle result articleService.listByCategory(categoryId, page, size); return Result.success(result); } }Valid触发ArticleCreateDTO中的NotBlank、Size等注解校验失败时自动返回400 Bad RequestRequestParam(defaultValue 1)避免page参数为空导致PageRequest.of(0, size)报错Spring Data Page 索引从 0 开始但用户习惯从 1 开始ResultT是自定义统一响应包装类包含code、message、data字段强制前端处理业务异常如code5001表示分类不存在。3.1.1 DTO 设计原则为什么不用 Entity 直接接收前端参数ArticleCreateDTO与ArticleEntity 必须分离Data public class ArticleCreateDTO { NotBlank(message 标题不能为空) Size(max 200, message 标题不能超过200字) private String title; NotBlank(message 内容不能为空) private String content; private String summary; private Long categoryId; NotNull(message 请选择分类) private Long userId; // 前端传入当前登录用户ID后端不信任前端token }userId不从 JWT Token 解析而由前端在请求头或 body 中显式传递后端校验其有效性防止越权Size校验title长度避免VARCHAR(200)字段被超长字符串截断summary允许为空由后端逻辑自动生成取content前 200 字符DTO 中不加NotBlank。3.2 Service 层封装业务逻辑与事务边界Service RequiredArgsConstructor public class ArticleService { private final ArticleMapper articleMapper; private final CategoryMapper categoryMapper; private final ArticleTagMapper articleTagMapper; Transactional(rollbackFor Exception.class) public Article create(ArticleCreateDTO dto) { // 1. 校验分类是否存在 if (dto.getCategoryId() ! null categoryMapper.selectById(dto.getCategoryId()) null) { throw new BusinessException(分类不存在); } // 2. 构建 Article 实体 Article article new Article(); article.setTitle(dto.getTitle()); article.setContent(dto.getContent()); article.setSummary(StringUtils.substring(dto.getContent(), 0, 200)); // 截取摘要 article.setUserId(dto.getUserId()); article.setCategoryId(dto.getCategoryId()); article.setStatus(1); // 默认发布状态 // 3. 插入文章主表 articleMapper.insert(article); // 4. 处理标签假设 dto.tagNames [Java, Spring] if (CollectionUtils.isNotEmpty(dto.getTagNames())) { ListTag tags tagMapper.selectBatchTags(dto.getTagNames()); ListArticleTag relations tags.stream() .map(tag - new ArticleTag(article.getId(), tag.getId())) .collect(Collectors.toList()); articleTagMapper.insertBatch(relations); } return article; } }Transactional保证插入文章和关联标签原子性任一环节失败则全部回滚StringUtils.substring()来自 Apache Commons Lang安全截取字符串String.substring()遇空指针直接抛异常selectBatchTags()是自定义批量查询方法避免 N1先查出所有标签 ID再一次性INSERT INTO article_tag而非对每个标签循环insert。3.3 Mapper 层用 XML 实现动态 SQL 与关联查询ArticleMapper.xml中定义按分类查询的动态 SQLselect idlistByCategory resultTypecom.example.blog.entity.Article SELECT a.id, a.title, a.summary, a.view_count, a.create_time, c.name AS categoryName FROM article a LEFT JOIN category c ON a.category_id c.id where a.status 1 !-- 只查已发布文章 -- if testcategoryId ! null and categoryId ! 0 AND a.category_id #{categoryId} /if /where ORDER BY a.create_time DESC LIMIT #{page}, #{size} /selectwhere标签自动处理AND前缀避免WHERE AND ...语法错误#{page}使用预编译参数防 SQL 注入LIMIT #{page}, #{size}中page (pageNum-1)*size由 MyBatis-Plus 的Page对象自动计算resultType指向Article实体c.name AS categoryName通过 MyBatis 的驼峰映射规则自动注入到Article.categoryName字段需在application.yml中开启map-underscore-to-camel-case。4. 前端集成与文件上传Thymeleaf 模板渲染与富文本图片存储4.1 Thymeleaf 模板用 fragment 复用头部与分页组件templates/layout.html定义公共布局!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head title th:text${title} ?: 个人博客博客首页/title /head body header th:fragmentheader nav a href/首页/a a href/categories分类/a a href/admin/login后台/a /nav /header main th:fragmentcontent !-- 页面内容占位 -- /main footer th:fragmentfooter pcopy; 2024 个人博客系统/p /footer /body /htmltemplates/index.html继承并填充内容!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org xmlns:layouthttp://www.ultraq.net.nz/thymeleaf/layout layout:decorate~{layout} head title博客首页/title /head body div layout:fragmentcontent h1最新文章/h1 div th:eacharticle : ${page.records} h2 th:text${article.title}文章标题/h2 p th:text${article.summary}摘要/p small发布时间span th:text${#dates.format(article.createTime, yyyy-MM-dd)}2024-01-01/span/small a th:href{/article/{id}(id${article.id})}阅读全文/a /div !-- 分页组件 -- div th:replace~{fragments/pagination :: pagination(page${page}, url/)}/div /div /body /htmlth:each遍历PageArticle.records#dates.format()格式化日期避免在 Java 层拼接字符串th:replace引入fragments/pagination.html实现分页链接动态生成/page/2、/page/3。4.2 富文本图片上传本地存储方案与路径安全处理Controller RequestMapping(/upload) RequiredArgsConstructor public class UploadController { private final String uploadDir src/main/resources/static/uploads/; PostMapping(/image) ResponseBody public ResultString uploadImage(RequestParam(file) MultipartFile file) { try { // 1. 校验文件类型 String contentType file.getContentType(); if (!image/jpeg.equals(contentType) !image/png.equals(contentType)) { return Result.fail(仅支持 JPG/PNG 格式); } // 2. 生成唯一文件名避免中文乱码与冲突 String originalName file.getOriginalFilename(); String extension StringUtils.substringAfterLast(originalName, .); String fileName UUID.randomUUID().toString() . extension; // 3. 保存到 static/uploads/ 目录Thymeleaf 可直接访问 Path uploadPath Paths.get(uploadDir); if (!Files.exists(uploadPath)) { Files.createDirectories(uploadPath); } Path targetPath uploadPath.resolve(fileName); file.transferTo(targetPath); // 4. 返回可访问的 URL注意static 目录下资源无需 controller 映射 return Result.success(/uploads/ fileName); } catch (IOException e) { return Result.fail(上传失败 e.getMessage()); } } }static/uploads/是 Spring Boot 默认静态资源路径返回/uploads/xxx.jpg即可通过浏览器直接访问UUID.randomUUID()保证文件名全局唯一防止恶意覆盖如上传../web.xmltransferTo()比getBytes()更省内存适合大文件前端 TinyMCE 配置images_upload_url: /upload/image即可自动调用此接口。5. 生产就绪关键配置解决数据库连接池、日志与 Actuator 安全问题5.1 HikariCP 连接池调优3 个必设参数防止连接耗尽在application.yml中添加spring: datasource: hikari: connection-timeout: 30000 # 获取连接超时时间毫秒 maximum-pool-size: 20 # 最大连接数根据 MySQL max_connections 设置 minimum-idle: 5 # 最小空闲连接数避免频繁创建销毁 idle-timeout: 600000 # 空闲连接存活时间毫秒 max-lifetime: 1800000 # 连接最大生命周期毫秒小于 MySQL wait_timeoutmax-lifetime必须小于 MySQL 的wait_timeout默认 28800 秒否则连接被 MySQL 主动断开后HikariCP 无法感知后续请求报Connection closedmaximum-pool-size不宜过大MySQL 默认max_connections151若应用集群有 3 个实例则单实例设50较安全idle-timeout配合minimum-idle确保低峰期释放多余连接节省数据库资源。5.2 日志分级与 SQL 输出用 logback-spring.xml 精确控制 MyBatis 日志src/main/resources/logback-spring.xml配置?xml version1.0 encodingUTF-8? configuration appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender !-- 仅在 dev 环境输出 MyBatis SQL -- springProfile namedev logger namecom.example.blog.mapper levelDEBUG additivityfalse appender-ref refCONSOLE/ /logger /springProfile root levelINFO appender-ref refCONSOLE/ /root /configurationcom.example.blog.mapper包下的日志级别设为DEBUG可看到Preparing: SELECT * FROM article WHERE id ?及绑定参数springProfile标签确保生产环境prod不输出 SQL避免敏感信息泄露additivityfalse防止日志重复打印父 logger 已输出。5.3 Actuator 安全加固关闭未授权访问风险端点management: endpoints: web: exposure: include: health,info,metrics,prometheus # 仅暴露必要端点 endpoint: health: show-details: when_authorized # 健康检查详情需认证 security: roles: ACTUATOR # 需 ACTUATOR 角色才能访问exposure.include显式声明开放端点*全部开放是严重安全隐患show-details: when_authorized防止/actuator/health泄露数据库连接状态等内部信息结合 Spring Security为/actuator/**路径配置hasRole(ACTUATOR)并通过UserDetailsService加载管理员账号。提示若使用 Spring Boot 3.x需额外添加spring-boot-starter-actuator依赖并确认management.endpoints.web.base-path/actuator默认值。本文还有配套的精品资源点击获取