1. 概述
在系统架构设计中,我们常需要将应用拆分为多个独立进程,每个进程负责不同功能模块。当某个进程需要同步获取另一个进程的数据时,Spring 框架提供的 Spring Remoting 工具集就能派上用场——它让我们能像调用本地方法一样调用远程服务。
本文将重点介绍基于 HTTP Invoker 的实现方案。它通过 Java 原生序列化和 HTTP 协议,在客户端与服务端之间实现远程方法调用,简单粗暴但高效。
2. 服务定义
假设我们要开发一个出租车预约系统,包含两个独立应用:
- 预约引擎应用:检查出租车是否可用
- 前端 Web 应用:供用户预约,并确认车辆可用性
2.1. 服务接口
使用 HTTP Invoker 时,必须通过接口定义远程服务,以便 Spring 在客户端和服务端创建代理对象。先定义预约服务的接口:
public interface CabBookingService {
Booking bookRide(String pickUpLocation) throws BookingException;
}
当成功分配车辆时,服务返回包含预约码的 Booking
对象。注意该类必须实现 Serializable
,因为 HTTP Invoker 需要通过网络传输实例:
public class Booking implements Serializable {
private String bookingCode;
@Override public String toString() {
return format("Ride confirmed: code '%s'.", bookingCode);
}
// 标准的 getter/setter 和构造方法
}
若预约失败则抛出 BookingException
。由于 Exception
本身已实现 Serializable
,无需额外标记:
public class BookingException extends Exception {
public BookingException(String message) {
super(message);
}
}
2.2. 服务打包
服务接口及所有自定义类(参数/返回值/异常)必须同时存在于客户端和服务端的类路径中。最佳实践是将这些类打包成独立的 JAR 文件,作为依赖引入。
我们创建名为 api
的 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>
添加 Web 启动器依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
引入前文创建的 api
模块:
<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());
}
}
这里用随机数模拟两种场景:
- ✅ 成功分配车辆(返回预约码)
- ❌ 车辆不可用(抛出异常)
3.3. 暴露服务
在 Spring 配置中定义 HttpInvokerServiceExporter
Bean,它负责暴露 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);
}
}
⚠️ 注意:Bean 名称 /booking
会作为 HTTP 接口的相对路径。启动服务端后保持运行,接下来配置客户端。
4. 客户端应用
4.1. Maven 依赖
使用与服务端相同的 api
模块和 Spring Boot 版本。Web 依赖需排除 Tomcat(客户端无需嵌入式容器):
<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"));
}
}
关键点说明:
HttpInvokerProxyFactoryBean
是 Spring 的FactoryBean
,实际注入的是其创建的代理对象- 通过
setServiceUrl()
指定服务端接口地址 - 通过
setServiceInterface()
设置服务接口
运行客户端多次,观察成功/失败场景的输出结果。
5. 踩坑指南
使用远程调用技术时,务必注意以下常见陷阱:
5.1. 网络异常处理
网络是不可靠的!当服务端不可达时,Spring Remoting 会抛出 RemoteAccessException
(RuntimeException
子类)。虽然编译器不强制处理,但实际开发中必须捕获:
try {
service.bookRide("location");
} catch (RemoteAccessException e) {
// 处理网络问题
}
5.2. 对象传递机制
HTTP Invoker 通过值传递而非引用传递对象:
- 服务端操作的是参数对象的副本
- 客户端操作的是返回对象的副本
- ❌ 无法通过修改返回对象影响服务端状态
5.3. 接口粒度控制
网络调用比本地调用慢几个数量级!设计远程服务接口时应:
- ✅ 使用粗粒度接口(单次调用完成完整业务)
- ❌ 避免细粒度接口(多次网络交互)
- 即使接口稍显复杂,也要优先减少网络往返次数
6. 总结
通过示例可以看到,Spring Remoting 让远程调用变得异常简单。虽然相比 REST/Web 服务等方案开放性稍弱,但在全 Spring 技术栈的场景下,它是一个高效快捷的替代方案。
完整代码请参考 GitHub 仓库