
Autowired可能是 Spring 项目里出现频率最高的注解但也是误解最多的一个。很多人写代码时习惯性在字段上一排Autowired问起来却说不清它到底是怎么工作的如果一个接口有两个实现Spring 到底注入哪一个为什么有时候启动就报错有时候注进去是 null我打算把这些年用Autowired的实战经验拆开讲一遍从底层匹配机制到几种注入姿势的取舍再到循环依赖、单测失效这些经典坑帮你一次理清楚。1. Autowired到底在帮你做什么从手写 new 到容器按需匹配1.1 没有依赖注入的年代Service 是怎么拿 Repository 的先回到没有 Spring 的年代或者你接手过一个没好好用 Spring 的项目。假设有个UserService它要操作数据库最常见的方式是直接在类里写public class UserService { private UserRepository userRepository new UserRepository(); }看起来没毛病但问题会很快暴露UserRepository如果构造方法加了参数比如要传DataSource所有 new 它的地方全要跟着改如果项目里有单元测试想替换成一个内存版 repository你得去改UserService的源码或者用反射硬塞进去。更重要的是当UserRepository需要被 Spring 的事务、AOP 代理时你自己 new 出来的对象完全不归容器管注解、事务拦截全部失效。Autowired解决的正是“谁负责把依赖交给谁”的问题。它让对象之间的组装动作从调用方代码里剥离出来统一交给 Spring 容器完成这就是依赖注入DI的基本含义。不过很多人对Autowired的理解停留在“有这个注解 Spring 就会帮我赋值”至于 Spring 是怎么找到值的、找不到怎么办、找到两个值怎么办完全没有概念。这正是后面各种疑难杂症的源头。1.2 Autowired 的工作本质按类型向容器查询 BeanAutowired的执行本质上包含一次“容器查询”。Spring 在启动时会把所有被扫描到的类实例化成 Bean放进一个大的缓存池里。当它需要给某个 Bean 的属性赋值时会走到AutowiredAnnotationBeanPostProcessor这个类会扫描 Bean 中被Autowired标记的字段、方法、构造器然后调用DefaultListableBeanFactory.resolveDependency()去“找符合条件的 Bean”。查找逻辑的第一步是按类型匹配。比如Autowired private UserRepository userRepository;Spring 会在容器里找所有类型是UserRepository或它的子类型的 Bean。这里有三种结果一个都找不到直接抛NoSuchBeanDefinitionException。找到一个直接返回完成注入。找到多个进入复杂的“多候选”决策流程这部分放到第 2 章细说。这里必须强调一个概念Autowired不是直接 new也不是通过反射拿到一个字段名去匹配。它背后是全套的 Bean 生命周期包括实例化、属性填充、初始化回调、代理生成等。所以被注入的对象永远是一个由容器管理的、可能被增强过的 Bean而不是一个普通对象。用生活类比来解释你不是自己去找水管工而是给物业容器打电话说“我需要一个水管工按类型”物业会从他们的花名册里挑一个合适的人派过来。你不需要知道这个水管工住在哪、怎么联系甚至不需要知道他的名字除非叫了多个人时指定一下。1.3 普通 new 出来的对象为什么没法 Autowired一个超高频问题我在工具类里写Autowired为什么会是 null答案很简单AutowiredAnnotationBeanPostProcessor只处理由 Spring 容器创建的 Bean。如果你自己new了一个UserService它完全脱离容器管理Spring 的 BeanPostProcessor 根本没有机会执行字段上的Autowired不会被解析自然就是 null。UserService userService new UserService(); // 里面的 Autowired 字段是 null包括在main方法里直接 new、在非 Spring 管理的监听器里 new、在定时任务框架里手动 new都会遇到同样的情况。正确做法是让 Spring 创建并管理需要依赖注入的类或者通过ApplicationContext.getBean()获取容器里的 Bean。这个“脱离容器”的认知是排查大量空指针的第一步。很多问题看似是Autowired写错了实际是对象根本不是容器创建的。2. 多个同类型 Bean 并存时Autowired 的匹配过程与决策顺序2.1 默认的按类型查找会得到几种结果如果容器里只有一个UserRepositoryAutowired的工作确实没什么可说的。但真实项目里同类型多个 Bean 的情况比比皆是一个接口有多个实现类、配置了多个DataSource、多个RestTemplate或ObjectMapper实例等。这时候Autowired不会靠猜它会走一套固定的决策流程。流程是这样的我简化了 Spring 源码里的主要分支先按类型找到所有候选 Bean。如果某个候选 Bean 上有Primary注解直接选择它。如果当前注入点的Qualifier注解指定了 beanName按名称精确匹配。如果注入点字段名恰好能匹配某个候选 Bean 的 beanName按字段名匹配。如果以上都匹配不上抛NoUniqueBeanDefinitionException。注意第 3 点和第 4 点的先后顺序实际上 Spring 在doResolveDependency中会先处理Qualifier再处理“用字段名回退匹配”。这解释了为什么有时候你什么都不加也能注入成功因为字段名和某个实现类的 beanName 恰好一样。但这非常脆弱不推荐依赖。2.2 Primary给容器一个“默认答案”Primary的作用是在多个同类型 Bean 中指定一个默认优先级最高的。被标注的 Bean 在按照类型注入时会被优先选中就像公司里有多位客服第一位是主客服。典型场景是主从数据源Bean Primary public DataSource primaryDataSource() { return createDataSource(main); } Bean public DataSource readDataSource() { return createDataSource(read); }然后在任何地方直接Autowired private DataSource dataSource;拿到的都是主数据源。如果你想要从库数据源必须配合Qualifier(readDataSource)明确指定。使用Primary有个隐蔽的坏处它让“类型注入”变得有歧义但你又看不到这个歧义因为容器把矛盾压下去了。如果系统里有三个同类型 Bean你标了 A 为Primary之后加了一个 B而 B 没标代码仍然不会报错将来的人很容易搞混到底注入的是哪个。所以我的建议是Primary用于明确区分“默认”和“特殊”的场景比如主从库而且要写清楚注释如果只是随便挑一个别用它。2.3 Qualifier精确指定 beanNameQualifier是更精确的选择工具Autowired Qualifier(readDataSource) private DataSource dataSource;这里的readDataSource是 Bean 的注册名称。默认情况下Spring 在组件扫描时会把类名首字母小写作为 beanName比如ReadDataSourceConfig的 beanName 是readDataSourceConfig通过Bean方法生成时方法名就是 beanName。一个容易踩的坑是开发同学把Qualifier(readDS)写在注入点但容器里根本没有叫readDS的 Bean启动时会报NoSuchBeanDefinitionException。这个错误的排查并不难但很烦人。建议让Qualifier的值和Bean方法名或组件名严格一致保持在 IDE 里能直接搜索到。更好的做法是自定义Qualifier注解把业务语义编码进去。比如Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) Qualifier public interface MasterDataSource { }然后在目标 Bean 上标注MasterDataSource注入点也标注MasterDataSource。这样业务组件不再依赖字符串魔数重构时 IDEA 也能帮你找到所有引用比写字符串优雅得多。我团队里后来统一用自定义限定符再也没有出现“复制错字符串”的问题。2.4 List、Map 注入与 Order 控制顺序Autowired还有一个很多人不知道的形态直接注入一个集合或 Map拿到该类型的所有 Bean。Service public class OrderProcessor { Autowired private ListOrderHandler orderHandlers; Autowired private MapString, OrderHandler orderHandlerMap; }Spring 在按类型匹配时如果注入点本身就是ListT或MapString, T会把所有类型为 T 的 Bean 收集起来。Map的 key 是 beanNamevalue 是对应 BeanList里的顺序默认由Order注解或Ordered接口决定。这个特性非常适合做策略模式一堆OrderHandler实现类每个用Order(1)、Order(2)排好序业务代码统一遍历执行。需要强调的是如果此时你只注入了一个ListOrderHandler但 Spring 容器里没有任何OrderHandlerAutowired会报错。和单值注入一样除非加required false。组合使用时要小心如果某个实现类同时被Primary标注它在List中不会因为是Primary就排到前面Order才是决定集合顺序的关键。这两个注解的职责不同别混用。3. 字段、Setter、构造器、方法参数四种注入位置的实际差异3.1 字段注入最流行也最容易写出坏味道Autowired private UserRepository userRepository;这种写法在 IDE 里会有警告Field injection is not recommended。很多初学者不理解为什么我举两个具体场景。第一它把依赖隐藏了。看类的方法签名时你根本不知道这个类依赖哪些东西只有翻到字段区才发现多少隐藏依赖。一个类里塞了七八个Autowired字段是很容易的事久而久之类的职责严重膨胀。第二字段不能是final的意味着对象在构造完成后还可能被替换破坏了不变性。而在实际运行时除非 Spring 重启这个字段通常不会被重新赋值换句话说它只是“看起来可变”。另外字段注入在测试里很别扭你想在单元测试里构造OrderService但它的字段是私有的只能用反射塞进去Spring 提供了ReflectionTestUtils来做这事但用多了你会发现自己在绕开构造函数去变戏法还不如一开始就给个构造函数。字段注入唯一比较适合的场景是快速写原型、临时脚本时图省事。长期维护的代码我建议还是往构造器注入走。3.2 Setter 注入适合可选依赖和运行期替换Autowired public void setUserRepository(UserRepository userRepository) { this.userRepository userRepository; }Setter 注入比字段注入好一点依赖至少通过公开的 setter 暴露出来别人可以看到类的方法列表就知道能设置什么。但它同样不能保证依赖在构造后一定存在依赖不是final。真正适合 Setter 注入的场景是“可选依赖”或者“运行期可以替换的实现”。比如一个SecurityService它可能需要PasswordEncoder但不是所有环境都有Autowired(required false) public void setPasswordEncoder(PasswordEncoder passwordEncoder) { this.passwordEncoder passwordEncoder; }更常见的现实场景是集成一些第三方库时它们的配置类提供了 setter 方法方便 Spring 在完整初始化之前填入配置。对我们自己的业务类Setter 注入用得越来越少了因为Optional和Nullable已经能处理“可选依赖”。3.3 构造器注入最推荐的方案和它暴露出的问题Spring 官方从很早之前就建议使用构造器注入。Spring Framework 4.x 开始如果类只有一个构造器甚至可以不写Autowired容器会自动使用这个构造器Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository userRepository; } }这样做的好处非常直接依赖全部是final对象一旦创建依赖不可变同时所有依赖都必须在构造时传入如果漏传了编译期就过不去而不是等到运行时空指针。测试时也简单不需要反射new UserService(mockRepo)就够了。构造器注入还能帮助你发现“类职责过重”当一个类需要 7、8 个构造参数时你会立刻意识到这个类做太多事了。这种“不适感”其实是对代码质量的早期预警比字段注入时的无感膨胀好太多。当然构造器注入也有缺点如果依赖本身是可选的你又不想传入 null就需要配合Optionalpublic UserService(OptionalPasswordEncoder passwordEncoder) { ... }或者用Nullable注解标记参数。Spring 5 之后对Optional参数的支持已经很成熟。另外要注意的是构造器注入在遇到循环依赖时会直接暴露问题第 4 章细讲。这其实是好事因为循环依赖本身就该被消除。3.4 Bean 方法参数与 Autowired配置类里的微妙差异很多人在写Configuration类时会遇到一个问题Autowired到底该不该加在Bean方法参数上Configuration public class AppConfig { Bean public UserService userService(UserRepository userRepository) { return new UserService(userRepository); } }答案是不需要加Autowired。Bean方法在被 Spring 调用时方法参数会自动从容器中按类型解析这本身就是“注入”。加了反而显得多余而且容易误导别人。在Configuration类内部你有时也会看到字段注入Configuration public class AppConfig { Autowired private DataSource dataSource; Bean public JdbcTemplate jdbcTemplate() { return new JdbcTemplate(dataSource); } }这种写法能用但不建议。因为Configuration本身也是一个 Bean字段注入会把它的生命周期提前绑定到dataSource上。更好的写法是把dataSource直接作为Bean方法参数传入。代码会更直观测试时也能直接调用方法传 mock 参数。4. requiredfalse、静态字段注入、循环依赖与测试场景的坑4.1 requiredfalse 什么时候真正有用Autowired默认required true意思是如果找不到 Bean启动就失败。强制约束其实是好东西早失败比线上空指针好。但有些场景确实需要“有就注入没有就算了”这时才考虑required false。经典例子是日志审计、消息发布器这类可选组件。有些环境启用了audit开关有些没启用你希望没有对应 Bean 时不影响主流程Autowired(required false) private AuditService auditService; public void saveUser(User user) { if (auditService ! null) { auditService.record(create, user); } }注意这样做的代价是你必须自己判断 null。代码里到处都是判空很容易漏判。Spring 5 之后我更推荐用ObjectProviderT或OptionalT它们更显式Autowired private ObjectProviderAuditService auditServiceProvider; public void saveUser(User user) { auditServiceProvider.ifAvailable(service - service.record(create, user)); }用ObjectProvider还可以延迟获取 Bean对解决一些启动顺序问题也有帮助。总而言之requiredfalse不是常态是特例能不用就不用能把依赖设计成必选就保持必选。4.2 静态字段上的 Autowired 为什么总是 null这是一个出现频率极高的坑Component public class EmailUtil { Autowired private static MailService mailService; public static void send(String to, String content) { ... } }静态字段属于类本身不属于 Spring 管理的对象实例。AutowiredAnnotationBeanPostProcessor在处理实例字段时只会遍历对象实例的字段不会给静态字段赋值。所以这个mailService永远是 null调用时立刻空指针。我的建议能用实例方法和实例依赖就尽量别写静态工具类。如果实在需要一个静态方法访问容器 Bean正确方式是构造一个静态字段接收实例Component public class EmailUtil { private static MailService mailService; Autowired public void setMailService(MailService mailService) { EmailUtil.mailService mailService; } }这里关键点是把Autowired放在实例 setter 上Spring 会调用这个 setter把容器里的mailService传进来然后赋给静态字段。虽然丑但在一些老代码里很常见能解燃眉之急。我更推荐用ApplicationContextAware写一个SpringContextHolder工具类但注意它会让代码对 Spring 容器产生直接依赖能不用尽量不用。4.3 循环依赖字段注入“能过”不是好事先看一个典型问题Service public class AService { Autowired private BService bService; } Service public class BService { Autowired private AService aService; }两个类相互依赖。在 Spring 的默认单例 Bean 缓存机制下字段注入可以“勉强”通过因为流程是先实例化 A此时 A 是半成品还没有注入属性A 需要 B实例化 BB 需要 A此时 A 已经被缓存虽然是半成品于是 B 拿到 AB 创建完成回头再完成 A 的属性注入。这个机制靠的是三级缓存。但如果把Autowired放在构造器上情况就完全不同public AService(BService bService) { this.bService bService; }容器创建 AService 时必须先把 BService 构造出来构造 BService 又需要 AService而 AService 还正在构造中没有缓存的半成品可以给 B 用于是直接抛出BeanCurrentlyInCreationException整个启动失败。这正是 Spring Boot 2.6 之后默认禁止循环依赖的原因之一。allow-circular-references被默认设为 false很多老项目升级后启动直接报错逼着大家改掉这种坏味道。我的经验是发现循环依赖首先要做的是重构把互相调用的逻辑抽出去或者通过事件机制、ObjectProvider延迟获取来处理而不是想方设法打开开关。字段注入掩盖了这个问题让它能“带病运行”上线后更容易在特定调用链路上出现诡异问题。4.4 没有 Spring 容器的单元测试为什么不会注入单元测试跑不起来Spring 容器没有启动Autowired字段当然永远是 null。这不是Autowired的问题而是测试设计的问题。如果你用构造器注入单元测试就很简单UserService service new UserService(mockRepository);直接用 mock 或 fake 对象构造目标类不需要 Spring。如果你用字段注入测试只能引入ReflectionTestUtilsUserService service new UserService(); ReflectionTestUtils.setField(service, userRepository, mockRepository);这两者的差异非常直观构造器注入天然便于测试字段注入则到处要求反射。在做集成测试时SpringBootTest会启动完整容器Autowired自然有效但集成测试数量应该远少于单元测试。我建议团队里定的规则是单元测试一律不启动 Spring 容器业务类必须支持直接new装配。这条规则一落地会倒逼大家减少字段注入。5. 结合 Spring Boot 的实战规则与一眼定位问题的调试经验5.1 在 Configuration 里尽量让参数说话Spring Boot 中大量使用自动配置很多情况你根本不需要手写Autowired。配置类里最舒服的写法是像函数式那样把依赖直接写在Bean方法参数里Configuration public class MyBatchConfig { Bean public JobLauncher jobLauncher(JobRepository jobRepository, JobExplorer jobExplorer) { return new SimpleJobLauncher(jobRepository, jobExplorer); } }方法参数由 Spring 自动解析不需要任何注解。它的好处是一眼看出该配置类需要哪些外部依赖测试时也可以直接new MyBatchConfig().jobLauncher(mockJobRepo, mockExplorer)。相比之下Configuration类里字段注入会让依赖关系变成隐式的IDE 的调用关系分析也会变得不准确。还有一个容易忽视的点Configuration的类被 CGLIB 代理字段注入如果处理不当可能导致代理对象内部字段没被正确填充。尽管 Spring 处理得已经很完善但用方法参数注入可以从根源上规避这些边界情况。5.2 一份能直接当团队规范的 Autowired 使用清单我总结了在团队代码 review 时会反复强调的几条规则现在放在一起供参考场景推荐写法不推荐写法必选依赖构造器注入参数或字段用 final字段Autowired可选依赖构造器参数用OptionalT或NullableAutowired(requiredfalse)后到处判空同类型多实例Qualifier或自定义Qualifier注解依赖字段名回退匹配策略模式收集全部实现ListT/MapString,T注入手动把多个实现塞进 List配置类Bean方法参数注入在Configuration里字段注入单元测试new目标类并传入 mock/fake反射塞字段这套规则不是教条。你可以在小项目中先用字段注入但一旦代码进入长期维护、多人协作构造器注入带来的好处会越来越明显。拿我参与过的项目来说把十几个核心类从字段注入改成构造器注入后单测启动时间从原来每次 30 多秒降到几秒代码 review 时依赖关系一目了然排查“哪个类用了哪个服务”再也不用翻字段区。5.3 在断点里观察依赖解析过程如果你遇到启动报错比如NoUniqueBeanDefinitionException或NoSuchBeanDefinitionException先不要急着点右键 Run可以把断点打在关键源码上观察 Spring 到底在做什么。常用的断点位置是DefaultListableBeanFactory.resolveDependency()和doResolveDependency()它们位于org.springframework.beans.factory.support包。加上依赖后IDE 会自动引入 spring-beans 源码。启动 Debug当某个 Bean 的属性填充走到断点时可以一步步看findAutowireCandidates()返回的候选列表、匹配到的名称、最后选择的结果。我看过太多人遇到NoUniqueBeanDefinitionException时第一反应是加Qualifier(xxx)把错误压下去。但正确的排查思路是先搞清楚为什么容器里同时存在多个该类型的 Bean每个 Bean 是哪里定义的。很多时候你会发现其中一个 Bean 本来就不该注册删掉它才是正道而不是靠注解选择性失明。断点能帮你快速列出所有候选 Bean 的名字、定义来源比对着配置文件漫无目的地查高效很多。最后分享一个小技巧在代码里看到一个字段上写着Autowired先问自己一句——这个依赖是必须的吗如果答案是肯定的试试把它改成构造器参数。改完之后你会发现代码反而更容易测试也更诚实了。