SaToken是什么?
是一个轻量级的Java权限认证框架,主要解决:登录认证、权限认证、单点认证、OAuth2.0、分布式Session会话、微服务网关鉴权等权限相关问题
Sa-Token 目前主要五大功能模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。
- 登录认证 ------ 单端登录、多端登录、同端互斥登录、七天内免登录。
- 权限认证 ------ 权限认证、角色认证、会话二级认证。
- 踢人下线 ------ 根据账号id踢人下线、根据Token值踢人下线。
- 注解式鉴权 ------ 优雅的将鉴权与业务代码分离。
- 路由拦截式鉴权 ------ 根据路由拦截鉴权,可适配 restful 模式。
- Session会话 ------ 全端共享Session,单端独享Session,自定义Session,方便的存取值。
- 持久层扩展 ------ 可集成 Redis,重启数据不丢失。
- 前后台分离 ------ APP、小程序等不支持 Cookie 的终端也可以轻松鉴权。
- Token风格定制 ------ 内置六种 Token 风格,还可:自定义 Token 生成策略。
- 记住我模式 ------ 适配 记住我 模式,重启浏览器免验证。
- 二级认证 ------ 在已登录的基础上再次认证,保证安全性。
- 模拟他人账号 ------ 实时操作任意用户状态数据。
- 临时身份切换 ------ 将会话身份临时切换为其它账号。
- 同端互斥登录 ------ 像QQ一样手机电脑同时在线,但是两个手机上互斥登录。
- 账号封禁 ------ 登录封禁、按照业务分类封禁、按照处罚阶梯封禁。
- 密码加密 ------ 提供基础加密算法,可快速 MD5、SHA1、SHA256、AES 加密。
- 会话查询 ------ 提供方便灵活的会话查询接口。
- Http Basic认证 ------ 一行代码接入 Http Basic、Digest 认证。
- 全局侦听器 ------ 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作。
- 全局过滤器 ------ 方便的处理跨域,全局设置安全响应头等操作。
- 多账号体系认证 ------ 一个系统多套账号分开鉴权(比如商城的 User 表和 Admin 表)
- 单点登录 ------ 内置三种单点登录模式:同域、跨域、同Redis、跨Redis、前后端分离等架构都可以搞定。
- 单点注销 ------ 任意子系统内发起注销,即可全端下线。
- OAuth2.0认证 ------ 轻松搭建 OAuth2.0 服务,支持openid模式 。
- 分布式会话 ------ 提供共享数据中心分布式会话方案。
- 微服务网关鉴权 ------ 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证。
- RPC调用鉴权 ------ 网关转发鉴权,RPC调用鉴权,让服务调用不再裸奔
- 临时Token认证 ------ 解决短时间的 Token 授权问题。
- 独立Redis ------ 将权限缓存与业务缓存分离。
- Quick快速登录认证 ------ 为项目零代码注入一个登录页面。
- 标签方言 ------ 提供 Thymeleaf 标签方言集成包,提供 beetl 集成示例。
- jwt集成 ------ 提供三种模式的 jwt 集成方案,提供 token 扩展参数能力。
- RPC调用状态传递 ------ 提供 dubbo、grpc 等集成包,在RPC调用时登录状态不丢失。
- 参数签名 ------ 提供跨系统API调用签名校验模块,防参数篡改,防请求重放。
- 自动续签 ------ 提供两种Token过期策略,灵活搭配使用,还可自动续签。
- 开箱即用 ------ 提供SpringMVC、WebFlux、Solon 等常见框架集成包,开箱即用。
- 最新技术栈 ------ 适配最新技术栈:支持 SpringBoot 3.x,jdk 17。

