资讯详情

构造器已经能传对象了,Spring还帮我们做了什么?

📅 2026/10/29 10:23:24 | 华诺云谱 👁 阅读
构造器已经能传对象了,Spring还帮我们做了什么?
看 Spring 的介绍经常会看到一句话对象不再自己创建依赖而是由外部提供。这里的“提供”到底是怎么提供先不启动 Spring拿一笔收据通知试试。假设订单处理完了要调用receipt(42)发一条收据消息。为方便观察本文的发送器只打印或记录字符串不发送真实短信也不涉及扣款。先看这个对象为什么不好换最开始可以这样写classReceiptService{privatefinalConsoleSendersendernewConsoleSender();voidreceipt(intorderId){sender.send(receipt #orderId);}}receipt(42)调用sender.send(...)打印receipt #42很直接。现在想检查它发出的字符串对不对。每次看控制台当然也行不过如果有一个发送器能把消息先放到列表里我们就可以直接比较列表不需要靠眼睛找输出。问题来了ReceiptService已经在自己的字段上写死了new ConsoleSender()。外面就算准备好一个记录消息的对象也没有地方把它交进去。不妨先改这个位置classReceiptService{privatefinalSendersender;ReceiptService(Sendersender){this.sendersender;}voidreceipt(intorderId){sender.send(receipt #orderId);}}这里的Sender是接口只有一个send(String message)方法。ConsoleSender和记录消息的RecordingSender都实现它。注意receipt的操作没有变变化发生在对象创建时现在必须先给它一个发送器。RecordingSenderrecordingnewRecordingSender();ReceiptServiceservicenewReceiptService(recording);service.receipt(42);把这三句连起来看1. 创建 recording recording.messages [] 2. 创建 service把 recording 传进去 service.sender ------ recording 3. 调用 service.receipt(42) 顺着上面的连接调用 send 消息列表变成 [receipt #42]它没有复制一份发送器也没有根据名字临时再找一个而是保存了刚才传入的对象引用。后面我们就检查同一个recording.messages。换回控制台也不难ReceiptServiceservicenewReceiptService(newConsoleSender());两种组装方式都没有修改receipt。业务类仍然需要发送器只是不再把“必须用哪种发送器”写死在自己内部。到这里依赖注入已经发生了。没有用 Spring也可以通过构造器把依赖传进去。接口负责让不同实现遵守同一套调用方式构造器负责接收具体对象。别把这两件事都理解为某个注解的效果。那还需要 Spring 干什么如果程序就这两个对象刚才的手工组装完全够用。为了演示依赖注入没必要强行引入框架。不过把例子继续往外扩通知服务需要发送器订单服务需要通知服务和仓库仓库又需要数据源。入口处就得先准备底层对象再一层层传上去。如果其中一些对象需要共用还得保留引用避免每条组装路径都重新创建。我们还是只保留眼前这两个对象看看 Spring 接过组装以后代码放到了哪里Configuration(proxyBeanMethodsfalse)publicstaticclassConfig{BeanShop.RecordingSendersender(){returnnewShop.RecordingSender();}BeanShop.ReceiptServicereceiptService(Shop.Sendersender){returnnewShop.ReceiptService(sender);}}Shop只是下方完整示例的外层类名。先看sender()我们告诉 Spring调用这个方法可以得到一个发送器返回的对象要作为 bean 管理。这里的 bean可以先理解为容器按注册信息管理的对象。再看receiptService(...)的参数。代码里没有手工调用sender()。Spring 为这个参数解析依赖这个例子中容器里只有一个符合Shop.Sender类型的 bean于是把它传进来再执行方法体里的new ReceiptService(sender)。为什么特意说“只有一个”因为这里没有演示多个候选对象的选择。如果以后注册了两个发送器就还需要明确选择规则不能认为接口总能自动对应到你心里想的实现。启动入口变成了try(varcontextnewAnnotationConfigApplicationContext(Config.class)){Shop.ReceiptServiceservicecontext.getBean(Shop.ReceiptService.class);service.receipt(42);}Config.class是组装说明context是按这些说明启动的容器。取到服务之后调用方式仍然是receipt(42)。把前后放在一起原来main 负责 创建 recording 创建 service(recording) 调用 service.receipt(42) 现在main 启动容器交给它 Config 容器执行 sender() 得到 recording 容器执行 receiptService(...) 参数传入 recording 得到 service main 取出 service 调用 receipt(42)所以new并没有凭空消失配置方法里就有。接手的是这部分对象的组装和管理而不是 Java 从此换了一种创建对象的语法。再回头看“控制反转”这个名字最初由业务类自己决定并创建发送器改为构造器以后这个决定交给了外部在 Spring 版本里又由容器根据配置完成组装。这样看比先背“松耦合”更容易知道它具体改变了哪一处代码。如果没有注册发送器它会偷偷帮我们 new 吗把配置中的sender()去掉只留下BeanShop.ReceiptServicereceiptService(Shop.Sendersender){returnnewShop.ReceiptService(sender);}此时 Spring 知道怎么创建服务但不知道拿哪个发送器来填参数。本文运行的结果是容器启动时抛出UnsatisfiedDependencyException根因是缺少Shop.Sender类型的 bean。还没走到业务调用那一步。这也能回答一个容易混淆的地方类放在项目里甚至能正常new不代表它已经注册成 bean。本例没有做组件扫描容器也不会遍历所有类猜测哪个类是我们想要的发送器。完整程序中的missing模式保留了这份错误配置并检查异常的具体根因不是把任何异常都当成实验成功。同一个服务会每取一次就创建一次吗在这里没有修改作用域同一个容器里连续两次getBean(Shop.ReceiptService.class)拿到的是同一个对象。但是另外创建一个容器再取同类型的服务就不是刚才那个对象了。完整程序的输出是container recorded: [receipt #42] same container: true different containers: false spring checks passed所以这里的默认 singleton不能理解为整个 JVM 无论哪里都只能存在一个这种类的对象。我们自己new RecordingSender()也只是另一个普通对象并不会自动替换已经传给服务的那个发送器。现在可以回去读那句定义了“对象给出依赖由外部提供。”在这个例子里它对应的就是构造器参数、配置方法以及参数最终连向的那个具体对象。业务代码没有变成注解列表也不需要在receipt里面调用容器查找发送器。完整代码与运行下面两个 Java 文件放在同一目录没有包名。实验使用 JDK 17.0.12 和 Spring Framework 6.1.12版本固定是为了复现不是建议新项目选旧版本。这里只演示普通容器不涉及 Spring Boot 自动配置。Shop.java不依赖 Spring先单独运行它importjava.util.ArrayList;importjava.util.List;importjava.util.Objects;publicclassShop{publicinterfaceSender{voidsend(Stringmessage);}publicstaticclassConsoleSenderimplementsSender{publicvoidsend(Stringmessage){System.out.println(message);}}publicstaticclassRecordingSenderimplementsSender{finalListStringmessagesnewArrayList();publicvoidsend(Stringmessage){messages.add(message);}}publicstaticclassReceiptService{privatefinalSendersender;publicReceiptService(Sendersender){this.senderObjects.requireNonNull(sender);}publicvoidreceipt(intorderId){sender.send(receipt #orderId);}publicbooleanuses(Sendercandidate){returnsendercandidate;}}publicstaticvoidcheck(booleanvalue,Stringlabel){if(!value)thrownewAssertionError(label);}publicstaticvoidmain(String[]args){ReceiptServicelivenewReceiptService(newConsoleSender());live.receipt(42);RecordingSenderrecordingnewRecordingSender();ReceiptServicetestednewReceiptService(recording);tested.receipt(42);check(tested.uses(recording),same injected object);check(recording.messages.equals(List.of(receipt #42)),recorded content);check(!tested.uses(newRecordingSender()),not any Sender);System.out.println(manual checks passed);}}RecordingSender.messages保存收到的消息uses只为了检查对象身份不是业务必备接口。显式抛出AssertionError所以检查不依赖java -ea开关。SpringDemo.java包含正常配置和缺失依赖的配置importorg.springframework.beans.factory.NoSuchBeanDefinitionException;importorg.springframework.beans.factory.UnsatisfiedDependencyException;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;publicclassSpringDemo{Configuration(proxyBeanMethodsfalse)publicstaticclassConfig{BeanShop.RecordingSendersender(){returnnewShop.RecordingSender();}BeanShop.ReceiptServicereceiptService(Shop.Sendersender){returnnewShop.ReceiptService(sender);}}Configuration(proxyBeanMethodsfalse)publicstaticclassMissingConfig{BeanShop.ReceiptServicereceiptService(Shop.Sendersender){returnnewShop.ReceiptService(sender);}}publicstaticvoidmain(String[]args){if(args.length0args[0].equals(missing)){try(varignorednewAnnotationConfigApplicationContext(MissingConfig.class)){thrownewAssertionError(missing dependency unexpectedly accepted);}catch(UnsatisfiedDependencyExceptionexpected){Throwablerootexpected;while(root.getCause()!null)rootroot.getCause();Shop.check(rootinstanceofNoSuchBeanDefinitionException,missing Sender cause);Shop.check(root.getMessage().contains(Shop$Sender),missing exact type);System.out.println(missing Sender rejected at startup);}return;}try(varfirstnewAnnotationConfigApplicationContext(Config.class);varsecondnewAnnotationConfigApplicationContext(Config.class)){Shop.ReceiptServiceservicefirst.getBean(Shop.ReceiptService.class);Shop.RecordingSendersenderfirst.getBean(Shop.RecordingSender.class);service.receipt(42);Shop.check(service.uses(sender),injected container Sender);Shop.check(sender.messages.equals(java.util.List.of(receipt #42)),bean executed);booleansameservicefirst.getBean(Shop.ReceiptService.class);booleancrossservicesecond.getBean(Shop.ReceiptService.class);Shop.check(same,default singleton in same container);Shop.check(!cross,not JVM global singleton);Shop.check(!service.uses(newShop.RecordingSender()),new object not container bean);Shop.check(second.getBean(Shop.RecordingSender.class).messages.isEmpty(),containers independent);System.out.println(container recorded: sender.messages);System.out.println(same container: same);System.out.println(different containers: cross);System.out.println(spring checks passed);}}}先检查只用 Java 的版本javac-encoding UTF-8 Shop.java java Shop输出receipt #42 manual checks passedSpring 版本还需要依赖 jar。可在当前目录保存这份pom.xml使用已安装的 Maven 下载运行依赖projectxmlnshttp://maven.apache.org/POM/4.0.0xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersiongroupIdexample/groupIdartifactIdspring-assembly/artifactIdversion1.0/versiondependenciesdependencygroupIdorg.springframework/groupIdartifactIdspring-context/artifactIdversion6.1.12/version/dependency/dependencies/project下面是 Windows PowerShell 命令javac、java和mvn需要已在 PATH 中。依赖下载需要网络已缓存则可复用mvn org.apache.maven.plugins:maven-dependency-plugin:3.6.1:copy-dependencies-DincludeScoperuntime-DoutputDirectorylib javac-encoding UTF-8-cp.;lib/*Shop.java SpringDemo.java java-cp.;lib/*SpringDemo java-cp.;lib/*SpringDemo missing正常模式会检查消息内容、传入对象身份、同一容器内复用和不同容器之间的隔离。missing模式会出现 Spring 的启动失败日志随后打印missing Sender rejected at startup这是刻意执行的缺失依赖分支。本地验证直接使用已有的对应版本 jar未重新下载依赖。还把构造器里的this.sender ...故意改为自行创建控制台发送器程序能编译但“是否仍使用传入对象”的检查会失败。这个检查守住的正是文章开头想解决的问题。参考Spring Bean 文档、Spring Configuration 文档。本文只沿着对象组装讲 IoC 和构造器注入没有把 AOP、事务和 MVC 混成同一件事。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