红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Spring Boot 学习笔记
  4. 正文

从零开始 Spring Boot 43:DI 注解

2023年6月18日 1199点热度 0人点赞 0条评论

spring boot

图源:简书 (jianshu.com)

Spring 通过注解实现 DI(依赖注入),本文详细讨论这些注解。

@Autowired

@Autowired是 Spring 定义的注解,属于包org.springframework.beans.factory.annotation。

@Autowired匹配 bean 的顺序是:

  1. 按类型(type)匹配

  2. 按限定符(qualifier)匹配

  3. 按名称(name)匹配

@Autowired是最常用的注解,用它可以通过属性注入、Setter注入、构造器注入来实现DI。在前文中讨论过这些内容,这里不再重复。

筛选备选项

一般来说,用@Autowired注入时必须存在类型匹配的唯一的 bean,没有匹配到和匹配到一个以上的 bean 都会报错:

@ToString
@EqualsAndHashCode
public abstract class Carrier {
    protected final String maker;
    protected final String model;
​
    protected Carrier(String maker, String model) {
        this.maker = maker;
        this.model = model;
    }
}
​
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Car extends Carrier {
    public Car(final String maker, final String model) {
        super(maker, model);
    }
}
​
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Motorcycle extends Carrier {
    public Motorcycle(final String maker, final String model) {
        super(maker, model);
    }
}
​
@Service
public class CarrierService {
    @Autowired
    private Carrier defaultCarrier;
​
    public Carrier getDefaultCarrier() {
        return defaultCarrier;
    }
}
​
@Configuration
public class WebConfig {
    @Lazy
    @Autowired
    private CarrierService carrierService;
​
    @Bean
    Car defaultCar(){
        return new Car("长安汽车","X5");
    }
​
    @Bean
    Motorcycle defaultMotorcycle(){
        return new Motorcycle("哈雷","A7");
    }
​
    @Bean
    ApplicationRunner applicationRunner(){
        return args -> {
            Carrier defaultCarrier = carrierService.getDefaultCarrier();
            System.out.println(defaultCarrier);
        };
    }
}

这里定义的两个 bean 都具备Carrier类型,因此用@Autowired注入Carrier defaultCarrier属性时会报错,Spring 不知道要注入的是哪一个。

可以通过@Qualifier缩小筛选范围:

@Service
public class CarrierService {
    @Autowired
    @Qualifier("defaultCar")
    private Carrier defaultCarrier;
    // ...
}

示例中使用@Qualifier("defaultCar")将注入限定为名称为defaultCar的 bean,因此可以正常注入。

除了使用限定符,还可以使用名称进行筛选,@Autowired使用属性名作为 bean 名称进行匹配:

@Service
public class CarrierService {
    @Autowired
    private Carrier defaultCar;
    // ...
}

在用于名称匹配时,@Autowired并不好用,因为它只能用属性名作为 bean 名称匹配,@Resource则可以用name属性指定 bean 名称,而@inject可以使用@Named注解指定 bean 名称。

@Qualifier还可以更灵活地对 bean 进行筛选,详细内容可以阅读我的另一篇文章。

除了@Qualifier,还可以使用@Primary指定一个主要的 bean 作为有多个备选项时的首选注入,详细情况可以参考我的同一篇文章。

多构造器注入

如果 bean 定义中只有一个构造器,则不需要用@Autowired标记该构造器。如果存在多个构造器,我们可以指定其中一个用于注入:

@Component
@ToString
public class MultiConstructorExample {
    private final Car car;
    private final Motorcycle motorcycle;
    private final String msg;
    private static final String DEFAULT_MSG = "hello";
​
    public MultiConstructorExample(Car car, Motorcycle motorcycle, String msg) {
        this.car = car;
        this.motorcycle = motorcycle;
        this.msg = msg;
    }
​
    @Autowired
    public MultiConstructorExample(Car car, Motorcycle motorcycle) {
        this(car, motorcycle, DEFAULT_MSG);
    }
}

