Java 中的網際網路位址解析 SPI
一、簡介
在本教程中,我們將討論 Java 的JEP 418 ,它為 Internet 主機和位址解析建立了新的服務提供者介面 (SPI)。
2. 網際網路位址解析
連接到電腦網路的任何裝置都會指派一個數值或 IP(網際網路通訊協定)位址。 IP 位址有助於唯一地識別網路上的設備,並且還有助於在設備之間路由資料包。
它們通常有兩種類型。 IPv4是第四代IP標準,是32位元位址。由於互聯網的快速發展,也發布了較新的 IP 標準 v6,該標準更大並且包含十六進製字元。
此外,還有另一種相關類型的位址。網路設備(例如乙太網路連接埠或網路介面卡 (NIC))具有 MAC(媒體存取控制)位址。它們是全球分佈的,並且所有網路介面設備都可以透過 MAC 位址進行唯一識別。
網際網路位址解析廣義上是指將較高層級的網路位址(例如網域(例如 baeldung)或 URL(https://www.baeldung.com))轉換為較低層級的網路位址(例如 IP)位址或MAC地址。
3. Java 中的 Internet 位址解析
如今,Java 使用java.net.InetAddress
API 提供了多種解析 Internet 位址的方法。 API 在內部使用作業系統的本機解析器進行 DNS 查找。
InetAddress
API 目前使用的作業系統本機位址解析涉及多個步驟。涉及系統級 DNS 緩存,其中儲存常用查詢的 DNS 映射。如果本機 DNS 快取中發生快取未命中,系統解析器配置會提供有關 DNS 伺服器的資訊以執行後續查找。
然後,作業系統會向上一步中取得的配置的 DNS 伺服器查詢該資訊。此步驟可能會遞歸發生幾次。
如果匹配和查找成功,則 DNS 位址將快取在所有伺服器上並返回原始用戶端。然而,如果沒有匹配,則會觸發根伺服器的迭代查找過程,提供有關權威 Nave 伺服器 (ANS) 的資訊。這些權威名稱伺服器 (ANS) 儲存有關頂級網域名稱 (TLD) 的信息,例如 .org、.com 等。
這些步驟最終將網域與 Internet 位址相符(如果該位址有效)或傳回失敗訊息給客戶端。
4.使用Java的InetAddress
API
InetAddress
API 提供了多種執行 DNS 查詢和解析的方法。這些 API 作為java.net
套件的一部分提供。
4.1. getAllByName()
API
getAllByName()
API 嘗試將主機名稱對應到一組 IP 位址:
InetAddress[] inetAddresses = InetAddress.getAllByName(host);
Assert.assertTrue(Arrays.stream(inetAddresses).map(InetAddress::getHostAddress).toArray(String[]::new) > 1);
這也稱為前向查找。
4.2. getByName()
API
getByName()
API 與先前的正向查找 API 類似,只不過它只將主機對應到第一個符合的 IP 位址:
InetAddress inetAddress = InetAddress.getByName("www.google.com"); Assert.assertNotNull(inetAddress.getHostAddress()); // returns an IP Address
4.3. getByAddress()
API
這是執行反向查找的最基本的 API,其中它將 IP 位址作為輸入並嘗試傳回與其關聯的主機:
InetAddress inetAddress = InetAddress.getByAddress(ip);
Assert.assertNotNull(inetAddress.getHostName()); // returns a host (eg. google.com)
4.4. getCanonicalHostName()
API 和getHostName()
API
這些 API 執行類似的反向查找並嘗試傳回與其關聯的完全限定網域名稱 (FQDN):
InetAddress inetAddress = InetAddress.getByAddress(ip);
Assert.assertNotNull(inetAddress.getCanonicalHostName()); // returns a FQDN
Assert.assertNotNull(inetAddress.getHostName());
5. 服務提供者介面(SPI)
服務提供者介面(SPI)模式是軟體開發中使用的重要設計模式。此模式的目的是允許特定服務的可插入元件和實作。
它允許開發人員在不修改服務的任何核心期望的情況下擴展系統的功能,並使用任何實現而不受單一實現的束縛。
5.1. InetAddress
中的 SPI 元件
遵循 SPI 設計模式,此 JEP 提出了一種以自訂解析器取代預設系統解析器的方法。 SPI 從 Java 18 開始可用。需要服務定位器來定位要使用的提供者。如果服務定位器無法識別任何提供者服務,它將返回預設實作。
與任何 SPI 實作一樣,有四個主要元件:
- 服務是第一個元件,是提供特定功能的介面和類別的集合。在我們的例子中,我們正在將互聯網位址解析作為服務來處理
- 服務提供者介面是充當服務代理的介面或抽象類別。該介面將其定義的所有操作委託給其實作。
InetAddressResolver
介面是我們用例的服務提供者接口,它定義了查找主機名稱和 IP 位址以進行解析的操作 - 第三個元件是服務提供者,它定義了服務提供者介面的具體實作。
InetAddressResolverProvider
是一個抽象類,其用途是充當解析器的許多自訂實作的工廠。我們將透過擴展這個抽象類別來定義我們的實作。 JVM 維護一個系統範圍的解析器,然後由InetAddress
使用,並且通常在 VM 初始化期間設置 - 最後一個元件 Service Loader 元件將所有這些連結在一起。
ServiceLoader
機制將找到符合條件的InetAddressResolverProvider
提供者實作並將其設定為預設的系統範圍解析器。如果發生故障,後備機制會在系統範圍內設定預設解析器
5.2. InetAddressResolverProvider
的自訂實現
透過此 SPI 進行的變更可在java.net.spi package,
並且新添加了以下類別:
-
InetAddressResolverProvider
-
InetAddressResolver
-
InetAddressResolver.LookupPolicy
-
InetAddressResolverProvider.Configuration
在本節中,我們將嘗試為InetAddressResolver
編寫自訂解析器實作來取代系統預設解析器。在編寫自訂解析器之前,我們可以定義一個小型實用程式類,它將地址映射註冊表從檔案載入到記憶體(或快取)。
根據登錄項,我們的自訂位址解析器將能夠將位址主機解析為 IP,反之亦然。
首先,我們透過從抽象類別 InetAddressResolverProvider 擴充功能來定義我們的類別CustomAddressResolverImpl
InetAddressResolverProvider.
這樣做需要我們立即提供兩個方法的實作: get(Configuration configuration)
和name().
我們可以使用name()
傳回目前實作類別的名稱或任何其他相關識別碼:
@Override
public String name() {
return "CustomInternetAddressResolverImpl";
}
現在讓我們實作get()
方法。 get()
方法傳回InetAddressResolver
類別的實例,我們可以內聯或單獨定義該實例。為了簡單起見,我們將內聯定義它。
InetAddressResolver
介面有兩種方法:
-
Stream<InetAddress> lookupByName(String host, LookupPolicy lookupPolicy) throws UnknownHostException
-
String lookupByAddress(byte[] addr) throws UnknownHostException
我們可以編寫任何自訂邏輯來將主機對應到其 IP 位址(以InetAddress
的形式),反之亦然。在這個例子中,我們將讓我們的Registry
功能處理相同的事情:
@Override
public InetAddressResolver get(Configuration configuration) {
LOGGER.info("Using Custom Address Resolver :: " + this.name());
LOGGER.info("Registry initialised");
return new InetAddressResolver() {
@Override
public Stream<InetAddress> lookupByName(String host, LookupPolicy lookupPolicy) throws UnknownHostException {
return registry.getAddressesfromHost(host);
}
@Override
public String lookupByAddress(byte[] addr) throws UnknownHostException {
return registry.getHostFromAddress(addr);
}
};
}
5.3. Registry
類實現
在本文中,我們將使用HashMap
在記憶體中儲存 IP 位址和主機名稱清單。我們也可以從系統上的檔案載入清單。
Map 的類型為Map<String, List<byte[]>>
,其中主機名稱儲存為鍵,IP 位址儲存為byte[]
List
。此資料結構允許將多個 IP 對應到單一主機。我們可以使用這個Map.
在這種情況下,正向查找是指我們將主機名稱作為參數傳遞並期望根據其 IP 位址解析它,例如,當我們輸入www.baeldung.com
時:
public Stream<InetAddress> getAddressesfromHost(String host) throws UnknownHostException {
LOGGER.info("Performing Forward Lookup for HOST : " + host);
if (!registry.containsKey(host)) {
throw new UnknownHostException("Missing Host information in Resolver");
}
return registry.get(host)
.stream()
.map(add -> constructInetAddress(host, add))
.filter(Objects::nonNull);
}
我們應該注意到,回應是一個InetAddress
Stream
,用於容納多個 IP。
反向查找的一個例子是當我們想知道與 IP 位址關聯的主機名稱時:
public String getHostFromAddress(byte[] arr) throws UnknownHostException {
LOGGER.info("Performing Reverse Lookup for Address : " + Arrays.toString(arr));
for (Map.Entry<String, List<byte[]>> entry : registry.entrySet()) {
if (entry.getValue()
.stream()
.anyMatch(ba -> Arrays.equals(ba, arr))) {
return entry.getKey();
}
}
throw new UnknownHostException("Address Not Found");
}
最後, ServiceLoader
模組載入我們的 InetAddress 解析的自訂實作。
為了發現我們的服務提供者,我們在resources/META-INF/services
層次結構下建立一個名為java.net.spi.InetAddressResolverProvider.
設定檔應將我們的提供者的完全限定路徑維護為com.baeldung.inetspi.providers.CustomAddressResolverImpl.java.
這告訴 JVM 依照 SPI 模式載入提供者的相應實作。
6.替代解決方案
如果我們不想添加地址解析的自訂實現,我們有一些解決方法:
- 使用 JNDI 及其 DNS 提供者是使用 InetAddress 進行解析的替代方法;但是,我們無法利用 InetAddress 提供的豐富 API 來更輕鬆地存取
- 我們可以透過巴拿馬計畫的 JNI 使用作業系統的本機解析器
- 最後,我們可以直接修改 JDK 系統屬性文件,例如
jdk.net.hosts.file
,以通知InetAddress
使用特定文件進行主機匹配。然而,要維持一份詳盡的清單是很困難的。
七、結論
在本文中,我們研究了 Internet 位址解析如何使用 InetAddress API 在 Java 中運作。我們也檢查了用於網際網路主機和位址解析的服務供應商介面 (SPI) 的 JEP 418。我們實作了一個用於互聯網位址解析的自訂提供程序,並討論了一些替代方案。
與往常一樣,範例的程式碼可以在 GitHub 上取得。