Spring筆記(9) - IOC實現方式詳解

碼猿手發表於2020-12-20

  IOC概念 

  控制反轉(Inversion of Control,IOC),是物件導向程式設計中的一種設計原則,它建議將不需要的職責移出類,讓類專注於核心職責,從而提供鬆散耦合,提高優化軟體程式設計。它把傳統上由程式程式碼直接操控的物件的呼叫權(new、get等操作物件)交給容器,通過容器來實現物件元件的裝配和管理,也就是對元件物件控制權的轉移,從程式程式碼本身轉移到了外部容器。 

  IOC的實現方式

  IOC有多種實現方式,其中最常見的叫做“依賴注入”(Dependency Injection,簡稱DI),另外還有“依賴查詢”(Dependency Lookup),其中“依賴查詢”可分為“依賴拖拽”(Dependency Pull)和“上下文依賴查詢”(Contextualized Dependency Lookup)。

  依賴的理解

  什麼是依賴呢?Java開發是物件導向程式設計,面向抽象程式設計容易產生類與類的依賴。看下面的程式碼中,UserManagerImpl 類中有個物件屬性 UserDao ,也就是說 UserManagerImpl 依賴 UserDao。也就是說,一個類A中有了類B的物件屬性或類A的構造方法需要傳遞類B物件來進行構造,那表示類A依賴類B。

public class UserManagerImpl implements UserManagerServie{
    private UserDao userDao;
}

  為什麼需要依賴呢?下面的程式碼中 UserDao 直接 new 寫死了,如果此時新需求需要代理物件來處理業務就不行了,所以為了程式的靈活,需要改成上面的依賴程式碼,由程式控制物件的建立(IOC);

public class UserManagerImpl implements UserManagerServie{
    public void addUser(){
     UserDao userDao = new UserDao();
    }
}

  依賴注入(Dependency Injection)

  依賴注入是一個過程,物件通過構造方法引數、工廠方法引數、構造或工廠方法返回後在物件例項上設定的屬性來定義它們的依賴項,從類外部注入依賴(容器在建立bean時注入這些依賴項),類不關心細節。這個過程從根本上說是bean自身的反向(因此得名控制反轉),通過使用直接構造類或服務定位器模式來控制依賴項的例項化或位置。   

  依賴注入的基本原則是:應用元件不應該負責查詢資源或者其他依賴物件,配置物件的工作由IOC容器負責,即元件不做定位查詢,只提供常規的Java方法讓容器去決定依賴關係。

  使用 DI 原則,程式碼會更清晰,並且當向物件提供它們的依賴時,解耦會更有效。物件不查詢其依賴項,也不知道依賴項的位置或類。因此,類變得更容易測試,特別是當依賴關係在介面或抽象類上時,它們允許在單元測試中使用 stub 或 mock 實現。

  Spring中依賴注入有四種方式:構造方法注入(Constructor Injection),set注入(Setter Injection)、介面注入(Interface Injection)和欄位注入(Field Injection),其中介面注入由於在靈活性和易用性比較差,現在從Spring4開始已被廢棄。

  (1) 構造方法注入(Constructor Injection):Spring Framework 更傾向並推薦使用構造方法注入

Spring筆記(9) - IOC實現方式詳解
public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;

    private int i;

    public ExampleBean(
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
        this.beanOne = anotherBean;
        this.beanTwo = yetAnotherBean;
        this.i = i;
    }
}
View Code

   xml配置檔案對應bean的定義資訊:

Spring筆記(9) - IOC實現方式詳解
bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
    <constructor-arg ref="anotherExampleBean"/>
    <constructor-arg ref="yetAnotherBean"/>
    <constructor-arg value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
View Code

   也可以配置成下面的模式:

Spring筆記(9) - IOC實現方式詳解
<bean id="exampleBean" class="examples.ExampleBean">
    <!-- constructor injection using the nested ref element -->
    <constructor-arg>
        <ref bean="anotherExampleBean"/>
    </constructor-arg>

    <!-- constructor injection using the neater ref attribute -->
    <constructor-arg ref="yetAnotherBean"/>
    <!-- 指定型別 -->
    <constructor-arg type="int" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
View Code

  1)建構函式引數解析:建構函式引數解析匹配通過使用引數型別實現。如果 bean definition 在建構函式引數中不存在潛在的歧義,那麼建構函式引數的bean definition定義的順序就是例項化bean時將這些引數提供給對應建構函式的順序。

