1. 概述
在某些情況下,我們需要將一個系統分解為多個進程,每個進程負責應用程序的不同方面。在這種情況下,一個進程通常需要從另一個進程同步獲取數據。
Spring Framework 提供了一系列工具,統稱為 Spring Remoting,它允許我們像調用本地服務一樣調用遠程服務。
在本文中,我們將基於 Spring 的 HTTP Invoker 設置一個應用程序,它利用了原生 Java 序列化和 HTTP 來提供客户端和服務器應用程序之間的遠程方法調用。
2. 服務定義
假設我們需要實現一個系統,允許用户預訂出租車。
假設我們決定構建兩個獨立的應用程序來實現這一目標:
- 一個預訂引擎應用程序,用於檢查出租車請求是否可以滿足,
- 一個前端 Web 應用程序,允許客户預訂他們的行程,並確保出租車可用性已確認。
2.1. 服務接口
當使用 Spring Remoting 與 HTTP 調用的方式 時,我們需要通過定義接口來讓 Spring 在客户端和服務器端創建代理,這些代理封裝了遠程調用的技術細節。 讓我們從一個允許預訂出租車的服務接口開始:
public interface CabBookingService {
Booking bookRide(String pickUpLocation) throws BookingException;
}當服務能夠分配出租車時,它會返回一個 Booking 對象,其中包含預訂代碼。 Booking 必須可序列化,因為 Spring 的 HTTP 調用器需要將實例從服務器傳輸到客户端:
public class Booking implements Serializable {
private String bookingCode;
@Override public String toString() {
return format("Ride confirmed: code '%s'.", bookingCode);
}
// standard getters/setters and a constructor
}如果服務無法預訂出租車,將會拋出一個 BookingException 異常。在這種情況下,無需將類標記為 Serializable,因為 Exception 類已經實現了它:
public class BookingException extends Exception {
public BookingException(String message) {
super(message);
}
}2.2. 服務打包
服務接口以及作為參數、返回值和異常使用的所有自定義類都需要在客户端和服務器的類路徑上可用。 最有效的方法之一是將其所有內容打包在一個 <em.jar 文件中,稍後將其作為服務器和客户端 pom.xml 中的依賴項進行包含。
讓我們將所有代碼放入一個名為“api”的專用 Maven 模塊中,對於本示例,我們將使用以下 Maven 座標:
<groupId>com.baeldung</groupId>
<artifactId>api</artifactId>
<version>1.0-SNAPSHOT</version>3. 服務器應用程序
讓我們使用 Spring Boot 構建用於暴露服務的預訂引擎應用程序。
3.1. Maven 依賴
首先,請確保你的項目正在使用 Spring Boot:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
</parent>您可以在這裏找到最新的 Spring Boot 版本 這裏。然後我們需要 Web starter 模塊:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>我們還需要在上一步驟中構建的服務定義模塊。
<dependency>
<groupId>com.baeldung</groupId>
<artifactId>api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>3.2. 服務實現
我們首先定義一個實現服務接口的類:
public class CabBookingServiceImpl implements CabBookingService {
@Override public Booking bookPickUp(String pickUpLocation) throws BookingException {
if (random() < 0.3) throw new BookingException("Cab unavailable");
return new Booking(randomUUID().toString());
}
}讓我們假定這代表了一個可能的實現。通過使用一個帶有隨機值的測試,我們可以重現兩個場景:當可用出租車被找到並返回預訂代碼,以及當拋出 BookingException 以指示沒有可用的出租車時。
3.3. 暴露服務
我們需要在上下文中定義一個具有 HttpInvokerServiceExporter 類型的 Bean。它將負責在 Web 應用程序中暴露一個 HTTP 入口點,該入口點稍後將被客户端調用:
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Server {
@Bean(name = "/booking") HttpInvokerServiceExporter accountService() {
HttpInvokerServiceExporter exporter = new HttpInvokerServiceExporter();
exporter.setService( new CabBookingServiceImpl() );
exporter.setServiceInterface( CabBookingService.class );
return exporter;
}
public static void main(String[] args) {
SpringApplication.run(Server.class, args);
}
}值得注意的是,Spring 的 HTTP 調用的實現 使用 HttpInvokerServiceExporter 這一 Bean 的名稱作為 HTTP 端點 URL 的相對路徑。
現在我們可以啓動服務器應用程序並保持其運行狀態,同時設置客户端應用程序。
4. 客户端應用程序
現在我們來編寫客户端應用程序。
4.1. Maven 依賴
我們將使用與服務端相同的服務定義和 Spring Boot 版本。 我們仍然需要 web starter 依賴,但由於我們不需要自動啓動嵌入式容器,因此可以從依賴中排除 Tomcat starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>4.2. 客户端實現
讓我們實現客户端:
@Configuration
public class Client {
@Bean
public HttpInvokerProxyFactoryBean invoker() {
HttpInvokerProxyFactoryBean invoker = new HttpInvokerProxyFactoryBean();
invoker.setServiceUrl("http://localhost:8080/booking");
invoker.setServiceInterface(CabBookingService.class);
return invoker;
}
public static void main(String[] args) throws BookingException {
CabBookingService service = SpringApplication
.run(Client.class, args)
.getBean(CabBookingService.class);
out.println(service.bookRide("13 Seagate Blvd, Key Largo, FL 33037"));
}
}@Bean 註解的 invoker() 方法創建了一個 HttpInvokerProxyFactoryBean 實例。我們需要通過 setServiceUrl() 方法提供遠程服務器響應的 URL。
類似於我們為服務器所做的事情,我們還應該通過 setServiceInterface() 方法提供我們想要通過遠程調用所引用的接口。
HttpInvokerProxyFactoryBean 實現 Spring 的 FactoryBean。FactoryBean 被定義為一個 Bean,但 Spring IoC 容器會注入它創建的對象,而不是工廠本身。您可以在我們的工廠 Bean 文章中找到關於 FactoryBean 的更多詳細信息。
main() 方法啓動獨立應用程序並從上下文中獲取 CabBookingService 實例。在底層,這個對象只是由 HttpInvokerProxyFactoryBean 創建的代理,它負責遠程調用中涉及的所有技術細節。 藉助它,我們現在可以像服務實現可用本地一樣輕鬆地使用代理。
讓我們多次運行該應用程序以執行多個遠程調用,以驗證客户端在出租車可用時和不可用時的行為。
5. 謹慎購買 (Caveat Emptor)
當我們使用允許遠程調用的技術時,我們應該充分意識到其中存在的潛在風險。
5.1. 注意網絡相關異常
當使用不可靠的網絡資源時,我們應時刻做好應對意外情況的準備。
假設客户端在無法訪問服務器的情況下調用服務器——這可能是由於網絡問題或服務器宕機造成的——Spring Remoting 將會拋出一個 RemoteAccessException,它是一個 RuntimeException。
編譯器不會強制我們把調用語句包含在 try-catch 塊中,但我們仍然應該考慮這樣做,以便妥善處理網絡問題。
5.2. 對象通過值傳遞,而非通過引用
Spring Remoting HTTP 會序列化方法參數和返回的值,以便通過網絡進行傳輸。這意味着服務器會操作提供的參數的副本,客户端會操作服務器創建的結果副本。
因此,我們不能期望,例如,調用結果對象上的方法會改變服務器端上的相同對象的狀態,因為客户端和服務器之間不存在共享的對象。
5.3. 謹防精細接口
跨網絡調用方法比在同一進程中的對象調用方法要慢得多。
因此,通常建議定義用於遠程調用的服務,這些服務應採用粗粒度的接口,能夠完成需要更少交互的業務事務,即使這意味着接口更復雜。
6. 結論
通過這個示例,我們看到了使用 Spring Remoting 輕鬆調用遠程進程的便捷性。
與 REST 或 Web 服務等更普遍的機制相比,該解決方案略顯封閉,但在所有組件都使用 Spring 開發的場景中,它仍然可以作為一種可行且更快捷的替代方案。