如果有多个构造器都可以用于注入,并且我们希望 Spring 能根据匹配结果选择一个“合适”的构造器用于注入,可以:

@Component
@ToString
public class MultiConstructorExample {
    // ...
​
    @Autowired(required = false)
    public MultiConstructorExample(Car car, Motorcycle motorcycle, String msg) {
        // ...
    }
​
    @Autowired(required = false)
    public MultiConstructorExample(Car car, Motorcycle motorcycle) {
        this(car, motorcycle, DEFAULT_MSG);
    }
}

注意,此时多个用于注入的构造器必须用@Autowired(required = false)标记,如果存在一个用@Autowired标记的构造器,将抛出一个这样的异常:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'multiConstructorExample': Invalid autowire-marked constructors: [public com.example.dianno.entity.MultiConstructorExample(com.example.dianno.entity.Car,com.example.dianno.entity.Motorcycle,java.lang.String)]. Found constructor with 'required' Autowired annotation: public com.example.dianno.entity.MultiConstructorExample(com.example.dianno.entity.Car,com.example.dianno.entity.Motorcycle)
...

大意是“存在多个用于注入的构造器”,且存在一个required=true的构造器,这种情况不被允许。

此时,Spring 将按照构造器参数去匹配 bean,并且选择匹配结果最多的构造器进行注入:

@Configuration
public class WebConfig {
    // ...
    @Bean
    String msg(){
        return "This is a String bean.";
    }
}

如果我们添加了上边这样的 bean,就可以看到输出:

MultiConstructorExample(car=Car(super=Carrier(maker=长安汽车, model=X5)), motorcycle=Motorcycle(super=Carrier(maker=哈雷, model=A7)), msg=This is a String bean.)

这说明构造器MultiConstructorExample(Car car, Motorcycle motorcycle, String msg)被调用并用于注入。否则,将看到MultiConstructorExample(Car car, Motorcycle motorcycle)被调用。

当然,无论如何,这两个构造器都需要一个Car类型的 bean,以及一个Motorcycle类型的 bean,缺少任意一个都将导致程序运行失败,因为没有合适的构造器用于创建对象。

此时可以指定一个默认的构造器:

@Component
@ToString
public class MultiConstructorExample {
    // ...
    public MultiConstructorExample(){
        this(null, null, DEFAULT_MSG);
    }
}

@Resource

@Resource是 Java 扩展包的注解,属于包javax.annotation.Resource。

@Autowired按照类型优先进行匹配,@Resource按照名称优先进行匹配,其匹配顺序是:

  1. 按名称(name)匹配

  2. 按类型(type)匹配

  3. 按限定符(qualifier)匹配

修改之前的示例,使用@Resource进行依赖注入:

@Service
public class CarrierService {
    @Resource
    private Carrier defaultCarrier;
    // ...
}

会报错,因为不能按照defaultCarrier找到 bean,如果按照类型Carrier匹配,能匹配到2个。

可以通过@Resource的name属性指定要匹配的 bean 名称:

@Service
public class CarrierService {
    @Resource(name = "defaultCar")
    private Carrier defaultCarrier;
    // ...
}

或者也可以修改属性名称:

@Service
public class CarrierService {
    @Resource
    private Carrier defaultCar;
​
    public Carrier getDefaultCarrier() {
        return defaultCar;
    }
}

或者按照类型匹配,提供一个唯一 bean 对应的类型:

@Service
public class CarrierService {
    @Resource
    private Car defaultCarrier;
    // ...
}

最后,同样可以使用@Qualifier:

@Service
public class CarrierService {
    @Resource
    @Qualifier("defaultCar")
    private Carrier defaultCarrier;
    // ...
}

@Resource还可以通过 Setter 注入,用法与属性注入一致,这里不再说明。

@Inject

@Inject同样属于 Java 扩展包,属于包javax.inject.Inject。

@Inject的匹配顺序是:

  1. 按类型匹配

  2. 按限定符匹配

  3. 按名称匹配

