Spring 依赖注入的三种方式,踩过坑之后我才知道该用哪个
学 Spring 的时候,依赖注入这个概念本身不算难,但是注入方式有三种,教程里各写各的,看完之后我反而更纠结了:到底该用哪个?
后来在项目中三种都试过一遍,也踩了一些坑,总算搞清楚了它们各自适合什么场景。这里记录一下。
先回顾一下依赖注入是什么
用一句话说就是:你不用自己new对象了,Spring 容器帮你把需要的东西送过来。
举个例子,UserService需要用到UserRepository来查数据库。传统写法是在类里面直接new一个出来,但这样 UserService 就和某个具体的 Repository 实现绑死了。依赖注入的做法是,你只声明"我需要一个 UserRepository",Spring 容器会在运行时把合适的实现塞给你。
那问题就来了:Spring 是怎么把东西塞给你的?这就有了三种方式。
第一种:构造器注入
在构造函数里接收依赖。
@ServicepublicclassUserService{privatefinalUserRepositoryuserRepository;publicUserService(UserRepositoryuserRepository){this.userRepository=userRepository;}}这是 Spring 官方推荐的方式。我一开始不理解为什么推荐它,觉得写起来还挺啰嗦的,每个依赖都要写一遍构造函数参数。后来踩了几个坑才明白它好在哪里。
第一个好处是,对象在创建出来的那一刻,所有必需的依赖就已经到位了。不会出现用到一半发现某个依赖是 null 的情况。这个听起来理所当然,但其他方式真的不保证这一点。
第二个好处是依赖字段可以声明为final。final意味着这个引用一旦赋值就不会再变,在多线程环境下天然安全,不用担心哪个地方偷偷把它改了。
第三个好处是做单元测试的时候特别方便。脱离了 Spring 容器,你直接new UserService(mockRepository)就能测试,不需要启动任何 Spring 的东西。这个在写测试的时候体感很明显。
还有一个小细节:Spring 4.3 之后,如果一个类只有一个构造函数,@Autowired注解可以省略。所以写起来也没那么啰嗦了。
第二种:Setter 注入
容器先把对象创建出来,然后调用 Setter 方法把依赖传进去。
publicclassPaymentService{privatePaymentGatewaygateway;@AutowiredpublicvoidsetGateway(PaymentGatewaygateway){this.gateway=gateway;}}这种方式的好处是灵活。对象创建之后,你还可以在运行过程中重新调用 Setter 换一个依赖实现进去。
但问题也很明显:对象刚创建出来的时候,依赖可能还没注入。如果在这期间有人调了业务方法,gateway还是 null,直接就空指针了。
我在一个小项目里用过 Setter 注入,当时觉得代码看着还行。后来有次改代码,忘了配置某个 Bean 的注入,跑起来之后隔了很久才报 NullPointerException,排查半天才发现是 Setter 没被调到。如果是构造器注入,启动的时候就会报错,不用等到运行时才炸。
所以 Setter 注入比较适合那种"有没有都行"的依赖。比如某个服务有一个可选的通知功能,不配就用默认的,配了就覆盖。这种场景下 Setter 注入的灵活性就有用了。
第三种:字段注入
直接在字段上加@Autowired,不写构造函数也不写 Setter。
@ServicepublicclassOrderService{@AutowiredprivateOrderRepositoryorderRepository;}代码最少,一眼看过去特别干净。我刚开始学 Spring 的时候最喜欢这种写法,因为省事。
但用了一段时间之后,问题就出来了。
首先是依赖被藏起来了。你从类的构造函数和公开方法上完全看不出这个类需要什么才能运行。新接手的人看代码,以为OrderService很简单,拿来用才发现背后还藏着一个OrderRepository,得先把这个也配好才行。
其次是没法声明final。因为@Autowired字段注入是 Spring 通过反射在对象创建之后才赋值的,final字段不允许这样操作。
最让我头疼的是写测试的时候。你想给orderRepository传一个 Mock 对象进去,但它是 private 字段,也没有 Setter。要么启动 Spring 测试容器,要么用反射工具强行塞进去。一个简单的单元测试搞得特别复杂。
后来我翻了一些开源项目的代码,发现成熟的代码库几乎不用字段注入。Spring 官方也不推荐在生产代码里大量使用。
所以到底该怎么选
踩完坑之后,我自己的规则是这样的:
大部分情况下用构造器注入。代码虽然多写几行,但依赖关系清清楚楚,启动时就能检查完所有依赖,写测试也最方便。
Setter 注入留给可选依赖。就是那种"没有也能跑,有了更好"的场景。
字段注入能不用就不用。它写起来确实最省事,但省下来的那几行代码,后面排查问题和写测试的时候都得还回去。
最后放一张总结表,方便以后回来翻:
| 方式 | 代码量 | 依赖安全 | final | 测试友好 | 推荐场景 |
|---|---|---|---|---|---|
| 构造器注入 | 稍多 | 保证 | 可以 | 好 | 核心业务,绝大多数场景 |
| Setter 注入 | 中等 | 不保证 | 不行 | 一般 | 可选依赖,有默认值 |
| 字段注入 | 最少 | 不保证 | 不行 | 差 | 尽量别用 |
