
1. 项目概述为什么依赖注入是Spring的灵魂如果你问一个干了几年Java开发的朋友Spring框架里最核心、最离不开的概念是什么十有八九他会告诉你是“依赖注入”Dependency Injection简称DI。这玩意儿听起来有点学术但说白了就是一种让代码更“懒”、更“灵活”的编程思想。想象一下你家里装修需要一把电钻。传统的做法也就是我们常说的“主动创建”是你自己跑去五金店挑品牌、看功率、付钱然后把电钻扛回家。而依赖注入呢就好比你请了一个万能的管家你只需要在清单上写下“我需要一把能打混凝土墙的电钻”管家就会在合适的时机把一把符合要求的、甚至已经充好电的电钻悄无声息地放到你的工作台上。在Spring的世界里这个“管家”就是IoC容器。我们不再在A类内部用new关键字去创建它所依赖的B类对象而是通过声明的方式告诉容器“我A类需要B”。至于B从哪里来、是哪个具体实现、生命周期如何管理全部交给容器去操心。这么做带来的好处是实实在在的降低耦合度。类与类之间不再死死绑在一起就像电钻和你解耦了你随时可以要求管家换一把更高级的冲击钻而不用改动你墙上的插座线路。提高可测试性你想测试A类完全可以给它注入一个模拟的、行为可控的B类Mock对象而不是一个真实的、可能连带着数据库的复杂对象。代码更清晰业务逻辑的归业务逻辑对象组装和生命周期的归容器管理各司其职。我见过很多项目初期为了图快在Service里直接newDAO在Controller里直接newService。项目小的时候没问题一旦业务复杂起来想换个数据库实现或者给某个Service加个缓存代理就得把代码翻个底朝天牵一发而动全身。而从一开始就拥抱DI的项目进行这类改造往往就是改个配置或者注解的事情后期的维护成本和迭代速度天差地别。所以深入理解DI绝不是为了应付面试而是为了写出真正易于维护和扩展的代码。2. DI的核心机制与Spring容器工作原理要玩转依赖注入首先得弄明白Spring这个“管家”——IoC容器——是怎么工作的。它可不是一个简单的对象工厂而是一个精密的运行时环境管理着所有你声明为Bean的对象。2.1 IoC容器Bean的诞生与管理工厂Spring IoC容器的核心接口是BeanFactory它提供了配置框架和基本功能。而我们更常用的是它的增强版ApplicationContext它在BeanFactory的基础上增加了更多企业级功能比如国际化的消息访问、事件发布、应用层特定的上下文如WebApplicationContext用于Web应用等。你可以把BeanFactory理解为基础款工厂而ApplicationContext是旗舰款功能更全启动时就会预加载并初始化所有单例Bean。容器管理Bean的整个生命周期从读取配置XML或注解开始到实例化、属性填充、初始化再到最终销毁。这个过程是高度可定制的通过一系列的后置处理器BeanPostProcessor和生命周期回调接口如InitializingBean、DisposableBean我们可以在Bean生命周期的关键节点插入自己的逻辑。比如你可以在Bean属性设置完成后执行一些数据的校验或资源的加载。注意很多初学者会混淆BeanFactory和ApplicationContext。在绝大多数现代Spring Boot应用中我们直接使用的就是ApplicationContext例如通过SpringApplication.run()返回的。除非在资源极其受限的移动环境否则没有理由使用基础的BeanFactory。2.2 Bean的定义与装配模式告诉容器你需要管理哪些对象就是定义Bean。主要有三种方式XML配置最传统的方式在applicationContext.xml文件中通过bean标签定义。这种方式集中管理一目了然但在大型项目中XML文件会变得非常臃肿。bean iduserService classcom.example.service.impl.UserServiceImpl property nameuserDao refuserDao/ /bean bean iduserDao classcom.example.dao.impl.UserDaoImpl/注解配置目前的主流方式。通过在类上标注Component、Service、Repository、Controller等注解配合类路径扫描ComponentScan自动发现和注册Bean。在Spring Boot中这几乎是默认方式。Service public class UserServiceImpl implements UserService { Autowired private UserDao userDao; // ... }Java配置类一种类型安全、支持重构的配置方式。使用Configuration注解标注一个类在其中通过Bean注解的方法来定义Bean。这种方式特别适合集成那些没有源码的第三方库。Configuration public class AppConfig { Bean public UserService userService(UserDao userDao) { return new UserServiceImpl(userDao); } Bean public UserDao userDao() { return new UserDaoImpl(); } }装配模式指的是容器解决依赖关系、将Bean注入到需要它的地方的过程。Spring支持多种装配模式byName根据属性名与Bean的id进行匹配。byType根据属性的类型在容器中查找匹配的Bean。如果有多个同类型Bean则需要配合Qualifier指定名称。构造器注入通过构造方法的参数进行注入。这是Spring团队最推荐的方式因为它能保证注入的依赖不可变final字段并且能完全初始化对象。Setter方法注入通过setter方法进行注入灵活性高。在注解驱动开发中Autowired默认采用byType模式。如果找到多个匹配类型它会退而尝试byName即用字段名或参数名去匹配Bean名。2.3 依赖查找 vs. 依赖注入这是一个重要的概念区分。依赖查找Dependency Lookup是“主动拉取”代码主动向容器索要依赖比如调用applicationContext.getBean(“userService”)。而依赖注入是“被动接收”容器在创建Bean时主动将依赖“推”给它。依赖注入是更优秀的方式。它实现了“好莱坞原则”——“Don‘t call us, we‘ll call you.”别找我我会找你。组件完全不用关心依赖从哪里来只需要声明“我需要什么”这使得组件更加纯粹更易于测试和复用。在现代Spring开发中除了在极少数框架集成或底层扩展的场景你应该尽量避免使用getBean()这种依赖查找的方式。3. Spring实现DI的三种主要方式详解Spring提供了三种核心的依赖注入方式各有其适用场景和优缺点。理解它们你才能在不同的情况下做出最合适的选择。3.1 构造器注入强依赖的优先选择构造器注入是通过类的构造方法来注入依赖。在Spring 4.3之后如果一个类只有一个构造方法那么Autowired注解可以省略。这是Spring官方最推荐的注入方式。为什么推荐构造器注入不可变性Immutability依赖可以通过final关键字修饰这意味着一旦Bean被实例化其依赖就无法被改变。这符合不可变对象的设计原则能有效避免线程安全问题并且使对象的状态更加清晰可预测。完全初始化的状态对象在构造完成后所有必需的依赖都已就位处于一个完全可用、一致的状态。避免了Setter注入可能出现的“部分初始化”状态即调用了无参构造创建了对象但某些Setter还没被调用。清晰的强制依赖构造器的参数列表明确地告诉阅读代码的人“创建这个对象你必须提供这些东西。”这本身就是一种良好的文档。便于测试在单元测试中你可以直接通过new调用构造器来创建对象无需依赖Spring容器或复杂的Mock框架设置。实操示例Service public class OrderService { // 依赖声明为final必须在构造时注入 private final PaymentProcessor paymentProcessor; private final InventoryService inventoryService; // 单个构造方法Autowired可省略 public OrderService(PaymentProcessor paymentProcessor, InventoryService inventoryService) { this.paymentProcessor paymentProcessor; this.inventoryService inventoryService; } public void placeOrder(Order order) { inventoryService.reserve(order.getItemId(), order.getQuantity()); paymentProcessor.charge(order.getTotalAmount()); // ... 创建订单逻辑 } }在上面的OrderService中支付和库存服务是其核心、不可或缺的依赖使用构造器注入完美地表达了这种强依赖关系。3.2 Setter注入可选依赖与灵活配置Setter注入通过类的setter方法进行。它适用于那些非必需的、或者可能在对象生命周期中发生变化的依赖。适用场景可选依赖某个依赖可能有默认实现或者在某些部署环境下不需要。循环依赖Spring解决循环依赖的一种方式虽然循环依赖本身是设计问题应尽量避免。需要重新配置的依赖对象创建后依赖可能需要被动态替换。示例与注意事项Service public class DataExporter { private Formatter formatter; // 非final可选依赖 // Setter方法Autowired标注在此处 Autowired public void setFormatter(Nullable Formatter formatter) { this.formatter formatter; // 可能为null } public void export(Data data) { String output data.toString(); if (this.formatter ! null) { output this.formatter.format(data); } // ... 导出逻辑 } }实操心得对于Setter注入我个人的经验是谨慎使用。除非依赖真的是可选的或者框架有特殊要求如某些XML配置的Bean否则优先使用构造器注入。滥用Setter注入会让对象的状态管理变得复杂你无法保证在调用某个方法时其依赖是否已经正确设置。3.3 字段注入便捷但需知其局限字段注入是直接在字段上使用Autowired注解。这是最简洁、也是最常见但可能被滥用的方式。Service public class ProductService { Autowired private ProductRepository productRepository; // 直接注入字段 // ... }它的优点很明显代码极其简洁没有样板代码构造器或setter。但缺点更为致命破坏了封装性字段通常应该是私有的但通过反射进行的字段注入绕过了类的公共API构造器或setter使得依赖对外部不可见测试时无法通过常规手段注入Mock对象必须借助反射或Spring测试上下文。导致依赖不明显类有多少依赖哪些是强制的从类的外部无法一目了然。这不利于代码理解和维护。与不可变原则冲突字段不能是final的因为Spring需要在对象构造完成后通过反射来设置它。对容器强耦合这个类几乎无法脱离Spring容器进行实例化因为依赖注入的魔法只发生在容器管理之下。那么字段注入就一无是处吗也不是。在一些特定场景下它仍有价值配置类Configuration中的Bean引用因为配置类本身也是由容器管理的且其目的就是定义Bean使用字段注入很直观。测试类中的MockBean注入在Spring Boot测试中使用MockBean模拟依赖并注入到被测类用字段注入非常方便。简单的、非核心的工具类或辅助类。我的建议是在主要的业务逻辑组件如Service、Controller中坚决使用构造器注入。将字段注入视为一种有特定适用场景的“语法糖”而非默认选择。很多团队的代码规范会明确禁止在业务类中使用字段注入。4. 自动装配的深度解析与常见问题处理Autowired是自动装配的核心注解但它的行为并非总是那么直观。理解其背后的机制和如何处理歧义是避免踩坑的关键。4.1 Autowired的工作机制与歧义解决Autowired默认按**类型byType**进行装配。容器会查找所有与目标字段/方法参数类型匹配的Bean。问题就出在“匹配”上。场景一找到零个Bean如果容器中找不到匹配类型的Bean启动就会报错NoSuchBeanDefinitionException。解决方法是确保对应的类已被正确定义为Bean例如有Component注解且所在包被ComponentScan扫描到。场景二找到一个Bean这是最理想的情况直接注入。场景三找到多个Bean歧义性这是最常见的问题。例如我们有两个Formatter接口的实现Component public class JsonFormatter implements Formatter { /*...*/ } Component public class XmlFormatter implements Formatter { /*...*/ } Service public class ReportService { Autowired // 错误找到两个Formatter不知道注入哪一个 private Formatter formatter; }启动时会抛出NoUniqueBeanDefinitionException。Spring提供了多种方式来解决这种歧义使用Primary在其中一个实现类上标注Primary表示它是首选的自动装配候选者。Component Primary // 当有多个Formatter时优先选我 public class JsonFormatter implements Formatter { /*...*/ }使用Qualifier通过指定Bean的名称来精确选择。Qualifier的值通常是Bean的默认名称类名首字母小写或者你在Component、Bean中自定义的名称。Service public class ReportService { Autowired Qualifier(xmlFormatter) // 明确指定要注入名为‘xmlFormatter’的Bean private Formatter formatter; } Component(xmlFormatter) // 自定义Bean名称 public class XmlFormatter implements Formatter { /*...*/ }使用ResourceJSR-250这是Java标准注解默认按**名称byName**装配。Resource(name “xmlFormatter”)等同于AutowiredQualifier(“xmlFormatter”)。但在Spring生态中Autowired功能更强大支持构造器、可选依赖等所以更常用。排查技巧遇到NoUniqueBeanDefinitionException不要慌。首先看报错信息它会列出所有冲突的Bean定义。然后问自己这里到底该用哪个如果某个实现是大多数情况下的默认选择就用Primary。如果不同场景需要不同的实现就用Qualifier进行精确指定。这实际上是在促使你思考依赖的语义是好事。4.2 可选依赖与Nullable的应用有时依赖并不是必须的。比如上面DataExporter的例子Formatter是可选的。除了在Setter方法上注入我们还可以在字段、构造器参数、方法参数上使用Autowired(required false)。使用Nullable注解来自JSR-305或Spring。这是更优雅的方式明确表达了该参数可为空。Service public class DataExporter { private final Formatter formatter; // 构造器参数使用Nullable public DataExporter(Nullable Formatter formatter) { this.formatter formatter; } }当容器中存在FormatterBean时它会被注入如果不存在formatter字段就是null程序需要做好空值判断。4.3 多种注入方式的混合使用与最佳实践在一个项目中三种注入方式可能会共存。如何选择这里有一个我总结的实践指南强制依赖使用构造器注入这是黄金法则。对于服务Service、数据访问对象DAO/Repository、控制器Controller的核心依赖一律使用构造器注入。它能保证对象的完整性和不变性。可选依赖或可能变化的依赖使用Setter注入并配合Nullable或requiredfalse。例如一个可插拔的缓存管理器、一个可配置的日志处理器。字段注入仅在特定场景使用如Configuration配置类、RestControllerAdvice、测试类或者一些简单的、无状态的工具类Bean。在团队中应对此有明确的约定。一个混合使用的例子RestController RequestMapping(/api/users) public class UserController { // 强制依赖用户服务构造器注入 private final UserService userService; // 可选依赖审计日志Setter注入 private AuditLogger auditLogger; // 构造器注入主要依赖 public UserController(UserService userService) { this.userService userService; } // Setter注入可选依赖 Autowired(required false) public void setAuditLogger(AuditLogger auditLogger) { this.auditLogger auditLogger; } PostMapping public ResponseEntityUser createUser(RequestBody User user) { User created userService.createUser(user); // 使用可选依赖前判空 if (auditLogger ! null) { auditLogger.log(“User created: “ created.getId()); } return ResponseEntity.ok(created); } }这样的代码结构清晰依赖关系明确既保证了核心组件的稳定性又提供了必要的灵活性。5. 高级特性应对复杂依赖关系当项目变得复杂简单的单类型注入可能不够用。Spring提供了一些高级特性来处理更复杂的依赖场景。5.1 集合与Map的注入Spring可以自动将容器中所有特定类型的Bean注入到一个List、Set、Map甚至数组中。这在实现“策略模式”或“插件体系”时非常有用。示例一个支持多种格式的文件处理器public interface FileParser { boolean supports(String fileType); String parse(File file); } Component public class JsonParser implements FileParser { /*...*/ } Component public class XmlParser implements FileParser { /*...*/ } Component public class CsvParser implements FileParser { /*...*/ } Service public class FileProcessor { // 注入所有FileParser实现到一个List顺序不确定 Autowired private ListFileParser parsers; // 或者注入到Mapkey是Bean的名字 Autowired private MapString, FileParser parserMap; public String processFile(File file, String fileType) { for (FileParser parser : parsers) { if (parser.supports(fileType)) { return parser.parse(file); } } throw new UnsupportedOperationException(“Unsupported file type: “ fileType); } }List中的顺序默认是Bean定义的顺序但可以通过Order注解或实现Ordered接口来控制。Map的key默认是Bean的名称value是Bean的实例。5.2 Resource与Inject注解除了AutowiredSpring还支持JSR-250的Resource和JSR-330的Inject。它们功能类似但有细微差别特性Autowired (Spring)Inject (JSR-330)Resource (JSR-250)来源Spring框架Java标准需javax.inject包Java标准需javax.annotation包默认装配方式byTypebyTypebyName是否必需requiredtrue可设false必需无required属性必需支持Primary是是否支持Qualifier是是javax.inject.Qualifier是但语义不同更接近byName与Spring耦合强弱弱如何选择如果你追求与Spring框架解耦希望代码能在其他支持JSR-330的容器如Guice中运行可以使用Inject。如果你明确想按名称装配可以使用Resource。在纯粹的Spring项目中Autowired因其功能最全面、与Spring生态集成最紧密支持Primary、required等仍然是首选。5.3 处理循环依赖与代理机制循环依赖是指两个或多个Bean相互依赖构成一个环。例如A的创建需要BB的创建又需要A。这是一个设计上的“坏味道”应该通过重构代码如引入第三个类、使用事件、或重新划分职责来避免。然而Spring通过三级缓存等机制在一定程度上支持了Setter注入和字段注入的循环依赖构造器注入的循环依赖无法解决。其核心原理是在Bean完全初始化调用初始化方法之前提前将正在创建中的Bean的“早期引用”暴露出去。三级缓存简述一级缓存单例池存放完全初始化好的单例Bean。二级缓存存放早期的Bean引用已实例化但未填充属性、未初始化。三级缓存存放Bean工厂用于生成早期引用解决代理对象的问题。当A创建时实例化后将自己早期的引用放入三级缓存然后去填充属性B。发现B不存在则触发B的创建。B在填充属性A时从三级缓存中拿到了A的早期引用可能是一个代理对象从而完成创建放入一级缓存。然后A拿到完整的B继续完成自己的属性填充和初始化最终也放入一级缓存。重要警告不要依赖Spring解决循环依赖的能力这应被视为Spring容器的一个“逃生舱口”而非一个可依赖的特性。循环依赖会掩盖设计缺陷使代码结构混乱测试困难并可能在未来升级或改变注入方式时如改用构造器注入导致应用启动失败。在代码审查中发现循环依赖应该亮起红灯。6. 基于Java Config与条件化装配在现代Spring Boot应用中基于Java的配置Configuration和条件化装配Conditional是构建灵活、可配置应用的关键技术。6.1 Configuration与Bean的精细控制Configuration类相当于传统的XML配置文件但它是类型安全的并且可以在其中编写复杂的初始化逻辑。Bean注解的方法用于向容器注册Bean。一个经典的数据库配置示例Configuration public class DatabaseConfig { // 从配置文件读取属性如spring.datasource.url Value(“${spring.datasource.url}“) private String url; Value(“${spring.datasource.username}“) private String username; Value(“${spring.datasource.password}“) private String password; // 定义一个DataSource Bean方法名默认就是Bean的名称 Bean public DataSource dataSource() { HikariConfig config new HikariConfig(); config.setJdbcUrl(url); config.setUsername(username); config.setPassword(password); config.setMaximumPoolSize(20); config.setMinimumIdle(5); // ... 其他配置 return new HikariDataSource(config); } // 这个JdbcTemplate Bean依赖上面的dataSource Bean // Spring会自动将同容器内的dataSource Bean注入进来 Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } }这里的关键点Bean方法可以接收参数Spring会自动从容器中寻找匹配的Bean进行注入这本身就是一种依赖注入。你可以完全控制Bean的实例化过程进行复杂的配置。在Configuration类内部Bean方法之间的调用会被Spring拦截确保返回的是同一个单例Bean默认作用域下而不是每次调用都new一个新对象。6.2 Conditional与Profile实现环境隔离Conditional是Spring 4.0引入的强大注解它允许根据特定条件来决定是否注册某个Bean。Spring Boot在此基础上提供了大量开箱即用的条件注解如ConditionalOnClass、ConditionalOnProperty、ConditionalOnBean等。Profile是条件装配的一个特例它根据激活的Spring Profile来决定Bean是否生效。实战场景为开发和生产环境配置不同的BeanConfiguration public class CacheConfig { // 开发环境使用简单的内存缓存如Caffeine Bean Profile(“dev”) // 仅在‘dev’ profile激活时生效 ConditionalOnClass(name “com.github.benmanes.caffeine.cache.Caffeine”) public CacheManager devCacheManager() { CaffeineCacheManager cacheManager new CaffeineCacheManager(); cacheManager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES)); return cacheManager; } // 生产环境使用分布式缓存Redis Bean Profile(“prod”) // 仅在‘prod’ profile激活时生效 ConditionalOnClass(name “org.springframework.data.redis.connection.RedisConnectionFactory”) public CacheManager prodCacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofHours(1)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(config) .build(); } // 默认缓存当没有显式激活‘dev’或‘prod’ profile或者上述条件不满足时生效 Bean ConditionalOnMissingBean(CacheManager.class) // 容器中没有其他CacheManager时才生效 public CacheManager defaultCacheManager() { return new ConcurrentMapCacheManager(); // 简单的ConcurrentMap实现 } }通过这种方式你的应用可以根据运行环境自动切换底层实现无需修改代码。只需要在启动时通过--spring.profiles.activeprod来指定激活的Profile。6.3 自定义条件与模块化装配你还可以创建自己的条件注解实现更复杂的装配逻辑。只需要实现Condition接口并在matches方法中定义你的判断逻辑。示例根据系统属性决定是否启用某个功能模块// 1. 定义自定义条件注解 Target({ElementType.TYPE, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) Conditional(OnMonitoringEnabledCondition.class) // 关联条件类 public interface ConditionalOnMonitoringEnabled { } // 2. 实现Condition接口 public class OnMonitoringEnabledCondition implements Condition { Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { // 检查环境变量或系统属性 Environment env context.getEnvironment(); // 假设我们通过‘app.monitoring.enabled’配置来控制 return “true”.equalsIgnoreCase(env.getProperty(“app.monitoring.enabled”, “false”)); } } // 3. 使用自定义条件注解 Configuration ConditionalOnMonitoringEnabled // 只有配置为true时这个配置类才生效 public class MonitoringAutoConfiguration { Bean public MetricsCollector metricsCollector() { return new PrometheusMetricsCollector(); } }这种模式在Spring Boot Starter的内部被大量使用用于实现“自动配置”。它让功能的开启和关闭变得极其灵活和透明。7. 常见问题排查与性能优化实战在实际开发中依赖注入相关的问题层出不穷。这里我总结了一些最常见的问题和排查思路以及一些关于性能的考量。7.1 典型异常分析与解决速查表异常信息可能原因排查步骤与解决方案NoSuchBeanDefinitionException: No qualifying bean of type ‘X‘ available1. 类X没有被Spring管理缺少Component等注解。2. 包路径不在ComponentScan的扫描范围内。3. Bean的条件不满足如ConditionalOnClass所需的类不存在。4. 在Configuration类中Bean方法被误定义为private或final。1. 检查目标类是否有正确的Spring注解。2. 检查启动类或配置类上的ComponentScan确保路径包含目标类。3. 检查类路径下是否有条件注解所依赖的jar包。4. 确保Bean方法是public的。NoUniqueBeanDefinitionException: No qualifying bean of type ‘X‘ expected single matching bean but found 2容器中存在多个X类型的Bean自动装配时出现歧义。1. 使用Primary指定一个首选Bean。2. 使用Qualifier按名称精确指定要注入的Bean。3. 如果不需要自动装配改用Resource(name“beanName”)按名注入。4. 重新设计考虑是否真的需要多个同类型Bean或许可以用不同的接口或父类来区分。BeanCreationException: Error creating bean with name ‘A‘: Requested bean is currently in creation: Is there an unresolvable circular reference?存在构造器注入的循环依赖。Spring无法解决这种循环。这是设计问题必须通过重构代码解决。1. 分析A和B的依赖关系看能否将公共逻辑抽取到第三个类C中。2. 考虑使用Setter/字段注入不推荐仅是临时绕过。3. 使用Lazy注解延迟加载其中一个Bean打破初始化时的循环。4. 使用事件ApplicationEvent进行解耦A创建完成后发布事件B监听事件进行后续操作。BeanNotOfRequiredTypeException注入的Bean实际类型与期望类型不匹配。常见于代理场景如AOP、Transactional。1. 确认Bean是否被AOP代理如使用了Transactional,Async,Cacheable。代理对象是目标类的子类或接口实现。2. 如果注入的是具体类ConcreteClass而非接口Interface且该类被代理则可能类型不匹配。最佳实践是始终面向接口编程。3. 尝试注入接口类型。NullPointerException(在看似已注入的字段上)1. 字段注入的Bean为null可能是因为该类是new出来的而非Spring容器管理的。2. 在构造方法中使用了被Autowired的字段。1. 确保使用该字段的类本身也是Spring Bean例如Controller, Service并且是从容器中获取的如通过Autowired注入。2.绝对不要在构造方法中访问被Autowired或Resource标注的字段。依赖注入发生在对象构造之后。如果需要在初始化时使用依赖请实现InitializingBean接口或使用PostConstruct注解方法。7.2 依赖注入的性能考量与最佳实践依赖注入本身带来的性能开销在绝大多数应用中都可以忽略不计。主要的开销在于容器的启动过程扫描类路径、解析元数据、创建Bean定义、实例化并装配Bean。对于大型应用可能有成千上万个Bean启动时间会变长。优化建议合理使用Lazy注解对于那些启动时不一定用到、或者创建成本很高的Bean可以标注Lazy。这样容器启动时不会立即创建它只有在第一次被请求时才会初始化。但要注意这可能会将启动时的问题延迟到运行时才发现。Service Lazy // 延迟初始化 public class ExpensiveToCreateService { // 这个Bean可能依赖外部资源初始化很慢 }精确控制ComponentScan的范围不要盲目地扫描整个根包ComponentScan不带参数默认扫描启动类所在包及其子包。明确指定需要扫描的包路径避免扫描到不需要的第三方库可以显著减少启动时的类路径扫描时间。SpringBootApplication ComponentScan(basePackages {“com.yourcompany.core”, “com.yourcompany.web”}) public class Application { ... }优先使用构造器注入除了之前提到的设计优点从性能角度看构造器注入允许将依赖字段声明为final这有助于JVM进行优化。同时它避免了Setter方法调用或反射设值字段注入的开销虽然这个开销很小。注意Bean的作用域默认是单例singleton这是性能最好的。原型prototype作用域每次请求都会创建新实例开销较大。请求request、会话session作用域在Web应用中常用但也会增加复杂性。非单例Bean要谨慎使用确保其生命周期被正确管理。避免过度使用AOP代理Transactional,Cacheable,Async等注解会为Bean创建代理。如果一个Bean被多层代理比如既有Transactional又有Cacheable创建和调用的开销会增大。虽然对于大多数业务操作来说影响不大但在极端性能敏感的场景下需要留意。7.3 在单元测试中模拟依赖注入依赖注入的一大优势就是便于测试。我们可以在完全不启动Spring容器的情况下对单个组件进行单元测试。示例测试UserService假设UserService依赖UserRepository。// 生产代码 Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository userRepository; } public User getUserById(Long id) { return userRepository.findById(id).orElseThrow(() - new UserNotFoundException(id)); } } // 测试代码 (使用JUnit 5和Mockito) ExtendWith(MockitoExtension.class) // 启用Mockito class UserServiceTest { Mock // 创建一个Mock的UserRepository private UserRepository mockRepository; InjectMocks // 创建UserService并将上面的mock注入进去 private UserService userService; Test void getUserById_WhenUserExists_ShouldReturnUser() { // 1. 准备测试数据 Long userId 1L; User expectedUser new User(userId, “Alice”); // 2. 定义Mock行为当调用findById(1L)时返回包含expectedUser的Optional when(mockRepository.findById(userId)).thenReturn(Optional.of(expectedUser)); // 3. 执行被测方法 User actualUser userService.getUserById(userId); // 4. 验证结果 assertEquals(expectedUser, actualUser); // 可以验证Mock的交互 verify(mockRepository).findById(userId); } Test void getUserById_WhenUserNotExists_ShouldThrowException() { Long userId 999L; when(mockRepository.findById(userId)).thenReturn(Optional.empty()); // 断言会抛出特定异常 assertThrows(UserNotFoundException.class, () - { userService.getUserById(userId); }); verify(mockRepository).findById(userId); } }关键点使用构造器注入使得在测试中可以直接new UserService(mockRepository)非常简单。Mock创建了一个虚拟的、可控制行为的UserRepository。InjectMocks会自动创建UserService实例并尝试将Mock标注的字段注入进去通过构造器、setter或字段反射。通过when(...).thenReturn(...)来设定Mock对象的行为。测试只关注UserService自身的逻辑完全隔离了数据库等外部依赖。如果使用的是字段注入测试时就需要用反射来设置私有字段或者必须借助Spring的测试上下文这会让测试变得更复杂、更慢。这再次证明了构造器注入在可测试性上的优势。依赖注入是Spring框架的基石理解其原理和各种应用场景能让你设计出更松耦合、更易测试、更易维护的代码。从强制性的构造器注入开始谨慎处理可选依赖善用条件装配来适应不同环境并在测试中充分利用DI带来的便利这些都是从“会用Spring”到“精通Spring”的必经之路。