最全的 Spring 依賴注入方式

Spring 正如其名字,給開發者帶來了春天,Spring 是爲解決企業級應用開發的複雜性而設計的一款框架,其設計理念就是:簡化開發。

Spring 框架中最核心思想就是:

本文,將主要介紹 Spring 中 IOC 的依賴注入

控制反轉 IOC

就 IOC 本身而言,其並不是什麼新技術,只是一種思想理念。IOC 的核心就是原先創建一個對象,我們需要自己直接通過 new 來創建,而 IOC 就相當於有人幫們創建好了對象,需要使用的時候直接去拿就行,IOC 主要有兩種實現方式:

這種就是說容器幫我們創建好了對象,我們需要使用的時候自己再主動去容器中查找,如:

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/application-context.xml");
Object bean = applicationContext.getBean("object");

依賴注入相比較依賴查找又是一種優化,也就是我們不需要自己去查找,只需要告訴容器當前需要注入的對象,容器就會自動將創建好的對象進行注入(賦值)。

依賴注入 DI

通過 xml 的注入方式我們不做討論,在這裏主要討論基於註解的注入方式,基於註解的常規注入方式通常有三種:

三種常規注入方式

接下來就讓我們分別介紹一下三種常規的注入方式。

屬性注入

通過屬性注入的方式非常常用,這個應該是大家比較熟悉的一種方式:

@Service
public class UserService {
    @Autowired
    private Wolf1Bean wolf1Bean;//通過屬性注入
}

setter 方法注入

除了通過屬性注入,通過 setter 方法也可以實現注入:

@Service
public class UserService {
    private Wolf3Bean wolf3Bean;
    
    @Autowired  //通過setter方法實現注入
    public void setWolf3Bean(Wolf3Bean wolf3Bean) {
        this.wolf3Bean = wolf3Bean;
    }
}

構造器注入

當兩個類屬於強關聯時,我們也可以通過構造器的方式來實現注入:

@Service
public class UserService {
  private Wolf2Bean wolf2Bean;
    
     @Autowired //通過構造器注入
    public UserService(Wolf2Bean wolf2Bean) {
        this.wolf2Bean = wolf2Bean;
    }
}

接口注入

在上面的三種常規注入方式中,假如我們想要注入一個接口,而當前接口又有多個實現類,那麼這時候就會報錯,因爲 Spring 無法知道到底應該注入哪一個實現類。

比如我們上面的三個類全部實現同一個接口 IWolf,那麼這時候直接使用常規的,不帶任何註解元數據的注入方式來注入接口 IWolf。

@Autowired
private IWolf iWolf;

此時啓動服務就會報錯:

這個就是說本來應該注入一個類,但是 Spring 找到了三個,所以沒法確認到底應該用哪一個。這個問題如何解決呢?

解決思路主要有以下 5 種:

通過配置文件和 @ConditionalOnProperty 註解實現

通過 @ConditionalOnProperty 註解可以結合配置文件來實現唯一注入。下面示例就是說如果配置文件中配置了 lonely.wolf=test1,那麼就會將 Wolf1Bean 初始化到容器,此時因爲其他實現類不滿足條件,所以不會被初始化到 IOC 容器,所以就可以正常注入接口:

@Component
@ConditionalOnProperty(name = "lonely.wolf",havingValue = "test1")
public class Wolf1Bean implements IWolf{
}

當然,這種配置方式,編譯器可能還是會提示有多個 Bean,但是隻要我們確保每個實現類的條件不一致,就可以正常使用。

通過其他 @Condition 條件註解

除了上面的配置文件條件,還可以通過其他類似的條件註解,如:

類似這種實現方式也可以非常靈活的實現動態化配置。

不過上面介紹的這些方法似乎每次都只能固定注入一個實現類,那麼如果我們就是想多個類同時注入,不同的場景可以動態切換而又不需要重啓或者修改配置文件,又該如何實現呢?

通過 @Resource 註解動態獲取

如果不想手動獲取,我們也可以通過 @Resource 註解的形式動態指定 BeanName 來獲取:

@Component
public class InterfaceInject {
    @Resource(name = "wolf1Bean")
    private IWolf iWolf;
}

如上所示則只會注入 BeanName 爲 wolf1Bean 的實現類。

通過集合注入

除了指定 Bean 的方式注入,我們也可以通過集合的方式一次性注入接口的所有實現類:

@Component
public class InterfaceInject {
    @Autowired
    List<IWolf> list;

    @Autowired
    private Map<String,IWolf> map;
}

上面的兩種形式都會將 IWolf 中所有的實現類注入集合中。如果使用的是 List 集合,那麼我們可以取出來再通過 instanceof 關鍵字來判定類型;而通過 Map 集合注入的話,Spring 會將 Bean 的名稱(默認類名首字母小寫)作爲 key 來存儲,這樣我們就可以在需要的時候動態獲取自己想要的實現類。

@Primary 註解實現默認注入

除了上面的幾種方式,我們還可以在其中某一個實現類上加上 @Primary 註解來表示當有多個 Bean 滿足條件時,優先注入當前帶有 @Primary 註解的 Bean:

@Component
@Primary
public class Wolf1Bean implements IWolf{
}

通過這種方式,Spring 就會默認注入 wolf1Bean,而同時我們仍然可以通過上下文手動獲取其他實現類,因爲其他實現類也存在容器中。

手動獲取 Bean 的幾種方式

在 Spring 項目中,手動獲取 Bean 需要通過 ApplicationContext 對象,這時候可以通過以下 5 種方式進行獲取:

直接注入

最簡單的一種方法就是通過直接注入的方式獲取 ApplicationContext 對象,然後就可以通過 ApplicationContext 對象獲取 Bean :

