3,IOC/DI注解开发
Spring的IOC/DI对应的配置开发就已经讲解完成,但是使用起来相对来说还是比较复杂的,复杂的地方在==配置文件==。
前面咱们聊Spring的时候说过,Spring可以简化代码的开发,到现在并没有体会到。
所以Spring到底是如何简化代码开发的呢?
要想真正简化开发,就需要用到Spring的注解开发,Spring对注解支持的版本历程:
-
2.0版开始支持注解
-
2.5版注解功能趋于完善
-
3.0版支持纯注解开发
关于注解开发,我们会讲解两块内容注解开发定义bean
和纯注解开发
。
注解开发定义bean用的是2.5版提供的注解,纯注解开发用的是3.0版提供的注解。
3.1 环境准备
在学习注解开发之前,先来准备下案例环境:
-
创建一个Maven项目
-
pom.xml添加Spring的依赖
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency> </dependencies>
-
resources下添加applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/> </beans>
-
添加BookDao、BookDaoImpl、BookService、BookServiceImpl类
public interface BookDao { public void save(); } public class BookDaoImpl implements BookDao { public void save() { System.out.println("book dao save ..." ); } } public interface BookService { public void save(); } public class BookServiceImpl implements BookService { public void save() { System.out.println("book service save ..."); } }
-
创建运行类App
public class App { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); BookDao bookDao = (BookDao) ctx.getBean("bookDao"); bookDao.save(); } }
最终创建好的项目结构如下:
🧠 理论理解
环境准备是注解开发的前置步骤,主要包含三部分:
1️⃣ 添加spring-context
依赖,核心是提供注解驱动的上下文功能。
2️⃣ 编写基础接口与实现类,形成标准的面向接口编程结构,符合IOC思想。
3️⃣ XML中完成最小配置,便于后续迁移到注解模式。
这一步核心是:建立可运行的IOC基础环境,并保证包路径与Bean命名合理,方便后续包扫描。
🏢 企业实战理解
-
字节跳动:内部所有Spring微服务项目强制建立清晰的包结构(如 dao、service、controller),并在环境搭建阶段就注重模块解耦,方便后续注解扫描。
-
阿里巴巴:在微服务场景下,通过标准Maven依赖管理+统一配置模板快速搭建基础环境,并使用
spring-boot-starter
等starter化包降低手工配置成本。 -
Google & OpenAI:云微服务体系下,初始环境依赖k8s自动注入配置,虽然用到Spring的地方较少,但环境依旧遵循“自动化+结构化”设计理念,强调工程标准。
字节跳动面试题
Q: Spring 项目中如何保证 IOC 环境准备的标准化?请说明包结构的设计原则及其在注解扫描中的作用。
A: 在大厂项目中(如字节跳动),Spring 项目的包结构要遵循分层清晰(如 controller、service、dao)原则。这样做的原因是 @ComponentScan
默认支持包及子包扫描,如果包结构混乱会导致扫描到非预期的类,进而引发 Bean 冲突或加载错误。字节跳动还会结合内部自动化工具校验包结构是否符合扫描规范,确保后续注解管理的准确性。
场景题:字节跳动 - 微服务模块初始化问题
▶️ 场景描述:
你在字节跳动负责新上线的内容推荐微服务,使用 Spring 框架。一次上线后,服务启动时未能扫描到部分 DAO 层 Bean,导致接口调用直接报错 500。排查后发现,Spring 注解开发环境配置正常,依赖版本也没问题。
请分析这个场景的潜在原因及解决方案。
✔️ 答案:
潜在原因是 @ComponentScan
的包扫描路径设置不完整/不精确,导致部分 DAO 实现类未被扫描到(如 DAO 被放在 com.bytedance.xxx.infrastructure
下,但配置只扫描了 com.bytedance.xxx.service
)。
解决方案:
1️⃣ 确保 @ComponentScan
指定的包路径能覆盖 DAO、Service、Controller 全层,推荐扫描根包 com.bytedance.xxx
。
2️⃣ 建议在 CI/CD 阶段,结合内部代码扫描器自动校验 Spring Bean 是否正常加载。
3️⃣ 本地调试时可通过 ctx.getBeanDefinitionNames()
打印全部 Bean 检查是否加载成功。
3.2 注解开发定义bean
在上述环境的基础上,我们来学一学Spring是如何通过注解实现bean的定义开发?
步骤1:删除原XML配置
将配置文件中的<bean>
标签删除掉
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
步骤2:Dao上添加注解
在BookDaoImpl类上添加@Component
注解
@Component("bookDao")
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ..." );
}
}
==注意:@Component注解不可以添加在接口上,因为接口是无法创建对象的。==
XML与注解配置的对应关系:
步骤3:配置Spring的注解包扫描
为了让Spring框架能够扫描到写在类上的注解,需要在配置文件上进行包扫描
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<context:component-scan base-package="com.itheima"/>
</beans>
说明:
component-scan
-
component:组件,Spring将管理的bean视作自己的一个组件
-
scan:扫描
base-package指定Spring框架扫描的包路径,它会扫描指定包及其子包中的所有类上的注解。
-
包路径越多[如:com.itheima.dao.impl],扫描的范围越小速度越快
-
包路径越少[如:com.itheima],扫描的范围越大速度越慢
-
一般扫描到项目的组织名称即Maven的groupId下[如:com.itheima]即可。
步骤4:运行程序
运行App
类查看打印结果
步骤5:Service上添加注解
在BookServiceImpl类上也添加@Component
交给Spring框架管理
@Component
public class BookServiceImpl implements BookService {
private BookDao bookDao;
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}
步骤6:运行程序
在App类中,从IOC容器中获取BookServiceImpl对应的bean对象,打印
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
System.out.println(bookDao);
//按类型获取bean
BookService bookService = ctx.getBean(BookService.class);
System.out.println(bookService);
}
}
打印观察结果,两个bean对象都已经打印到控制台
说明:
-
BookServiceImpl类没有起名称,所以在App中是按照类型来获取bean对象
-
@Component注解如果不起名称,会有一个默认值就是
当前类名首字母小写
,所以也可以按照名称获取,如BookService bookService = (BookService)ctx.getBean("bookServiceImpl"); System.out.println(bookService);
对于@Component注解,还衍生出了其他三个注解@Controller
、@Service
、@Repository
通过查看源码会发现:
这三个注解和@Component注解的作用是一样的,为什么要衍生出这三个呢?
方便我们后期在编写类的时候能很好的区分出这个类是属于表现层
、业务层
还是数据层
的类。
知识点1:@Component等
名称 | @Component/@Controller/@Service/@Repository |
---|---|
类型 | 类注解 |
位置 | 类定义上方 |
作用 | 设置该类为spring管理的bean |
属性 | value(默认):定义bean的id |
🧠 理论理解
@Component
等注解是Spring提供的“元注解”,本质上是把类标记为IOC容器可管理的Bean。它简化了XML配置中大量的<bean>
声明,通过包扫描机制,让容器自动检测、注册对象。
另外,@Controller
、@Service
、@Repository
等注解虽功能与@Component
一样,但语义区分了层级架构,便于团队协作和可维护性。
🏢 企业实战理解
-
美团点评:服务注册中心中采用
@ComponentScan
机制批量注册微服务Bean,结合内部插件化框架做动态Bean扩展。 -
字节跳动:大规模代码库利用
@Service
、@Repository
注解加上内部代码扫描工具,自动生成依赖关系图,帮助开发者理解系统架构。 -
OpenAI:内部部分API服务在早期用Spring Boot开发,
@ComponentScan
机制结合自定义条件注解实现灰度发布和动态装载模块。
阿里巴巴面试题
Q: 你如何理解 @Component
与 @Service
等注解的区别?在什么场景下会选择自定义注解组合?
A: @Component
是一个通用注解,而 @Service
、@Controller
、@Repository
都是它的语义化衍生。阿里内部为了更好区分层次,经常使用这些语义化注解。自定义注解组合(比如 @MyService
包含 @Service + @Transactional
)在阿里用来做 业务层切面增强,可以减少重复代码,并在某些特殊场景下结合元注解实现动态功能扩展。
腾讯面试题
Q: 注解开发中,Spring 如何处理包扫描路径不一致的情况?出现冲突时如何排查?
A: Spring 会扫描 @ComponentScan
指定的路径及其所有子包。如果路径不规范(如路径漏写或写错),就会导致扫描不到 Bean 或重复注册 Bean。腾讯的经验是:1️⃣ 使用全局配置类统一定义包扫描路径;2️⃣ 结合内部扫描检测工具实时校验包结构完整性;3️⃣ 出现冲突时用 ApplicationContext#getBeanDefinitionNames()
打印已加载的 Bean,结合日志定位重复或漏加载的类。
场景题:阿里云 - 多模块应用中 Bean 冲突
▶️ 场景描述:
阿里云某项目为 SaaS 应用,采用模块化架构,每个租户模块都定义了自己的 @Service
Bean,如 UserServiceImpl
。由于所有模块共用一个 @ComponentScan("com.aliyun.saas")
,导致多个模块的 UserServiceImpl
同时被扫描注册,启动时报出 NoUniqueBeanDefinitionException
。
你会如何优雅解决这个问题?
✔️ 答案:
此问题是注解 Bean 命名冲突引发的,模块化架构下,每个模块应确保 Bean 唯一性。
解决方案:
1️⃣ 为不同模块的 Service 提供唯一的 Bean 名称,如:
@Service("tenantAUserService")
public class TenantAUserServiceImpl implements UserService { ... }
@Service("tenantBUserService")
public class TenantBUserServiceImpl implements UserService { ... }
2️⃣ 在依赖注入时结合 @Qualifier
指定注入哪一个实现,避免类型冲突。
3️⃣ 长期方案:阿里内部会用分模块扫描机制,比如:
@ComponentScan(basePackages = {"com.aliyun.saas.tenantA"})
实现模块隔离扫描,让每个模块只加载自己需要的 Bean,提升解耦和可维护性。
3.2 纯注解开发模式
上面已经可以使用注解来配置bean,但是依然有用到配置文件,在配置文件中对包进行了扫描,Spring在3.0版已经支持纯注解开发
-
Spring3.0开启了纯注解开发模式,使用Java类替代配置文件,开启了Spring快速开发赛道
具体如何实现?
3.2.1 思路分析
实现思路为:
-
将配置文件applicationContext.xml删除掉,使用类来替换。
3.2.2 实现步骤
步骤1:创建配置类
创建一个配置类SpringConfig
public class SpringConfig {
}
步骤2:标识该类为配置类
在配置类上添加@Configuration
注解,将其标识为一个配置类,替换applicationContext.xml
@Configuration
public class SpringConfig {
}
步骤3:用注解替换包扫描配置
在配置类上添加包扫描注解@ComponentScan
替换<context:component-scan base-package=""/>
@Configuration
@ComponentScan("com.itheima")
public class SpringConfig {
}
步骤4:创建运行类并执行
创建一个新的运行类AppForAnnotation
public class AppForAnnotation {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
System.out.println(bookDao);
BookService bookService = ctx.getBean(BookService.class);
System.out.println(bookService);
}
}
运行AppForAnnotation,可以看到两个对象依然被获取成功
至此,纯注解开发的方式就已经完成了,主要内容包括:
-
Java类替换Spring核心配置文件
-
@Configuration注解用于设定当前类为配置类
-
@ComponentScan注解用于设定扫描路径,此注解只能添加一次,多个数据请用数组格式
@ComponentScan({com.itheima.service","com.itheima.dao"})
-
读取Spring核心配置文件初始化容器对象切换为读取Java配置类初始化容器对象
//加载配置文件初始化容器 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); //加载配置类初始化容器 ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
知识点1:@Configuration
名称 | @Configuration |
---|---|
类型 | 类注解 |
位置 | 类定义上方 |
作用 | 设置该类为spring配置类 |
属性 | value(默认):定义bean的id |
知识点2:@ComponentScan
名称 | @ComponentScan |
---|---|
类型 | 类注解 |
位置 | 类定义上方 |
作用 | 设置spring配置类扫描路径,用于加载使用注解格式定义的bean |
属性 | value(默认):扫描路径,此路径可以逐层向下扫描 |
小结:
这一节重点掌握的是使用注解完成Spring的bean管理,需要掌握的内容为:
-
记住@Component、@Controller、@Service、@Repository这四个注解
-
applicationContext.xml中
<context:component-san/>
的作用是指定扫描包路径,注解为@ComponentScan -
@Configuration标识该类为配置类,使用类替换applicationContext.xml文件
-
ClassPathXmlApplicationContext是加载XML配置文件
-
AnnotationConfigApplicationContext是加载配置类
🧠 理论理解
纯注解模式的核心是:
1️⃣ 用@Configuration
取代XML配置文件(本质是一个带元数据的配置类)。
2️⃣ 用@ComponentScan
实现包扫描。
3️⃣ 用AnnotationConfigApplicationContext
启动容器,直接加载配置类。
这种模式完全告别了XML,符合现代**“零XML”理念**,便于DevOps环境快速交付。
🏢 企业实战理解
-
阿里云:PaaS服务内部大量微服务采用纯注解开发,结合Spring Boot的自动配置能力,实现“开箱即用”效果。
-
字节跳动:抖音后台通过纯注解开发+YAML配置,实现配置中心的无缝切换。
-
谷歌 GCP:在内部Java微服务框架中,也提倡“零XML”理念,虽然不直接用Spring,但其思想类似,采用注解+自动装配的组合方案。
美团点评面试题
Q: 纯注解开发中,@Configuration
与 @Component
的本质区别是什么?为什么推荐在配置类中使用 @Configuration
而不是 @Component
?
A: @Configuration
是 @Component
的扩展,除具备组件扫描能力外,还支持 CGLIB 动态代理增强,保证 @Bean
方法之间的依赖能够被 Spring 管理。美团曾踩过坑:开发者用 @Component
定义配置类导致 @Bean
方法失效,原因是 Spring 不会增强普通 @Component
类中的方法。美团因此强制在配置类上必须使用 @Configuration
以保证容器行为一致。
字节跳动面试题
Q: AnnotationConfigApplicationContext
与 ClassPathXmlApplicationContext
有什么底层机制上的不同?生产环境如何平滑迁移?
A: 前者基于注解扫描+Java配置类,后者基于 XML 文件解析。字节的经验是:在平滑迁移时,推荐分阶段——1️⃣ 先把 XML 与注解共存运行;2️⃣ 再逐步用注解替代 XML 中的配置,保证可回滚;3️⃣ 完全迁移后,弃用 XML 配置。字节的微服务化改造中就是采用这种策略,将十余年的老项目顺利切换至纯注解模式。
场景题:美团外卖 - XML 迁移到纯注解踩坑
▶️ 场景描述:
你参与美团外卖的 Spring 应用改造工作,目标是将原来的 XML 配置迁移到纯注解开发模式。迁移完成后,启动时发现 @Bean
方法之间的依赖未被正确注入,导致业务逻辑异常。
请解释原因及最佳实践。
✔️ 答案:
原因是开发者在配置类上错误使用了 @Component
而不是 @Configuration
。
虽然 @Component
也能让配置类被扫描,但它不会启用 Spring 的 CGLIB 动态代理机制,导致 @Bean
方法间依赖失效,每次调用 @Bean
方法都会创建新的对象,破坏单例性质。
最佳实践:
-
配置类必须用
@Configuration
标注,确保方法间依赖可被 Spring 管理。 -
美团内部强制扫描工具校验,任何配置类都不允许只用
@Component
。 -
迁移时推荐单元测试覆盖
@Bean
依赖场景,验证是否生效。
3.3 注解开发bean作用范围与生命周期管理
使用注解已经完成了bean的管理,接下来按照前面所学习的内容,将通过配置实现的内容都换成对应的注解实现,包含两部分内容:bean作用范围
和bean生命周期
。
3.3.1 环境准备
老规矩,学习之前先来准备环境:
-
创建一个Maven项目
-
pom.xml添加Spring的依赖
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency> </dependencies>
-
添加一个配置类
SpringConfig
@Configuration @ComponentScan("com.itheima") public class SpringConfig { }
-
添加BookDao、BookDaoImpl类
public interface BookDao { public void save(); } @Repository public class BookDaoImpl implements BookDao { public void save() { System.out.println("book dao save ..." ); } }
-
创建运行类App
public class App { public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); BookDao bookDao1 = ctx.getBean(BookDao.class); BookDao bookDao2 = ctx.getBean(BookDao.class); System.out.println(bookDao1); System.out.println(bookDao2); } }
最终创建好的项目结构如下:
3.3.2 Bean的作用范围
(1)先运行App类,在控制台打印两个一摸一样的地址,说明默认情况下bean是单例
(2)要想将BookDaoImpl变成非单例,只需要在其类上添加@scope
注解
@Repository
//@Scope设置bean的作用范围
@Scope("prototype")
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
}
再次执行App类,打印结果:
知识点1:@Scope
名称 | @Scope |
---|---|
类型 | 类注解 |
位置 | 类定义上方 |
作用 | 设置该类创建对象的作用范围 可用于设置创建出的bean是否为单例对象 |
属性 | value(默认):定义bean作用范围, ==默认值singleton(单例),可选值prototype(非单例)== |
3.3.3 Bean的生命周期
(1)在BookDaoImpl中添加两个方法,init
和destroy
,方法名可以任意
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
public void init() {
System.out.println("init ...");
}
public void destroy() {
System.out.println("destroy ...");
}
}
(2)如何对方法进行标识,哪个是初始化方法,哪个是销毁方法?
只需要在对应的方法上添加@PostConstruct
和@PreDestroy
注解即可。
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
@PostConstruct //在构造方法之后执行,替换 init-method
public void init() {
System.out.println("init ...");
}
@PreDestroy //在销毁方法之前执行,替换 destroy-method
public void destroy() {
System.out.println("destroy ...");
}
}
(3)要想看到两个方法执行,需要注意的是destroy
只有在容器关闭的时候,才会执行,所以需要修改App的类
public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao1 = ctx.getBean(BookDao.class);
BookDao bookDao2 = ctx.getBean(BookDao.class);
System.out.println(bookDao1);
System.out.println(bookDao2);
ctx.close(); //关闭容器
}
}
(4)运行App,类查看打印结果,证明init和destroy方法都被执行了。
==注意:@PostConstruct和@PreDestroy注解如果找不到,需要导入下面的jar包==
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
找不到的原因是,从JDK9以后jdk中的javax.annotation包被移除了,这两个注解刚好就在这个包中。
知识点1:@PostConstruct
名称 | @PostConstruct |
---|---|
类型 | 方法注解 |
位置 | 方法上 |
作用 | 设置该方法为初始化方法 |
属性 | 无 |
知识点2:@PreDestroy
名称 | @PreDestroy |
---|---|
类型 | 方法注解 |
位置 | 方法上 |
作用 | 设置该方法为销毁方法 |
属性 | 无 |
小结
🧠 理论理解
-
@Scope
解决Bean作用范围问题,核心是单例与多例切换。 -
@PostConstruct
和@PreDestroy
是生命周期回调注解,本质是告诉容器:对象实例化后/销毁前你要执行什么逻辑。
这对应了Spring管理Bean时的完整生命周期:实例化 → 初始化 → 使用 → 销毁。
🏢 企业实战理解
-
美团外卖:商品定时任务使用
@Scope("prototype")
实现多例任务实例并发运行。 -
腾讯云:云监控Agent模块中,采用
@PostConstruct
动态加载监控指标,@PreDestroy
完成缓存清理。 -
OpenAI:早期API限流模块中,生命周期方法用于统计模块的初始化与关闭时的日志打点,确保调试信息完整。
阿里巴巴面试题
Q: @Scope("prototype")
有哪些应用场景?阿里内部如何处理多例 Bean 的生命周期管理?
A: prototype
适合临时对象(如文件上传流、线程内对象)且避免资源竞争。阿里内部经验是:多例 Bean 不能依赖注入到单例 Bean 内,否则生命周期不一致会导致内存泄漏等问题。解决方案是用 ObjectFactory<T>
或 Provider<T>
懒加载获取新实例。
OpenAI面试题
Q: @PostConstruct
和 @PreDestroy
如何在分布式环境中保持一致执行?有哪些限制?
A: 这两个注解由容器调用,分布式环境下如使用 Spring Cloud,多个实例各自执行一次。OpenAI的经验是:涉及外部资源(如注册中心、分布式缓存)时,要避免重复初始化或销毁引发并发问题。通常建议结合分布式锁/全局状态检查机制,保证生命周期方法的幂等性和可重入性。
场景题:腾讯云 - 多线程下 Bean 生命周期异常
▶️ 场景描述:
腾讯云开发的日志采集模块,使用 @Scope("prototype")
声明了一个任务处理 Bean,并在单例的调度器中注入该 Bean。运行时发现 Bean 作用范围失效,始终是单例对象,且 @PostConstruct
方法执行顺序异常。
请解释错误点及改进方案。
✔️ 答案:
错误点:Spring 在容器初始化时注入 Bean(无论单例还是多例),所以即使标记了 @Scope("prototype")
,注入到单例调度器中时,Spring只会注入一次(单例持有多例 Bean 的引用)。
此外,@PostConstruct
只在 Bean 初始化时执行一次,而不是每次 new 的时候都执行。
改进方案:
-
改用
ObjectFactory<T>
或ApplicationContext.getBean()
的方式,每次任务执行时动态获取新实例,如:
@Autowired
private ObjectFactory<TaskProcessor> taskProcessorFactory;
TaskProcessor task = taskProcessorFactory.getObject();
-
腾讯云内部也会用 自定义 BeanScopeHandler 封装多例 Bean 获取逻辑,避免开发者误用。
3.4 注解开发依赖注入
Spring为了使用注解简化开发,并没有提供构造函数注入
、setter注入
对应的注解,只提供了自动装配的注解实现。
3.4.1 环境准备
在学习之前,把案例环境介绍下:
-
创建一个Maven项目
-
pom.xml添加Spring的依赖
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency> </dependencies>
-
添加一个配置类
SpringConfig
@Configuration @ComponentScan("com.itheima") public class SpringConfig { }
-
添加BookDao、BookDaoImpl、BookService、BookServiceImpl类
public interface BookDao { public void save(); } @Repository public class BookDaoImpl implements BookDao { public void save() { System.out.println("book dao save ..." ); } } public interface BookService { public void save(); } @Service public class BookServiceImpl implements BookService { private BookDao bookDao; public void setBookDao(BookDao bookDao) { this.bookDao = bookDao; } public void save() { System.out.println("book service save ..."); bookDao.save(); } }
-
创建运行类App
public class App { public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); BookService bookService = ctx.getBean(BookService.class); bookService.save(); } }
最终创建好的项目结构如下:
环境准备好后,运行后会发现有问题
出现问题的原因是,在BookServiceImpl类中添加了BookDao的属性,并提供了setter方法,但是目前是没有提供配置注入BookDao的,所以bookDao对象为Null,调用其save方法就会报控指针异常
。
3.4.2 注解实现按照类型注入
对于这个问题使用注解该如何解决?
(1) 在BookServiceImpl类的bookDao属性上添加@Autowired
注解
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
// public void setBookDao(BookDao bookDao) {
// this.bookDao = bookDao;
// }
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}
注意:
-
@Autowired可以写在属性上,也可也写在setter方法上,最简单的处理方式是
写在属性上并将setter方法删除掉
-
为什么setter方法可以删除呢?
-
自动装配基于反射设计创建对象并通过暴力反射为私有属性进行设值
-
普通反射只能获取public修饰的内容
-
暴力反射除了获取public修饰的内容还可以获取private修改的内容
-
所以此处无需提供setter方法
-
(2)@Autowired是按照类型注入,那么对应BookDao接口如果有多个实现类,比如添加BookDaoImpl2
@Repository
public class BookDaoImpl2 implements BookDao {
public void save() {
System.out.println("book dao save ...2");
}
}
这个时候再次运行App,就会报错
此时,按照类型注入就无法区分到底注入哪个对象,解决方案:按照名称注入
-
先给两个Dao类分别起个名称
@Repository("bookDao") public class BookDaoImpl implements BookDao { public void save() { System.out.println("book dao save ..." ); } } @Repository("bookDao2") public class BookDaoImpl2 implements BookDao { public void save() { System.out.println("book dao save ...2" ); } }
此时就可以注入成功,但是得思考个问题:
-
@Autowired是按照类型注入的,给BookDao的两个实现起了名称,它还是有两个bean对象,为什么不报错?
-
@Autowired默认按照类型自动装配,如果IOC容器中同类的Bean找到多个,就按照变量名和Bean的名称匹配。因为变量名叫
bookDao
而容器中也有一个booDao
,所以可以成功注入。 -
分析下面这种情况是否能完成注入呢?
-
-
-
不行,因为按照类型会找到多个bean对象,此时会按照
bookDao
名称去找,因为IOC容器只有名称叫bookDao1
和bookDao2
,所以找不到,会报NoUniqueBeanDefinitionException
-
3.4.3 注解实现按照名称注入
当根据类型在容器中找到多个bean,注入参数的属性名又和容器中bean的名称不一致,这个时候该如何解决,就需要使用到@Qualifier
来指定注入哪个名称的bean对象。
@Service
public class BookServiceImpl implements BookService {
@Autowired
@Qualifier("bookDao1")
private BookDao bookDao;
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}
@Qualifier注解后的值就是需要注入的bean的名称。
==注意:@Qualifier不能独立使用,必须和@Autowired一起使用==
3.4.4 简单数据类型注入
引用类型看完,简单类型注入就比较容易懂了。简单类型注入的是基本数据类型或者字符串类型,下面在BookDaoImpl
类中添加一个name
属性,用其进行简单类型注入
@Repository("bookDao")
public class BookDaoImpl implements BookDao {
private String name;
public void save() {
System.out.println("book dao save ..." + name);
}
}
数据类型换了,对应的注解也要跟着换,这次使用@Value
注解,将值写入注解的参数中就行了
@Repository("bookDao")
public class BookDaoImpl implements BookDao {
@Value("itheima")
private String name;
public void save() {
System.out.println("book dao save ..." + name);
}
}
注意数据格式要匹配,如将"abc"注入给int值,这样程序就会报错。
介绍完后,会有一种感觉就是这个注解好像没什么用,跟直接赋值是一个效果,还没有直接赋值简单,所以这个注解存在的意义是什么?
3.4.5 注解读取properties配置文件
@Value
一般会被用在从properties配置文件中读取内容进行使用,具体如何实现?
步骤1:resource下准备properties文件
jdbc.properties
name=itheima888
步骤2: 使用注解加载properties配置文件
在配置类上添加@PropertySource
注解
@Configuration
@ComponentScan("com.itheima")
@PropertySource("jdbc.properties")
public class SpringConfig {
}
步骤3:使用@Value读取配置文件中的内容
@Repository("bookDao")
public class BookDaoImpl implements BookDao {
@Value("${name}")
private String name;
public void save() {
System.out.println("book dao save ..." + name);
}
}
步骤4:运行程序
运行App类,查看运行结果,说明配置文件中的内容已经被加载到
注意:
-
如果读取的properties配置文件有多个,可以使用
@PropertySource
的属性来指定多个@PropertySource({"jdbc.properties","xxx.properties"})
-
@PropertySource
注解属性中不支持使用通配符*
,运行会报错@PropertySource({"*.properties"})
-
@PropertySource
注解属性中可以把classpath:
加上,代表从当前项目的根路径找文件@PropertySource({"classpath:jdbc.properties"})
知识点1:@Autowired
名称 | @Autowired |
---|---|
类型 | 属性注解 或 方法注解(了解) 或 方法形参注解(了解) |
位置 | 属性定义上方 或 标准set方法上方 或 类set方法上方 或 方法形参前面 |
作用 | 为引用类型属性设置值 |
属性 | required:true/false,定义该属性是否允许为null |
知识点2:@Qualifier
名称 | @Qualifier |
---|---|
类型 | 属性注解 或 方法注解(了解) |
位置 | 属性定义上方 或 标准set方法上方 或 类set方法上方 |
作用 | 为引用类型属性指定注入的beanId |
属性 | value(默认):设置注入的beanId |
知识点3:@Value
名称 | @Value |
---|---|
类型 | 属性注解 或 方法注解(了解) |
位置 | 属性定义上方 或 标准set方法上方 或 类set方法上方 |
作用 | 为 基本数据类型 或 字符串类型 属性设置值 |
属性 | value(默认):要注入的属性值 |
知识点4:@PropertySource
名称 | @PropertySource |
---|---|
类型 | 类注解 |
位置 | 类定义上方 |
作用 | 加载properties文件中的属性值 |
属性 | value(默认):设置加载的properties文件对应的文件名或文件名组成的数组 |
🧠 理论理解
-
@Autowired
:按类型注入,解决了Setter/构造注入繁琐问题。 -
@Qualifier
:指定Bean名称,解决多实现冲突。 -
@Value
:用于注入简单数据类型,支持直接赋值 & 读取配置文件参数。 -
@PropertySource
:为Spring加载外部properties文件提供支持。
这些注解大大简化了IOC容器管理依赖注入的复杂度,尤其是应对多实现、参数配置场景。
🏢 企业实战理解
-
字节跳动:服务治理框架结合
@Autowired + @Qualifier
实现多数据源动态切换,后台API实现灰度流量分发。 -
阿里巴巴:内部RPC框架封装
@Value
动态读取注册中心地址,结合Nacos配置中心实现动态参数注入。 -
英伟达:在GPU云微服务中,
@Value
结合@PropertySource
用于从私有云配置文件中动态获取GPU分配策略参数。
字节跳动面试题
Q: @Autowired
注入时,如果遇到多个实现类且忘记使用 @Qualifier
会发生什么问题?如何避免?
A: Spring 按类型注入时,如果多个实现类存在,IOC 容器会抛出 NoUniqueBeanDefinitionException
。字节在内部开发中通过自定义代码扫描器自动检测是否存在多实现且缺少 @Qualifier
场景,触发预警,防止上线报错。
腾讯云面试题
Q: @Value
从配置文件注入参数时,出现找不到配置项会发生什么?腾讯云如何做容错处理?
A: 如果配置项未找到,Spring 默认会启动失败报错。腾讯云的解决方案是:
1️⃣ 设置 @Value("${xxx:默认值}")
提供兜底值;
2️⃣ 用配置中心(如Apollo)做动态参数注入,结合 自动刷新机制 确保系统能实时感知参数变化。
美团点评面试题
Q: @Autowired(required=false)
场景下,Bean 注入失败会怎样?这在什么业务中应用最多?
A: 当 required=false
时,Bean 找不到不会抛出异常,注入的字段为 null。这在灰度部署、可选组件场景中非常实用,比如美团外卖支付模块中,有些支付渠道是可插拔式的,系统允许对应 Bean 为 null,由上层逻辑自行判断是否支持该支付方式。
场景题:字节跳动 - 自动注入失效
▶️ 场景描述:
字节跳动有个后台管理系统,某次上线后发现 BookServiceImpl
的 bookDao
注入失效,导致 NullPointerException。排查后发现 @Autowired
标记正常,且 BookDao
的实现类 BookDaoImpl
也已加载。
你会如何排查并解决这个问题?
✔️ 答案:
可能原因:
-
BookDaoImpl
上缺少@Repository("bookDao")
,导致 Bean 名称与@Autowired
默认按属性名注入的逻辑对不上。 -
或者存在多个
BookDao
实现类,未使用@Qualifier
指定明确的注入对象。
排查步骤:
1️⃣ 检查 BookDaoImpl
是否被正确扫描,打印 Bean 定义。
2️⃣ 检查是否存在多个实现,查看 ctx.getBeanNamesForType(BookDao.class)
输出。
解决方案:
-
确保
BookDaoImpl
有正确的注解声明:
@Repository("bookDao")
public class BookDaoImpl implements BookDao { ... }
-
或在
BookServiceImpl
上用@Qualifier
:
@Autowired
@Qualifier("bookDao")
private BookDao bookDao;
字节内部有工具链在 CI 流程中自动校验所有自动注入点的唯一性,防止类似问题。