Spring中如何为带参数化构造函数的类实现依赖注入
解决方案
核心问题在于你将持有运行时动态参数的类直接声明为Spring单例Bean:Spring容器启动阶段就会尝试实例化单例Bean,而selectedOptions、image两个参数只有在请求处理阶段才能拿到,自然无法直接注入。
以下是两种生产环境常用的最佳实践,优先推荐第一种工厂模式实现。
方案1:工厂模式(可读性、可测试性最优)
思路是将Spring托管的固定依赖和运行时动态参数拆分:Spring只管理无动态参数的工厂类,工厂提前注入好需要的Spring Bean,业务逻辑拿到运行时参数后,通过工厂方法创建目标类实例。
步骤1:移除目标类的Spring注解
ExtractDataFromImage不需要直接被Spring扫描为Bean,去掉类上的@Component和字段上的@Autowired,统一用构造函数传参,字段加final保证不可变:
public class ExtractDataFromImage implements ExtractData{ private final Map<String, Boolean> selectedOptions; private final BufferedImage image; private final TesseractService tesseractService; public ExtractDataFromImage( Map<String, Boolean> selectedOptions, BufferedImage image, TesseractService tesseractService ) { this.selectedOptions = selectedOptions; this.image = image; this.tesseractService = tesseractService; } @Override public Map<String, ArrayList<String>> extractData() { // 业务逻辑 } }
步骤2:创建Spring托管的工厂类
工厂类只持有Spring可直接注入的固定依赖,提供创建目标实例的方法接收运行时参数:
@Component public class ExtractDataFromImageFactory { private final TesseractService tesseractService; // Spring 4.3+ 单构造函数无需显式标注@Autowired public ExtractDataFromImageFactory(TesseractService tesseractService) { this.tesseractService = tesseractService; } public ExtractDataFromImage create(Map<String, Boolean> selectedOptions, BufferedImage image) { return new ExtractDataFromImage(selectedOptions, image, tesseractService); } }
步骤3:在控制器中注入工厂使用
请求进入拿到动态参数后,调用工厂方法创建实例即可:
@RestController public class ImageExtractController { private final ExtractDataFromImageFactory dataFactory; public ImageExtractController(ExtractDataFromImageFactory dataFactory) { this.dataFactory = dataFactory; } @PostMapping("/image/extract") public Map<String, ArrayList<String>> handleExtract( @RequestParam Map<String, Boolean> selectedOptions, @RequestParam MultipartFile imageFile ) throws IOException { BufferedImage image = ImageIO.read(imageFile.getInputStream()); ExtractData extractor = dataFactory.create(selectedOptions, image); return extractor.extractData(); } }
该方案无Spring黑魔法,依赖关系完全显式,单元测试时可以直接Mock工厂和依赖,是工业界最常用的实现方式。
方案2:原型作用域+ObjectProvider(轻量场景适用)
如果不想额外编写工厂类,可以将目标类声明为原型作用域Bean,通过ObjectProvider在运行时获取新实例并传入动态参数。
步骤1:修改目标类作用域
给目标类加原型作用域注解,保留全参构造:
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Component public class ExtractDataFromImage implements ExtractData{ // 类内容和方案1中的目标类一致,保留全参构造即可 }
步骤2:在控制器中通过ObjectProvider获取实例
不要直接注入ExtractDataFromImage,否则单例控制器只会持有一次创建的原型实例,失去动态传参能力:
@RestController public class ImageExtractController { private final ObjectProvider<ExtractDataFromImage> extractorProvider; public ImageExtractController(ObjectProvider<ExtractDataFromImage> extractorProvider) { this.extractorProvider = extractorProvider; } @PostMapping("/image/extract") public Map<String, ArrayList<String>> handleExtract( @RequestParam Map<String, Boolean> selectedOptions, @RequestParam MultipartFile imageFile ) throws IOException { BufferedImage image = ImageIO.read(imageFile.getInputStream()); // 传入运行时参数,剩余构造参数由Spring自动从容器注入 ExtractData extractor = extractorProvider.getObject(selectedOptions, image); return extractor.extractData(); } }
- 该方案要求Spring版本≥4.3,
getObject方法支持传入构造函数所需的动态参数,未传入的参数会自动从容器中查找匹配的Bean注入 - 每次调用
getObject都会创建新的目标类实例,符合请求级别的实例隔离需求
注意事项
- 不要在带运行时参数的类上使用默认的
singleton作用域,否则Spring启动阶段就会尝试实例化,因找不到动态参数抛出注入异常 - 避免同时使用字段注入和构造函数注入同个依赖,统一使用构造函数注入+
final字段的写法,保证依赖不可变、初始化完整 - 不要直接用
new关键字创建目标类实例,否则实例内的Spring依赖(如TesseractService)不会被注入,会出现空指针问题
内容的提问来源于stack exchange,提问作者Toji
相关产品推荐
相关产品推荐

