JavaRush /Java Blog /Random-TW /簡單介紹一下依賴注入或“CDI 還有什麼?”
Viacheslav
等級 3

簡單介紹一下依賴注入或“CDI 還有什麼?”

在 Random-TW 群組發布
現在建立最受歡迎框架的基礎是依賴注入。我建議看看 CDI 規範對此有何規定,我們擁有哪些基本功能以及如何使用它們。
簡要介紹依賴注入或

介紹

我想將這篇簡短的評論專門用於 CDI 這樣的事情。這是什麼?CDI 代表上下文和依賴注入。這是一個描述依賴注入和上下文的 Java EE 規格。有關信息,您可以查看網站http://cdi-spec.org。由於 CDI 是一個規範(對它如何運作的描述,一組介面),我們還需要一個實作來使用它。其中一個實作是 Weld - http://weld.cdi-spec.org/ 為了管理依賴項並建立項目,我們將使用 Maven - https://maven.apache.org 所以,我們已經安裝了Maven,現在我們會在實踐中理解它,以免理解抽象。為此,我們將使用 Maven 建立一個專案。讓我們打開命令列(在Windows中,可以使用Win+R打開“運行”視窗並執行cmd)並要求Maven為我們做一切。為此,Maven 有一個稱為原型的概念:Maven Archetype
簡要介紹依賴注入或
之後,在「選擇數位或套用篩選器」和「選擇 org.apache.maven.archetypes:maven-archetype-quickstart 版本」問題上,只需按 Enter 鍵即可。接下來,輸入項目標識符,即所謂的 GAV(請參閱命名約定指南)。
簡要介紹依賴注入或
成功建立專案後,我們將看到“BUILD SUCCESS”字樣。現在我們可以在我們最喜歡的 IDE 中開啟我們的專案。

將 CDI 新增至項目

在介紹中,我們看到 CDI 有一個有趣的網站 - http://www.cdi-spec.org/。有一個下載部分,其中包含一個表,其中包含我們需要的資料:
簡要介紹依賴注入或
這裡我們可以看到Maven是如何描述我們在專案中使用CDI API這一事實的。API是應用程式接口,即某種程式設計接口。我們使用介面而不用擔心介面背後的內容和工作方式。API是一個jar檔案,我們將在我們的專案中開始使用它,也就是說,我們的專案開始依賴這個jar。因此,我們專案的 CDI API 是一個依賴項。在 Maven 中,專案在 POM.xml 檔案(POM - 專案物件模型)中進行描述。依賴項在依賴項區塊中描述,我們需要向其中新增一個條目:
<dependency>
	<groupId>javax.enterprise</groupId>
	<artifactId>cdi-api</artifactId>
	<version>2.0</version>
</dependency>
您可能已經注意到,我們沒有指定所提供的值的範圍。為什麼會有這樣的差異呢?這個範圍意味著有人將為我們提供依賴。當一個應用程式運行在Java EE伺服器上時,表示該伺服器將為該應用程式提供所有必要的JEE技術。為了使本次審查簡單起見,我們將在 Java SE 環境中工作,因此沒有人會為我們提供這種依賴項。您可以在此處閱讀有關依賴範圍的更多資訊:「依賴範圍」。好的,我們現在可以使用介面了。但我們還需要執行。我們記得,我們​​將使用焊接。有趣的是,到處都給了不同的依賴關係。但我們會遵循文檔。因此,讓我們閱讀“ 18.4.5.設定類路徑”並按照其說明進行操作:
<dependency>
	<groupId>org.jboss.weld.se</groupId>
	<artifactId>weld-se-core</artifactId>
	<version>3.0.5.Final</version>
</dependency>
重要的是 Weld 的第三行版本支援 CDI 2.0。因此,我們可以信賴這個版本的API。現在我們準備要寫程式碼了。
簡要介紹依賴注入或

初始化 CDI 容器

CDI是一種機制。必須有人控制這個機制。正如我們上面已經讀到的,這樣的管理器是一個容器。因此,我們需要創建它;它本身不會出現在SE環境中。讓我們將以下內容新增到 main 方法中:
public static void main(String[] args) {
	SeContainerInitializer initializer = SeContainerInitializer.newInstance();
	initializer.addPackages(App.class.getPackage());
	SeContainer container = initializer.initialize();
}
我們手動建立 CDI 容器是因為... 我們在 SE 環境中工作。在典型的戰鬥專案中,程式碼運行在伺服器上,伺服器為程式碼提供各種技術。因此,如果伺服器提供了CDI,這表示伺服器已經有一個CDI容器,我們不需要添加任何東西。但出於本教學的目的,我們將採用 SE 環境。另外,容器就在這裡,清晰易懂。為什麼我們需要容器?裡面的容器包含豆子(CDI豆)。
簡要介紹依賴注入或