package x.y;

public class ThingOne {

    public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
        // ...
    }
}

  假設 ThingTwo 和 ThingThree 類與繼承無關,那麼就不存在潛在的歧義。因此,以下配置可以很好地工作,不需要在 <constructor-arg/> 標籤中顯式地指定建構函式引數索引或型別;

<beans>
    <bean id="beanOne" class="x.y.ThingOne">
        <constructor-arg ref="beanTwo"/>
        <constructor-arg ref="beanThree"/>
    </bean>

    <bean id="beanTwo" class="x.y.ThingTwo"/>

    <bean id="beanThree" class="x.y.ThingThree"/>
</beans>

   2)建構函式引數型別匹配

  當引用另一個 bean 時,型別是已知的,可以進行匹配(就像前面的例子一樣)。當使用簡單型別時,例如<value>true</value>, Spring 無法確定值的型別,因此如果沒有幫助,就無法按型別進行匹配。如下面的例子:

package examples;

public class ExampleBean {

    // Number of years to calculate the Ultimate Answer
    private int years;

    // The Answer to Life, the Universe, and Everything
    private String ultimateAnswer;

    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}

   如果使用 type 屬性顯式地指定建構函式引數的型別,則容器可以使用簡單型別的型別匹配。

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg type="int" value="7500000"/>
    <constructor-arg type="java.lang.String" value="42"/>
</bean>

   3)建構函式引數索引匹配:可以使用 index 屬性顯式地指定建構函式引數的索引,從0開始;

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg index="0" value="7500000"/>
    <constructor-arg index="1" value="42"/>
</bean>

   index 除了解決多個簡單值的模糊性之外,還可以解決建構函式有兩個相同型別的引數時的模糊性。

  4)建構函式引數的名字匹配:除了上面的型別、索引匹配,還可以使用名字進行匹配;

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg name="years" value="7500000"/>
    <constructor-arg name="ultimateAnswer" value="42"/>
</bean>

   請記住,要使它開箱即用,程式碼編譯時必須啟用debug標誌,以便 Spring 可以從建構函式中查詢引數名進行例項化建立。如果不能或不想使用 debug 標誌編譯程式碼,可以使用 JDK註解 @ConstructorProperties 顯式地命名建構函式引數。

public class Point {
       @ConstructorProperties({"x", "y"})
       public Point(int x, int y) {
           this.x = x;
           this.y = y;
       }

       public int getX() {
           return x;
       }

       public int getY() {
           return y;
       }

       private final int x, y;
   }

  關於 @ConstructorProperties 的作用:

    一些序列化框架使用 @ConstructorProperties 將建構函式引數與相應的欄位及其 getter 和 setter 方法關聯起來,比如上面引數 x 和 y 對應的是 getX() 和 getY();

    為此,它依賴於為欄位命名 getter 和 setter 方法時使用相同的常見命名約定: getter 和 setter 方法名稱通常是通過大寫欄位名稱並在字首加get或set建立的(或者對於布林型別的 getter 是 is)。但是,使用單字母欄位名的示例並不能很好地展示這一點。

    一個最好的案例是:someValue 變成 getSomeValue 和 setSomeValue;

    因此在建構函式屬性的上下文中,@ConstructorProperties({"someValue"})表示第一個引數與 getter方法 getSomeValue和setter方法 setSomeValue相關聯;

    請記住,方法引數名在執行時是不可見的。重要的是引數的順序。建構函式引數的名稱或建構函式實際設定的欄位並不重要。下面仍然引用名為getSomeValue()的方法,然後對方法裡面的值進行序列化:

import com.fasterxml.jackson.databind.ObjectMapper;

import java.beans.ConstructorProperties;
import java.beans.XMLEncoder;
import java.io.ByteArrayOutputStream;

public class Point {

    private final int x;
    private final int y=10;

    @ConstructorProperties({"someValue"})
    public Point(int a) {
        this.x = a;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getSomeValue() {
        return y;
    }
    
    public static void main(String[] args) throws Exception {
        //將bean資訊進行xml格式輸出:進行序列化
        ByteArrayOutputStream stream = new ByteArrayOutputStream();

        XMLEncoder encoder = new XMLEncoder(stream);
        encoder.writeObject(new Point(1));
        encoder.close();

        System.out.println(stream);
    }
}
======結果======
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_191" class="java.beans.XMLDecoder">
 <object class="test.Point">
  <int>10</int>
 </object>
</java>

