SpringBoot+Thymeleaf快速搭建个人博客系统实战项目
简介:本项目基于SpringBoot与Thymeleaf框架,提供一套完整的个人博客系统开发方案,涵盖从前端展示到后端管理的全流程实现。内容包括SpringBoot自动配置、Thymeleaf模板引擎集成、Spring Data JPA操作MySQL数据库、RESTful API设计、权限控制及项目快速搭建与部署方法。适合Java初学者进行Web开发实践或课程设计,帮助掌握Spring生态核心技术并提升全栈开发能力。
SpringBoot 全栈开发实战:从零构建博客系统
在当今快速迭代的软件开发领域,一个高效、稳定且易于维护的技术栈是项目成功的关键。对于 Java 开发者而言, Spring Boot + Thymeleaf + Spring Data JPA + MySQL 组合无疑是一套成熟可靠的全栈解决方案。它不仅简化了传统 SSM(Spring + Spring MVC + MyBatis)架构中的繁琐配置,还通过“约定优于配置”的理念极大提升了开发效率。
你有没有遇到过这样的场景?
刚接手一个老项目,光是搞清楚 web.xml 、 applicationContext.xml 和 spring-mvc.xml 之间的依赖关系就花了三天;
或者为了实现一个简单的用户列表展示功能,写了上百行 XML 映射和 DAO 接口……
而如今,借助 Spring Boot 的自动装配机制,这些曾经令人头疼的问题几乎被彻底终结了。我们只需要关注业务逻辑本身,框架会帮你完成大部分底层基础设施的搭建工作。
本文将以一个完整的 博客系统 为例,带你深入理解这套技术体系是如何协同工作的——从项目初始化开始,到数据库建模、后端接口设计、前端页面渲染,再到权限控制与管理后台实现。我们将不再停留在“怎么用”的层面,而是探究其背后的运行原理,并穿插大量工程实践建议,帮助你在真实项目中避免踩坑。
准备好了吗?🚀 让我们一起开启这段全栈之旅吧!
项目初始化与自动配置原理解密
起步依赖:告别复杂的 Maven 配置地狱
还记得当年手动添加各种 jar 包的日子吗?版本冲突、依赖缺失、兼容性问题……简直是开发者的噩梦 😫。而现在,只需访问 https://start.spring.io ,选择几个核心依赖,就能一键生成标准的 Spring Boot 工程骨架。
比如我们要做一个 Web 应用,通常会选上:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
这短短一行代码背后到底发生了什么?
其实,“starter” 并不是一个真正的库,而是一个 依赖管理聚合包 。当你引入 spring-boot-starter-web 时,Maven 会自动拉取一整套与 Web 开发相关的组件,包括:
- Spring MVC(用于处理 HTTP 请求)
- Tomcat(内嵌服务器,默认启动)
- Jackson(JSON 序列化/反序列化)
- Validation API(参数校验支持)
更重要的是,所有这些依赖都已经由 Spring Boot 团队进行了严格的版本匹配测试,确保它们之间不会出现兼容性问题。换句话说,你再也不需要去查某个 Spring 版本是否兼容特定版本的 Hibernate 了。
这就是所谓的“起步依赖”(Starter Dependency),它的本质是一种 最佳实践封装 。
💡 小贴士:如果你看到某个依赖以
spring-boot-starter-*命名,那它一定是某种场景下的开箱即用模板。例如:
spring-boot-starter-data-jpa→ 数据持久化spring-boot-starter-thymeleaf→ 模板引擎spring-boot-starter-security→ 安全认证只需按需引入即可,无需关心内部细节。
自动配置:Spring Boot 的灵魂所在
有了起步依赖还不够,真正让 Spring Boot “起飞”的,是它的 自动配置机制 (Auto-configuration)。
想象一下:以前我们需要在 XML 或 Java Config 中显式地声明 DispatcherServlet 、 ViewResolver 、 DataSource 等 Bean;现在呢?只要 classpath 下有相关类,Spring Boot 就能自动帮你注册对应的 Bean!
这一切都归功于 @EnableAutoConfiguration 注解(虽然我们通常不直接写它,而是通过 @SpringBootApplication 间接启用)。
那么它是如何做到的呢?
🧠 核心原理剖析
Spring Boot 在启动过程中会扫描所有 jar 包下的这个文件路径:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
这个文件里记录了一长串自动配置类的名字,比如:
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
...
Spring 容器会逐个加载这些类,并根据一系列 @ConditionalOnXXX 条件注解来决定是否生效。举个例子:
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
public class WebMvcAutoConfiguration {
// ...
}
这意味着只有当应用是一个 Web 应用,且 classpath 中存在 Servlet 和 DispatcherServlet 类时,该配置才会被激活。
是不是很聪明?🧠
这种基于条件判断的自动装配机制,使得 Spring Boot 能够根据不同环境灵活调整行为。比如:
- 如果检测到 H2 数据库驱动,就自动配置内存数据库;
- 如果发现 Redis 客户端库,则自动连接 Redis 实例;
- 如果没有指定视图解析器,就默认使用 Thymeleaf。
整个过程对开发者透明,真正做到“开箱即用”。
⚠️ 注意:自动配置虽好,但也可能导致意料之外的行为。建议在生产环境中明确关闭不必要的模块,或使用
@ConditionalOnMissingBean防止 Bean 冲突。
Thymeleaf:服务端模板的现代演绎
为什么还需要服务端模板?
听到“Thymeleaf”,也许你会疑惑:现在不是都流行前后端分离了吗?React/Vue 打天下,谁还用服务端渲染啊?
的确,在大型复杂应用中,前后端分离+RESTful API 是主流趋势。但在某些场景下, 服务端模板依然有着不可替代的优势 :
| 场景 | 优势 |
|---|---|
| 内部管理系统 | 开发成本低,响应快,无需额外部署前端工程 |
| SEO 敏感型网站 | HTML 内容直出,搜索引擎友好 |
| 快速原型验证 | 几乎零配置即可展示动态数据 |
而且,Thymeleaf 的设计理念非常人性化——它支持 自然模板 (Natural Templates),也就是说 .html 文件可以直接在浏览器中打开预览,不需要启动后端服务!这对前端协作特别友好 👏。
表达式语言:简洁而强大
Thymeleaf 提供了一套直观的表达式语法,全部基于标准 HTML 属性扩展,使用 th:* 命名空间注入动态行为。这种设计既保持了 HTML 的合法性,又赋予了页面强大的动态能力。
来看看几种最常用的表达式类型:
✅ 变量表达式 ${...}
这是最基础也是最频繁使用的语法,用于获取上下文中的变量值。
控制器传参示例:
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model) {
model.addAttribute("username", "zhangsan");
model.addAttribute("age", 28);
return "home";
}
}
对应模板写法:
<p th:text="${username}"></p>
<p th:text="'Age: ' + ${age}"></p>
执行后输出:
<p>zhangsan</p>
<p>Age: 28</p>
可以看到, th:text 会替换元素的文本内容,而不是像 jQuery 那样操作 DOM。整个过程发生在服务端,最终发送给浏览器的是纯静态 HTML。
✅ 选择表达式 *{...}
当你已经通过 th:object 指定了当前作用域对象时,可以用 *{...} 简化访问路径。
<div th:object="${user}">
<span th:text="*{name}"></span>
<span th:text="*{email}"></span>
</div>
等价于:
<span th:text="${user.name}"></span>
<span th:text="${user.email}"></span>
但前者更清晰,尤其适合嵌套对象结构。
✅ 消息表达式 #{...}
国际化支持是企业级应用的基本要求。Thymeleaf 配合 Spring 的 MessageSource 可轻松实现多语言切换。
资源文件 messages.properties :
welcome.message=Hello, {0}!
button.save=Save
模板调用:
<p th:text="#{welcome.message(${username})}"></p>
<button th:text="#{button.save}">Default Save</button>
如果当前语言为中文,且 username="李四" ,则输出:
<p>Hello, 李四!</p>
<button>保存</button>
✅ URL 表达式 @{...}
生成动态链接时,硬编码路径容易出错。 @{...} 能自动拼接上下文路径(Context Path),保证链接正确。
<a th:href="@{/article/{id}(id=${article.id})}">查看文章</a>
<form th:action="@{/submit}" method="post">
假设应用部署在 /blog 路径下, @{/submit} 会被解析为 /blog/submit ,避免 404 错误。
下面是 Thymeleaf 整体处理流程的可视化表示:
graph TD
A[Controller返回ModelAndView] --> B{Thymeleaf Template Resolver}
B --> C[解析HTML模板]
C --> D[执行表达式引擎]
D --> E[替换${...}, #{...}, @{...}]
E --> F[输出最终HTML响应]
整个链条高度自动化,开发者只需专注于数据填充和视图设计。
动态逻辑控制:条件判断与循环遍历
再好的模板引擎也离不开流程控制。Thymeleaf 提供了丰富的属性指令来实现常见的 UI 逻辑。
条件渲染: th:if / th:unless / th:switch
<!-- 登录欢迎 -->
<div th:if="${not #strings.isEmpty(username)}">
Welcome back, <b th:text="${username}"></b>!
</div>
<!-- 非管理员提示 -->
<div th:unless="${isAdmin}">
You don't have admin privileges.
</div>
<!-- 角色面板分支 -->
<div th:switch="${user.role}">
<p th:case="'ADMIN'">Administrator Panel</p>
<p th:case="#{'USER'}">User Dashboard</p>
<p th:case="*">Guest View</p>
</div>
这里有几个关键点要注意:
#strings.isEmpty()是 Thymeleaf 内置的工具方法,属于utility objects。th:unless相当于!condition,语义更清晰。th:case="*"表示默认情况,类似 switch-case 中的default。
列表遍历: th:each 的艺术
展示集合数据几乎是每个系统的刚需。 th:each 指令可以轻松应对表格、菜单、文章列表等结构化内容。
Java 控制器传递数据:
model.addAttribute("articles", Arrays.asList(
new Article(1L, "First Post", "Intro to Thymeleaf"),
new Article(2L, "Second Post", "JPA Best Practices")
));
前端模板渲染:
<table>
<tr th:each="article, iter : ${articles}">
<td th:text="${iter.index + 1}">序号</td>
<td th:text="${article.title}">标题</td>
<td th:text="${article.summary}">摘要</td>
<td th:text="${article.createdAt?.format(#dates.format('yyyy-MM-dd'))}">
创建时间
</td>
</tr>
</table>
其中:
iter.index从 0 开始,加 1 后变成自然序号;?.是安全导航操作符,防止空指针异常;#dates.format()是日期格式化工具函数。
th:each 还提供了一个强大的状态变量(status variable),包含以下属性:
| 属性 | 含义 | 示例 |
|---|---|---|
index |
当前索引(从0开始) | iter.index → 0,1,2… |
count |
当前计数(从1开始) | iter.count → 1,2,3… |
size |
集合总长度 | iter.size |
even/odd |
是否偶数/奇数行 | iter.even → true/false |
first/last |
是否首/尾元素 | iter.first |
利用这些状态信息,我们可以轻松实现隔行变色效果:
<tr th:class="${iter.odd}? 'odd-row' : 'even-row'">
配合 CSS:
.odd-row { background-color: #f9f9f9; }
.even-row { background-color: #fff; }
甚至还能做嵌套循环,比如分类目录下的文章展示:
<ul th:each="category : ${categories}">
<li th:text="${category.name}"></li>
<ul>
<li th:each="article : ${category.articles}"
th:text="${article.title}"></li>
</ul>
</ul>
完美呈现层级结构 ✅
Spring Data JPA:让 CRUD 成为过去式
ORM 的终极进化形态
在 Java 持久层技术演进史上,JDBC → MyBatis → JPA 是一条清晰的发展脉络。而 Spring Data JPA 更是在 JPA 基础上进一步抽象,提出了“ 仓库模式 ”(Repository Pattern),让我们只需定义接口,就能获得完整的 CRUD 能力。
看看这是多么不可思议:
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
}
没错,接口里什么方法都没写!但只要你把它注入 Service 层,就可以直接调用:
@Autowired
private PostRepository postRepo;
// 新增
postRepo.save(post);
// 查询全部
postRepo.findAll();
// 分页查询
Pageable pageable = PageRequest.of(0, 10);
postRepo.findAll(pageable);
// 删除
postRepo.deleteById(id);
这些方法哪来的?其实是 Spring Data JPA 在运行时为你的接口生成了代理实现,自动绑定到 JPA 的 EntityManager 上。
方法名解析:会说话的查询语句
除了内置方法,Spring Data JPA 最惊艳的功能莫过于 方法名解析机制 。你可以通过命名规则自动生成查询逻辑,完全不用写 SQL!
看几个例子:
public interface PostRepository extends JpaRepository<Post, Long> {
// 根据标题模糊查询
List<Post> findByTitleContaining(String title);
// 查找已发布文章并按时间倒序
List<Post> findByStatusOrderByCreatedAtDesc(String status);
// 查找某作者的文章并分页
Page<Post> findByAuthorName(String authorName, Pageable pageable);
// 统计某分类下的文章数量
long countByCategory_Id(Long categoryId);
}
框架会根据这些方法名自动推导出对应的 JPQL 查询:
findByTitleContaining("Spring")→SELECT p FROM Post p WHERE p.title LIKE '%Spring%'findByStatusOrderByCreatedAtDesc("PUBLISHED")→SELECT p FROM Post p WHERE p.status = 'PUBLISHED' ORDER BY p.createdAt DESC
这简直就像是在用自然语言描述需求 🤯
当然,也不是所有关键字都能记住。下面这张表值得收藏:
| 关键字 | 示例方法 | 对应操作 |
|---|---|---|
Containing |
findByTitleContaining |
LIKE '%?%' |
Like |
findByTitleLike |
LIKE ? |
Between |
findByPriceBetween |
BETWEEN ? AND ? |
In |
findByIdIn(List ids) |
IN (?) |
IsNull |
findByAuthorIsNull |
IS NULL |
GreaterThan |
findByViewsGreaterThan |
> ? |
OrderBy |
findByStatusOrderByViewsDesc |
ORDER BY views DESC |
💡 提示:方法名越长,可读性越好,但也要注意不要过度复杂。超过三个条件建议改用
@Query。
复杂查询: @Query 注解登场
当业务逻辑变得复杂时,比如涉及多表联查、子查询或性能优化,推荐使用 @Query 注解直接编写 JPQL 或原生 SQL。
public interface PostRepository extends JpaRepository<Post, Long> {
// 使用 JPQL 查询带标签的文章
@Query("SELECT DISTINCT p FROM Post p LEFT JOIN p.tags t WHERE t.name IN :tagNames")
Page<Post> findPostsByTags(@Param("tagNames") List<String> tagNames, Pageable pageable);
// 使用原生 SQL 查询热门文章(阅读量 > 1000)
@Query(value = "SELECT * FROM blog_post WHERE view_count > 1000", nativeQuery = true)
List<Post> findPopularPosts();
// 更新操作需配合 @Modifying
@Modifying
@Query("UPDATE Post p SET p.viewCount = p.viewCount + 1 WHERE p.id = ?1")
int incrementViewCount(Long id);
}
几点说明:
- JPQL 面向实体模型,不暴露数据库表名;
- 原生 SQL 性能更高,适合复杂查询;
- 修改类操作必须加上
@Modifying,否则会报错; @Param注解增强参数可读性和安全性。
来看一下方法调用时的内部执行流程:
sequenceDiagram
participant Client
participant RepositoryProxy
participant QueryLookupStrategy
participant EntityManager
Client->>RepositoryProxy: findByTitleContaining("Java")
RepositoryProxy->>QueryLookupStrategy: 解析方法名
QueryLookupStrategy-->>RepositoryProxy: 生成 JPQL: SELECT p FROM Post p WHERE p.title LIKE %?%
RepositoryProxy->>EntityManager: 创建 TypedQuery 执行
EntityManager-->>RepositoryProxy: 返回结果集
RepositoryProxy-->>Client: List<Post>
整个过程由 Spring AOP 动态代理拦截完成,对开发者完全透明。
数据库设计:从 E-R 图到物理表
博客系统的业务模型抽象
一个好的数据库设计,始于对业务的深刻理解。我们的博客系统主要包括以下几个核心实体:
- 用户(User) :注册账户,发布文章
- 文章(Post) :主要内容单元
- 分类(Category) :主题归类
- 标签(Tag) :关键词标记
- 评论(Comment) :互动交流
它们之间的关系如下:
erDiagram
USER ||--o{ POST : writes
CATEGORY ||--o{ POST : contains
CATEGORY }o--|| CATEGORY : "sub-category"
POST ||--o{ COMMENT : has
POST ||--o{ POST_TAG : has
TAG ||--o{ POST_TAG : tagged_in
USER {
bigint id PK
varchar username
varchar password
varchar email
datetime created_time
}
POST {
bigint id PK
varchar title
text content
datetime publish_time
tinyint status
bigint user_id FK
bigint category_id FK
}
CATEGORY {
bigint id PK
varchar name
bigint parent_id FK
}
TAG {
bigint id PK
varchar name
}
POST_TAG {
bigint post_id PK,FK
bigint tag_id PK,FK
}
COMMENT {
bigint id PK
text content
datetime create_time
bigint post_id FK
bigint parent_comment_id FK
}
这张 E-R 图清晰表达了各实体间的关联方式,尤其是:
- 用户与文章:一对多
- 文章与分类:多对一
- 文章与标签:多对多(通过中间表
post_tag实现) - 分类自关联:支持无限层级嵌套
范式理论的应用与权衡
设计表结构时,我们应遵循三大范式原则:
| 范式 | 要求 | 目标 |
|---|---|---|
| 1NF | 字段原子性 | 消除重复组 |
| 2NF | 完全依赖主键 | 防止部分依赖 |
| 3NF | 无传递依赖 | 消除冗余 |
以 POST 表为例:
CREATE TABLE post (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
content TEXT,
publish_time DATETIME DEFAULT CURRENT_TIMESTAMP,
status TINYINT DEFAULT 1,
user_id BIGINT NOT NULL,
category_id BIGINT,
FOREIGN KEY (user_id) REFERENCES user(id),
FOREIGN KEY (category_id) REFERENCES category(id)
);
该表满足 1NF~3NF。但如果把 category_name 冗余进来:
ALTER TABLE post ADD COLUMN category_name VARCHAR(100); -- ❌ 反模式
就会导致违反 3NF,因为 category_name 实际上依赖于 category_id ,形成传递依赖。一旦分类名变更,所有相关文章都要同步更新,极易出错。
所以正确的做法是:只存外键,查询时 JOIN 获取名称。
不过,在高并发读取场景下,频繁 JOIN 可能成为瓶颈。这时可以适度进行 反规范化 ,比如:
- 缓存作者昵称、分类名到 Redis;
- 使用物化视图预计算统计结果;
- 引入 Elasticsearch 做全文检索。
但一定要记住: 先规范化,再根据性能需求局部反规范 ,切忌一开始就堆大宽表。
索引优化:B+树的秘密武器
MySQL 默认使用 B+ 树作为索引结构,特点是:
- 支持范围查询
- 叶子节点形成链表,便于顺序扫描
- 高扇出,减少磁盘 I/O
常见查询场景及索引建议:
| 查询语句 | 推荐索引 |
|---|---|
WHERE username = ? |
UNIQUE(username) |
WHERE category_id = ? |
INDEX(category_id) |
ORDER BY publish_time DESC |
INDEX(publish_time DESC) |
WHERE category_id=? AND status=? |
INDEX(category_id, status) |
联合索引要遵守 最左前缀原则 :
-- 联合索引 (A, B, C)
ALTER TABLE t ADD INDEX idx_abc (A, B, C);
它可以加速:
WHERE A = ?WHERE A = ? AND B = ?WHERE A = ? AND B = ? AND C = ?
但不能有效支持:
WHERE B = ?(跳过 A)WHERE C = ?(跳过 A 和 B)
因此索引顺序很重要!一般把区分度高的字段放前面。
验证索引是否命中:
EXPLAIN SELECT * FROM post WHERE category_id = 3 AND status = 1;
查看 key 字段是否显示预期索引名。
定期清理无用索引也很重要:
SELECT * FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE object_schema = 'blog_db' AND count_read = 0;
count_read = 0 的索引可以考虑删除,节省存储空间和写入开销。
数据库初始化:SQL 脚本自动化加载
使用 schema.sql 和 data.sql 快速搭建环境
在 Spring Boot 中,我们可以通过两个简单脚本来实现数据库自动初始化:
src/main/resources/schema.sql:建表语句src/main/resources/data.sql:初始数据
示例 schema.sql :
DROP TABLE IF EXISTS post_tag;
DROP TABLE IF EXISTS tag;
DROP TABLE IF EXISTS comment;
DROP TABLE IF EXISTS post;
DROP TABLE IF EXISTS category;
DROP TABLE IF EXISTS user;
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
created_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
示例 data.sql :
INSERT INTO user (username, password, email) VALUES
('admin', '$2a$10$GRLdNyeVRuKUHILmHcJZlO5QvYtVwx/xHAUw2fW7T9SxPRqIUkOR6', 'admin@example.com');
INSERT INTO category (name, parent_id) VALUES
('技术', NULL),
('生活', NULL),
('Java', 1);
🔐 密码已用 BCrypt 加密,绝不能明文存储!
启动流程如下:
flowchart TD
A[Spring Boot Application Start] --> B{spring.sql.init.mode}
B -->|equal to always or embedded| C[Execute schema.sql]
C --> D[Create Tables]
D --> E[Execute data.sql]
E --> F[Insert Initial Data]
F --> G[Start ApplicationContext]
G --> H[Application Ready]
生产环境注意事项
虽然 schema.sql 很方便,但不适合用于生产环境的数据迁移。建议:
- 开发/测试环境:使用
spring.sql.init.mode=always - 生产环境:关闭自动初始化,改用 Flyway 或 Liquibase 做版本化管理
配置示例:
spring:
sql:
init:
mode: always
schema-locations: classpath:schema.sql
data-locations: classpath:data.sql
如果需要更精细控制,也可以自定义 DataSourceInitializer Bean:
@Configuration
public class DatabaseInitConfig {
@Bean
public DataSourceInitializer dataSourceInitializer(final DataSource dataSource) {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScript(new ClassPathResource("custom-schema.sql"));
populator.addScript(new ClassPathResource("custom-data.sql"));
DataSourceInitializer initializer = new DataSourceInitializer();
initializer.setDataSource(dataSource);
initializer.setDatabasePopulator(populator);
return initializer;
}
}
这样可以在不同环境下动态选择脚本路径。
前端功能实现:首页、分类、归档一体化设计
首页内容聚合与分页展示
首页是用户的第一印象,必须兼顾美观与性能。我们采用分页机制避免一次性加载过多数据。
Repository 层定义分页查询:
@Query("SELECT a FROM Article a WHERE a.status = 'PUBLISHED' ORDER BY a.createdAt DESC")
Page<Article> findPublishedArticles(Pageable pageable);
Service 层封装参数:
public Page<Article> getPublishedArticles(int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"));
return articleRepository.findPublishedArticles(pageable);
}
Controller 传递模型:
@GetMapping("/home")
public String home(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
Model model) {
Page<Article> articlePage = articleService.getPublishedArticles(page, size);
model.addAttribute("articles", articlePage.getContent());
model.addAttribute("currentPage", articlePage.getNumber());
model.addAttribute("totalPages", articlePage.getTotalPages());
return "index";
}
Thymeleaf 渲染分页控件:
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="page-item" th:classappend="${currentPage == 0} ? 'disabled' : ''">
<a class="page-link" th:href="@{/home(page=${currentPage - 1}, size=${#request.getParameter('size')})}" th:if="${currentPage > 0}">上一页</a>
</li>
<li th:each="i : ${#numbers.sequence(0, totalPages - 1)}"
class="page-item" th:classappend="${i == currentPage} ? 'active' : ''">
<a class="page-link" th:href="@{/home(page=${i}, size=${#request.getParameter('size')})}" th:text="${i + 1}">1</a>
</li>
<li class="page-item" th:classappend="${currentPage == totalPages - 1} ? 'disabled' : ''">
<a class="page-link" th:href="@{/home(page=${currentPage + 1}, size=${#request.getParameter('size')})}" th:if="${currentPage < totalPages - 1}">下一页</a>
</li>
</ul>
</nav>
还可以结合 AJAX 实现无刷新分页,提升用户体验。
分类树形结构递归渲染
分类常采用自引用表设计:
CREATE TABLE category (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
parent_id BIGINT,
FOREIGN KEY (parent_id) REFERENCES category(id)
);
Java 实体:
@Entity
public class Category {
@Id
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Category parent;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Category> children = new ArrayList<>();
}
构建树形结构:
public List<Category> buildTree() {
List<Category> all = categoryRepo.findAll();
Map<Long, Category> map = all.stream().collect(Collectors.toMap(Category::getId, c -> c));
List<Category> roots = new ArrayList<>();
for (Category c : all) {
if (c.getParent() != null) {
map.get(c.getParent().getId()).getChildren().add(c);
} else {
roots.add(c);
}
}
return roots;
}
Thymeleaf 递归模板:
<ul class="category-tree">
<li th:each="cat : ${categories}">
<span th:text="${cat.name}"></span>
<div th:replace="this :: subtree(${cat.children})"></div>
</li>
</ul>
<th:block th:fragment="subtree(children)">
<ul th:if="${not #lists.isEmpty(children)}">
<li th:each="child : ${children}">
<span th:text="${child.name}"></span>
<div th:replace="this :: subtree(${child.children})"></div>
</li>
</ul>
</block>
标签云权重算法
标签热度可通过字体大小体现:
public Map<Tag, Integer> calculateTagWeights(List<Tag> tags) {
if (tags.isEmpty()) return Collections.emptyMap();
int max = tags.stream().mapToInt(Tag::getCount).max().orElse(1);
int min = tags.stream().mapToInt(Tag::getCount).min().orElse(1);
int range = Math.max(max - min, 1);
return tags.stream().collect(Collectors.toMap(
Function.identity(),
tag -> 12 + (int)(((double)(tag.getCount() - min) / range) * 18) // 12~30px
));
}
前端样式联动:
<div class="tag-cloud">
<a th:each="entry : ${tagWeights}"
th:text="${entry.key.name}"
th:style="'--weight:' + ${entry.value} + 'px'"
th:href="@{/tags/{id}(id=${entry.key.id})}">标签</a>
</div>
CSS:
.tag-cloud a {
font-size: var(--weight);
margin: 5px;
}
时间轴归档:Java 8 时间 API 的妙用
按月归档文章:
public Map<YearMonth, List<Article>> groupByMonth(List<Article> articles) {
return articles.stream()
.filter(a -> "PUBLISHED".equals(a.getStatus()))
.collect(Collectors.groupingBy(
a -> YearMonth.from(a.getCreatedAt()),
LinkedHashMap::new,
Collectors.toList()
));
}
Thymeleaf 渲染:
<div class="archive-timeline">
<div th:each="entry : ${archives}" class="month-group">
<h4 th:text="${#temporals.format(entry.key.atDay(1), 'yyyy年MM月')}"></h4>
<ul>
<li th:each="article : ${entry.value}">
<a th:href="@{/article/{id}(id=${article.id})}" th:text="${article.title}"></a>
(<span th:text="${#temporals.format(article.createdAt, 'dd日')}"></span>)
</li>
</ul>
</div>
</div>
视觉上可用垂直时间线布局增强可读性。
后端管理功能:Spring Security 权限控制实战
管理员认证流程集成
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
配置类:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/login", "/css/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/admin/login")
.loginProcessingUrl("/admin/doLogin")
.defaultSuccessUrl("/admin/index", true)
.failureUrl("/admin/login?error=true")
.and()
.logout()
.logoutUrl("/admin/logout")
.logoutSuccessUrl("/admin/login")
.and()
.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
}
自定义 UserDetailsService :
@Service
public class AdminUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepo;
@Override
public UserDetails loadUserByUsername(String username) {
User user = userRepo.findByUsername(username);
if (user == null || !"ADMIN".equals(user.getRole())) {
throw new UsernameNotFoundException("Invalid admin user");
}
return User.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRole())
.build();
}
}
登录页面:
<form th:action="@{/admin/doLogin}" method="post">
<input type="text" name="username" required />
<input type="password" name="password" required />
<button type="submit">登录</button>
<div th:if="${param.error}">
<span style="color:red;">用户名或密码错误</span>
</div>
</form>
完整认证流程:
sequenceDiagram
participant User
participant Browser
participant SpringSecurity
participant UserDetailsService
User->>Browser: 访问 /admin/index
Browser->>SpringSecurity: 重定向到 /admin/login
User->>Browser: 输入用户名密码
Browser->>SpringSecurity: POST /admin/doLogin
SpringSecurity->>UserDetailsService: loadUserByUsername
UserDetailsService-->>SpringSecurity: 返回 UserDetails
SpringSecurity->>SpringSecurity: 校验密码 & 权限
alt 认证成功
SpringSecurity-->>Browser: 302 跳转 /admin/index
else 认证失败
SpringSecurity-->>Browser: 重定向至 failureUrl
end
这套机制保障了后台入口的安全性,是现代 Web 应用不可或缺的一环。
这种高度集成的设计思路,正引领着智能音频设备向更可靠、更高效的方向演进。
简介:本项目基于SpringBoot与Thymeleaf框架,提供一套完整的个人博客系统开发方案,涵盖从前端展示到后端管理的全流程实现。内容包括SpringBoot自动配置、Thymeleaf模板引擎集成、Spring Data JPA操作MySQL数据库、RESTful API设计、权限控制及项目快速搭建与部署方法。适合Java初学者进行Web开发实践或课程设计,帮助掌握Spring生态核心技术并提升全栈开发能力。
更多推荐




所有评论(0)