要使用@Inject注解要先添加依赖:

<dependency>
    <groupId>jakarta.inject</groupId>
    <artifactId>jakarta.inject-api</artifactId>
    <version>2.0.1</version>
</dependency>

先看按类型匹配的示例:

@Service
public class CarrierService {
    @Inject
    private Car defaultCarrier;
    // ...
}

如果按类型匹配出多个候选项,可以通过限定符进一步匹配:

@Service
public class CarrierService {
    @Inject
    @Qualifier("defaultCar")
    private Carrier defaultCarrier;
    // ...
}

也可以通过名称进一步匹配:

@Service
public class CarrierService {
    @Inject
    @Named("defaultMotorcycle")
    private Carrier defaultCarrier;
    // ...
}

@Named同样属于 Java 扩展包,属于包jakarta.inject.Named。

如果@Named注解没有起作用,可以检查一下 maven 依赖中的jakarta.inject模块的版本是否正确。

@Inject用于 Setter 注入的方式与属性注入类似,这里不再赘述。

@Inject也可以用于构造器注入:

@Service
public class CarrierService {
    private Carrier defaultCarrier;
​
    @Inject
    public CarrierService(@Named("defaultCar") Carrier defaultCarrier) {
        this.defaultCarrier = defaultCarrier;
    }
    // ...
}

注入集合

可以通过@Autowired注入集合(Collection),且此时如果用构造器注入且缺省 bean 时会有一些不一样的行为,这些在另一篇文章中有过介绍。

BeanPostProcessor

在官方文档中,有一段这样的描述:

@Autowired 、 @Inject 、 @Value 和 @Resource 注释由Spring BeanPostProcessor 实现处理。这意味着您不能在自己的 BeanPostProcessor 或 BeanFactoryPostProcessor 类型(如果有的话)中应用这些注释。这些类型必须通过使用XML或Spring @Bean 方法显式地“连接”。

但实际测试我发现在 BeanPostProcessor中依然可以用@Resource进行连接:

@Component
@Log4j2
public class MyBeanPostProcessor implements BeanPostProcessor {
    @Resource(name = "defaultMotorcycle")
    private Carrier defaultCarrier;
​
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }
​
    @SneakyThrows
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof CarrierService) {
            var cs = (CarrierService) bean;
            var defaultCarField = cs.getClass().getDeclaredField("defaultCar");
            defaultCarField.setAccessible(true);
            defaultCarField.set(bean, defaultCarrier);
            log.info("CarrierService's defaultCarrier field is set: %s".formatted(defaultCarrier));
        }
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
}
​
@Service
public class CarrierService {
    private Carrier defaultCar;
    // ...
}

当然,按照官方文档的建议,对BeanPostProcessor中的属性进行"手动连接"同样可行:

@Log4j2
public class MyBeanPostProcessor implements BeanPostProcessor {
    private final Carrier defaultCarrier;
​
    public MyBeanPostProcessor(Carrier defaultCarrier) {
        this.defaultCarrier = defaultCarrier;
    }
    // ...
}
​
@Configuration
public class WebConfig {
    // ...
    @Bean
    MyBeanPostProcessor myBeanPostProcessor(){
        return new MyBeanPostProcessor(defaultMotorcycle());
    }
}

总结

如果需要按照类型优先匹配,可以使用@Autowired或@inject,如果需要按照 bean 名称优先匹配,可以使用@Resource。

本文的所有示例代码可以从这里获取。

参考资料

  • 从零开始 Spring Boot 27:IoC - 红茶的个人站点 (icexmoon.cn)

  • 从零开始 Spring Boot 36:注入集合 - 红茶的个人站点 (icexmoon.cn)

  • Using JSR 330 Standard Annotations :: Spring Framework

  • Wiring in Spring: @Autowired, @Resource and @Inject | Baeldung

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: di 依赖注入 注解
最后更新:2023年6月19日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

取消回复

*

code

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号