简介:这是一套基于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项目实战范例。读者可直接导入IDEA(JDK1.8+MySQL 8.0)运行,快速掌握用户端(博客展示、归档、友链、关于)与管理端(文章发布、评论审核、分类标签、文件上传、系统设置)双模块开发要点,并通过预览中的Controller、VoExample、工具类(如DateKit、TaleUtils)深入学习RESTful接口设计与通用组件封装实践。
1. 为什么一个“基于 Spring Boot 的个人博客系统”仍是 Java 后端新人最值得动手的练手项目?
不是因为功能多炫酷,而是它精准卡在「够用、可控、可延展」的黄金交点上:它不依赖第三方 SaaS 服务,所有核心链路——用户注册登录、文章 CRUD、分类标签管理、富文本编辑、静态资源托管、数据库增删改查——全部由你亲手串联;它天然适配 Spring Boot 四层架构(Controller-Service-Repository-Entity),让你在写@PostMapping的同时,自然理解分层职责边界;它对数据库的要求明确且典型(MySQL 或 H2),既需要建表设计(用户表、文章表、分类表、评论表),又必须处理一对多(用户→文章)、多对多(文章↔标签)等真实关系;更重要的是,它不涉及分布式事务或高并发压测,但会真实暴露@Transactional失效、N+1 查询、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 的
Page<T>分页插件底层仍走LIMIT offset, size,而某些 JPA 实现(如早期 Hibernate)在offset过大时会全表扫描,新人难以察觉。
提示:不要用 H2 内存数据库做最终部署。它适合单元测试(启动快、无依赖),但无法模拟 MySQL 的
GROUP BY排序行为、TEXT字段索引限制、datetime时区处理等真实约束,建议开发阶段即用 Docker 启动 MySQL 8.0 容器。
2.2 初始化工程:通过命令行快速生成标准目录结构
执行以下命令(需提前安装spring-boot-cli):
spring init \ --dependencies=web,thymeleaf,mybatis-plus-boot-starter,mysql,validation,lombok,devtools \ --package-name=com.example.blog \ --name=personal-blog-system \ --type=maven-project \ --java-version=17 \ personal-blog-system该命令生成的目录严格遵循 Spring Boot 目录规范:
src/main/java/com/example/blog/下自动创建controller/、service/、mapper/、entity/、config/子包;src/main/resources/包含application.yml(非 properties,YAML 支持层级缩进,更易读)和mapper/文件夹(存放 XML 映射文件);src/main/resources/static/托管 CSS/JS,templates/托管 Thymeleaf HTML 模板。
2.2.1 关键配置项解析:application.yml 中必须显式声明的 3 个参数
spring: datasource: url: jdbc:mysql://localhost:3306/blog_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true 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 映射serverTimezone=Asia/Shanghai是 MySQL 8.0+ 强制要求,否则datetime字段插入为0000-00-00 00:00:00;map-underscore-to-camel-case: true让 MyBatis-Plus 自动转换下划线命名字段(如create_time→createTime),避免在每个@Select中手动AS;mapper-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 Result<Article> createArticle(@Valid @RequestBody ArticleCreateDTO dto) { Article saved = articleService.create(dto); return Result.success(saved); } // 按分类 ID 查询文章列表(GET /api/articles?categoryId=1&page=1&size=10) @GetMapping public Result<Page<Article>> listByCategory( @RequestParam(required = false) Long categoryId, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { Page<Article> result = articleService.listByCategory(categoryId, page, size); return Result.success(result); } }@Valid触发ArticleCreateDTO中的@NotBlank、@Size等注解校验,失败时自动返回400 Bad Request;@RequestParam(defaultValue = "1")避免page参数为空导致PageRequest.of(0, size)报错(Spring Data Page 索引从 0 开始,但用户习惯从 1 开始);Result<T>是自定义统一响应包装类,包含code、message、data字段,强制前端处理业务异常(如code=5001表示分类不存在)。
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())) { List<Tag> tags = tagMapper.selectBatchTags(dto.getTagNames()); List<ArticleTag> 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()是自定义批量查询方法,避免 N+1:先查出所有标签 ID,再一次性INSERT INTO article_tag,而非对每个标签循环insert。
3.3 Mapper 层:用 XML 实现动态 SQL 与关联查询
ArticleMapper.xml中定义按分类查询的动态 SQL:
<select id="listByCategory" resultType="com.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 test="categoryId != null and categoryId != 0"> AND a.category_id = #{categoryId} </if> </where> ORDER BY a.create_time DESC LIMIT #{page}, #{size} </select><where>标签自动处理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:th="http://www.thymeleaf.org"> <head> <title th:text="${title} ?: '个人博客'">博客首页</title> </head> <body> <header th:fragment="header"> <nav> <a href="/">首页</a> <a href="/categories">分类</a> <a href="/admin/login">后台</a> </nav> </header> <main th:fragment="content"> <!-- 页面内容占位 --> </main> <footer th:fragment="footer"> <p>© 2024 个人博客系统</p> </footer> </body> </html>templates/index.html继承并填充内容:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}"> <head> <title>博客首页</title> </head> <body> <div layout:fragment="content"> <h1>最新文章</h1> <div th:each="article : ${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> </html>th:each遍历Page<Article>.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 Result<String> 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.xml);transferTo()比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 closed;maximum-pool-size不宜过大:MySQL 默认max_connections=151,若应用集群有 3 个实例,则单实例设50较安全;idle-timeout配合minimum-idle,确保低峰期释放多余连接,节省数据库资源。
5.2 日志分级与 SQL 输出:用 logback-spring.xml 精确控制 MyBatis 日志
src/main/resources/logback-spring.xml配置:
<?xml version="1.0" encoding="UTF-8"?> <configuration> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- 仅在 dev 环境输出 MyBatis SQL --> <springProfile name="dev"> <logger name="com.example.blog.mapper" level="DEBUG" additivity="false"> <appender-ref ref="CONSOLE"/> </logger> </springProfile> <root level="INFO"> <appender-ref ref="CONSOLE"/> </root> </configuration>com.example.blog.mapper包下的日志级别设为DEBUG,可看到Preparing: SELECT * FROM article WHERE id = ?及绑定参数;springProfile标签确保生产环境(prod)不输出 SQL,避免敏感信息泄露;additivity="false"防止日志重复打印(父 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(默认值)。
本文还有配套的精品资源,点击获取