ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring构造注入实战:解决空指针与循环依赖的推荐方案

Spring构造注入实战:解决空指针与循环依赖的推荐方案 你有没有遇到过这种奇怪的现象代码里明明写了一个Autowired字段启动时也没报错跑一段时间后突然某个依赖变成 null排查半天发现是对象被手动 new 出来的Spring 压根没管这个字段我反正是被这种问题折磨过很多次。后来把整个项目里的字段注入逐步改造成Spring的构造注入之后这类问题几乎绝迹了。这篇文章就把我对构造注入的理解、踩过的坑、以及它和 Spring Boot、Spring Cloud Alibaba、Spring AI 这些生态组件配合时的细节分享出来希望能帮你少走一点弯路。1. 为什么我推荐你优先用构造注入1.1 从一次空指针事故说起先说个我印象很深的事故。当时是一个订单服务代码里大量使用Autowired的字段注入比如Service public class OrderService { Autowired private ProductService productService; public BigDecimal calculateOrderAmount(Long productId) { return productService.getPrice(productId); } }表面上看没什么问题Spring 容器启动时只要 ProductService 这个 Bean 在容器里就能正常注入。但问题出在单元测试里或者某些定时任务、异步线程中有人直接new OrderService()去调用方法这时候productService就是 null直接抛空指针。我当时排查这个 Bug 花了一个下午最后发现是一个定时任务里手动了 new 对象而那个对象依赖链上有七八个字段注入的 Service全都在非 Spring 环境下变成了 null。构造注入为什么能根治这种问题因为依赖是通过构造函数参数传进来的你new OrderService(productService)的时候缺哪个参数编译器就直接报错根本不会把空对象放进去运行。这种“缺依赖就编译不过”的特性在我看来是它最大的价值也是我后来始终坚持用它的核心原因。1.2 三种注入方式到底差在哪Spring 官方文档里其实提到过三种依赖注入方式分别是字段注入Field Injection、Setter 注入Setter Injection、构造注入Constructor Injection。我在团队里做code review时经常看到大家混用三种方式我把它们的差异整理成了一张表对比维度字段注入Setter 注入构造注入代码简洁性最简洁一般稍显啰嗦依赖不可变性可被修改可被修改final 不可变依赖完整性编译期无法保证编译期无法保证构造期强制保证循环依赖默认支持默认支持不支持直接报错单元测试需要反射或 mock 工具需要调用 setter 逐个设置直接 new 传入参数与 Spring 容器耦合度高中低Spring 官方推荐度不推荐部分场景推荐说实话字段注入最大的问题不是性能而是“藏依赖”。一个类依赖了十几个其他类如果你全用字段注入看类定义根本看不出它依赖了谁只有逐个看字段才能发现。而构造注入把依赖关系全部暴露在构造函数签名里一眼就能看出这个类需要什么这种显式化对后期维护的帮助太大了。1.3 构造注入背后隐藏的设计哲学如果抛开 Spring 框架本身构造注入其实是一种很经典的面向对象设计原则叫“依赖倒置”或者说“组合优于继承”的具体落地。它的本质是对象在创建时就把依赖通过外部传入而不是在自己内部去 new 出来。这背后有几层设计考量第一是不可变性。用final修饰的字段一旦在构造函数中赋值之后整个生命周期内都不会被改变这在多线程环境下天然线程安全不用担心某个依赖在运行期被替换导致状态不一致。第二是依赖的显式化类到底依赖什么构造函数一目了然这在代码可读性上的提升是实打实的。第三是容器的解耦一个用构造注入的类其实完全不知道 Spring 的存在你完全可以在普通 Java 程序里手动 new 出来用这种“框架无关”的特性让代码的可测试性和可复用性大幅提升。因此构造注入不仅是 Spring 框架里的一种技术写法更是一种值得贯穿整个设计过程的思维方式。理解这一点之后你会发现它不仅适用于 Spring在任何 DI依赖注入容器里甚至写一个普通 Java 类时都值得优先考虑。2. 构造注入的核心细节与底层原理2.1 单构造函数为什么可以省略 Autowired先说一个很多初学者不知道的小细节。Spring 4.3 之后如果你在类里只写了一个构造函数那么即便你不加AutowiredSpring 也会自动把容器中的 Bean 注入到这个构造函数参数里。也就是说下面这段代码是合法的Service public class OrderService { private final ProductService productService; private final StockService stockService; public OrderService(ProductService productService, StockService stockService) { this.productService productService; this.stockService stockService; } }不需要在构造函数上标注任何注解Spring 看到唯一构造函数时会默认进行自动注入。这里有个隐含前提参数类型必须能在容器中找到对应的 Bean。如果有多个同类型的 BeanSpring 就不知道选哪个了就需要配合Qualifier或者Primary显式指定。2.2 多个构造函数时要怎么选开发中偶尔会遇到一个类有多个构造函数的重载场景。比如一个订单服务既想在 Spring 容器中注入完整依赖又想提供一种无依赖供纯业务测试使用。此时如果写了两个构造函数Spring 会因为没有唯一构造函数而不知道怎么选最终启动报错。解决方式有两种第一种是在指定构造函数上标注Autowired告诉 Spring 用这个构造函数Service public class OrderService { private final ProductService productService; private final StockService stockService; Autowired public OrderService(ProductService productService, StockService stockService) { this.productService productService; this.stockService stockService; } public OrderService() { this.productService null; this.stockService null; } }第二种是直接用Autowired(required false)配合一个无参构造函数实现“有依赖就注入没依赖就用无参构造”的降级逻辑。我个人经验是能不动多个构造函数就别动因为这会引入不必要的复杂性测试时可以单独用 Mock 手动构造不一定非要给生产类留无参构造。2.3 为什么说构造注入能“天然避免”循环依赖这里就要提到 Spring 三级缓存了。很多面试题里问“Spring 怎么解决循环依赖”标准答案都是通过三级缓存。但我发现很多人不理解三级缓存为什么解决不了构造注入的循环依赖我把 Bean 的完整创建流程拆开讲一下。Spring 容器创建 Bean 的过程大致分三个阶段实例化调用构造函数创建对象、属性填充给字段或 setter 赋值、初始化执行 Aware 回调和 PostConstruct 等。字段注入和 Setter 注入的循环依赖发生在属性填充阶段此时对象实例已经创建出来了虽然属性还不完整但已经有一个“半成品”可以放进三级缓存里让别人引用等对方创建完之后再回来把属性填上这就是三级缓存解决循环依赖的关键。而构造注入发生在实例化阶段此时对象根本还不存在你没法把一个不存在的对象放进缓存里去“打白条”。比如 A 的构造函数需要 BB 的构造函数需要 A那创建 A 时发现需要 B去创建 BB 又发现需要 A但 A 还在构造函数里没出来三级缓存里也找不到 A 的实例只能抛出BeanCurrentlyInCreationException。所以我常说构造注入不是不能解决循环依赖而是它把循环依赖问题直接“炸”出来了让你在启动阶段就发现代码设计有环而不是运行到某个时刻才暴雷。2.4 与 Bean 生命周期、Spring 容器启动流程的关联Spring 中 Bean 的生命周期大家应该都熟悉实例化、属性填充、初始化、使用、销毁。构造注入把“实例化”和“属性填充”合二为一了因为所有依赖都是在构造函数阶段就传入的。这一点会在某些场景产生微妙的影响。举个例子如果你在一个 Bean 的构造函数里读取了某个配置值而那个配置值本身依赖于另一个 Bean 的实例或者某个后置处理器BeanPostProcessor做的增强就会引发问题。因为构造阶段是整个生命周期最早的一步此时 BeanPostProcessor 可能还没有完全就绪你做不了太多的事。我之前遇到过有人在构造函数里调用Value注解解析出来的配置结果发现值还没解析出来这就是没理清容器启动流程导致的误用。正确的做法是在PostConstruct初始化阶段或者InitializingBean回调里处理配置读取。我建议所有写 Spring 的人都去看一眼 Spring 容器的启动流程源码重点关注AbstractApplicationContext.refresh()里的finishBeanFactoryInitialization这一步。理解了 Bean 是“分阶段初始化”的你就很容易明白构造注入为什么被官方推荐也更加清楚什么代码应该放在构造函数里、什么代码应该放在初始化阶段。3. 实操构造注入在 Spring Boot 项目中的完整落地3.1 一个最小可跑的 Spring Boot 3 示例项目空谈原理没有意义我直接给出一个基于 Spring Boot 3.2 的最小项目。首先创建一个 Maven 工程pom.xml里只需要引入 Web 依赖和 Lombok为了简化模板代码parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies然后写一个非常简单的业务结构包含一个 ProductService 和一个 OrderServiceOrderService 通过构造函数注入 ProductServiceService public class ProductService { public BigDecimal getPrice(Long productId) { // 模拟从数据库取价格 return new BigDecimal(99.80); } } Service public class OrderService { private final ProductService productService; public OrderService(ProductService productService) { this.productService productService; } public BigDecimal calculateOrderAmount(Long productId, int quantity) { return productService.getPrice(productId) .multiply(BigDecimal.valueOf(quantity)); } }Controller 再注入 OrderServiceRestController RequestMapping(/order) public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService orderService; } GetMapping(/amount) public BigDecimal amount(RequestParam Long productId, RequestParam int quantity) { return orderService.calculateOrderAmount(productId, quantity); } }启动后访问/order/amount?productId1quantity3返回299.40说明依赖链完全打通了。这套结构简单清晰核心思想就是每个 Bean 的依赖都在构造函数里显式声明Controller 依赖 ServiceService 依赖 Repository 或者更底层的组件形成一条清晰的依赖链。3.2 配置类中怎么用构造注入除了在Service、Component这类注解类里使用构造注入在Configuration配置类中也有一个非常经典的用法Bean方法参数注入。比如你写一个配置类需要给某个组件传入一个数据源Configuration public class AppConfig { Bean public RestTemplate restTemplate() { return new RestTemplate(); } Bean public PaymentService paymentService(RestTemplate restTemplate, DataSource dataSource) { return new PaymentService(restTemplate, dataSource); } }这里的paymentService方法参数restTemplate和dataSource会自动从容器中获取本质上也是对构造注入的一种运用。它比在类里塞一堆Autowired字段要干净得多因为配置类本身就是集中管理 Bean 的地方方法参数的显式传递让依赖一目了然。如果你用 Java Config 手动声明第三方的 Bean比如一个 RedisTemplate、一个 KafkaTemplate建议都用这种Bean方法参数注入的方式。它最大的优势是让配置类里的每个 Bean 都清楚自己依赖什么并且这些依赖的创建顺序由 Spring 根据依赖关系自动判断你不用关心谁先谁后。3.3 与 Spring Cloud Alibaba、Spring Security 等生态组件的搭配把构造注入放到一个更大的微服务体系里依然适用。我这几年做 Spring Cloud Alibaba 项目比较多就拿它举几个例子。在 Feign 客户端的使用上很多人的习惯是Autowired注入 FeignClient 接口FeignClient(name inventory-service) public interface InventoryClient { GetMapping(/inventory/{productId}) InventoryDTO getInventory(PathVariable Long productId); }其实完全可以换一种写法在某个Service的构造函数里声明类型为InventoryClient的参数Spring 会自动把 FeignClient 的动态代理对象注入进来。Feign 底层生成的是 JDK 动态代理用构造注入传进来的对象表面看是一个接口类型实际是代理类实例但这个细节你完全不用关心Spring 容器会处理好一切。同理还有 Sentinel 限流组件、Nacos 配置相关的 facade 类都能通过构造注入绑定到业务 Service 中。再比如 Spring Security 中常见的 SecurityFilterChain 配置Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception { return http .csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth.anyRequest().authenticated()) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) .build(); } }注意这里filterChain方法的参数http、jwtFilter都来自容器本质上仍然是构造注入的变体靠方法参数把依赖显式传递进来。在写 Spring Security 的各种自定义 Filter 时我也建议在 Filter 的构造函数里注入需要的 UserDetailsService、TokenManager 等组件这样你在过滤器链中拿到的依赖一定是最完整的不存在某个 Filter 直接被new出来而依赖缺失的问题。如果你刚开始接触 Spring AI构造注入同样适用。比如创建一个 ChatClientService public class AIService { private final ChatClient chatClient; public AIService(ChatClient.Builder builder) { this.chatClient builder.build(); } }ChatClient.Builder本身也是容器中的 Bean通过构造函数拿进来再 build这就是典型的“依赖一个构建器、自己产出所需组件”的构造注入模式。不管后面接的是通义千问、OpenAI 还是本地模型这个结构都不会变替换模型实现只需要改动配置。3.4 非 Spring 管理的类怎么复用构造注入的思路有些场景下你需要在非 Spring 管理的普通类里复用一套具备依赖的对象。比如一个工具类ExcelExporter里面需要 OrderService 提供数据。此时不能直接在工具类里Autowired因为工具类通常不是容器中的 BeanSpring 不会管理它的字段。我常用的做法有两种。第一种让工具类本身也变成 Bean注册到容器中这样可以正常用构造注入Component public class ExcelExporter { private final OrderService orderService; public ExcelExporter(OrderService orderService) { this.orderService orderService; } }第二种在某个 Spring Bean 中手动构造工具类并传参Service public class ExportFacade { private final OrderService orderService; public ExportFacade(OrderService orderService) { this.orderService orderService; } public void export() { ExcelExporter exporter new ExcelExporter(orderService); exporter.doExport(); } }这种“手动 new 时要记得把依赖全部传进去”的习惯就是构造注入思想在非容器场景下的延续。你会发现一旦习惯了这种写法你写代码时会更审慎地思考每个类的依赖边界而不是依赖容器帮你搞定一切。3.5 Spring Boot 2.x 与 3.x 版本差异如果你还在维护 Spring Boot 2.3.x 或者 2.6.x 的老项目上述构造注入的写法基本一致因为构造注入的核心行为从 Spring 4.3 开始就没有变过。但要特别注意 Java 版本和包路径的差异Spring Boot 3.x 基于 Spring Framework 6javax 包变成了 jakarta 包很多注解的包路径变了不过Autowired依然在org.springframework.beans.factory.annotation下不受影响。还有一个差异是循环依赖的默认处理策略。Spring Boot 2.6 之前循环依赖默认是允许的只是容器启动时会输出一条警告。从 2.6 开始Spring 官方把循环依赖的默认行为改成了“禁止”如果你的老项目用Autowired字段注入形成了环升级到 2.6 之后可能突然启动失败此时一定要借助报错信息找到环优先通过重构去掉而不是直接把spring.main.allow-circular-references设成 true 想绕过去。这一点我在 5.2 节还会重点讲。4. 构造注入与 AOP、事务协作的实战细节4.1 构造函数里调用依赖的方法为什么可能出问题用构造注入之后你很容易写出类似下面的代码Service public class OrderService { private final ProductService productService; public OrderService(ProductService productService) { this.productService productService; this.initPrice productService.initCache(); } }表面上看没问题但实际运行中我遇到过两个坑。第一个坑productService.initCache()调用的是一个由 AOP 代理增强的方法比如标注了Transactional的方法而 Bean 的代理对象通常是在 Bean 初始化阶段由BeanPostProcessor创建的构造发生时代理可能还没生成完毕或者代理生成过程中又会触发其他依赖创建容易引发奇怪的启动顺序问题。第二个坑你在构造函数里做耗时操作会显著拖慢 Spring 容器启动因为构造注入把依赖构建压缩到了创建阶段任何阻塞都会累积到启动流程里。我的建议是构造函数里只做“赋值”和“必要的防御性校验”不要做任何“业务调用”。如果你需要在 Bean 创建后做一些初始化缓存、预热逻辑请放到PostConstruct方法或者ApplicationRunner里那是 Spring 框架专门留给你的“启动后置动作”比在构造函数里挂业务逻辑安全得多。4.2 Spring 事务注解失效的经典场景构造注入本身不会导致Transactional失效但它会和“代理机制”叠加造成误判。我举一个真实踩过的例子Service public class OrderService { private final OrderRepository orderRepository; public OrderService(OrderRepository orderRepository) { this.orderRepository orderRepository; } Transactional public void createOrder(Order order) { orderRepository.save(order); this.notifyUser(order); } public void notifyUser(Order order) { // 做一些通知逻辑 } }这个例子其实没问题因为调用方是通过 Spring 容器拿到的代理对象再调用createOrder事务依然生效。事务失效真正高频的原因是“自调用”在类内部直接调用另一个被Transactional标注的方法绕过了代理对象。构造注入并不能解决自调用问题因为无论依赖以什么方式注入你拿到的最终都是同一个类实例内部方法之间调用天然不走代理。如果你必须在一个类里依赖同类的另一个方法的事务行为我建议拆类把带事务的方法放到另一个 Service 中然后通过构造注入进来这样不仅事务生效而且职责更清晰。这也是构造注入在事务场景下的一个隐性好处它倒逼你把事务边界和调用边界分开避免一个大 Service 里互相调用导致事务边界混乱。4.3 用构造注入写单元测试有多爽构造注入对单元测试的友好程度是字段注入无法比拟的。测试代码不需要启动 Spring 容器不需要反射直接 new 一个业务对象把 Mock 对象传进去就可以。比如class OrderServiceTest { Test void calculateOrderAmount_shouldMultiplyPriceByQuantity() { ProductService productService Mockito.mock(ProductService.class); when(productService.getPrice(1L)).thenReturn(new BigDecimal(99.80)); OrderService orderService new OrderService(productService); BigDecimal result orderService.calculateOrderAmount(1L, 3); assertEquals(new BigDecimal(299.40), result); verify(productService).getPrice(1L); } }整个测试创建对象的过程完全透明没有SpringBootTest的启动开销测试运行速度极快。这也是很多团队在 CI 中跑几百个单测但耗时只有几十秒的原因之一。字段注入的类要做单测就麻烦得多要么用反射设置私有字段要么依赖ReflectionTestUtils.setField代码很难看维护成本也高。4.4 构造注入时依赖多的情况怎么优雅起来一个类依赖四五个组件时构造参数会显得很长很冗长这时候有两种优化方向。第一种是语义上的把紧密耦合的依赖组合成一个门面类或聚合服务比如原来OrderService依赖UserClient、AddressClient、CouponClient可以抽象出一个CustomerInfoService把前三个依赖收进去再让OrderService只依赖这个聚合服务。第二种是用 Lombok 的RequiredArgsConstructor简化代码Service RequiredArgsConstructor public class OrderService { private final ProductService productService; private final StockService stockService; private final PromotionService promotionService; }RequiredArgsConstructor会对所有final字段生成构造函数这是目前团队中最常见的构造注入写法。不过我建议团队统一约定所有注入字段一律用final修饰这样既能享受 Lombok 的简化也能保证依赖不被运行期替换。5. 常见问题与排查技巧实录5.1 启动报错Parameter 0 of constructor required a bean of type xxx这是构造注入最常见的启动报错错误信息一般长这样Parameter 0 of constructor in com.example.OrderService required a bean of type com.example.ProductService that could not be found.看到这个报错不要慌按三个方向排查第一步确认ProductService类有没有标注Service、Component等注解或者有没有通过Bean方法注册到容器中。第二步确认ProductService是不是被ComponentScan扫描到了如果它所在的包和启动类不在同一个包或子包下需要手动添加ComponentScan或者使用Import。第三步如果容器中有多个ProductService实现比如ProductServiceImplA和ProductServiceImplBSpring 不知道选哪个也会报这个错这时应该在构造参数上加上Qualifier(productServiceImplA)或者在其中某一个实现类上标注Primary。从实际经验看第三类问题最多尤其是在抽象接口和多个实现的场景下。有个小习惯可以帮助快速定位把报错信息中的构造函数参数索引从 0 开始数一下对应的是构造函数的第几个参数然后用 ID 的依赖关系图插件查看这个类型的候选 Bean 列表。5.2 循环依赖启动失败是上 Lazy 还是重构Spring Boot 2.6 之后的启动报错常见这样一段The dependencies of some of the beans in the application context form a cycle这时很多人第一反应是在构造函数参数上加Lazy来打破循环。这个用法确实有效原理是让 Spring 先注入一个代理对象等到真正调用依赖时再从容器中获取完整实例相当于把“创建依赖”的动作延迟到了第一次使用。但它只是“绕过”了循环依赖并没有“消除”循环依赖如果团队里到处用Lazy绕过就会让依赖关系变得模糊出现运行期才暴露的问题。我个人的经验是遇到循环依赖先看依赖环出现在哪里。通常是因为两个 Service 互相调用造成的比如OrderService在处理订单时需要调用PromotionService而PromotionService又需要OrderService查询订单。此时可以直接把被环上的某一段依赖拆出去或者抽出第三个 Service 专门承载双方都用到的公共逻辑循环自然消失比用Lazy干净得多。5.3 Lombok RequiredArgsConstructor 的隐藏小坑RequiredArgsConstructor很香但有一个隐藏规则容易踩坑它只对final字段生成构造参数非 final 字段和NonNull字段的生成策略不同。如果你在类里写了一个非 final 字段并标注了Autowired同时RequiredArgsConstructor也在场这个字段仍会走字段注入这样整个类的注入方式就混用了破坏了构造注入的统一性。我在代码 review 时经常发现这种情况建议团队规范里明确规定使用RequiredArgsConstructor的类中所有注入项一律写private final非注入项也不能标Autowired。还有一个细节RequiredArgsConstructor生成的构造函数参数顺序默认按照字段在类中出现的顺序排列如果你把一个比较小的字段写在前面、把大量依赖写在后面构造参数的可读性会变差。建议把核心依赖字段放在类顶部其余依赖按依赖强度依次排列这能让构造函数签名更清晰。5.4 构造器参数爆炸了怎么办当构造函数的参数超过 4 个代码可读性就开始下降了。实践中我遇到过参数 7 个的 Service 类每次阅读和测试都是一场灾难。解决方案有三个方向第一用ConfigurationProperties把很多配类型参数收拢到一个配置对象里比如数据库连接、超时、重试次数等。第二把职责相近的依赖合并成一个聚合服务比如把多个*Repository封装成一个OrderReadService。第三使用 Builder 模式构造复杂依赖对象但这在 Spring 容器里通常不太适用更适合手动 new 的场景。这里说法不重要重要的是你要意识到构造参数过多往往是类职责过多的信号。与其在写法上抠细节不如重新审视类的拆分是否合理该拆的拆该合的合。5.5 面试官为什么喜欢问构造注入为什么 Spring 相关面试题里几乎必有一道构造注入因为这个问题能考核一个候选人太多内容Spring IoC 的核心概念、Bean 的生命周期、循环依赖和三级缓存原理、AOP 代理机制、代码设计原则。面试官通常从“你平时怎么注入依赖”问起接着追问“为什么不用字段注入”“构造注入遇到循环依赖怎么办”“三级缓存为什么解决不了构造注入的循环依赖”这一连串问题下来基本功扎实与否基本就能看出来。我在面试别人时最看重两点第一候选人能不能把三级缓存和构造注入的关系讲清楚讲到“实例化阶段还没产生对象无法放入缓存”说明真的理解了第二候选人有没有项目中使用构造注入踩坑的真实经历那种张口就是标准答案的人往往没有经过线上问题的拷打。如果你能把本文前面提到的那些实际场景和排查方法说给他听面试通过率会直线上升。写在最后的一点经验做了这么多年 Java 项目我越来越觉得构造注入不是一种“写法偏好”而是一种能影响代码质量的工程习惯。它把依赖关系显式化、让对象保持不可变、让单元测试变得简单还逼着你在设计阶段就把循环依赖、职责爆炸这些问题暴露出来。早年间我最怕看到“依赖都是注入的但一上线就空指针”这种问题而实践下来构造注入确实把这类问题挡在了编译期和启动期。最后分享一个小技巧。在 IDEA 里可以设置构造注入的生成模板在类中按Alt Insert选择Generate里的Constructor勾选你要注入的依赖字段IDEA 会自动生成完整的构造注入代码。配合 Lombok 的RequiredArgsConstructor日常开发效率能提升不少。如果你在项目里也遇到过字段注入带来的烦心事不妨从明天开始把新写的 Bean 全部改成构造注入试试我猜你很快会发现依赖问题从此安静了很多。
返回列表