  什麼情況下使用@ConstructorProperties註解呢?

    一般的POJO bean 都有set 和 get 方法,所以是可變的。在預設情況下,Jackson 將使用Java bean模式進行反序列化:首先通過使用預設(或零args)建構函式建立bean類的例項,然後使用一系列對setter的呼叫來設定每個屬性值。但如果是一個不可變bean(沒有set方法),比如上面的Point案例呢?現在沒有了set方法,或者建構函式也是無參的,這時你就使用 @JsonProperty and @JsonCreator註解來進行序列號和反序列化了,如下面的案例:

Spring筆記(9) - IOC實現方式詳解
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class JacksonBean {
  private final int value;
  private final String another;
  
  @JsonCreator
  public JacksonBean(@JsonProperty("value") int value, @JsonProperty("another") String another) {
    this.value = value;
    this.another = another;
  }
  
  public int getValue() {
    return value;
  }
  
  public String getAnother() {
    return another;
  }
}
View Code

     但這裡存在一個問題,比如我在程式的多模組下使用了這個bean,在其中一個模組中我將它序列化成 JSON,但在另外一個模組中,我可能選擇不同的序列號機制(比如YAML、XML),但由於Jackson不支援 YAML,我們將不得不使用不同的框架來序列號這些bean,而這些庫可能需要它們自己的註解集,所以我們需要在這個 bean中新增大量的註解以支援對應的序列號框架,這樣很不友好。這時就可以使用 @ConstructorProperties 註解來解決這個問題了,序列號框架比如 Jackson 框架從2.7版本就支援這個註解了;

Spring筆記(9) - IOC實現方式詳解
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.beans.ConstructorProperties;

public class JacksonBean {
    private final int value;
    private final String another;

    @ConstructorProperties({"value", "another"})
    public JacksonBean(int value, String another) {
        this.value = value;
        this.another = another;
    }

    public int getValue() {
        return value;
    }

    public String getAnother() {
        return another;
    }

    public static void main(String[] args) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            JacksonBean jacksonBean = new JacksonBean(1, "hrh");
            String jsonString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jacksonBean);
            System.out.println(jsonString);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }

}
======結果======
{
  "value" : 1,
  "another" : "hrh"
}
View Code

     只要對應的序列化框架支援該註解,就可以使用更少的註解來被這些支援的框架進行序列化和反序列化了。

參考:https://liviutudor.com/2017/09/15/little-known-yet-useful-java-annotation-constructorproperties/

  對於序列化,框架使用物件getter獲取所有值,然後使用這些值序列化物件。當需要反序列化物件時,框架必須建立一個新例項。如果物件是不可變的,它沒有任何可以用來設定其值的setter。建構函式是設定這些值的唯一方法。@ConstructorProperties註解用於告訴框架如何呼叫建構函式來正確地初始化物件。

  Spring還可以使用@ConstructorProperties註解按名稱查詢建構函式引數:

    <bean id="point" class="testPackage.Point">
        <constructor-arg name="xx" value="10"/>
        <constructor-arg name="yy" value="20"/>
    </bean>
public class Point {

    private final int x;
    private final int y;

    @ConstructorProperties({"xx", "yy"})
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }


    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        Point point = (Point) context.getBean("point");
        System.out.println(point.getX());
        System.out.println(point.getY());
    }
}

   參考:https://stackoverflow.com/questions/26703645/dont-understand-constructorproperties

  5)當構造方法是私有時,可以提供一個靜態工廠方法供外部使用:

Spring筆記(9) - IOC實現方式詳解
public class ExampleBean {

    // 一個私有構造方法
    private ExampleBean(...) {
        ...
    }

    //一個靜態工廠方法:引數是這個ExampleBean例項化後bean的依賴項,不需要管這些引數實際上是如何被使用的;
    public static ExampleBean createInstance (
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {

        ExampleBean eb = new ExampleBean (...);
        // 一些其他的操作
        ...
        return eb;
    }
}
View Code

