跳到主要内容

依赖注入与注入模式

前言

在面试的时候几乎每个面试官经常会问到这样一个问题:你知道 Spring 吗?可以说下 Spring 的原理吗?

然后所有候选者都会很自觉的回答:嗯,Spring 是一个开源框架,该框架使用了两大核心技术,分别是 依赖注入(DI 或 控制翻转)和 AOP。然后吧啦吧啦...

等等,先打住,本节就来重温下 Spring 的依赖注入这个有意思的问题。不过在之前我们先来说下控制翻转这个名词,很多时候初学者会傻傻分不清控制翻转和依赖注入的关系。其实大白话说就是控制翻转是一个思想,而 DI 是具体的实现。

这个就像代理一样,然后我们天天说代理。那代理分为哪些呢?答曰动态代理和静态代理。那在 Java 中动态代理又有哪些技术呢?答曰 AOP 动态代理和 JDK 动态代理。

所以你发现没有,控制翻转也是一个思想。在 Spring 中有具体的两个实现:依赖注入 DI 和 依赖查找 DL(我们一般都统说为 DI)。

Spring 官网对依赖注入的描述

Dependency injection (DI) is a process whereby objects define their dependencies (that is, the other objects with which they work) only through constructor arguments, arguments to a factory method, or properties that are set on the object instance after it is constructed or returned from a factory method. The container then injects those dependencies when it creates the bean. This process is fundamentally the inverse (hence the name, Inversion of Control) of the bean itself controlling the instantiation or location of its dependencies on its own by using direct construction of classes or the Service Locator pattern.

文档地址:https://docs.spring.io/spring-framework/docs/5.1.x/spring-framework-reference/core.html#beans-factory-collaborators

知道这些后,我们再来看下依赖注入是如何实现的。我们写一个示例:

public class User {

private Order order;

public User(Order order) {
this.order = order;
}
}

public class Order {
// omit ...
}

public class App {
public static void main(String[] args) {
Order order = new Order();

User user = new User(order);
}
}

在这个示例中我们手动创建一个 Order 对象然后通过构造器参数传递给 User 对象。并且在 User 这个构造方法中将参数 order 赋值给 User 类中的 Order 属性,现在我们就可以在 user 对象中操作 order 对象了,这就是所谓的依赖注入!!!!

是不是有种大吃一鲸的感 jio?我个人理解所谓的依赖注入就是被注入者持有注入者的引用。所以我们只要能够做到 User 这个对象持有 Order 这个对象的引用那我们就能够说 order 注入到 user 了。

因此,在学习 Spring 的依赖注入时我们实际上的关注点是 order 这个对象是通过哪种方式传递给 user 的。这也是 Spring 依赖注入的核心点,也是本节的重点。

另外一点,各位除了依赖注入之外有没有听过与之相关的另一个名词:注入模式。

关于依赖注入模式在 Spring 中有一个相应的枚举类:org.springframework.beans.factory.annotation.Autowire。在这个枚举来中定义来三个枚举值,分别是 NOBY_NAMEBY_TYPE

public enum Autowire {

NO(AutowireCapableBeanFactory.AUTOWIRE_NO),

BY_NAME(AutowireCapableBeanFactory.AUTOWIRE_BY_NAME),

BY_TYPE(AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE);

// omit ...

}

这三个值就是所有的注入模式。

注意

实际上除了这三个模式之外还有一个 BY_CONSTRUCTORBY_AUTODETECT,而 BY_CONSTRUCTOR 实际上使用的就是 BY_NAMEBY_TYPE 技术,所以在枚举类中并没有定义,而 BY_CONSTRUCTOR 则是已经被废弃的注入模式,所以本篇不做任何说明。

有关注入模式的详细信息可以看下 org.springframework.beans.factory.config.AutowireCapableBeanFactory 类,所有的注入模式都是在该类中定义。

依赖注入注解:@Autowired

我们在使用 Spring 时需要将某个 Bean 注入到要使用的类时第一个会想到的可能就是 Spring 提供的 @Autowired 注解了。先看下该注入的定义:

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {

/**
* Declares whether the annotated dependency is required.
* <p>Defaults to {@code true}.
*/
boolean required() default true;

}

这个类中定义了一个 Boolean 类型的 required 方法,该值表示要注入的属性是否必须存在,即是否允许属性值为 NULL。当值为 true 时表示要注入的属性不能为 NULL,如果为 NULL 就会在运行时抛出异常,示例:

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'io.gitee.XX' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

下面我们来演示下 @Autowired 注解的使用。

定义几个类,都是很简单的代码(所有代码都定义在 io.gitee.ituknown.di 包中):

@Component
public class User {
}