在父项目pom添加依赖,后在auth模块引入依赖。配置Sa-Token
java
Plain Text
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: satoken
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
is-share: true
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
token-style: uuid
# 是否输出操作日志
is-log: true
通过调用来测试
java
StpUtil.login(userId);
SptUtil.isLogin();
|---|
| |
整合RedisTemplate
在父项目和子模块添加完依赖后,在application-dev.yml配置
java
YAML
spring:
datasource:
// 省略...
data:
redis:
database: 0 # Redis 数据库索引(默认为 0)
host: 127.0.0.1 # Redis 服务器地址
port: 6379 # Redis 服务器连接端口
password: qwe123!@# # Redis 服务器连接密码(默认为空)
timeout: 5s # 读超时时间
connect-timeout: 5s # 链接超时时间
lettuce:
pool:
max-active: 200 # 连接池最大连接数
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 0 # 连接池中的最小空闲连接
max-idle: 10 # 连接池中的最大空闲连接
接下来便是创建Config文件进行RedisTemplate的自定义配置:
主要是:设置连接工厂->用StringRedisSerializer来序列化和反序列化redis的key,确保key以可读的形式进行保存->用Jackson2JsonRedisSerializer来序列化和反序列化redis的value,确保value以JSON存储
java
package com.quanxiaoha.xiaohashu.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author: 犬小哈
* @date: 2024/4/6 15:51
* @version: v1.0.0
* @description: RedisTemplate 配置
**/
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置 RedisTemplate 的连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值,确保 key 是可读的字符串
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值, 确保存储的是 JSON 格式
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
获取手机短信验证码接口开发

