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 会抛出 RemoteAccessExceptionRuntimeException 子类)。虽然编译器不强制处理,但实际开发中必须捕获:

try {
    service.bookRide("location");
} catch (RemoteAccessException e) {
    // 处理网络问题
}

5.2. 对象传递机制

HTTP Invoker 通过值传递而非引用传递对象:

  • 服务端操作的是参数对象的副本
  • 客户端操作的是返回对象的副本
  • ❌ 无法通过修改返回对象影响服务端状态

5.3. 接口粒度控制

网络调用比本地调用慢几个数量级!设计远程服务接口时应:

  • ✅ 使用粗粒度接口(单次调用完成完整业务)
  • ❌ 避免细粒度接口(多次网络交互)
  • 即使接口稍显复杂,也要优先减少网络往返次数

6. 总结

通过示例可以看到,Spring Remoting 让远程调用变得异常简单。虽然相比 REST/Web 服务等方案开放性稍弱,但在全 Spring 技术栈的场景下,它是一个高效快捷的替代方案。

完整代码请参考 GitHub 仓库


原始标题:Intro to Spring Remoting with HTTP Invokers