@Component
public class Order {

@Autowired
private User user;

public Order() {
}

public void print() {
System.out.println("持有 User 对象: " + user);
}
}

@Configuration
@ComponentScan("io.gitee.ituknown.di")
public class Config {

}

之后定义一个 Main 测试类:

public class Main {

public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Config.class);

Order bean = applicationContext.getBean(Order.class);
bean.print();
}
}

当我们运行 Main 类时输出结果如下,这说明 User 这个对象被注入到了 Order 中。

持有 User 对象: io.gitee.ituknown.di.User@4df828d7

这说明了 User 之所以能够被注入到 Order 中与注解 @Autowired 有些关系。那我们就来移除掉这个注解,再次运行看输出结果:

持有 User 对象: null

结果显而易见。

@Autowired 进阶使用

我们都知道基于 JavaConfig 的配置形式定义的 Bean,如果不明确声明 Bean 的名称时,Spring 的默认策略是使用类的首字母小写作为 Bean 的名称,具体见源码:

org.springframework.beans.factory.support.BeanNameGenerator
org.springframework.context.annotation.AnnotationBeanNameGenerator
org.springframework.beans.factory.support.DefaultBeanNameGenerator

而接下来我们的目的是为了验证 @Autowired 注解所注入的 Bean 是与 Bean 的类型有关还是与名称有关。

--

定义一个 PayMethod 接口:

public interface PayMethod {

void printPayName();
}

接着写两个实现类,并注入到 Spring 容器中(使用 @Component 注解):

@Component
public class AliPay implements PayMethod {
@Override
public void printPayName() {
System.out.println("支付宝支付");
}
}
@Component
public class WechatPay implements PayMethod {
@Override
public void printPayName() {
System.out.println("微信支付");
}
}

接下来修改下 Order 这个类,如下所示:

@Component
public class Order {

@Autowired
private PayMethod[] payMethod; // 注意这里使用的数组接收

public Order() {
}

public void print() {
System.out.println(payMethod.length);
}
}

注意看我们要注入的属性 payMethod,该属性的类型是一个 PayMethod 数组。再看前面我们定义的两个实现类(AliPay 和 WechatPay),这两个实现类都是 PayMethod 类型。

因此 Spring 在执行属性注入后,payMethod 数组大小应该为2。看下结果:

从截图中我们可以看出这个结果确实是我们理想中的结果。

这两个 Bean 确实被注入进来了,但是我们需要测试验证的是 @Autowired 注解到底与类型有关还是名称有关。

因此,我们继续将代码做下修改,将之前的属性名修改为 aliPay:

private PayMethod[] aliPay

这样我们就能够验证一件事,如果 @Autowired 注解在执行属性注入时如果与名称有关那么就会找到 BeanName 为 aliPay 的 Bean 并注入到该属性中。即 aliPay 的值只会有一个,看下测试结果:

这个测试结果与我们想象中的有些不一样,不过却说明了属性输入时与 Bean 的类型有关,与名称无关。

真的吗?我不信!!!!

我们将属性修改成下面的样子:

private PayMethod aliPay

这回我们不适用数组接收了,而是使用普通的类型。现在再次看下结果:

这回有意思,注入的 Bean 是 AliPay,而不是 WechatPay。现在与之前的结果反过来了,@Autowired 注入结果居然又变成了与 BeanName 有关。

emmmm.....

继续修改属性如下:

private PayMethod payMethod;

现在没有了匹配的 Bean 名称会是什么结果呢?

当我们启动测试类时却抛出了异常:

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'io.gitee.ituknown.di.PayMethod' available: expected single matching bean but found 2: aliPay,wechatPay

从提示信息中我们可以看到,Spring 在执行属性注入时找到了属于 PayMethod 类型的两个 Bean:aliPay,wechatPay。但是在执行注入时却没有匹配到 Bean 名称。

这个就比较有意思了,在之前类型为数组时为什么没有这个错误呢?

不过从上面的测试结果我们也可以总结一点:

1.1. @Autowired  在执行属性注入时会首先去容器中查找所有属于该类型的 Bean。之后继续判断属性类型是否为集合或数组,如果是则直接将找到的所有 Bean 注入到属性中。

2.2. 如果属性类型是普通的 Bean 对象且只找到一个符合该类型的 Bean 则直接注入。但如果匹配到了该类型 Bean 的多个 Bean 对象,则继续根据属性名称进行匹配,如果匹配到了则将匹配到的 Bean 注入到属性中。

实际上我们知道的依赖注入注解除了 @Autowired 之外似乎还要一个 java 提供的注解:@Resource

依赖注入注解:@Resource

同样的,先看下该注解的定义:

@Target({TYPE, FIELD, METHOD})
@Retention(RUNTIME)
public @interface Resource {

String name() default "";

String lookup() default "";

Class<?> type() default java.lang.Object.class;

enum AuthenticationType {
CONTAINER,
APPLICATION
}

AuthenticationType authenticationType() default AuthenticationType.CONTAINER;

boolean shareable() default true;

String mappedName() default "";

String description() default "";
}

@Resource 注解与 @Autowired 注解在使用上没有什么区别,匹配机制也是相同的,所以不做多余的介绍,有兴趣的可以自行测试一下。

唯一样说明的是 @Resource 注解中定义的两个方法:

String name() default "";
Class<?> type() default java.lang.Object.class;

这两个方法允许我们手动指定要注入的 Bean 的名称和类型,看下示例:

@Resource(name = "aliPay")
private PayMethod[] payMethods; // 注意看属性名

属性名为 payMethods,如果我们不再注解上通过 name 指定注入的 Bean 名称那么会匹配到两个 Bean,不过现在我们通过 name 限制之后那么就只会注入一个名为 aliPay 的 Bean。

另外,type 属性用法也是类似,比如下面我们通过 type 去限制要注入的 Bean 的类型:

@Resource(type = AliPay.class)
private PayMethod[] payMethods; // 注意看属性名

这个注解在多个继承类上注入指定类型的 Bean 是很有用的,其他的就不做过多介绍了。

下面来看下注入模式:

注入模式

如果使用过 struts 框架的话当我们需要进行属性注入时一定见多下面的写法(即 Getter/Setter),然后我们就可以在 Order 类中使用 user 对象了:

public class Order {

private User user;

public Order() {
}

public User getUser() {
return user;
}

public void setUser(User user) {
this.user = user;
}
}

而这种写法在 Spring MVC 中如果不做一些配置的话是无法注入的,struts 之所以能够实现这种注入方式主要是因为这个框架使用的是原型设计模式以及对 Spring 做了扩展。其他的不多说,现在我们就来聊下 Setter/Getter 注入是怎么回事。

首先呢,Order 类注入到 Spring 容器中,并且保留 user 属性的 Setter/Getter 方法。这样做的目的是看下 user 属性在只有 Setter/Getter 得情况下是否会实现属性注入:

@Component
public class Order {

private User user;

public Order() {
}

public void print() {
System.out.println(user);
}

public User getUser() {
return user;
}

public void setUser(User user) {
this.user = user;
}
}

之后启动 Main 类进行测试,看 user 属性值:

不出意外,你会发现 user 属性值为 NULL,这就说明 Spring 容器在初始化时并没有执行属性注入。

现在我们要想一个问题,为什么在 struts 框架中写一个 Setter 就会自动执行属性注入,而我们这里测试却不行呢?这是一个很有趣的问题。

顺便说下,这个问题如果阅读过 MyBatis 的源码就会知道是什么原因。在使用 MyBatis 时如果想要扫描 Mapper 接口文件我们该怎么做?

在配置类上使用一个 @MapperSacn 注解(org.mybatis.spring.annotation.MapperScan)即可。然后如果深入查看源码的话你会发现在内部借助了 Spring 的扩展点,并且在解决依赖时使用了一行代码(MyBatis 2.0):

具体源码见:https://github.com/mybatis/spring/blob/mybatis-spring-2.0.0/src/main/java/org/mybatis/spring/mapper/ClassPathMapperScanner.java

这就是传说中的注入模式!!!!

在具体介绍注入模式之前这里先提前对 注入模式 做下说明。

Setter 注入原理

Spring 2.0 开始就提供了三种注入模式,分别是 NO、BY_NAME 和 BY_TYPE。我们在之前使用的 @Autowired 注解就是基于 NO 注入模式的实现。

另外,当我们使用 IDE 开发工具编写代码时,如果将 @Autowired 写在要注入的属性上,经常会提示推荐使用 Setter 或者构造器实现属性注入。究其原始就是因为 @Autowired 注解所使用的注入模式是 BY_NO,而这种注入模式是利用 Java 的反射机制。

现在我们就来看下如何基于 Setter 实现属性注入!

首先说明一点,不管是 Setter 还是构造器注入,使用的注入模式都是 BY_TYPE 或者 BY_NAME。因此,在枚举类 org.springframework.beans.factory.annotation.Autowire 中并没有定义 BY_CONSTRUCTOR 这种注入模式。

想要实现 Setter 属性注入我们需要借助后置处理器

@Component
public class AutowireModePostProcessor implements BeanFactoryPostProcessor {

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 获取 order 对应的 BeanDefinition
GenericBeanDefinition orderBeanDefinition = (GenericBeanDefinition) beanFactory.getBeanDefinition("order");
// 手动设置在执行依赖注入时使用的注入模式
orderBeanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
}