整个流程逻辑为:
- 前端以手机号作为入参,请求验证码
- 后端拿到手机号,构造redis的verificationkey
- 查询redis中是否有这个key,如果有说明验证码还未过期,提示用户请求过于频繁
- 若不存在,生成随机6位验证码,调用阿里云短信发送服务发送验证码,同时存储到redis中设置过期时间为3分钟,这样可以用于判断用户输入的验证码正确性已经是否请求频繁
接口地址:
|-----------------------------------------------|
| Java java POST /verification/code/send |
入参:
|----------------------------------------------------|
| Java java { "phone": "18019939108" // 手机号 } |
出参:
java
{
"success": false,
"message": "请求太频繁,请3分钟后再试",
"errorCode": "AUTH-20000",
"data": null
}
根据流程,先进行VO的创建
java
package com.quanxiaoha.xiaohashu.auth.model.vo.verificationcode;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SendVerificationCodeReqVO {
@NotBlank(message = "手机号不能为空")
private String phone;
}
加上无参构造、全参构造等注解,方便调用
接下来需要完善redis中的verificationkey,为了方便使用以及保证代码的规范性,创建constants类来管理rediskey,加上属性和静态方法便于其他的调用
java
package com.quanxiaoha.xiaohashu.auth.constant;
public class RedisKeyConstants {
/**
* 验证码 KEY 前缀
*/
private static final String VERIFICATION_CODE_KEY_PREFIX = "verification_code:";
/**
* 构建验证码 KEY
* @param phone
* @return
*/
public static String buildVerificationCodeKey(String phone) {
return VERIFICATION_CODE_KEY_PREFIX + phone;
}
}
由于有可能出现"请求频繁"的错误,需要在ResponeEnum类中添加业务状态码
接下来便是具体业务的开发,创建Controller和Service。
Service需要结合阿里云的号码认证服务,先添加相关依赖;在阿里云创建AccessKey并配置到.yml文件;封装sms工具类,进行短信服务,包括Properties、Config与具体的实现类。其中Properties主要用于管理AccessKey,方便使用的同时防止硬编码导致的信息不安全问题
java
package com.quanxiaoha.xiaohashu.auth.sms;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "aliyun")
@Component
@Data
public class AliyunAccessKeyProperties {
private String accessKeyId;
private String accessKeySecret;
}
其中的注释
java
@ConfigurationProperties(prefix = "aliyun")
指定了从.yml哪个地方加载具体值
Config类主要用于配置发送短信的必要信息
java
Java
package com.quanxiaoha.xiaohashu.auth.sms;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author: 犬小哈
* @Date: 2025/10/12 17:29
* @Version: v1.0.0
* @Description: 短信发送客户端
**/
@Configuration
@Slf4j
public class AliyunSmsClientConfig {
@Resource
private AliyunAccessKeyProperties aliyunAccessKeyProperties;
@Bean
public com.aliyun.dypnsapi20170525.Client smsClient() {
try {
com.aliyun.credentials.Client credential = new com.aliyun.credentials.Client();
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
.setCredential(credential);
// Endpoint 请参考 https://api.aliyun.com/product/Dypnsapi
config.endpoint = "dypnsapi.aliyuncs.com";
config.accessKeyId = aliyunAccessKeyProperties.getAccessKeyId(); // 必填
config.accessKeySecret = aliyunAccessKeyProperties.getAccessKeySecret(); // 必填
return new com.aliyun.dypnsapi20170525.Client(config);
} catch (Exception e) {
log.error("初始化阿里云短信发送客户端错误: ", e);
return null;
}
}
}
其中的config作为中间件来存放具体信息,避免了麻烦的配置与冗余的代码
主要信息是:credential、endpoint、accessKeyId、accessKeySecret
最后是具体的发送类:
java
package com.quanxiaoha.xiaohashu.auth.sms;
import com.aliyun.dypnsapi20170525.models.SendSmsVerifyCodeResponse;
import com.quanxiaoha.framework.common.util.JsonUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @Author: 犬小哈
* @Date: 2025/10/12 17:43
* @Version: v1.0.0
* @Description: 短信发送工具类
**/
@Component
@Slf4j
public class AliyunSmsHelper {
@Resource
private com.aliyun.dypnsapi20170525.Client client;
/**
* 发送短信
* @param signName
* @param templateCode
* @param phone
* @param templateParam
* @return
*/
public boolean sendMessage(String signName, String templateCode, String phone, String templateParam) {
com.aliyun.dypnsapi20170525.models.SendSmsVerifyCodeRequest sendSmsVerifyCodeRequest = new com.aliyun.dypnsapi20170525.models.SendSmsVerifyCodeRequest()
.setSignName(signName)
.setTemplateCode(templateCode)
.setPhoneNumber(phone)
.setTemplateParam(templateParam);
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
try {
log.info("==> 开始短信发送, phone: {}, signName: {}, templateCode: {}, templateParam: {}", phone, signName, templateCode, templateParam);
// 发送短信
SendSmsVerifyCodeResponse response = client.sendSmsVerifyCodeWithOptions(sendSmsVerifyCodeRequest, runtime);
log.info("==> 短信发送成功, response: {}", JsonUtils.toJsonString(response));
return true;
} catch (Exception error) {
log.error("==> 短信发送错误: ", error);
return false;
}
}
}
到这里,就可以实现service的发送短信功能了。但是为了可以提升性能,增加系统的响应速度,可以采用异步发送短信的方法。为了实现异步发送,需要进行自定义线程池
自定义线程池
创建Config类,进行线程池的配置
配置完成后就可以开始实现具体业务了
主要流程是:
- 获取手机号并构建rediskey
- 根据rediskey来判断redis中是否已经发送验证码,已发送则终止流程并抛出验证码发送频繁错误
- 若没有,生成验证码,调用阿里云验证码工具发送验证码
- 存储验证码到redis,TTL3分钟
java
package com.quanxiaoha.xiaohashu.auth.service.impl;
import cn.hutool.core.util.RandomUtil;
import com.quanxiaoha.framework.common.exception.BizException;
import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.xiaohashu.auth.constant.RedisKeyConstants;
import com.quanxiaoha.xiaohashu.auth.enums.ResponseCodeEnum;
import com.quanxiaoha.xiaohashu.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
import com.quanxiaoha.xiaohashu.auth.service.VerificationCodeService;
import com.quanxiaoha.xiaohashu.auth.sms.AliyunSmsHelper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class VerificationCodeServiceImpl implements VerificationCodeService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource(name = "taskExecutor")
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Resource
private AliyunSmsHelper aliyunSmsHelper;
/**
* 发送短信验证码
*
* @param sendVerificationCodeReqVO
* @return
*/
@Override
public Response<?> send(SendVerificationCodeReqVO sendVerificationCodeReqVO) {
// 手机号
String phone = sendVerificationCodeReqVO.getPhone();
// 构建验证码 redis key
String key = RedisKeyConstants.buildVerificationCodeKey(phone);
// 判断是否已发送验证码
boolean isSent = redisTemplate.hasKey(key);
if (isSent) {
// 若之前发送的验证码未过期,则提示发送频繁
throw new BizException(ResponseCodeEnum.VERIFICATION_CODE_SEND_FREQUENTLY);
}
// 生成 6 位随机数字验证码
String verificationCode = RandomUtil.randomNumbers(6);
log.info("==> 手机号: {}, 已生成验证码:【{}】", phone, verificationCode);
// 调用第三方短信发送服务
threadPoolTaskExecutor.submit(() -> {
String signName = "速通互联验证码"; // 签名,个人测试签名无法修改
String templateCode = "100001"; // 短信模板编码
// 短信模板参数,code 表示要发送的验证码;min 表示验证码有时间时长,即 3 分钟
String templateParam = String.format("{\"code\":\"%s\",\"min\":\"3\"}", verificationCode);
aliyunSmsHelper.sendMessage(signName, templateCode, phone, templateParam);
});
// 存储验证码到 redis, 并设置过期时间为 3 分钟
redisTemplate.opsForValue().set(key, verificationCode, 3, TimeUnit.MINUTES);
return Response.success();
}
}
自定义 @PhoneNumber 手机号校验注解
自定义注解的通用流程:
- 创建自定义校验类,实现ConstraintValidator<PhoneNumber, String>接口
- PhoneNumber:自定义注解类型。
- String:被校验的属性类型。
用于添加自己的校验逻辑,在isValid 方法实现具体的自定义校验
- 创建自定义注解类
java
package com.quanxiaoha.framework.common.validator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
/**
* @author: 犬小哈
* @date: 2024/4/15 22:22
* @version: v1.0.0
* @description: 自定义手机号校验注解
**/
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface PhoneNumber {
String message() default "手机号格式不正确, 需为 11 位数字";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
其中
@Target 注解用于指定自定义注解可以应用的 Java 元素类型。在 @PhoneNumber 中,@Target 的参数包括以下几个元素类型:
- ElementType.METHOD:可以应用于方法。
- ElementType.FIELD:可以应用于字段。
- ElementType.ANNOTATION_TYPE:可以应用于其他注解。
- ElementType.PARAMETER:可以应用于方法参数。
这种组合使得 @PhoneNumber 注解可以被广泛使用在方法、字段、注解和参数上。
@Retention 注解用于指定自定义注解的保留策略。RetentionPolicy.RUNTIME 表示该注解在运行时仍然可用(可以通过反射机制访问)。这对于校验注解非常重要,因为校验框架需要在运行时读取注解并执行相应的校验逻辑。
@Constraint 注解用于指定关联的验证器类。在 @PhoneNumber 中,validatedBy 属性指向 PhoneNumberValidator.class,即自定义注解 @PhoneNumber 使用 PhoneNumberValidator 类进行校验。
message 元素用于定义验证失败时的错误消息。在使用注解时可以覆盖默认消息。default 关键字用于提供该元素的默认值。
鉴权设计:RBAC 权限模型
Role-Base Access Control基于角色的访问控制。通过角色来管理用户的权限。核心是将用户和角色相关联,不同的角色有不同的权限,而不是直接将权限分配给用户
在实际业务中又扩展了RBAC1、RBAC2、RBAC3,在最基础的功能上添加了功能来适应不同场景
要实现RBAC权限控制,需要5张表:用户表、角色表、角色权限关联表、用户角色关联表
由于生产环境中,请求不能直接打到内网,鉴权放置位置有不同选择
- 每个微服务各自鉴权;
- 网关统一鉴权;
- 混合策略;
其中混合策略即在网关进行初步鉴权,进行粗粒度的控制,然后在关键微服务中进行细粒度的二次鉴权。这种方式可以兼顾性能和安全性。
用户注册/登录接口开发
接口地址:
java
POST /user/login
入参:
java
{
"phone": "18011119108", // 手机号
"code": "218603", // 登录验证码,验证码登录时,需要填写
"password": "xx", // 密码登录时,需要填写
"type": 1 // 登录类型,1表示手机号验证码登录;2表示账号密码登录
}
出参:
java
{
"success": true,
"message": null,
"errorCode": null,
"data": "xxxxx" // 登录成功后,返回 Token 令牌
}
整体流程与验证码获取部分差不多,但是这里要考虑DAO层的操作
service的流程为:
- 拿到入参实体类中的 type 字段,通过 LoginTypeEnum.valueOf() 方法,获取具体的类型枚举值;
- 对枚举进行 switch 判断,若是手机号验证码登录;
- 获取提交上来的验证码,并与存储在 Redis 中的验证码进行比对;
- 若不一致,返回验证码错误提示信息;
- 否则,通过手机号查询数据库;
- 若 userDO 为空,说明是新用户,系统需要自动为该用户注册用户信息。这里代码块中,先写个 todo , 后面小节中,再写具体的逻辑;
- 若 userDO 不为空,则说明是老用户,获取其用户 ID;
- 若是账号密码登录,校验密码是否正确;
- SaToken 登录用户,并返回 token 令牌;
编程式事务使用:更细粒度的事务控制
声明式注解事务失效,主要由以下几点:
- 方法可见性 :@Transactional 仅在 public 方法上生效。
- 自调用 :当类中的方法调用同一个类中的另一个 @Transactional 方法时,事务可能不会生效。这是因为事务注解是通过 AOP 实现的,而 Spring 的 AOP 代理机制在这种情况下不会被触发。
- 异常处理 :只有 RuntimeException 和 Error 类型的异常会触发事务回滚。如果你抛出的是 checked exception,事务不会回滚,除非你明确指定 rollbackFor 属性。
- 代理对象 :确保你是在 Spring 管理的代理对象上调用方法。如果你直接使用 new 关键字实例化对象,Spring 的 AOP 代理机制将不会被应用。
什么是编程式事务?有哪些优点?
编程式事务(Programmatic Transaction)是一种通过代码显式地管理事务的方式,而不是依赖声明式事务(Declarative Transaction)中使用的注解或 XML 配置。在编程式事务中,开发人员通过编写代码来开启、提交和回滚事务,以精细控制事务的边界和行为。
使用编程式事务优点如下:
- 精细控制:编程式事务允许开发者通过代码精细地控制事务的生命周期,包括开始、提交和回滚。可以根据具体业务需求,灵活地管理事务。
- 动态处理:在运行时可以根据业务逻辑的不同情况动态决定事务的行为。特别适合需要在代码执行过程中,根据某些条件来开启、提交或回滚事务的场景。
- 适用于复杂事务:在一个方法中需要多次开启和关闭事务,或需要嵌套事务的复杂场景中,编程式事务可以提供更大的灵活性和控制力。
- 灵活性高:能够在代码中实现复杂的事务逻辑,可以精确控制事务的边界和行为。这在需要多个步骤或调用之间共享事务上下文时非常有用。
- 性能提升:通过精细控制事务的边界,减少不必要的事务开启和提交,从而减少事务开销;通过明确控制事务的开始和结束,可以确保事务范围尽可能小,减少长时间占用数据库资源,提高系统的并发性;通过灵活的事务管理,可以在必要时才进行事务回滚,减少回滚操作带来的性能开销。
四种方式:
- 通过 transactionManager.getTransaction 方法获取一个新的事务状态。DefaultTransactionDefinition 用于定义事务的默认属性,例如传播行为和隔离级别。该方法会返回一个TransactionStatus对象,用于管理事务的状态。
- TransactionTemplate是一个简化了事务管理的工具类,可以避免直接处理 TransactionStatus
代码优化:Guava Preconditions 参数校验
Guava 是一个广泛使用的 Java 库,提供了许多有用的工具和实用程序,其中包括参数校验工具。Guava 的参数校验功能主要通过 com.google.common.base.Preconditions 类来实现。Preconditions 提供了一组静态方法,用于在方法执行前验证参数的有效性。这些方法在条件不满足时抛出异常,从而确保方法得到合法的输入。
注意checkArgument()方法源码是抛出了IllegaArgumentException,需要在全局异常类中添加捕获IllegaArgumentException的方法
同步【角色-权限集合】数据到 Redis 中
登录成功后redis中会同步用户-角色的关联关系,但是光有角色 ID 是不够的,因为每个角色对应的权限数据,还没有同步到 Redis 中。这块的工作,可以放到项目启动后,同时也将角色-权限数据同步到 Redis 中。
可以通过多种方式在项目启动时执行初始化工作。以下是一些常见的方法:
- 使用 @PostConstruct 注解
- 实现 ApplicationRunner 接口
- 实现 CommandLineRunner 接口
- 使用 @EventListener 注解监听 ApplicationReadyEvent
- 使用 SmartInitializingSingleton 接口
- 使用 Spring Boot 的 InitializingBean 接口
总结:
- @PostConstruct:适合简单的初始化逻辑,执行时机较早。
- ApplicationRunner 和 CommandLineRunner:适合需要访问命令行参数的初始化逻辑,执行时机在 Spring Boot 应用启动完成后。
- ApplicationReadyEvent 监听器:适合在整个应用准备好后执行的初始化逻辑。
- SmartInitializingSingleton:适合需要在所有单例 bean 初始化完成后执行的初始化逻辑。
- InitializingBean:适合需要在 bean 属性设置完成后执行的初始化逻辑。