@Component
public class InterfaceInject {
    @Autowired
    private ApplicationContext applicationContext;//注入

    public Object getBean(){
        return applicationContext.getBean("wolf1Bean");//獲取bean
    }
}

通過 ApplicationContextAware 接口獲取

通過實現 ApplicationContextAware 接口來獲取 ApplicationContext 對象,從而獲取 Bean。需要注意的是,實現 ApplicationContextAware 接口的類也需要加上註解,以便交給 Spring 統一管理(這種方式也是項目中使用比較多的一種方式):

@Component
public class SpringContextUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * 通過名稱獲取bean
     */
    public static <T>T getBeanByName(String beanName){
        return (T) applicationContext.getBean(beanName);
    }

    /**
     * 通過類型獲取bean
     */
    public static <T>T getBeanByType(Class<T> clazz){
        return (T) applicationContext.getBean(clazz);
    }
}

封裝之後,我們就可以直接調用對應的方法獲取 Bean 了:

Wolf2Bean wolf2Bean = SpringContextUtil.getBeanByName("wolf2Bean");
Wolf3Bean wolf3Bean = SpringContextUtil.getBeanByType(Wolf3Bean.class);

通過 ApplicationObjectSupport 和 WebApplicationObjectSupport 獲取

這兩個對象中,WebApplicationObjectSupport 繼承了 ApplicationObjectSupport,所以並無實質的區別。

同樣的,下面這個工具類也需要增加註解,以便交由 Spring 進行統一管理:

@Component
public class SpringUtil extends /*WebApplicationObjectSupport*/ ApplicationObjectSupport {
    private static ApplicationContext applicationContext = null;

    public static <T>T getBean(String beanName){
        return (T) applicationContext.getBean(beanName);
    }

    @PostConstruct
    public void init(){
        applicationContext = super.getApplicationContext();
    }
}

有了工具類,在方法中就可以直接調用了:

@RestController
@RequestMapping("/hello")
@Qualifier
public class HelloController {
    @GetMapping("/bean3")
    public Object getBean3(){
        Wolf1Bean wolf1Bean = SpringUtil.getBean("wolf1Bean");
        return wolf1Bean.toString();
    }
}

通過 HttpServletRequest 獲取

通過 HttpServletRequest 對象,再結合 Spring 自身提供的工具類 WebApplicationContextUtils 也可以獲取到 ApplicationContext 對象,而 HttpServletRequest 對象可以主動獲取(如下 getBean2 方法),也可以被動獲取(如下 getBean1 方法):

@RestController
@RequestMapping("/hello")
@Qualifier
public class HelloController {

    @GetMapping("/bean1")
    public Object getBean1(HttpServletRequest request){
        //直接通過方法中的HttpServletRequest對象
        ApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
        Wolf1Bean wolf1Bean = (Wolf1Bean)applicationContext.getBean("wolf1Bean");

        return wolf1Bean.toString();
    }

    @GetMapping("/bean2")
    public Object getBean2(){
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();//手動獲取request對象
        ApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());

        Wolf2Bean wolf2Bean = (Wolf2Bean)applicationContext.getBean("wolf2Bean");
        return wolf2Bean.toString();
    }
}

其他方式獲取

當然,除了上面提到的方法,我們也可以使用最開始提到的 DL 中代碼示例去手動 new 一個 ApplicationContext 對象,但是這樣就意味着重新初始化了一次,所以是不建議這麼去做,但是在寫單元測試的時候這種方式是比較適合的。

談談 @Autowrite 和 @Resource 以及 @Qualifier 註解的區別

上面我們看到了,注入一個 Bean 可以通過 @Autowrite,也可以通過 @Resource 註解來注入,這兩個註解有什麼區別呢?

@Resource(name = "wolf2Bean",type = Wolf2Bean.class)
private IWolf iWolf;

@Qualifier 註解是用來標識合格者,當 @Autowrite@Qualifier 一起使用時,就相當於是通過名字來確定唯一:

@Qualifier("wolf1Bean")
@Autowired
private IWolf iWolf;

那可能有人就會說,我直接用 @Resource 就好了,何必用兩個註解結合那麼麻煩,這麼一說似乎顯得 @Qualifier 註解有點多餘?

@Qualifier 註解是多餘的嗎

我們先看下面聲明 Bean 的場景,這裏通過一個方法來聲明一個 Bean (MyElement),而且方法中的參數又有 Wolf1Bean 對象,那麼這時候 Spring 會幫我們自動注入 Wolf1Bean:

@Component
public class InterfaceInject2 {
    @Bean
    public MyElement test(Wolf1Bean wolf1Bean){
        return new MyElement();
    }
}

然而如果說我們把上面的代碼稍微改一下,把參數改成一個接口,而接口又有多個實現類,這時候就會報錯了:

@Component
public class InterfaceInject2 {
    @Bean
    public MyElement test(IWolf iWolf){//此時因爲IWolf接口有多個實現類,會報錯
        return new MyElement();
    }
}

@Resource 註解又是不能用在參數中,所以這時候就需要使用 @Qualifier 註解來確認唯一實現了(比如在配置多數據源的時候就經常使用 @Qualifier 註解來實現):

@Component
public class InterfaceInject2 {
    @Bean
    public MyElement test(@Qualifier("wolf1Bean") IWolf iWolf){
        return new MyElement();
    }
}

總結

本文主要講述瞭如何在 Spring 中使用靈活的方式來實現各種場景的注入方式,並且着重介紹了當一個接口有多個實現類時應該如何注入的問題,最後也介紹了常用幾個注入註解的區別,通過本文,相信大家對如何使用 Spring 中的依賴注入會更加的熟悉。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/sdzjKL97w621o9Fjy9Symw