CDI 豆

所以,豆子。什麼是 CDI 垃圾箱?這是一個遵循一些規則的 Java 類別。這些規則在規範的「2.2. bean 是什麼類型?」一章中進行了描述。讓我們將 CDI bean 加入到與 App 類別相同的套件中:
public class Logger {
    public void print(String message) {
        System.out.println(message);
    }
}
現在我們可以從我們的方法呼叫這個 bean main
Logger logger = container.select(Logger.class).get();
logger.print("Hello, World!");
正如您所看到的,我們沒有使用 new 關鍵字建立 bean。我們詢問 CDI 容器:“CDI 容器。我真的需要 Logger 類別的實例,請給我。” 這種方法稱為“ Dependency Lookup ”,即尋找依賴關係。現在讓我們建立一個新類別:
public class DateSource {
    public String getDate() {
        return new Date().toString();
    }
}
傳回日期的文字表示形式的原始類別。現在讓我們將日期輸出新增到訊息中:
public class Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(dateSource.getDate() + " : " + message);
    }
}
出現了一個有趣的@Inject註解。如 cdi 焊接文件的「 4.1. 注入點」一章中所述,我們使用此註釋定義注入點。在俄語中,這可以理解為「實施點」。CDI 容器使用它們在實例化 bean 時注入依賴項。正如您所看到的,我們沒有為 dateSource 欄位指派任何值。原因是 CDI 容器允許內部 CDI bean(僅是它自己實例化的那些 bean,也就是它所管理的 bean)使用「依賴注入」。這是控制反轉的另一種方式,在這種方式中,依賴關係由其他人控制,而不是由我們明確地創建物件。依賴注入可以透過方法、建構函式或欄位來完成。更多細節請參考CDI規範章節「5.5.依賴注入」。確定需要實作什麼的過程稱為型別安全解析,這就是我們需要討論的內容。
簡要介紹依賴注入或

名稱解析或型別安全解析

通常,介面用作要實現的物件類型,CDI 容器本身決定選擇哪個實作。這很有用,有很多原因,我們將討論這些原因。所以我們有一個記錄器介面:
public interface Logger {
    void print(String message);
}
他說,如果我們有一些記錄器,我們可以向它發送一條訊息,它就會完成它的任務 - 記錄。在這種情況下,如何以及在哪裡並不重要。現在讓我們為記錄器建立一個實作:
public class SystemOutLogger implements Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(message);
    }
}
正如您所看到的,這是一個寫入 System.out 的記錄器。精彩的。現在,我們的 main 方法將像以前一樣工作。 Logger logger = container.select(Logger.class).get(); 記錄器仍會收到該行。美妙之處在於我們只需要知道接口,CDI 容器已經為我們考慮了實現。假設我們有第二個實現,應該將日誌傳送到遠端儲存的某個位置:
public class NetworkLogger implements Logger {
    @Override
    public void print(String message) {
        System.out.println("Send log message to remote log system");
    }
}
如果我們現在運行程式碼而不進行任何更改,我們將收到錯誤,因為 CDI 容器看到該介面的兩個實現,但無法在它們之間進行選擇: org.jboss.weld.exceptions.AmbiguousResolutionException: WELD-001335: Ambiguous dependencies for type Logger 要做什麼?有幾種可用的變體。最簡單的一個是CDI bean 的@Vetoed註釋,以便 CDI 容器不會將此類視為 CDI bean。但還有一種更有趣的方法。可以使用Weld CDI 文件的「 4.7. 替代品@Alternative」章節中所述的註解將 CDI bean 標記為「替代品」 。這是什麼意思?這意味著除非我們明確地說使用它,否則它不會被選擇。這是 bean 的替代版本。讓我們將 NetworkLogger bean 標記為@Alternative,我們可以看到程式碼再次被執行並被 SystemOutLogger 使用。為了啟用替代方案,我們必須有一個beans.xml檔。可能會出現這樣的問題:“ beans.xml,我把你放在哪裡? ” 因此,讓我們正確放置文件:
簡要介紹依賴注入或
一旦我們有了這個文件,包含我們程式碼的工件將被稱為「顯式 bean 存檔」。現在我們有 2 個獨立的設定:軟體和 xml。問題是他們將加載相同的數據。例如,DataSource bean 定義將載入 2 次,我們的程式在執行時會崩潰,因為 CDI 容器會將它們視為 2 個獨立的 bean(儘管實際上它們是同一個類,CDI 容器了解了兩次)。為了避免這種情況,有兩種選擇:
  • 刪除該行initializer.addPackages(App.class.getPackage())並新增 xml 檔案的替代指示:
