
1. 从“new”到“Autowired”为什么依赖注入是Spring的基石如果你写过几年Java尤其是经历过从Servlet/JSP到Spring Boot的转变那你一定对“new”这个关键字又爱又恨。爱它的直接了当恨它带来的紧耦合和难以测试的代码。我至今还记得早期维护一个没有使用任何框架的“祖传”项目一个业务类里层层嵌套了十几个new Service()、new Dao()改一处逻辑测试跑通需要半小时牵一发而动全身。后来接触Spring第一次看到Autowired注解时感觉就像打开了新世界的大门——原来对象之间的关系可以不用我来“硬编码”框架能帮我管理。这就是依赖注入Dependency Injection DI最直观的价值。它不是什么高深莫测的黑科技而是一种设计思想一种让代码变得更清晰、更灵活、更易于管理的编程范式。Spring框架的核心可以说就是围绕DI容器展开的。很多人学Spring一上来就搞配置、写注解却忽略了理解DI“为什么”如此重要。今天我们就抛开那些复杂的配置文件和注解变体回到最根本的问题依赖注入到底解决了什么痛点Spring又是如何把它玩出花的简单说依赖注入就是将对象依赖关系的创建和管理从对象内部转移到外部容器。原本是“我需要什么我自己去new”变成了“我需要什么你容器给我”。这个转变带来了至少三个核心好处解耦、可测试性和可管理性。你的类不再关心它的依赖从哪来、怎么来只声明“我需要什么”容器则负责在合适的时机把合适的对象“注入”给它。这种模式下替换一个实现、进行单元测试比如注入一个Mock对象、管理对象的生命周期单例、原型等都变得轻而易举。2. DI的三种实现方式XML、注解与Java Config的演进与选择Spring从诞生之初就在不断演进其DI的配置方式这背后反映的是开发效率和配置清晰度之间的权衡。理解这三种方式不仅能帮你应对历史项目更能让你明白Spring设计的脉络。2.1 XML配置声明式的起点与“配置地狱”最早的Spring完全依赖XML。你需要在一个通常是applicationContext.xml文件里像搭积木一样声明所有的Bean以及它们之间的依赖关系。beans !-- 声明一个数据源Bean -- bean iddataSource classcom.zaxxer.hikari.HikariDataSource property namejdbcUrl valuejdbc:mysql://localhost:3306/test/ property nameusername valueroot/ property namepassword value123456/ /bean !-- 声明一个Repository Bean并注入dataSource -- bean iduserRepository classcom.example.repository.UserRepositoryImpl property namedataSource refdataSource/ /bean !-- 声明一个Service Bean并注入repository -- bean iduserService classcom.example.service.UserServiceImpl property nameuserRepository refuserRepository/ /bean /beans这种方式是声明式的配置和代码分离理论上很清晰。但问题也随之而来当项目规模变大Bean数量成百上千时这个XML文件会变得极其臃肿难以维护。查找一个Bean的定义如同大海捞针依赖关系全靠人眼在标签中寻找。这就是所谓的“XML配置地狱”。它的优点在于集中管理缺点则是繁琐且容易出错重构时比如修改类名需要同步修改XML工具支持也不够好。2.2 注解驱动约定大于配置的胜利为了简化配置Spring 2.5引入了基于注解的配置。核心思想是“约定大于配置”。通过在类上、字段上、方法上添加注解来声明Bean和依赖关系。Component,Service,Repository,Controller: 这些是模式注解标记一个类是Spring容器管理的Bean。它们本质是一样的但用不同的名字表达了语义层次方便理解和进行特殊处理比如Repository能和Spring的数据访问异常转换结合。Autowired: 这是依赖注入的核心注解。可以标注在字段、构造方法、Setter方法甚至普通方法上告诉容器“这里需要注入一个依赖”。Qualifier: 当同一个类型有多个Bean时比如有多个DataSource实现用此注解指定具体要注入哪一个Bean的名称。组件扫描: 光有注解还不够需要告诉Spring去哪里找这些带注解的类。通过XML的context:component-scan base-packagecom.example/或Java Config的ComponentScan实现。Service // 声明这是一个Service层的Bean public class UserServiceImpl implements UserService { Autowired // 自动注入UserRepository类型的Bean private UserRepository userRepository; // 也可以用在构造器上这是Spring团队推荐的方式因为它明确了依赖不可变且便于测试 // public UserServiceImpl(UserRepository userRepository) { // this.userRepository userRepository; // } Override public User findUserById(Long id) { return userRepository.findById(id); } } Repository public class UserRepositoryImpl implements UserRepository { Autowired private DataSource dataSource; // ... 实现 }注解方式极大提升了开发效率代码即配置直观且便于重构IDE能很好地支持。但它也有缺点配置分散在了各个类中对于第三方库的类你无法修改其源码或者需要根据条件动态创建的Bean注解方式就力不从心了。此外过度的Autowired也可能导致代码的Spring耦合度变高。2.3 Java Config类型安全与编程式配置的回归Spring 3.0引入了基于Java的配置用Configuration和Bean注解以纯Java代码的方式来定义Bean。这可以看作是XML配置的“类型安全”版本。Configuration // 声明这是一个配置类相当于一个XML文件 public class AppConfig { Bean // 声明一个Bean方法名默认作为Bean的id返回类型是Bean的类型 public DataSource dataSource() { HikariDataSource ds new HikariDataSource(); ds.setJdbcUrl(jdbc:mysql://localhost:3306/test); ds.setUsername(root); ds.setPassword(123456); return ds; } Bean public UserRepository userRepository(DataSource dataSource) { // 方法参数会自动注入 UserRepositoryImpl repo new UserRepositoryImpl(); repo.setDataSource(dataSource); // 假设有setter方法 return repo; } Bean public UserService userService(UserRepository userRepository) { return new UserServiceImpl(userRepository); // 构造器注入 } }Java Config的优势非常明显类型安全编译器会检查类型错误重构友好。强大灵活你可以在Bean方法里写任何Java逻辑实现复杂的Bean创建和初始化过程。易于集成非常适合配置那些来自第三方库、无法添加Spring注解的类。便于条件化装配可以轻松结合Conditional等注解实现根据环境、属性等条件动态创建Bean。在现代Spring Boot应用中这三种方式往往是共存的。Boot的自动配置本身就是大量使用Configuration和Bean的Java Config。而我们自己的业务代码则大量使用Service,Autowired等注解。对于需要集中管理或复杂初始化的基础设施Bean如线程池、客户端等也推荐使用Java Config。我的经验是业务Bean用注解基础设施Bean用Java Config彻底告别XML。3. 注入方式详解构造器注入为何成为官方推荐依赖注入不止有Autowired一种姿势根据注入发生的位置和时机主要分为三种字段注入、Setter方法注入和构造器注入。Spring都支持但社区和官方的最佳实践已经发生了明显变化。3.1 字段注入最方便但隐患最大这就是我们最常见的形式直接在字段上标注Autowired。Service public class OrderService { Autowired private PaymentService paymentService; Autowired private InventoryService inventoryService; // ... 业务方法直接使用 paymentService 和 inventoryService }优点写法简洁代码量少。致命缺点不能声明不可变对象final字段因为注入发生在对象构造之后所以被注入的字段不能是final的这意味着你的类状态在理论上是可以被改变的虽然通常不会不符合不可变对象的设计原则。隐藏了类依赖这个类到底依赖了多少外部服务不看完整代码你无法一目了然。如果依赖很多这个类很可能违反了单一职责原则但字段注入掩盖了这一点。与容器强耦合你的类必须运行在Spring容器中才能正常实例化因为Autowired是Spring的注解。这降低了代码的可移植性和可测试性。你想写个简单的单元测试脱离Spring容器直接new OrderService()会发现所有依赖都是null。容易导致循环依赖字段注入让Spring有机会在对象还未完全构造好时就通过反射注入依赖这在一定程度上“纵容”了循环依赖的设计。而构造器注入会在启动时就暴露出循环依赖问题。3.2 Setter方法注入一种折中方案通过Setter方法进行注入。Service public class OrderService { private PaymentService paymentService; private InventoryService inventoryService; Autowired public void setPaymentService(PaymentService paymentService) { this.paymentService paymentService; } Autowired public void setInventoryService(InventoryService inventoryService) { this.inventoryService inventoryService; } }优点比较灵活可以在对象创建后重新注入尽管很少需要。比字段注入稍微好一点因为依赖是公开的。缺点同样不能用于final字段对象在构造后可能处于一种“部分注入”的不完整状态如果某个Setter忘了调用。同样与Spring容器耦合。3.3 构造器注入现代Spring应用的首选这是Spring官方自4.x版本以来推荐的方式。在Spring Framework 5.1的文档中明确建议使用构造器注入来注入强制依赖。Service public class OrderService { private final PaymentService paymentService; private final InventoryService inventoryService; // 构造器Spring会自动将PaymentService和InventoryService类型的Bean注入进来 public OrderService(PaymentService paymentService, InventoryService inventoryService) { this.paymentService paymentService; this.inventoryService inventoryService; } // ... 业务方法 }从Spring 4.3开始如果类只有一个构造器那么Autowired注解甚至可以省略。在Spring Boot中这已经成为默认的、鼓励的做法。为什么构造器注入是更好的选择不可变性依赖可以被声明为final字段这意味着一旦OrderService实例被创建它的依赖就不可改变。这保证了对象状态在生命周期内的一致性和线程安全性。完全初始化的对象对象在构造完成时所有必需的依赖都已经就位它立即就处于一个完整、可用的状态。不存在“部分注入”的中间态。清晰的依赖契约构造器的参数列表就是这个类所必须的依赖的明确声明。一眼就能看出这个类的复杂度。如果参数太多比如超过5个这就是一个强烈的信号这个类可能做了太多事情需要考虑重构。便于测试你可以毫不费力地进行单元测试。不需要Spring容器不需要反射工具直接new OrderService(mock(PaymentService.class), mock(InventoryService.class))即可。避免循环依赖如果A和B相互通过构造器注入依赖Spring在启动时就会抛出BeanCurrentlyInCreationException迫使你重新审视设计消除循环依赖。这比运行时才发现问题要好得多。注意对于可选依赖即没有也能工作但有的话功能更强Setter方法注入仍然是一个合理的选择。但绝大多数业务依赖都是强制性的因此构造器注入应作为默认选项。4. 深入原理Spring IoC容器如何工作与三级缓存破解循环依赖理解了怎么用我们再来探探底看看Spring的IoC容器ApplicationContext到底是怎么完成依赖注入这个魔术的。这个过程通常被称为“Bean的生命周期”而其中最让人头疼又不得不提的就是“循环依赖”。4.1 Bean的生命周期与依赖解析流程一个Bean从定义到可用大致经历以下关键步骤其中依赖注入发生在“属性填充”阶段实例化容器调用Bean的构造方法或工厂方法创建一个“原始对象”。此时对象内的字段都是默认值null, 0等依赖也还未注入。可以理解为刚new出来。属性填充依赖注入容器解析该Bean所依赖的其他Bean并通过反射字段注入、调用Setter方法Setter注入或已在第一步完成的构造器构造器注入将依赖对象设置进去。这是DI发生的核心阶段。初始化如果Bean实现了InitializingBean接口会调用afterPropertiesSet()方法如果配置了init-method也会在此调用。此时Bean的所有依赖都已就位可以进行一些自定义的初始化工作。就绪Bean完全初始化完毕放入容器的单例池中等待被其他Bean获取和使用。销毁容器关闭时如果Bean实现了DisposableBean接口或配置了destroy-method会执行销毁逻辑。对于构造器注入步骤1和2是合并的容器需要先解析出构造器所有参数对应的Bean然后才能调用构造器完成实例化。这为循环依赖埋下了伏笔。4.2 循环依赖的典型场景与三级缓存机制什么是循环依赖很简单A依赖B同时B也依赖A。Service public class AService { Autowired private BService bService; } Service public class BService { Autowired private AService aService; }如果是构造器注入Spring会直接报错因为无法决定先创建谁。但如果是字段注入或Setter注入Spring却能够成功启动这是如何做到的答案就是三级缓存。Spring容器内部维护了三个重要的Map被称为三级缓存一级缓存singletonObjects存放已经完全初始化好的单例Bean。我们平时getBean就是从这里取。二级缓存earlySingletonObjects存放早期的单例Bean对象。这些Bean已经实例化但尚未完成属性填充和初始化。用于解决循环依赖。三级缓存singletonFactories存放单例Bean的工厂对象ObjectFactory。这个工厂能在需要时返回该Bean的早期引用可能是原始对象也可能是经过AOP代理后的对象。破解循环依赖的流程以AService和BService字段注入为例开始创建AService。实例化AService调用构造器得到一个原始对象a。此时将a对应的ObjectFactory放入三级缓存。开始为a进行属性填充。发现它依赖BService。转而开始创建BService。实例化BService得到原始对象b。将b对应的ObjectFactory放入三级缓存。开始为b进行属性填充。发现它依赖AService。尝试获取AService。首先从一级缓存找没有从二级缓存找没有从三级缓存找找到了AService的ObjectFactory。调用这个ObjectFactory.getObject()。这是一个关键点如果AService不需要AOP代理则直接返回原始对象a如果需要例如AService被Transactional标注则这里会返回一个AOP代理对象但此时代理对象内部的target仍是原始对象a。将这个早期对象可能是a也可能是a的代理放入二级缓存并从三级缓存移除其工厂。BService成功获取到AService的早期引用可能是代理完成属性填充然后进行初始化。完成后将完整的BService对象b放入一级缓存并清理二、三级缓存中关于BService的记录。此时流程回到第3步AService拿到了完整的BService对象b完成属性填充和初始化。最后将完整的AService对象a或它的代理放入一级缓存。通过这个机制Spring在对象尚未完全初始化时就通过三级缓存暴露了一个“早期引用”从而打破了循环依赖的死锁。重要提示三级缓存主要解决的是单例Bean且是字段/Setter注入下的循环依赖。对于原型BeanScope(“prototype”)的循环依赖Spring直接会抛出异常因为容器不管理原型Bean的完整生命周期。尽管Spring提供了这个机制但在设计上应尽可能避免循环依赖因为它通常是代码结构不佳职责不清、耦合过高的信号。使用构造器注入能强制在编译期或启动期暴露这类问题。5. 自动装配的歧义与解决Primary, Qualifier与Resource当你的应用变得复杂同一个接口有多个实现类时Autowired就会遇到麻烦它默认按类型byType装配现在有多个相同类型的Bean该注入哪一个Spring会抛出NoUniqueBeanDefinitionException。public interface SmsService { void send(String message); } Service public class AliyunSmsServiceImpl implements SmsService { ... } Service public class TencentSmsServiceImpl implements SmsService { ... } Service public class OrderService { Autowired // 这里会报错找到两个SmsService类型的Bean private SmsService smsService; }有几种标准方式来解决这个歧义5.1 Primary设定默认首选在其中一个实现类上加上Primary注解它将成为该接口的默认实现。Service Primary // 当有多个SmsService时优先注入我 public class AliyunSmsServiceImpl implements SmsService { ... }这种方式简单粗暴适用于有一个明显是“默认”或“主要”实现的情况。但它的控制粒度较粗在OrderService里你无法选择注入TencentSmsServiceImpl。5.2 Qualifier按名称精确指定Qualifier注解可以伴随Autowired使用通过指定Bean的名称默认是类名首字母小写或通过Component(“beanName”)自定义来精确选择。Service public class OrderService { Autowired Qualifier(tencentSmsServiceImpl) // 指定注入名为tencentSmsServiceImpl的Bean private SmsService smsService; }或者你也可以在Bean定义处就声明Qualifier标签然后在注入处匹配标签。Service Qualifier(tencent) // 给这个Bean打上“tencent”的标签 public class TencentSmsServiceImpl implements SmsService { ... } Service public class OrderService { Autowired Qualifier(tencent) // 注入带有“tencent”标签的Bean private SmsService smsService; }Qualifier提供了更精细的控制适合根据不同场景注入不同实现的场景。5.3 ResourceJSR-250标准注解Resource是Java EE现Jakarta EE标准注解JSR-250Spring也支持它。它的行为和Autowired略有不同Resource默认按名称byName装配。如果未指定名称则使用字段名或属性名作为Bean名称查找。如果按名称找不到则会回退到按类型byType装配。它不支持Primary也不支持像Autowired那样的requiredfalse可选依赖。Service public class OrderService { Resource // 先找名为“smsService”的Bean找不到再找SmsService类型的Bean private SmsService smsService; Resource(name tencentSmsServiceImpl) // 明确指定Bean名 private SmsService smsService; }如何选择我的建议是在纯Spring环境中优先使用AutowiredQualifier因为它是Spring原生且功能最全的支持可选依赖、构造器注入等。如果需要按名称装配的语义非常明确或者希望代码减少对Spring特定注解的依赖虽然Resource是标准但通常还是和Spring一起用可以考虑Resource。Primary用于定义全局默认值。6. 条件化装配与Profile让Bean根据环境“智能”出现在实际项目中我们经常需要根据不同的环境开发、测试、生产或不同的条件如某个类是否存在、某个属性是否启用来动态决定是否注册某个Bean。Spring提供了强大的条件化装配机制。6.1 Profile环境隔离的利器Profile注解可以标注在Configuration配置类或Bean方法上表示只有在指定的Profile激活时对应的配置或Bean才会生效。Configuration public class DataSourceConfig { Bean Profile(dev) // 仅在 dev 环境激活 public DataSource devDataSource() { // 返回一个内存数据库或本地测试数据库 return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build(); } Bean Profile(prod) // 仅在 prod 环境激活 public DataSource prodDataSource() { // 返回生产环境的数据源 HikariDataSource ds new HikariDataSource(); ds.setJdbcUrl(jdbc:mysql://prod-db:3306/app); // ... 其他生产配置 return ds; } }激活Profile的方式有很多在application.properties中设置spring.profiles.activedev通过命令行参数--spring.profiles.activedev,debug或者通过环境变量等。Spring Boot的多Profile配置文件如application-dev.yml也是基于此机制。6.2 Conditional更精细的条件控制Conditional是比Profile更底层的条件注解。它接收一个或多个实现了Condition接口的类这些类决定了条件是否匹配。Spring Boot在此基础上提供了大量开箱即用的条件注解极大地简化了自动配置ConditionalOnClass当类路径下存在指定的类时生效。ConditionalOnMissingClass当类路径下不存在指定的类时生效。ConditionalOnBean当容器中存在指定的Bean时生效。ConditionalOnMissingBean当容器中不存在指定的Bean时生效。这是自动配置中非常关键的一个它保证了“用户自定义配置优先”。ConditionalOnProperty当指定的配置属性有特定值时生效。ConditionalOnWebApplication/ConditionalOnNotWebApplication根据是否是Web应用生效。你可以组合使用这些注解实现非常复杂的装配逻辑。例如Spring Boot的DataSourceAutoConfiguration类中就大量使用了ConditionalOnClass,ConditionalOnMissingBean等注解来确保只有在类路径下有对应的数据库驱动、且用户没有自己定义DataSourceBean时才自动配置一个默认的数据源。自定义条件你也可以实现自己的Condition。public class OnWindowsCondition implements Condition { Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { return context.getEnvironment().getProperty(os.name, ).toLowerCase().contains(win); } } Configuration public class CustomConfig { Bean Conditional(OnWindowsCondition.class) public SomeService windowsOnlyService() { return new WindowsSpecificService(); } }灵活运用Profile和Conditional系列注解可以让你的应用配置变得极其灵活和清晰轻松应对多环境部署和功能开关的需求。7. 常见陷阱与最佳实践从理论到生产的经验之谈理解了原理和用法在实际项目中依然会踩坑。下面是我总结的几个高频陷阱和对应的最佳实践。7.1 陷阱一滥用Autowired导致过度耦合症状到处都是Autowired甚至在一些工具类、POJO里也用了导致这些类无法脱离Spring容器进行单元测试或复用。最佳实践遵循依赖注入的原则只对由Spring管理的组件如Service,Controller,Repository,Configuration中的Bean使用Autowired。对于纯粹的数据对象、工具类如StringUtils、DateUtils应避免注入保持其纯净性。如果需要使用Spring管理的Bean可以考虑通过方法参数传递或者使用ApplicationContextAware接口需谨慎。7.2 陷阱二循环依赖虽可解但设计需反思症状利用字段注入和三级缓存项目中出现大量循环依赖代码结构混乱模块边界不清。最佳实践将三级缓存视为一种“安全网”而不是设计依据。优先使用构造器注入迫使你在编码阶段就思考并消除循环依赖。常见的解决手段包括提取公共逻辑将A和B共同依赖的逻辑提取到一个新的C类中。使用事件或消息将同步调用改为异步事件A完成某工作后发布事件B监听事件并处理解耦直接调用。应用层服务协调在更高的应用层如一个FacadeService中同时注入A和B由它来协调两者的调用打破A和B之间的直接依赖。7.3 陷阱三原型Bean注入单例Bean时的作用域失效症状在一个单例Bean中注入了一个原型Scope(“prototype”)Bean但每次从单例Bean中获取这个原型Bean时发现都是同一个实例。Component Scope(prototype) public class PrototypeBean { ... } Component public class SingletonBean { Autowired private PrototypeBean prototypeBean; // 问题在这里 public PrototypeBean getPrototypeBean() { return prototypeBean; // 每次返回的都是同一个实例 } }原因依赖注入只发生一次。在SingletonBean初始化时Spring注入了一个PrototypeBean实例之后这个字段引用的对象就固定了。解决方案方法注入Lookup Method Injection比较古老的方式通过抽象方法和CGLIB代理实现。使用ObjectFactory或Provider推荐Component public class SingletonBean { Autowired private ObjectFactoryPrototypeBean prototypeBeanFactory; // 或者使用 javax.inject.Provider // Autowired // private ProviderPrototypeBean prototypeBeanProvider; public PrototypeBean getPrototypeBean() { return prototypeBeanFactory.getObject(); // 每次调用getObject()都会返回新的实例 // return prototypeBeanProvider.get(); } }在每次需要时从ApplicationContext获取不推荐因为这增加了与容器的耦合。7.4 陷阱四静态字段/方法中无法直接使用Autowired症状在工具类的静态方法中想使用一个被Spring管理的Bean但直接Autowired一个静态字段是无效的Spring不会为静态字段注入。解决方案将工具类本身也交由Spring管理Component然后通过实例方法调用在需要的地方注入这个工具类实例。如果必须保持静态方法可以使用PostConstruct在初始化后为静态字段赋值不优雅。更优雅的方式是避免这种设计考虑是否真的需要静态方法。很多时候将工具类设计为单例Bean是更好的选择因为它同样保证了全局唯一性且能享受依赖注入的所有好处。依赖注入是Spring的灵魂理解它不仅仅是会用几个注解。从“什么是DI”到“为什么用DI”再到“Spring如何实现DI”以及“如何用好DI”这是一个层层递进的过程。掌握构造器注入、理解循环依赖的原理、熟练运用条件装配这些都能让你写出更健壮、更清晰、更易于测试的Spring应用。记住框架是工具好的设计思想才是根本。当你开始习惯性地思考“这个依赖应该怎么注入更好”时你就真正掌握了Spring DI的精髓。