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 注解。
相关文章:
Spring急速入门
Spring 是 企业级开发的一站式框架,核心是 IOC(控制反转) 和 AOP(面向切面编程) 一、Spring 核心:IOC 理论 1. 什么是 IOC? IOC(Inversion of Control,控制反转&…...
#在 CentOS 7 中手动编译安装软件操作及原理
在 CentOS 7 中,手动编译安装软件(即从源代码编译安装)是一种高度灵活的方式,适用于需要定制化软件功能、优化性能或安装官方仓库未提供的软件版本的场景。以下是针对手动编译安装的详细说明,包括原理、步骤、注意事项…...
【Kubernetes】初识基础理论(第一篇)
前言 单机容器编排: docker-compose 容器集群编排: docker swarm、mesosmarathon、kubernetes 应用编排: ansible 一、Kubernetes概述 Kubernetes 是一个可移植的、可扩展的开源平台,用于管理容器化的…...
配置集群(yarn)
在配置 YARN 集群前,要先完成以下准备工作: 集群环境规划:明确各节点的角色,如 ResourceManager、NodeManager 等。网络环境搭建:保证各个节点之间能够通过网络互通。时间同步设置:安装 NTP 服务࿰…...
按钮导航组件 | 纯血鸿蒙组件库AUI
摘要: 按钮导航组件(A_ButtonNav):可设置导航数据(含文本及路由),可设置按钮颜色、导航标题及导航子标题。 一、组件调用方式 1.1.极简调用: 用 A_ButtonNav 调用“按钮导航组件”,只需要给属性 data (导…...
自适应主从复制模拟器的构建与研究
自适应主从复制模拟器的构建与研究 摘要: 本文旨在构建一个自适应主从复制模拟器,深入研究主从复制原理及优化方法。从研究者视角出发,详细阐述模拟器的设计、实现与实验过程,通过表格、图表及代码等辅助手段,逐步探讨如何在不同网络条件和负载下,自动调整主从复制参数和…...
015枚举之滑动窗口——算法备赛
滑动窗口 最大子数组和 题目描述 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 原题链接 思路分析 见代码注解 代码 int maxSubArray(vector<int>& num…...
【Dv3Admin】工具视图配置文件解析
在开发后台管理系统时,处理复杂的 CRUD 操作是常见的需求。Django Rest Framework(DRF)通过 ModelViewSet 提供了基础的增删改查功能,但在实际应用中,往往需要扩展更多的功能,如批量操作、权限控制、查询优化等。dvadmin/utils/viewset.py 模块通过继承并扩展 ModelViewS…...
在MyBatis Plus里处理LocalDateTime类型
在MyBatis Plus里处理LocalDateTime类型 在MyBatis Plus里处理LocalDateTime类型时,你要确保数据库字段和Java实体类属性之间的类型映射是正确的。下面为你介绍处理这种情况的方法: 1. 数据库字段类型对应设置 要保证数据库字段类型和LocalDateTime相…...
编程技能:字符串函数03,strncpy
专栏导航 本节文章分别属于《Win32 学习笔记》和《MFC 学习笔记》两个专栏,故划分为两个专栏导航。读者可以自行选择前往哪个专栏。 (一)WIn32 专栏导航 上一篇:编程技能:字符串函数02,strcpy 回到目录…...
edge设置位IE模式打开网页
打开Edge浏览器->在浏览器工具栏右键->自定义工具栏->外观->选择要在工具栏上显示的按钮->找到“Internet Explorer 模式”按钮->开启,将其添加到工具栏中...
代码随想录训练营第二十二天| 101.对称二叉树 100.相同的树
101.对称二叉树: 文档讲解:代码随想录|101.对称二叉树 视频讲解:新学期要从学习二叉树开始! | LeetCode:101. 对称二叉树_哔哩哔哩_bilibili 状态:已做出 思路: 这道题目我初始做的时候想着使用…...
nvm管理node版本
To manage Node.js versions on Windows, I recommend using nvm-windows (Node Version Manager for Windows). Here’s how we can handle this: First, let’s install nvm-windows. I’ll propose a command to check if it’s already installed: nvm versionGreat! I s…...
智能手表测试计划文档(软/硬件)
📄 智能手表测试计划文档(软/硬件) 项目名称:Aurora Watch S1 文档编号:AW-S1-QA-TP-001 编制日期:2025-xx-xx 版本:V1.0 编写人:xxx(测试主管) 一、测试目标…...
基于大模型的原发性醛固酮增多症全流程预测与诊疗方案研究
目录 一、引言 1.1 研究背景与意义 1.2 国内外研究现状 1.3 研究目的与方法 二、原发性醛固酮增多症概述 2.1 疾病定义与发病机制 2.2 临床表现与诊断标准 2.3 流行病学特征 三、大模型预测原理与技术 3.1 大模型简介 3.2 预测原理与算法 3.3 数据收集与预处理 四…...
spring中的@Lazy注解详解
一、核心功能与作用 Lazy 注解是 Spring 框架中用于延迟 Bean 初始化的核心工具,通过将 Bean 的创建推迟到首次使用时,优化资源利用和启动性能。其核心功能包括: 延迟初始化 默认情况下,Spring 在容器启动时立即初始化所有单例 …...
Docker快速入门与应用
1. 什么是 Docker? Docker 就像一个“魔法箱子”,可以把你开发的应用(代码、环境、配置)打包成一个标准化的容器,这个容器可以在任何支持 Docker 的系统上运行,无需担心环境差异导致的问题。 类比…...
判断一个数是不是素数的最高效的算法
判断一个数是否是素数,有从简单到复杂多种方法。最高效的算法取决于输入规模(是几个亿以内的数,还是上百位的大整数),我会按实用场景分类讲解: ✅ 常规范围内(比如 ≤ 1e12)判断素数…...
《Head First 设计模式》第一章 - 笔记
本书是本人写的设计模式的笔记,写下核心要点,如果你掌握过设计模式,想快速阅读本书内容,这个笔记适合你阅读。如果你是新手,有 java 基础和 oo 设计原则基础,你适合跟我一样从零阅读本书。 第一章 策略模式…...
GPT系列:自然语言处理的演进与多模态的探索
GPT系列:自然语言处理的演进与多模态的探索 GPT系列的发展一、GPT-1 :通过生成式的预训练改进自然语言GPT-1的动机做一个预训练模型的难点GPT-1的微调模式GPT-1的训练数据Bert 二、GPT-2语言模型是非监督的GPT-2的动机引入promptGPT-2模型架构的改变GPT-…...
Linux驱动:驱动编译流程了解
要求 1、开发板中的linux的zImage必须是自己编译的 2、内核源码树,其实就是一个经过了配置编译之后的内核源码。 3、nfs挂载的rootfs,主机ubuntu中必须搭建一个nfs服务器。 内核源码树 解压 tar -jxvf x210kernel.tar.bz2 编译 make x210ii_qt_defconfigmakeCan’t use ‘…...
【MySQL】数据库基础
目录 1.什么是数据库2.见一见数据库3.服务器、表、库之间的关系4.MySQL架构5.sql语句分类6.查看MySQL存储引擎6.1 查看存储引擎6.2 常见存储引擎对比 1.什么是数据库 概念:数据库一般是指,在磁盘或者内存中存储的特定结构组织的数据 – 将来在磁盘上存储…...
1.1 文章简介
前因后果链 行业需求 → 技能断层 → 课程设计响应 (高薪岗位要求数学基础) → (符号/公式理解困难) → (聚焦原理与应用) 行业驱动因素 • 前因:机器学习/AI等领域的高薪岗位激增,但数学能力成为主要门槛 • 关键矛盾:算法论文中的数学…...
laravel 中使用的pdf 扩展包 laravel-snappy(已解决中文乱码)
Centos7 安装 wkhtmltopdf 1、先查看系统是 32 位的还是 64 位的 uname -a2、通过 composer 安装 wkhtmltopdf 32位: $ composer require h4cc / wkhtmltopdf-i386 0.12.x $ composer require h4cc / wkhtmltoimage-i386 0.12.x 64位: $ composer require h4cc/wkhtmltopdf-…...
java反序列化commons-collections链6
cc链6,最好用的cc链,因为它不受jdk版本的限制和cc版本的限制,前半段很像urldns链,后半段是cc1链 先来看一下它的利用链 Gadget chain:java.io.ObjectInputStream.readObject()java.util.HashSet.readObject()java.util.HashMap.p…...
WebSocket的原理及QT示例
一.WebSocket 介绍 1.概述 WebSocket 是一种在单个 TCP 连接上进行全双工通讯的协议,它在 2011 年被 IETF 定为标准 RFC 6455,并由 RFC7936 补充规范。与传统的 HTTP 协议不同,WebSocket 允许服务器和客户端之间进行实时、双向的数据传输&a…...
css 点击后改变样式
背景: 期望实现效果:鼠标点击之后,保持选中样式。 实现思路:在css样式中,:active 是一种伪类,用于表示用户当前正在与被选定的元素进行交互。当用户点击或按住鼠标时,元素将被激活,此…...
AI 在模仿历史语言方面面临挑战:大型语言模型在生成历史风格文本时的困境与研究进展
概述 在当今数字化时代,人工智能(AI)技术在诸多领域展现出了强大的能力,但在处理历史语言这一特定任务时,却遭遇了不小的挑战。美国和加拿大的研究人员通过合作发现,像 ChatGPT 这样的大型语言模型&#x…...
C++.Windows图形
Windows图形 1. 基础知识1.1 Windows图形编程基础1.2 GDI与GDI1.3 窗口消息处理2.1 注册窗口类2.2 创建窗口2.3 显示窗口3.1 创建按钮3.2 按钮消息处理4.1 设置窗口透明度4.2 透明窗口示例5.1 使用区域创建异形窗口5.2 异形窗口示例6.1 GDI抗锯齿设置6.2 抗锯齿绘图示例7.1 Dir…...
【Vue3】使用vite创建Vue3工程、Vue3基本语法讲解
一、什么是Vite Vite是新一代前端构建工具,官网地址:Vite中文网,vite的优势如下: 轻量快速的热重载(HMR),能实现极速的服务启动对TypeScript、JSX、CSS等支持开箱即用真正的按需编译ÿ…...
专题二:二叉树的深度优先搜索
以leetcode2331题为例 题目分析: 以第一个示例为例 算法原理分析: 从宏观角度,也就是我的算法之回溯的第一篇 我们发现我们在研究示例的时候,必须从下往上推 也就是我在研究一个结点是true还是false的时候,必须…...
Termius ssh连接服务器 vim打开的文件无法复制问题
你的问题是: • 在 Termius (macOS) SSH 连接到 VMware Ubuntu,使用 vim 打开 .cpp 文件时,可以复制文本; • 但在 Windows 10 上 SSH 到 VMware 的 Red Hat 6.4 时,复制操作无效。 ⸻ 🎯 初步分析 复制…...
搭建大数据学习的平台
一、基础环境准备 1. 硬件配置 物理机:建议 16GB 内存以上,500GB 硬盘,多核 CPU虚拟机:至少 3 台(1 主 2 从),每台 4GB 内存,50GB 硬盘 2. 操作系统 Ubuntu 20.04 LTS 或 CentOS…...
Matlab 模糊控制节水洗衣机模型
1、内容简介 Matlab 232-模糊控制节水洗衣机模型 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略...
如何找正常运行虚拟机
1.新建虚拟机。Linux centos7,给虚拟机改个名字不要放在c盘 2.安装操作系统。cd/dvd->2009.iso 启动虚拟机...
python二手书交易管理系统
目录 技术栈介绍具体实现截图系统设计研究方法:设计步骤设计流程核心代码部分展示研究方法详细视频演示试验方案论文大纲源码获取/详细视频演示 技术栈介绍 Django-SpringBoot-php-Node.js-flask 本课题的研究方法和研究步骤基本合理,难度适中…...
使用本地部署的 LLaMA 3 模型进行中文对话生成
以下程序调用本地部署的 LLaMA3 模型进行多轮对话生成,通过 Hugging Face Transformers API 加载、预处理、生成并输出最终回答。 程序用的是 Chat 模型格式(如 LLaMA3 Instruct 模型),遵循 ChatML 模板,并使用 apply…...
C++编程练习,认识面向对象权限,如何进行封装
#include <iostream> #include <string> using namespace std; /* 银行的账户是一个模板,是一个类,有存款人信息和账户额度,而具体的存款人视为一个对象, 一个对象不能私自修改账户额度,需要通过一个操作流…...
A Survey of Learning from Rewards:从训练到应用的全面剖析
A Survey of Learning from Rewards:从训练到应用的全面剖析 你知道大语言模型(LLMs)如何通过奖励学习变得更智能吗?这篇论文将带你深入探索。从克服预训练局限的新范式,到训练、推理各阶段的策略,再到广泛…...
电脑端音乐播放器推荐:提升你的听歌体验!
在快节奏的职场环境中,许多上班族都喜欢用音乐为工作时光增添色彩。今天要分享的这款音乐工具,或许能为你的办公时光带来意想不到的惊喜。 一、软件介绍-澎湃 澎湃音乐看似是个普通的播放器,实则藏着强大的资源整合能力。左侧功能栏清晰陈列着…...
小刚说C语言刷题—1149 - 回文数个数
1.题目描述 一个正整数,正读和反读都相同的数为回文数。 例如 22, 131, 2442 , 37073, 66,…… 所有 11位数都是回文数。 给出一个正整数 n ( 1≤n≤10000 ),求出 1,2…...
基于SpringBoot的博客系统测试报告
一、编写目的 本报告为博客系统测试报告,本项目模拟了csdn,实现了包括了用户登录,发布博客文章,查看博客等功能。 二、项目背景 博客系统采用前后端分离的方法来实现,同时使用了数据库来存储相关的数据,…...
Koa知识框架
一、核心概念 1. 基本特点 由 Express 原班人马开发的下一代 Node.js Web 框架 基于中间件的洋葱圈模型 轻量级核心(仅约 600 行代码) 完全使用 async/await 异步流程控制 没有内置任何中间件,高度可定制 2. 核心对象 Application (Ko…...
React Native踩坑实录:解决NativeBase Radio组件在Android上的兼容性问题
React Native踩坑实录:解决NativeBase Radio组件在Android上的兼容性问题 问题背景 在最近的React Native项目开发中,我们的应用在iOS设备上运行良好,但当部署到Android设备时,进入语言设置和隐私设置页面后应用崩溃。我们遇到了…...
RCE联系
过滤 绕过空格 ● 进制绕过 题目练习 数字rce 使用$0执行bash,<<<将后面的字符串传递给左边的命令。 例如: <?php highlight_file(__FILE__); function waf($cmd) { $whiteList [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, \\, \, $, <]; $cmd_ch…...
区块链大纲笔记
中心化出现的原因是由于网络的形成(不然就孤立了,这显然不符合现实,如,社会,计算机网路),接着由于网络中结点能力一般不对等同时为了便于管理等一系列问题,导致中心化网络的出现。&a…...
SQL:JOIN 进阶
目录 JOIN 是什么? 🔹OUTER JOIN(外连接) 外连接的分类 外连接与内连接的区别 🔹USING 子句 语法结构 和 ON 的对比 📘USING 的内部逻辑 🧩 多个字段的 USING USING 的 SELECT 特性&a…...
SATA—Link层状态机
一、概述 Link层的状态机大致可以分为五类: 链路层空闲状态机通信异常处理状态机链路层发送状态机链路层接收状态机链路层电源管理下的状态机 二、链路层空闲状态机 链路层空闲状态机共包含两个状态L_IDLE、L_SyncEscape,每个状态下的处理机制与条状…...
12.2.2 allocator类
allocator类将分配内存空间、调用构造函数、调用析构函数、释放内存空间这4部分操作分开,全部交给程序员来执行,不像new和delete #include <iostream> #include <string>int main() {const int n 10;std::allocator<std::string> al…...
Qwen:Qwen3,R1 在 Text2SQL 效果评估
【对比模型】 Qwen3 235B-A22B(2350亿总参数,220亿激活参数),32B,30B-A3B;QwQ 32B(推理模型)DeepSeek-R1 671B(满血版)(推理模型) 1&a…...