<beans
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://xmlns.jcp.org/xml/ns/javaee
        http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd">
    <alternatives>
        <class>ru.javarush.NetworkLogger</class>
    </alternatives>
</beans>
  • bean-discovery-mode將值為「none 」的屬性新增至 beans 根元素並以程式設計方式指定替代方案:
initializer.addPackages(App.class.getPackage());
initializer.selectAlternatives(NetworkLogger.class);
因此,使用 CDI 替代方案,容器可以確定選擇哪個 bean。有趣的是,如果 CDI 容器知道同一介面的多個替代方案,那麼我們可以透過使用註解指示優先權來告訴它@Priority(自 CDI 1.1 起)。
簡要介紹依賴注入或

預選賽

另外,值得討論一下像限定符這樣的事情。限定符由 bean 上方的註解指示,並細化對 bean 的搜尋。現在還有更多細節。有趣的是,任何 CDI bean 在任何情況下都至少有一個限定符 - @Any。如果我們沒有在 bean 上方指定任何限定符,但 CDI 容器本身會@Any在限定符中新增另一個限定符 - @Default。如果我們指定任何內容(例如,明確指定@Any),則不會自動新增@Default 限定符。但預選賽的美妙之處在於您可以創建自己的預選賽。限定符與註解幾乎沒有什麼不同,因為 本質上,這只是一個以特殊方式編寫的註釋。例如,您可以輸入 Enum 作為協定類型:
public enum ProtocolType {
    HTTP, HTTPS
}
接下來我們可以建立一個將這種類型考慮在內的限定詞:
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Protocol {
    ProtocolType value();
    @Nonbinding String comment() default "";
}
值得注意的是,標記為的欄位@Nonbinding並不影響限定符的確定。現在您需要指定限定符。它顯示在 bean 類型上方(以便 CDI 知道如何定義它)和注入點上方(使用 @Inject 註釋,以便您了解在該位置查找哪個 bean 進行注入)。例如,我們可以加入一些有限定符的類別。為簡單起見,在本文中,我們將在 NetworkLogger 中執行這些操作:
public interface Sender {
	void send(byte[] data);
}

@Protocol(ProtocolType.HTTP)
public static class HTTPSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sended via HTTP");
	}
}

@Protocol(ProtocolType.HTTPS)
public static class HTTPSSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sended via HTTPS");
	}
}
然後,當我們執行 Inject 時,我們將指定一個限定符來影響將使用哪個類別:
@Inject
@Protocol(ProtocolType.HTTPS)
private Sender sender;
太棒了,不是嗎?)看起來很美,但不清楚為什麼。現在想像一下:
Protocol protocol = new Protocol() {
	@Override
	public Class<? extends Annotation> annotationType() {
		return Protocol.class;
	}
	@Override
	public ProtocolType value() {
		String value = "HTTP";
		return ProtocolType.valueOf(value);
	}
};
container.select(NetworkLogger.Sender.class, protocol).get().send(null);
這樣我們就可以覆寫獲取值,以便可以動態計算它。例如,它可以從某些設定中取得。然後我們甚至可以即時更改實現,而無需重新編譯或重新啟動程序/伺服器。它變得更有趣,不是嗎?)
簡要介紹依賴注入或

製片人

CDI 的另一個有用的功能是生產者。這些是特殊方法(它們以特殊註解標記),當某些 bean 請求依賴項注入時呼叫。更多詳細資訊在文件的「2.2.3. 生產者方法」部分中進行了描述。最簡單的例子:
@Produces
public Integer getRandomNumber() {
	return new Random().nextInt(100);
}
現在,當注入 Integer 類型的欄位時,將呼叫此方法並從中取得值。這裡我們應該立即明白,當我們看到關鍵字new時,我們必須立即明白這不是一個CDI bean。也就是說,Random 類別的實例不會因為它衍生自控制 CDI 容器的物件(在本例中為生產者)而成為 CDI bean。
簡要介紹依賴注入或

攔截器