   靜態工廠方法的引數是由xml配置檔案的<constructor-arg/>標籤提供的,與實際使用建構函式時完全相同。工廠方法返回的類的型別不必與包含靜態工廠方法的類的型別相同(上面的案例中是相同的)。

  (2) set注入(Setter Injection):由容器在呼叫無引數建構函式或無引數靜態工廠方法來例項化bean之後呼叫bean上的setter方法來完成的;

Spring筆記(9) - IOC實現方式詳解
public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;

    private int i;

    public void setBeanOne(AnotherBean beanOne) {
        this.beanOne = beanOne;
    }

    public void setBeanTwo(YetAnotherBean beanTwo) {
        this.beanTwo = beanTwo;
    }

    public void setIntegerProperty(int i) {
        this.i = i;
    }
}
View Code

   xml配置檔案對應bean的定義資訊:

Spring筆記(9) - IOC實現方式詳解
<bean id="exampleBean" class="examples.ExampleBean">
    <!-- setter injection using the nested ref element -->
    <property name="beanOne">
        <ref bean="anotherExampleBean"/>
    </property>

    <!-- setter injection using the neater ref attribute -->
    <property name="beanTwo" ref="yetAnotherBean"/>
    <property name="integerProperty" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
View Code

  (3) 介面注入(Interface Injection)

    • 若根據 wikipedia 的定義,介面注入只是客戶端向客戶端依賴項的setter方法釋出一個角色介面,它可以用來建立注入器在注入依賴時應該如何與客戶端通訊
      Spring筆記(9) - IOC實現方式詳解
      // Service setter interface.
      public interface ServiceSetter {
          public void setService(Service service);
      }
      
      // Client class
      public class Client implements ServiceSetter {
          // Internal reference to the service used by this client.
          private Service service;
      
          // Set the service that this client is to use.
          @Override
          public void setService(Service service) {
              this.service = service;
          }
      }
      View Code

      Spring為 ResourceLoaders, ApplicationContexts, MessageSource和其他資源提供了開箱即用的資源外掛介面:ResourceLoaderAware, ApplicationContextAware, MessageSourceAware等等,這裡就使用到了介面注入;

      我們以ApplicationContextAware介面為例,它的作用是Spring容器在建立bean時會掃描實現了這個介面的類,然後將這個容器注入給這個實現類,這樣這個實現類就可以通過容器去獲取bean等其他操作了。

      public interface ApplicationContextAware extends Aware {
      
          void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
      
      }

      那我們什麼時候會需要用到 ApplicationContextAware這個介面呢?如果你需要查詢一些bean或訪問一些應用程式檔案資源,甚至釋出一些應用程式範圍的事件,這時你就需要用到這個介面了。

      @Component
      public class MyClass implements ApplicationContextAware {
      
          private ApplicationContext context;
      
          @Override
          public void setApplicationContext(ApplicationContext applicationContext)
                  throws BeansException {
              context = applicationContext;
          }
      
          public void work() {
              MyOtherClass otherClass = context.getBean(MyOtherClass.class);
              Resource image = context.getResource("logo.img");
          }
      }

      當然了,現在我們也可以通過註解方式來獲取到程式的上下文環境:@Inject ApplicationContext context 或者  @Autowired ApplicationContext context

    • Martin Fowler的定義: 為介面的定義和使用提供的一種注入技術,通過實現依賴bean的關聯介面將bean依賴項注入到實際物件中。因此容器呼叫該介面的注入器,該介面是在實際物件被例項化時實現的。

    I.電影:

Spring筆記(9) - IOC實現方式詳解
public class Movie {
    private String director;
    private String title;

    public Movie(String director, String title) {
        this.director = director;
        this.title = title;
    }

    public String getDirector() {
        return director;
    }

    public void setDirector(String director) {
        this.director = director;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}
View Code

    II.電影查詢器注入介面實現:

Spring筆記(9) - IOC實現方式詳解
//電影查詢器
public interface MovieFinder {
    List findAll();
}
public interface Injector {
    public void inject(Object  target);
}

//注入介面,將影片查詢器注入到物件的一個介面
public interface InjectFinder {
    void injectFinder(MovieFinder finder);
}
//實現注入介面
public class MovieLister implements InjectFinder {
    private MovieFinder finder;

    public void injectFinder(MovieFinder finder) {
        this.finder = finder;
    }


