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

  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 更傾向並推薦使用構造方法注入

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的定義信息:

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

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

<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註解來進行序列號和反序列化了,如下面的案例:

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版本就支持這個註解了;

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)當構造方法是私有時,可以提供一個靜態工廠方法供外部使用:

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方法來完成的;

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的定義信息:

<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方法發佈一個角色接口,它可以用來建立注入器在注入依賴時應該如何與客戶端通信
      // 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.電影:

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.電影查找器注入接口實現:

//電影查找器
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.文件名注入接口實現:

//文件名注入
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.測試:

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註解指定使用哪個構造方法來注入依賴項。

public class ExampleBean {

    private AnotherBean beanOne;
    
    @Autowired
    public void setBeanOne(AnotherBean beanOne) {
        this.beanOne = beanOne;
    }  
    
}
View Code
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注入方法注入依賴項:

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();
}

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章