攔截器是「幹擾」工作的攔截器。在 CDI 中,這一點做得非常清楚。讓我們看看如何使用解釋器(或攔截器)進行日誌記錄。首先,我們需要描述與攔截器的綁定。與許多事情一樣,這是使用註釋完成的:
@Inherited
@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface ConsoleLog {
}
這裡最重要的是,這是一個攔截器(@InterceptorBinding)的綁定,它將被extends(@InterceptorBinding)繼承。現在讓我們來寫攔截器本身:
@Interceptor
@ConsoleLog
public class LogInterceptor {
    @AroundInvoke
    public Object log(InvocationContext ic) throws Exception {
        System.out.println("Invocation method: " + ic.getMethod().getName());
        return ic.proceed();
    }
}
您可以在規格的範例中閱讀有關如何編寫攔截器的更多資訊:「1.3.6.攔截器範例」。好吧,我們所要做的就是打開inerceptor。為此,請在正在執行的方法上方指定綁定註解:
@ConsoleLog
public void print(String message) {
現在還有另一個非常重要的細節。攔截器預設被停用,必須以與替代方案相同的方式啟用。例如,在beans.xml檔中:
<interceptors>
	<class>ru.javarush.LogInterceptor</class>
</interceptors>
正如您所看到的,這非常簡單。
簡要介紹依賴注入或

事件與觀察員

CDI 也提供了事件和觀察者的模型。這裡一切都不像攔截器那麼明顯。因此,本例中的事件絕對可以是任何類別;描述不需要任何特殊內容。例如:
public class LogEvent {
    Date date = new Date();
    public String getDate() {
        return date.toString();
    }
}
現在應該有人等待該事件:
public class LogEventListener {
    public void logEvent(@Observes LogEvent event){
        System.out.println("Message Date: " + event.getDate());
    }
}
這裡最主要的是指定@Observes註解,它顯示這不只是一個方法,而是一個應該作為觀察LogEvent類型事件的結果而被呼叫的方法。好吧,現在我們需要有人來觀看:
public class LogObserver {
    @Inject
    private Event<LogEvent> event;
    public void observe(LogEvent logEvent) {
        event.fire(logEvent);
    }
}
我們有一個方法可以告訴容器事件類型 LogEvent 的 Event 事件已發生。現在剩下的就是使用觀察者了。例如,在 NetworkLogger 中,我們可以加入觀察者的注入:
@Inject
private LogObserver observer;
在 print 方法中,我們可以通知觀察者我們有一個新事件:
public void print(String message) {
	observer.observe(new LogEvent());
重要的是要知道事件可以在一個或多個線程中處理。對於非同步處理,請使用方法.fireAsync(而不是 .fire)和註釋@ObservesAsync(而不是 @Observes)。例如,如果所有事件都在不同的執行緒中執行,那麼如果 1 個執行緒拋出異常,則其他執行緒將能夠為其他事件執行其工作。像往常一樣,您可以在規範中的「10. 事件」一章中閱讀有關 CDI 中事件的更多資訊。
簡要介紹依賴注入或

裝飾器

正如我們在上面看到的,各種設計模式都收集在 CDI 翼下。這是另一個 - 裝飾器。這是一件非常有趣的事。我們來看看這個類別:
@Decorator
public abstract class LoggerDecorator implements Logger {
    public final static String ANSI_GREEN = "\u001B[32m";
    public static final String ANSI_RESET = "\u001B[0m";

    @Inject
    @Delegate
    private Logger delegate;

    @Override
    public void print(String message) {
        delegate.print(ANSI_GREEN + message + ANSI_RESET);
    }
}
透過將其聲明為裝飾器,我們可以說,當使用任何 Logger 實現時,都會使用這個“附加元件”,它知道真正的實現,該實現存儲在 delegate 字段中(因為它是用註釋標記的)@Delegate。裝飾器只能與 CDI bean 關聯,它本身既不是攔截器也不是裝飾器。規格中還可以看到一個例子:「1.3.7.裝飾器範例」。裝飾器和攔截器一樣,必須打開。例如,在beans.xml中:
<decorators>
	<class>ru.javarush.LoggerDecorator</class>
</decorators>
有關更多詳細信息,請參閱焊接參考:“第 10 章裝飾器”。

生命週期

Bean 有自己的生命週期。它看起來像這樣:
簡要介紹依賴注入或
從圖中可以看出,我們有所謂的生命週期回檔。這些註解將告訴 CDI 容器在 bean 生命週期的某個階段呼叫某些方法。例如:
@PostConstruct
public void init() {
	System.out.println("Inited");
}
當容器實例化 CDI bean 時將呼叫此方法。當 bean 不再需要時被銷毀時,@PreDestroy 也會發生同樣的情況。首字母縮寫 CDI 包含字母 C(Context)並非毫無意義。CDI 中的 Bean 是上下文相關的,這意味著它們的生命週期取決於它們在 CDI 容器中存在的上下文。為了更好地理解這一點,您應該閱讀規範部分「7.上下文實例的生命週期」。另外值得了解的是,容器本身俱有生命週期,您可以在「容器生命週期事件」中閱讀有關該生命週期的資訊。
簡要介紹依賴注入或

全部的

上面我們看到了名為 CDI 的冰山一角。CDI 是 JEE 規範的一部分,用於 JavaEE 環境。使用Spring的不是使用CDI,而是DI,也就是這些是略有不同的規範。但了解並理解上述內容後,您可以輕鬆改變主意。考慮到 Spring 支援來自 CDI 世界的註釋(相同的 Inject)。附加資料: #維亞切斯拉夫
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION