文档站点
Skip to content

容器与依赖注入 ​

Laravel 里你见过「服务容器」和「依赖注入」:app(UserService::class) 从容器里取对象,构造函数类型提示让容器自动注入。Spring 的 IoC 容器是同一套思想,只是默认全部用注解声明,而且「容器里的对象」有个专属名字叫 Bean。

什么是 IoC 容器 ​

IoC(Inversion of Control,控制反转):对象的创建和生命周期管理,不归你管,归容器管。

对比一下:

php
// Laravel / PHP:自己 new,或从容器取
$service = new UserService(new UserRepository());
// 或
$service = app(UserService::class);
java
// Spring:只需要声明"我要一个 UserService"
@Service
public class PostService {
    private final PostRepository repository;

    public PostService(PostRepository repository) {   // 构造函数声明需求
        this.repository = repository;
    }
}

注意:PostService 自己从不 new PostRepository,也不从什么全局函数取。它只是「声明需求」,容器在创建它时自动把 PostRepository 传进来。这就是依赖注入(DI)。

为什么叫"反转"?

在 Laravel/Spring 里,不是你的代码去「拉取」依赖(new、app()),而是容器「推送」依赖进来(构造器参数)。控制权反转了,所以叫控制反转。

容器里的对象叫 Bean ​

容器里管理着成千上万的对象,统称 Bean。把类交给容器,叫「注册为 Bean」。Spring Boot 里三种最常用的注册方式:

注解用途类比 Laravel
@Component通用组件让容器管理这个类
@Service业务服务层(语义更清晰)其实也是 Component
@Repository数据访问层(自带异常转换)也是 Component
@Controller / @RestController控制器也是 Component
@Configuration + @Bean手动装配一个对象在 ServiceProvider 的 register() 里绑定
java
@Service
public class PostService { ... }

加上 @Service,这个类的实例就归容器管了,任何地方需要它都会被自动注入。相当于 Laravel 里被容器管理的服务类(可能加 singleton() 绑定的那种)。

三种注入方式 ​

1. 构造器注入(推荐) ​

java
@Service
public class PostService {
    private final PostRepository repository;
    private final CacheService cache;

    public PostService(PostRepository repository, CacheService cache) {
        this.repository = repository;
        this.cache = cache;
    }
}
  • 依赖不可变(final),对象创建时依赖就齐了
  • 无法构造出不完整的对象,空指针风险最低
  • 测试时直接手动 new PostService(mockRepo, mockCache) 即可,不需要容器

2. 字段注入 ​

java
@Service
public class PostService {
    @Autowired
    private PostRepository repository;
}

写起来最短,但字段可能为 null、没法 final、测试要借助 Spring 容器。团队协作建议不用。

3. Setter 注入 ​

java
@Autowired
public void setRepository(PostRepository repository) {
    this.repository = repository;
}

用于依赖可变的场景,用得少。

只有一个构造器时可以省略 @Autowired

Spring 4.3 之后,只要类只有一个构造器,容器会自动用它的参数做注入,@Autowired 可以不写。上面的 PostService 就是这么工作的。

从容器取对象:类似 app() ​

PHP 里你随时 app(SomeClass::class);Java 里 Spring 也允许手动取,只是不太鼓励:

java
@RestController
public class DemoController {
    @Autowired
    private ApplicationContext context;   // 容器本体

    @GetMapping("/fetch")
    public Object fetch() {
        return context.getBean(PostService.class);  // ≈ app(PostService::class)
    }
}

日常开发你会用容器吗?几乎不会。因为注入就够了。手动 getBean 相当于 Laravel 里到处 app() 的坏味道,能少用就少用。

一个接口多个实现怎么办 ​

Laravel 用「接口 + 绑定哪个实现」解决;Spring 用 @Qualifier 或「按名字」指定:

java
public interface PaymentGateway {
    void pay(BigDecimal amount);
}

@Service
@Qualifier("alipay")
public class AlipayGateway implements PaymentGateway { ... }

@Service
@Qualifier("wechat")
public class WechatGateway implements PaymentGateway { ... }

注入时指名要哪个:

java
@Service
public class OrderService {
    private final PaymentGateway gateway;

    public OrderService(@Qualifier("wechat") PaymentGateway gateway) {
        this.gateway = gateway;
    }
}

对照 Laravel

Laravel:$this->app->bind(PaymentGateway::class, WechatPay::class)。Spring:接口 + @Qualifier("名字")。思想一致:面向接口编程,具体实现交给容器。

默认生命周期:单例 ​

Laravel 的容器绑定默认是 singleton;Spring 的 Bean 默认也是单例(一个类在容器里只有一个实例,全局共享)。这一点对并发、状态、性能影响巨大,详见 Bean 作用域与生命周期。

依赖注入解决了什么问题 ​

场景没有 DI 的写法有 DI 的写法
测试时换掉数据库访问改生产代码,或用复杂 mocknew PostService(mockRepository) 一行搞定
换支付渠道实现改所有 new 的地方只改容器的装配,调用方零改动
保证全局唯一连接到处传单例引用容器默认单例,天然只有一个

依赖注入不是「花架子」,它是 Java 世界实现 可测试、可扩展、低耦合 的基石。把这个概念吃透,后面所有章节都会顺畅很多。

面向 PHP / Laravel 开发者的 Spring Boot 中文文档