Spring急速入门
Spring 是 企业级开发的一站式框架,核心是 IOC(控制反转) 和 AOP(面向切面编程)
一、Spring 核心:IOC 理论
1. 什么是 IOC?
IOC(Inversion of Control,控制反转)是一种对象创建与管理的设计思想:
传统开发中,对象由程序主动创建(如 new UserService()),而 IOC 模式下,对象的创建、依赖关系的绑定由 Spring 容器 统一管理,程序只需 “被动接收” 容器注入的对象。
核心优势:解耦(对象间不再硬编码依赖)、可维护性(配置集中管理)、方便测试(模拟对象易注入)。
2. IOC 容器的实现
Spring 中 IOC 容器的核心接口是 BeanFactory(基础容器)和 ApplicationContext(扩展容器,企业级特性更丰富)。
容器启动时会读取配置(XML / 注解),解析并创建所有 Bean(被容器管理的对象),最终通过 getBean() 方法获取对象。
二、Bean 的注入方法
Bean 的依赖注入(DI,Dependency Injection)是 IOC 的具体实现,常见方式有以下 4 种:
1. 构造器注入
通过构造方法参数注入依赖,适合 “必须依赖” 的场景(如数据库连接)。
public class UserService {
private final UserDao userDao; // 必须依赖 UserDao
// 构造器注入(@Autowired 可省略,Spring 6+ 推荐显式声明)
@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}
}
2. Setter 方法注入
通过 Setter 方法注入依赖,适合 “可选依赖” 的场景(如日志工具)。
public class OrderService {
private Logger logger; //要注册bean
// Setter 注入
@Autowired
public void setLogger(Logger logger) {
this.logger = logger;
}
}
3. 字段注入(直接注入)
通过 @Autowired、@Resource 等注解直接标注在字段上,代码更简洁(但可能隐藏依赖关系,测试时需注意)。 建议Resource
public class ProductService {
// @Autowired(Spring 原生) 或 @Resource(JSR-250 标准)
@Autowired
private ProductDao productDao;
}
4. 自动注入(@Autowired 进阶)
按类型注入:默认根据类型匹配 Bean(如 UserDao 类型的 Bean)。
按名称注入:通过 @Qualifier("beanName") 指定具体 Bean(解决同类型多 Bean 的冲突)。
可选注入:@Autowired(required = false) 允许依赖为 null(避免容器启动报错)。
后面还会介绍List注入,Map注入
三、工厂模式与 Spring 的整合
工厂模式用于解耦对象的创建逻辑,Spring 支持两种工厂方式:
1. 普通工厂(FactoryBean)
通过实现 FactoryBean 接口,自定义 Bean 的创建逻辑(如复杂对象初始化)。
// 自定义工厂(创建 RedisClient 对象)
public class RedisClientFactoryBean implements FactoryBean<RedisClient> {
@Override
public RedisClient getObject() {
return new RedisClient("localhost", 6379); // 复杂初始化逻辑
}
@Override
public Class<?> getObjectType() {
return RedisClient.class;
}
}
// 注册工厂到 Spring 容器(XML 或 @Bean),这里是在config.里面注册的
@Bean
public RedisClientFactoryBean redisClientFactory() {
return new RedisClientFactoryBean();
}
最终通过 getBean("redisClientFactory") 获取的是 RedisClient 对象(而非工厂本身)。
下面的体现更为明显
2. 静态工厂(静态方法创建)
通过静态方法直接返回对象(无需实例化工厂类)。
public class DataSourceFactory {
// 静态方法创建 DataSource
public static DataSource createDataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://localhost:3306/test");
return ds;
}
}
// 注册到 Spring 容器(XML 配置)
<bean id="dataSource" class="com.example.DataSourceFactory" factory-method
="createDataSource"/>
虽然我们在这里看似注册的是DataSourceFactory,但是我们实际上注册的是HikariDataSource ,不相信可以去getbean()看看能不能获得DataSourceFactory ,
四、SpEL 表达式(Spring 表达式语言)
SpEL 是 Spring 的动态表达式语言,支持在配置或注解中动态计算值,常见场景:
1. 外部属性注入(读取 application.properties)
通过 @Value("${property.key}") 读取配置文件中的值。
${}是占位符
// application.properties 中配置:
redis.host=localhost
@Value("${redis.host}")
private String redisHost;
(这里的@Value还可以作为参数注入哦~)
@Servicepublic class UserService {
// 方法参数注入(查询用户时,注入默认分页大小)
public List<User> getUsers(@Value("${page.size}") int pageSize) {
return userMapper.selectUsers(pageSize); // 使用注入的 pageSize
}
}
2. 集合类操作(List/Map/ 数组)
SpEL 支持直接操作集合,例如:
// 注入 List(逗号分隔)
@Value("#{'1,2,3'.split(',')}")
private List<Integer> numbers;
// 注入 Map(键值对)
@Value("#{{'name':'张三', 'age':20}}")
private Map<String, Object> userInfo;
3. 方法调用与逻辑运算
// 调用 String 的 length() 方法
@Value("#{'hello'.length()}")
private int strLength;
// 条件判断(结果为 true)
@Value("#{2 > 1 && 'a' == 'a'}")
private boolean condition;
五、AOP(面向切面编程)
AOP 用于解决跨多个对象的通用逻辑(如日志、事务、权限校验),核心概念:
切面(Aspect):封装通用逻辑的类(如 LogAspect)。
切点(Pointcut):定义哪些方法需要被拦截(如 execution(* com.example.service.*.*(..)))。
通知(Advice):通用逻辑的执行时机(前置 / 后置 / 环绕 / 异常 / 最终通知)。
1. XML 配置 AOP(传统方式)
通过 XML 定义切面、切点和通知。
xml:
<!-- 配置目标对象 --> 注册目标为bean
<bean id="userService" class="com.example.service.UserService"/>
<!-- 配置切面类 -->注册切面类为bean
<bean id="logAspect" class="com.example.aspect.LogAspect"/>
<!-- 配置 AOP -->
<aop:config>
<aop:aspect ref="logAspect">
<!-- 定义切点(拦截 UserService 的所有方法*) -->
<aop:pointcut id="userServicePointcut"
expression="execution(* com.example.service.UserService.*(..))"/>
//这里的excution语法:(语法:execution(返回值类型 包名.类名.方法名(参数)))
<!-- 前置通知 -->
<aop:before method="beforeLog" pointcut-ref="userServicePointcut"/>
</aop:aspect>
</aop:config>
目标类:
UserService 是被 AOP 拦截的业务类,包含需要被监控的方法(如 addUser、getUser)。
public class UserService {
// 方法 1:添加用户(带参数)
public void addUser(String username, int age) {
System.out.println("执行 addUser:添加用户 " + username + "(年龄:" + age + ")");
}
// 方法 2:获取用户(带返回值)
public String getUser(Long id) {
System.out.println("执行 getUser:查询用户 ID=" + id);
return "用户_" + id; // 模拟返回用户信息
}
}
//切面类:
public class LogAspect {
// 前置通知:在 UserService 的方法执行前调用
public void beforeLog(JoinPoint joinPoint) {
// 获取目标方法的信息
Signature signature = joinPoint.getSignature(); // 方法签名(包含类名、方法名)
String className = signature.getDeclaringTypeName(); // 目标类的完整类名(如 com.example.service.UserService)
String methodName = signature.getName(); // 方法名(如 addUser、getUser)
Object[] args = joinPoint.getArgs(); // 方法参数数组
// 打印前置日志
System.out.println("【前置通知】准备执行方法:" + className + "." + methodName);
System.out.println("【前置通知】方法参数:" + String.join(", ", parseArgs(args)));
}
// 辅助方法:解析参数数组(转为字符串)
private String[] parseArgs(Object[] args) {
if (args == null || args.length == 0) {
return new String[]{"无参数"};
}
String[] argStrs = new String[args.length];
for (int i = 0; i < args.length; i++) {
argStrs[i] = args[i] == null ? "null" : args[i].toString();
}
return argStrs;
}
}
2. 接口实现 AOP(基于 MethodInterceptor)
通过实现 MethodInterceptor 接口定义环绕通知(Spring 早期方式)。
// 自定义拦截器public class PerformanceInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object result = invocation.proceed(); // 执行目标方法
long cost = System.currentTimeMillis() - start;
System.out.println("方法执行耗时:" + cost + "ms");
return result;
}}
// 配置拦截器(XML)
<aop:config>
<aop:pointcut id="servicePointcut"
expression="execution(* com.example.service.*.*(..))"/>
<aop:advisor advice-ref="performanceInterceptor" pointcut-ref="servicePointcut"/>
</aop:config>
3. 注解实现 AOP(推荐方式)****
注解实现 AOP 是 Spring 6 推荐的主流方式,通过 @Aspect 注解 + 切点表达式 + 通知注解 简化配置,无需 XML 即可完成切面逻辑。以下从核心注解、切点表达式、通知类型、参数传递到实际案例,详细拆解实现过程。
First:
切面类的定义:@Aspect + @Component
@Aspect:声明当前类是一个 切面类(封装通用逻辑)。
@Component:将切面类注册到 Spring 容器(必须,否则 Spring 无法扫描到切面)。
(component多用于自己的项目,不好导入外部的一些依赖)
@Aspect // 声明这是一个切面类
@Component // 注册到 Spring 容器(否则无法被扫描)
public class LogAspect {
// 切面逻辑写在这里
}
Second:
切点定义:@Pointcut
@Pointcut 用于定义可复用的切点表达式,避免重复编写相同的匹配逻辑。
语法:@Pointcut("切点表达式") + 一个无参数、无返回值的方法(方法名即切点的 “别名”)。
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {
// 方法体为空,仅作为切点的“命名”
}
切点表达式的详细介绍:
表达式类型 | 说明 | 示例 |
execution() | 最常用,匹配方法执行(语法:execution(返回值类型 包名.类名.方法名(参数))) | execution(* com.example.service.UserService.*(..)):拦截 UserService 所有方法 |
within() | 匹配类范围(仅拦截指定类或包下的方法) | within(com.example.service.*):拦截 service 包下所有类的方法 |
this() | 匹配当前代理对象的类型(基于 JDK 动态代理) | this(com.example.service.UserService):代理对象是 UserService 类型 |
target() | 匹配目标对象的类型(基于 CGLIB 代理) | target(com.example.service.UserService):目标对象是 UserService 类型 |
args() | 匹配方法参数类型 | args(String, Integer):拦截参数为 String 和 Integer 的方法 |
@annotation() | 匹配方法上有指定注解的情况 | @annotation(com.example.annotation.Log):拦截有 @Log 注解的方法 |
Thrid具体的通知类型:
1. 前置通知 @Before
在目标方法执行前执行(无法阻止目标方法执行,除非抛出异常)。
参数:可通过 JoinPoint 获取目标方法的信息(如方法名、参数)。
@Before("servicePointcut()") // 使用已定义的切点
public void beforeLog(JoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getSimpleName(); // 目标类名
String methodName = joinPoint.getSignature().getName(); // 方法名
Object[] args = joinPoint.getArgs(); // 方法参数
System.out.printf("[前置通知] 类=%s,方法=%s,参数=%s\n", className, methodName, Arrays.toString(args));
}
2.后置通知 @After
在目标方法执行后执行(无论是否抛出异常,都会执行,类似 finally)。
@After("servicePointcut()")
public void afterLog() {
System.out.println("[后置通知] 方法执行完毕(无论是否异常)");
}
3. 返回后通知 @AfterReturning
在目标方法成功返回后执行(若方法抛出异常则不执行)。
参数:通过 returning 属性指定变量名,获取方法的返回值。
@AfterReturning(value = "servicePointcut()", returning = "result")
public void afterReturningLog(Object result) {
System.out.printf("[返回后通知] 方法返回值=%s\n", result);
}
4. 异常后通知 @AfterThrowing
在目标方法抛出异常后执行(仅当方法抛出异常时触发)。
参数:通过 throwing 属性指定变量名,获取异常对象。
@AfterThrowing(value = "servicePointcut()", throwing = "ex")
public void afterThrowingLog(Exception ex) {
System.out.printf("[异常后通知] 方法抛出异常=%s\n", ex.getMessage());
}
5. 环绕通知 @Around(最强大)******
一、@Around 的核心机制
1. 关键参数:ProceedingJoinPoint
@Around 的通知方法必须接收一个 ProceedingJoinPoint 类型的参数(JoinPoint 的子接口),它提供了两个核心能力:
proceed():触发目标方法的执行(类似 “开关”,调用它才会执行目标方法)。
getArgs()/setArgs():获取或修改目标方法的入参。
2. 执行流程
@Around 的逻辑分为两部分:
前置逻辑:在 proceed() 调用前执行(类似 @Before)。
后置逻辑:在 proceed() 调用后执行(类似 @AfterReturning)。
若目标方法抛出异常,异常会在 proceed() 调用时抛出,可在 @Around 中捕获处理。
示例:从简单到进阶
示例 1:基础用法(记录方法耗时)
最常见的场景是监控方法执行时间,通过 @Around 可以轻松实现:
@Aspect
@Component
public class PerformanceAspect {
// 定义切点:拦截所有 Service 方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {}
@Around("servicePointcut()")
public Object logTime(ProceedingJoinPoint pjp) throws Throwable {
// 前置逻辑:记录开始时间
long startTime = System.currentTimeMillis();
// 触发目标方法执行(必须调用 proceed(),否则目标方法不会执行)
Object result = pjp.proceed();
// 后置逻辑:计算耗时并打印
long cost = System.currentTimeMillis() - startTime;
String methodName = pjp.getSignature().getName(); // 方法名
System.out.printf("方法 %s 执行耗时:%dms\n", methodName, cost);
return result; // 返回目标方法的结果(可修改返回值)
}
}
效果:调用任意 Service 方法时,控制台会输出该方法的执行耗时。
示例 2:修改方法参数(参数校验)
若目标方法需要校验参数(如禁止传入空值),可通过 @Around 修改或拦截:
@Aspect
@Component
public class ParamCheckAspect {
@Pointcut("execution(* com.example.service.UserService.addUser(..))")
public void addUserPointcut() {}
@Around("addUserPointcut()")
public Object checkParams(ProceedingJoinPoint pjp) throws Throwable {
// 获取原始参数(假设目标方法是 addUser(User user))
Object[] args = pjp.getArgs();
User user = (User) args[0];
// 校验参数:姓名不能为空
if (user.getName() == null || user.getName().trim().isEmpty()) {
throw new IllegalArgumentException("用户姓名不能为空");
}
// 可选:修改参数(例如自动补全默认值)
if (user.getAge() == null) {
user.setAge(18); // 年龄默认 18 岁
args[0] = user; // 更新参数数组
}
// 触发目标方法(使用修改后的参数)
return pjp.proceed(args);
}
}
效果:调用 addUser 时,若姓名为空则抛出异常;若年龄未传则自动补全为 18 岁。
示例 3:修改返回值(数据脱敏)
若目标方法返回敏感数据(如用户手机号),可通过 @Around 对返回值脱敏:
@Aspect
@Component
public class DataMaskAspect {
@Pointcut("execution(* com.example.service.UserService.getUser(..))")
public void getUserPointcut() {}
@Around("getUserPointcut()")
public Object maskSensitiveData(ProceedingJoinPoint pjp) throws Throwable {
// 执行目标方法,获取原始返回值
User user = (User) pjp.proceed();
// 对手机号脱敏(138****1234)
if (user.getPhone() != null) {
String maskedPhone =
user.getPhone().replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
//对中间4个数字隐藏
user.setPhone(maskedPhone);
}
return user; // 返回脱敏后的对象
}
}
效果:调用 getUser 获取用户信息时,手机号会被自动脱敏。
@Around 与其他通知的对比
通知类型 | 能否控制目标方法执行 | 能否修改参数 / 返回值 | 适用场景 |
@Around | ✅(通过 proceed()) | ✅(修改参数 / 返回值) | 复杂控制(性能监控、参数校验) |
@Before | ❌(无法阻止执行) | ❌(只能读取参数) | 简单前置逻辑(日志记录) |
@AfterReturning | ❌(已执行完毕) | ✅(仅能修改返回值) | 后置处理(返回值加工) |
@AfterThrowing | ❌(已抛出异常) | ❌ | 异常处理(日志记录) |
注意事项
1.必须调用 proceed():若 @Around 中不调用 proceed(),目标方法永远不会执行(可能导致业务逻辑缺失)。
2.异常处理:若 proceed() 抛出异常,异常会传递到 @Around 外部,需在 @Around 中捕获处理(否则上层调用会收到异常)。
@Around("...")
public Object handleException(ProceedingJoinPoint pjp) {
try {
return pjp.proceed();
} catch (Throwable e) {
System.out.println("方法执行异常:" + e.getMessage());
return null; // 或返回默认值
}
}
3.内部方法调用不生效:若 A 方法调用本类的 B 方法,B 方法的 @Around 不会触发(因为代理对象调用才会触发 AOP)。
解决方案:通过 ApplicationContext 获取代理对象,再调用 B 方法(但不推荐,可能破坏代码结构)。
若一个方法被多个通知拦截,执行顺序如下(以无异常场景为例):
@Around→ @Before → 目标方法执行 → @Around→ @AfterReturning → @After
六、Spring 与 MyBatis 整合
整合目标:通过 Spring 管理 MyBatis 的 SqlSessionFactory、Mapper 接口等,简化数据库操作。
1. 关键配置步骤(基于 Spring Boot)
添加依赖:spring-boot-starter-mybatis 和数据库驱动(如 MySQL)。
配置数据源:在 application.properties 中设置 spring.datasource.url、username、password。
配置 SqlSessionFactory:通过 @Bean 定义 SqlSessionFactoryBean,关联数据源和 MyBatis 配置(如别名包)。
扫描 Mapper 接口:使用 @MapperScan("com.example.mapper") 自动注册 Mapper 到容器。
// 配置类(Spring Boot 自动装配可简化)
@Configuration
@MapperScan("com.example.mapper") // 扫描 Mapper 接口
public class MyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
// 可选:设置 MyBatis 配置(如驼峰命名)
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setMapUnderscoreToCamelCase(true);
factoryBean.setConfiguration(configuration);
return factoryBean.getObject();
}
}
2. 使用示例
// Mapper 接口(无需实现类)
public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);
}
// Service 中直接注入 Mapper
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User getUser(Long id) {
return userMapper.selectById(id);
}
}
七、Spring 与 JUnit 整合
整合后可在测试类中直接注入 Spring 容器的 Bean,方便测试业务逻辑。
1. 配置(Spring Boot)
添加依赖:spring-boot-starter-test(包含 JUnit 5、Mockito 等)。
测试类使用 @SpringBootTest 注解,自动加载 Spring 上下文。
@SpringBootTest // 启动 Spring 容器public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testGetUser() {
User user = userService.getUser(1L);
assertNotNull(user);
assertEquals("张三", user.getName());
}
}
2. 进阶:模拟外部依赖(Mockito)
使用 @MockBean 模拟 Mapper 等外部依赖,避免真实数据库调用。
@SpringBootTestpublic class UserServiceTest {
@Autowired
private UserService userService;
@MockBean // 模拟 UserMapper
private UserMapper userMapper;
@Test
public void testGetUser() {
// 模拟 Mapper 返回数据
User mockUser = new User(1L, "张三");
when(userMapper.selectById(1L)).thenReturn(mockUser);
User user = userService.getUser(1L);
assertEquals("张三", user.getName());
}
}
add:一个类多个注册,如何确定我们需要的?
Bean是默认按照Type匹配的
一、@Qualifier:通过名称精准匹配
@Qualifier 是 Spring 提供的显式指定 Bean 名称的注解,用于在注入时明确选择目标 Bean。
1. 注册多个同类型 Bean
通过 @Bean 的 name/value 属性为每个 Bean 命名(或通过 @Component 的 value 属性)。
// 配置类:注册两个 RedisClient 实例(不同配置)
@Configuration
public class RedisConfig {
@Bean("redisMaster") // 指定 Bean 名称为 "redisMaster"
public RedisClient redisMaster() {
return new RedisClient("master.redis.com", 6379);
}
@Bean("redisSlave") // 指定 Bean 名称为 "redisSlave"
public RedisClient redisSlave() {
return new RedisClient("slave.redis.com", 6379);
}
}
2. 注入时使用 @Qualifier 指定名称
在 @Autowired 时,通过 @Qualifier("beanName") 明确选择要注入的 Bean。
@Service
public class OrderService {
// 注入名称为 "redisMaster" 的 Bean
@Autowired
@Qualifier("redisMaster")
private RedisClient redisMaster;
// 注入名称为 "redisSlave" 的 Bean
@Autowired
@Qualifier("redisSlave")
private RedisClient redisSlave;
public void syncData() {
redisMaster.set("order:1", "data"); // 使用主库
redisSlave.get("order:1"); // 使用从库
}
}
适用场景
需要明确区分不同实例(如主从数据库、不同环境配置)。
第三方类无法修改(通过 @Bean 命名)。
二、@Primary:标记主选 Bean
@Primary 用于标记一个同类型 Bean 作为默认选择,当未显式指定 @Qualifier 时,Spring 会优先注入被 @Primary 标记的 Bean。
示例:主从数据源
@Configuration
public class DataSourceConfig {
@Primary // 主数据源(默认注入)
@Bean("mainDataSource")
public DataSource mainDataSource() {
return new HikariDataSource(new HikariConfig("main-db.properties"));
}
@Bean("slaveDataSource") // 从数据源(需显式指定)
public DataSource slaveDataSource() {
return new HikariDataSource(new HikariConfig("slave-db.properties"));
}
}
注入时不指定 @Qualifier
@Service
public class UserService {
// 未指定 @Qualifier,自动注入 @Primary 标记的 mainDataSource
@Autowired
private DataSource dataSource;
public User getUser(Long id) {
return dataSource.getConnection().query("SELECT * FROM user WHERE id=?", id);
}
}
注意
若存在多个 @Primary Bean,仍会冲突(需结合 @Qualifier 解决)。
@Primary 的优先级低于 @Qualifier(即 @Qualifier 明确指定时,@Primary 失效)。
三、@Resource:JSR-250 标准的名称匹配
@Resource 是 JSR-250 标准注解( 规范),默认通过 Bean 名称 注入(若未指定 name,则通过类型匹配)。
示例:通过名称注入
@Service
public class ProductService {
// 直接通过 name 指定 Bean(等价于 @Autowired + @Qualifier)
@Resource(name = "redisMaster")
private RedisClient redisMaster;
// 未指定 name 时,通过类型匹配(若存在多个同类型 Bean 会报错)
// @Resource
// private RedisClient redisClient; // 报错!
}
与 @Autowired 的区别
特性 | @Autowired(Spring 原生) | @Resource(JSR-250 标准) |
默认匹配方式 | 类型(Type) | 名称(Name) |
支持指定名称 | 需结合 @Qualifier | 通过 name 属性直接指定 |
依赖注入阶段 | 支持构造器、字段、方法参数 | 仅支持字段和 setter 方法 |
第三方库兼容性 | 强(Spring 生态) | 弱(需依赖 JSR-250 实现) |
四、Map/List 自动注入:获取所有同类型 Bean
Spring 支持将所有同类型的 Bean 注入到 Map 或 List 中(Map 的 Key 是 Bean 名称,Value是实例,List 是所有实例)。
示例 1:Map 注入(Key 为 Bean 名称)
@Service
public class RedisManager {
// Map 的 Key 是 Bean 名称(如 "redisMaster"、"redisSlave"),Value 是实例
@Autowired
private Map<String, RedisClient> redisClients; //会自动获取
public void printAllClients() {
redisClients.forEach((name, client) -> {
System.out.println("Bean 名称:" + name + ",地址:" + client.getHost());
});
}
}
输出:
Bean 名称:redisMaster,地址:master.redis.com
Bean 名称:redisSlave,地址:slave.redis.com
示例 2:List 注入(按优先级排序)
通过 @Order 或 Ordered 接口指定 Bean 的优先级,List 会按优先级升序排列(数值越小优先级越高)。
// 定义优先级(数值越小越优先)
@Bean("redisMaster")
@Order(1)
public RedisClient redisMaster() { ... }
@Bean("redisSlave")
@Order(2)
public RedisClient redisSlave() { ... }
// 注入 List(顺序:redisMaster → redisSlave)
@Autowired
private List<RedisClient> redisClientList; //会自动获取
适用场景
需要动态选择 Bean(如根据参数从 Map 中获取对应实例)。
需要批量操作同类型 Bean(如遍历所有数据源执行健康检查)。
五、自定义 Qualifier 注解:语义化标记
通过元注解(如 @Target、@Retention)自定义 Qualifier 注解,替代硬编码的字符串,提高代码可读性和类型安全。
1. 定义自定义 Qualifier
// 元注解:标记该注解是一个 Qualifier
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier // 关键:继承 Spring 的 Qualifier 语义
public @interface RedisType {
String value(); // 支持传入参数(如 "master"、"slave")
}
2. 注册 Bean 时标记自定义 Qualifier
@Configuration
public class RedisConfig {
@Bean
@RedisType("master") // 标记为 "master" 类型
public RedisClient redisMaster() {
return new RedisClient("master.redis.com", 6379);
}
@Bean
@RedisType("slave") // 标记为 "slave" 类型
public RedisClient redisSlave() {
return new RedisClient("slave.redis.com", 6379);
}
}
3. 注入时使用自定义 Qualifier
@Service
public class OrderService {
// 通过 @RedisType("master") 注入对应 Bean
@Autowired
@RedisType("master")
private RedisClient redisMaster;
// 通过 @RedisType("slave") 注入对应 Bean
@Autowired
@RedisType("slave")
private RedisClient redisSlave;
}
优势
避免硬编码字符串(如 "redisMaster"),减少拼写错误。
语义更清晰(@RedisType("master") 比 @Qualifier("redisMaster") 更直观)。
总结:方案对比与选择
方案 | 核心机制 | 适用场景 | 优点 |
@Qualifier | 按 Bean 名称显式匹配 | 明确区分多个同类型 Bean(如主从实例) | 灵活、通用 |
@Primary | 标记默认 Bean | 大多数场景使用默认 Bean,偶尔需要其他实例 | 简化默认注入逻辑 |
@Resource | JSR-250 标准名称匹配 | 兼容 标准规范 | 与 Spring 解耦 |
Map/List 注入 | 批量获取同类型 Bean | 动态选择或批量操作 Bean(如策略模式) | 无需硬编码名称,支持动态扩展 |
自定义 Qualifier | 语义化标记 Bean 类型 | 需要类型安全和清晰语义的场景(如多环境配置) | 代码更易读、类型安全 |
建议:
常规场景优先使用 @Qualifier(灵活且通用)。
需要默认 Bean 时结合 @Primary。
动态选择或批量操作时使用 Map/List 注入。
复杂业务场景(如多类型、多环境)推荐自定义 Qualifier 注解。