    public Movie[] moviesDirectedBy(String arg) {
        List allMovies = finder.findAll();
        for (Iterator it = allMovies.iterator(); it.hasNext(); ) {
            Movie movie = (Movie) it.next();
            if (!movie.getDirector().equals(arg)) it.remove();
        }
        return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
    }

}
View Code

    III.檔名注入介面實現:

Spring筆記(9) - IOC實現方式詳解
//檔名注入
public interface InjectFinderFilename {
    void injectFilename (String filename);
}

public interface Injector {
    public void inject(Object  target);
}

public class ColonMovieFinder implements MovieFinder, InjectFinderFilename, Injector {
    private String filename;

    //注入檔名
    @Override
    public void injectFilename(String filename) {
        this.filename = filename;
    }

    //找到所有的電影
    @Override
    public List findAll() {
        List<Movie> list = new ArrayList(10);
        list.add(new Movie("Sergio Leone","Once Upon a Time in the West"));
        list.add(new Movie("See","hrh"));
        list.add(new Movie("Sere","hrh"));
        list.add(new Movie("Serge","hrh"));
        list.add(new Movie("Sergie","hrh"));
        list.add(new Movie("Sergioe","hrh"));
        return list;
    }
    
    @Override
    public void inject(Object target) {
        ((InjectFinder) target).injectFinder(this);
    }

}
View Code

    IV.測試:

Spring筆記(9) - IOC實現方式詳解
public class Tester {
    //容器
    GenericApplicationContext container;
    //private Container container;

    private void configureContainer() {
        //建立容器
        container = new GenericApplicationContext();
        registerComponents();
        container.refresh();
        registerInjectors();
        container.start();
    }

    private void registerComponents() {
//        container.registerComponent("MovieLister", MovieLister.class);
//        container.registerComponent("MovieFinder", ColonMovieFinder.class);
        container.registerBean("MovieLister", MovieLister.class);
        container.registerBean("MovieFinder", ColonMovieFinder.class);
    }

    private void registerInjectors() {
//        container.registerInjector(InjectFinder.class, container.lookup("MovieFinder"));
//        container.registerInjector(InjectFinderFilename.class, new FinderFilenameInjector());
        container.registerBean(InjectFinder.class, container.getBean("MovieFinder"));
        container.registerBean(InjectFinderFilename.class, new FinderFilenameInjector());
    }

    public static class FinderFilenameInjector implements Injector {
        @Override
        public void inject(Object target) {
            ((InjectFinderFilename) target).injectFilename("movies1.txt");
        }
    }
    @Test
    public void testIface() {
        configureContainer();
        MovieLister lister = (MovieLister) container.getBean("MovieLister");
        lister.injectFinder((MovieFinder) container.getBean("MovieFinder"));
        Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
        assertEquals("Once Upon a Time in the West", movies[0].getTitle());
    }

}
View Code

    V.測試通過,檢視容器的bean效果圖:

    參考:https://stackoverflow.com/questions/2827147/doesnt-spring-really-support-interface-injection-at-all

  (4)欄位注入(Field Injection):它實際上不是一種新的注入型別,它是基於註解(@Autowired、@Resource等)實現的,在依賴項屬性上直接使用註解進行注入,在底層中,Spring使用反射來設定這些值;

    下面以@Autowired註解為例:

public class ExampleBean {
    @Autowired
    private AnotherBean beanOne;

}

    欄位注入可以與構造方法注入和setter注入相結合,在Spring 4.3之前,使用構造方法注入我們必須在構造方法上新增@Autowired註解,在4.3之後,如果只有一個構造方法,該註解是可選項,但如果是多個構造方法,需要在其中一個新增@Autowired註解指定使用哪個構造方法來注入依賴項。

Spring筆記(9) - IOC實現方式詳解
public class ExampleBean {

    private AnotherBean beanOne;
    
    @Autowired
    public void setBeanOne(AnotherBean beanOne) {
        this.beanOne = beanOne;
    }  
    
}
View Code
Spring筆記(9) - IOC實現方式詳解
public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;


    public ExampleBean(AnotherBean beanOne) {
        this.beanOne = beanOne;
    }
    
    @Autowired
    public ExampleBean(AnotherBean beanOne,YetAnotherBean beanTwo) {
        this.beanOne = beanOne;
        this.beanTwo = beanTwo;
    }
    
}
View Code

     當欄位注入同時應用在屬性和setter注入方法時,Spring會優先使用setter注入方法注入依賴項:

Spring筆記(9) - IOC實現方式詳解
public class ExampleBean {

    @Autowired
    private AnotherBean beanOne;
    
    @Autowired
    public void setBeanOne(AnotherBean beanOne) {
        this.beanOne = beanOne;
    }  
    
}
View Code

    當然了,在上面這個例子中,單個類中混合注入型別是不太友好的,因為它降低了程式碼的可讀性。  

    依賴注入幾種方式的探討

    (1)構造方法注入(Constructor Injection):

      • 明顯、可靠、不可變的:類的依賴關係在構造方法中很明顯,所有的依賴項在構造方法中,所以所有的依賴項都第一時間被注入到類中,且無法更改,即構造的物件是不可變的;

      • 可以與setter注入或欄位注入相結合,構造方法引數指示所必需的依賴,其他-可選,即構造方法指定強依賴項(final屬性),其他靈活可選依賴項選擇setter注入或欄位注入;
      • 使程式碼更加健壯,可以防止空指標異常;

      • 缺乏靈活性:以後不可能更改物件的依賴關係,導致重構比較麻煩;

      • 依賴項數量增多的問題:依賴越多,建構函式越大,是一種糟糕的程式碼質量,可能需要進行重構;

      • 產生迴圈依賴的可能性增大;
      • 關於構造方法注入的更多探討可參考:https://reflectoring.io/constructor-injection/

    (2)setter注入(Setter Injection):

      • 靈活、可變物件:使用setter注入可以在bean建立後選擇注入依賴項,而從產生可變物件,但這些物件在多執行緒環境中可能不是執行緒安全的;
      • 可控性:可以在任何時候進行依賴注入,這種自由度解決了構造方法注入導致的迴圈依賴問題;

      • 可以在setter方法上使用@Required註解使屬性成為必需依賴項,當然,這個需求使用構造方法注入是更可取的做法;
      • 需要進行Null檢查,因為可能會忘記設定依賴項導致獲取依賴項為空報錯;

      • 由於會存在重寫依賴項的可能性,比構造方法注入更容易出錯,安全性更低;

      • 關於setter注入的更多探討可參考:https://spring.io/blog/2007/07/11/setter-injection-versus-constructor-injection-and-the-use-of-required

    (3)介面注入(Interface Injection):擴充套件可參考https://blogs.oracle.com/jrose/interface-injection-in-the-vm

    (4)欄位注入(Field Injection):

      • 快速方便,與IOC容器相耦合;
      • 易於使用 ,不需要構造方法或setter方法;

      • 可以和構造方法、setter相結合使用;

      • Spring允許我們通過在setter方法中新增@Autowired(required = false)來指定可選的依賴,Spring會跳過不滿足的依賴項,不會將這些不滿足的依賴項進行注入;

      • 在構造方法注入中,無法使用@Autowired(required = false)來指定可選依賴項,構造方法注入是強依賴項,是必需的,沒有依賴項進行注入無法進行物件例項化;
      • 對物件例項化的控制較少,為了測試例項化後的物件,你需要額外對Sring容器進行一些配置,比如使用SpringBoot對@Autowired依賴項進行測試時,需要在測試類上加上@RunWith(SpringJUnit4ClassRunner.class)和@SpringBootTest註解;

      • 相容問題:使用欄位注入意味著縮小類對依賴注入環境的相容性,前面說了欄位注入是依賴註解的,使用反射來設定值的,而這些註解依賴於特定的環境和平臺,如果是一些Java平臺但不支援反射的(比如GWT),會導致不相容問題;

      • 效能問題:構造方法注入比一堆反射欄位賦值快。依賴注入框架來反射分析來構造依賴樹並建立反射建構函式,會導致額外的效能開銷;

      • 從哲學的角度來看,欄位注入打破了封裝,而封裝是物件導向程式設計的特性之一,而物件導向程式設計是Java的主要正規化;

      • 關於欄位注入的更多討論可參考:

        • https://softwareengineering.stackexchange.com/questions/300706/dependency-injection-field-injection-vs-constructor-injection

        • https://www.vojtechruzicka.com/field-dependency-injection-considered-harmful/

    依賴查詢(Dependency Lookup):

    依賴查詢也叫服務定位器(Service Locator), 物件工廠(Object Factory), 元件代理(Component Broker), 元件登錄檔(Component Registry)

    依賴注入和依賴查詢的主要區別是:誰負責檢索依賴項;

    依賴項查詢是一種模式,呼叫者向容器物件請求具有特定名稱或特定型別的物件;依賴項注入是一種模式,容器通過構造方法、setter方法、屬性或工廠方法按名稱將物件傳遞給其他物件; 

    通常,在DI(依賴注入)中,你的元件不知道DI容器,依賴“自動”出現(通過宣告setter/構造方法引數,DI容器為你填充它們);

    但在DL(依賴查詢)中,你必須明確地詢問你需要什麼(顯示查詢資源),這意味著你必須依賴於上下文(在Spring中是Application context),並從它檢索你需要的東西,這種方式其實叫做“上下文依賴查詢”(Contextualized Dependency Lookup):  

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

    我們從JNDI登錄檔獲取JDBC資料來源物件引用的方法稱為“依賴拖拽”(Dependency Pull):

public class PigServiceImpl {
    private DataSource dataSource;
    private PigDao pigDao;
 
    public PigServiceImpl(){
        Context context = null;
        try{
            context = new InitialContext();
            dataSource = (DataSource)context.lookup(“java:comp/env/dataSourceName”);
            pigDao = (PigDao) context.lookup(“java:comp/env/PigDaoName”);
        } catch (Exception e) {
        
        }
    }
}

     DL(依賴查詢)存在兩個問題:

      • 緊密耦合:依賴查詢使程式碼緊密耦合,如果資源發生了改變,我們需要在程式碼中執行大量修改;
      • 測試難:在測試應用程式時會產生一些問題,尤其是在黑盒測試中;  

    那什麼時候需要應用到DL(依賴查詢)呢?

    我們都知道,預設情況下,Spring中所有的bean建立都是單例模式,這意味著它們將在容器中只被建立一次,而同一個物件將被注入到請求它的任何地方。然而,有時需要不同的策略,比如每個方法呼叫都應該從一個新物件執行。現在想象一下,如果一個短生命週期的物件被注入到單例物件中,Spring會在每次呼叫時自動重新整理這個依賴嗎?答案當然是不會,除非我們指出這種特殊依賴型別的存在。

    假設我們有3個服務(類),其中一個依賴於其他服務,Service2是常見物件,可以使用前面講到的任何DI(依賴注入)技術注入到DependentService中,比如setter注入。Service1的物件將是不同的,它不能一次注入,每次呼叫都應該訪問一個新的例項 ---- 我們建立一個方法來提供這個物件,並讓Spring 知道它。

abstract class DependentService {
    private Service2 service2;

    public void setService2(Service2 service2) {
        this.service2 = service2;
    }

    void doSmth() {
        createService1().doSmth();
        service2.doSmth();
    }

    protected abstract Service1 createService1();
}

    在上面的程式碼中,我們沒有將Service1的物件宣告為通常的依賴項,相反,我們指定了將被 Spring Framework覆蓋的方法,以便返回 Service1類的最新例項。

    接下來我們進行xml檔案的配置,我們必須宣告Service1是一個生命週期較短的物件,在Spring中我們可以使用prototype作用域,因為它比單例物件小,通過look-method標籤,我們可以指定方法的名稱,它將注入依賴項:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="service1" class="example.Service1" scope="prototype"/>
    <bean id="service2" class="example.Service2"/>

    <bean id="dependentService" class="example.DependentService">
        <lookup-method name="createService1" bean="service1"/>
        <property name="service2" ref="service2"/>
    </bean>

</beans>

    當然,我們也可以使用註解方式來實現上面功能和配置:

@Service
@Scope(value = "prototype")
class Service1 {
    void doSmth() {
        System.out.println("Service1");
    }
}

@Service
abstract class DependentService {
    private Service2 service2;

    @Autowired
    public void setService2(Service2 service2) {
        this.service2 = service2;
    }

    void doSmth() {
        createService1().doSmth();
        service2.doSmth();
    }

    @Lookup
    protected abstract Service1 createService1();
}

    綜上所述,依賴查詢不同於其他注入型別,它適用於較小範圍的注入依賴,即生命週期更短的依賴注入。

相關文章