Intro to Spring Remoting with HTTP Invokers – 使用HTTP Invokers的Spring Remoting介绍

最后修改: 2017年 1月 24日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In some cases, we need to decompose a system into several processes, each taking responsibility for a different aspect of our application. In these scenarios is not uncommon that one of the processes needs to synchronously get data from another one.

在某些情况下,我们需要将一个系统分解成几个进程,每个进程负责我们应用程序的不同方面。在这些情况下,其中一个进程需要从另一个进程中同步获取数据的情况并不罕见。

The Spring Framework offers a range of tools comprehensively called Spring Remoting that allows us to invoke remote services as if they were, at least to some extent, available locally.

Spring框架提供了一系列被全面称为Spring Remoting的工具,允许我们调用远程服务,就好像它们至少在某种程度上是本地可用的一样。

In this article, we will set up an application based on Spring’s HTTP invoker, which leverages native Java serialization and HTTP to provide remote method invocation between a client and a server application.

在这篇文章中,我们将建立一个基于Spring的 HTTP invoker的应用程序,它利用本地Java序列化和HTTP来提供客户端和服务器应用程序之间的远程方法调用。

2. Service Definition

2.服务定义

Let’s suppose we have to implement a system that allows users to book a ride in a cab.

假设我们要实现一个允许用户预订出租车的系统。

Let’s also suppose that we choose to build two distinct applications to obtain this goal:

我们还假设,我们选择建立两个不同的应用程序来获得这个目标。

  • a booking engine application to check whether a cab request can be served, and
  • a front-end web application that allows customers to book their rides, ensuring the availability of a cab has been confirmed

2.1. Service Interface

2.1.服务接口

When we use Spring Remoting with HTTP invoker, we have to define our remotely callable service trough an interface to let Spring create proxies at both client and server side that encapsulate the technicalities of the remote call. So let’s start with the interface of a service that allows us to book a cab:

当我们使用Spring RemotingHTTP invoker时,我们必须通过一个接口来定义我们的可远程调用的服务,让Spring在客户端和服务器端创建代理,以封装远程调用的技术特性。因此,让我们从允许我们预订出租车的服务接口开始。

public interface CabBookingService {
    Booking bookRide(String pickUpLocation) throws BookingException;
}

When the service is able to allocate a cab, it returns a Booking object with a reservation code. Booking has to be a serializable because Spring’s HTTP invoker has to transfer its instances from the server to the client:

当服务能够分配到一辆出租车时,它会返回一个带有预订代码的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
}

If the service is not able to book a cab, a BookingException is thrown. In this case, there’s no need to mark the class as Serializable because Exception already implements it:

如果该服务无法预订出租车,就会抛出一个BookingException。在这种情况下,没有必要将该类标记为Serializable,因为Exception已经实现了它。

public class BookingException extends Exception {
    public BookingException(String message) {
        super(message);
    }
}

2.2. Packaging the Service

2.2.包装服务

The service interface along with all custom classes used as arguments, return types and exceptions have to be available in both client’s and server’s classpath. One of the most effective ways to do that is to pack all of them in a .jar file that can be later included as a dependency in the server’s and client’s pom.xml.

服务接口以及所有用作参数的自定义类、返回类型和异常都必须在客户端和服务器的classpath中可用。最有效的方法之一是将所有这些类打包在一个.jar文件中,然后作为依赖项包含在服务器和客户端的pom.xml中。

Let’s thus put all the code in a dedicated Maven module, called “api”; we’ll use the following Maven coordinates for this example:

因此,让我们把所有代码放在一个专门的Maven模块中,称为 “api”;本例中我们将使用以下Maven坐标。

<groupId>com.baeldung</groupId>
<artifactId>api</artifactId>
<version>1.0-SNAPSHOT</version>

3. Server Application

3.服务器应用

Let’s build the booking engine application to expose the service using Spring Boot.

让我们使用Spring Boot构建预订引擎应用程序来暴露服务。

3.1. Maven Dependencies

3.1.Maven的依赖性

First, you’ll need to make sure your project is using Spring Boot:

首先,你需要确保你的项目使用的是Spring Boot。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.2</version>
</parent>

You can find the last Spring Boot version here. We then need the Web starter module:

您可以在这里找到最新的Spring Boot版本。然后我们需要Web starter模块。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

And we need the service definition module that we assembled in the previous step:

而我们需要在上一步组装的服务定义模块。

<dependency>
    <groupId>com.baeldung</groupId>
    <artifactId>api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

3.2. Service Implementation

3.2.服务实施

We firstly define a class that implements the service’s interface:

我们首先定义一个实现服务接口的类。

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());
    }
}

Let’s pretend that this is a likely implementation. Using a test with a random value we’ll be able to reproduce both successful scenarios – when an available cab has been found and a reservation code returned – and failing scenarios – when a BookingException is thrown to indicate that there is not any available cab.

让我们假设这是一个可能的实现。使用一个随机值的测试,我们将能够重现成功的场景–当找到可用的出租车并返回预订代码时,以及失败的场景–当抛出BookingException表示没有任何可用的出租车时。

3.3. Exposing the Service

3.3.暴露服务

We then need to define an application with a bean of type HttpInvokerServiceExporter in the context. It will take care of exposing an HTTP entry point in the web application that will be later invoked by the client:

然后我们需要在上下文中定义一个带有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);
    }
}

It is worth noting that Spring’s HTTP invoker uses the name of the HttpInvokerServiceExporter bean as a relative path for the HTTP endpoint URL.

值得注意的是,Spring的HTTP调用器使用HttpInvokerServiceExporterBean的名称作为HTTP端点URL的相对路径。

We can now start the server application and keep it running while we set up the client application.

现在我们可以启动服务器应用程序,并在设置客户端应用程序时保持其运行。

4. Client Application

4.客户应用

Let’s now write the client application.

现在我们来写一下客户端的应用程序。

4.1. Maven Dependencies

4.1.Maven的依赖性

We’ll use the same service definition and the same Spring Boot version we used at server-side. We still need the web starter dependency, but since we don’t need to automatically start an embedded container, we can exclude the Tomcat starter from the dependency:

我们将使用相同的服务定义和我们在服务器端使用的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. Client Implementation

4.2.客户端实施

Let’s implement the client:

让我们来实现这个客户端。

@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"));
    }
}

The @Bean annotated invoker() method creates an instance of HttpInvokerProxyFactoryBean. We need to provide the URL that the remote server responds at through the setServiceUrl() method.

@Bean注释的invoker()方法创建了一个HttpInvokerProxyFactoryBean的实例。我们需要通过setServiceUrl()方法提供远程服务器所响应的URL。

Similarly to what we did for the server, we should also provide the interface of the service we want to invoke remotely through the setServiceInterface() method.

与我们对服务器所做的类似,我们也应该通过setServiceInterface()方法提供我们想要远程调用的服务的接口。

HttpInvokerProxyFactoryBean implements Spring’s FactoryBean. A FactoryBean is defined as a bean, but the Spring IoC container will inject the object it creates, not the factory itself. You can find more details about FactoryBean in our factory bean article.

HttpInvokerProxyFactoryBean实现了Spring的FactoryBeanFactoryBean被定义为一个Bean,但是Spring IoC容器将注入它所创建的对象,而不是工厂本身。您可以在我们的factory bean文章中找到关于FactoryBean的更多细节。

The main() method bootstraps the stand alone application and obtains an instance of CabBookingService from the context. Under the hood, this object is just a proxy created by the HttpInvokerProxyFactoryBean that takes care of all technicalities involved in the execution of the remote invocation. Thanks to it we can now easily use the proxy as we would do if the service implementation had been available locally.

main()方法引导独立的应用程序,并从上下文中获得一个CabBookingService的实例。在引擎盖下,这个对象只是一个由HttpInvokerProxyFactoryBean创建的代理,它负责执行远程调用中涉及的所有技术问题。多亏了它,我们现在可以轻松地使用代理,就像我们在本地提供服务实现时那样。

Let’s run the application multiple times to execute several remote calls to verify how the client behaves when a cab is available and when it is not.

让我们多次运行该应用程序,执行几个远程调用,以验证客户端在出租车可用和不可用时的表现。

5. Caveat Emptor

5.注意事项

When we work with technologies that allow remote invocations, there are some pitfalls we should be well aware of.

当我们使用允许远程调用的技术时,有一些陷阱是我们应该很清楚的。

5.1. Beware of Network Related Exceptions

5.1.警惕与网络有关的异常情况

We should always expect the unexpected when we work with an unreliable resource as the network.

当我们与网络这种不可靠的资源一起工作时,我们应该始终期待意外的发生。

Let’s suppose the client is invoking the server while it cannot be reached – either because of a network problem or because the server is down – then Spring Remoting will raise a RemoteAccessException that is a RuntimeException.

假设客户端在调用服务器的时候,由于网络问题或服务器宕机而无法到达,那么Spring Remoting将引发一个RemoteAccessException,即一个RuntimeException。

The compiler will not then force us to include the invocation in a try-catch block, but we should always consider to do it, to properly manage network problems.

编译器不会强迫我们在try-catch块中包含调用,但我们应该始终考虑这样做,以正确管理网络问题。

5.2. Objects Are Transferred by Value, Not by Reference

5.2.对象是通过值而不是通过引用来转移的

Spring Remoting HTTP marshals method arguments and returned values to transmit them on the network. This means that the server acts upon a copy of the provided argument and the client acts upon a copy of the result created by the server.

Spring Remoting HTTP将方法参数和返回的值打包,在网络上传输。这意味着服务器根据所提供的参数的副本行事,而客户端则根据服务器创建的结果的副本行事。

So we cannot expect, for instance, that invoking a method on the resulting object will change the status of the same object on the server side because there is not any shared object between client and server.

因此,我们不能期望,例如,在结果对象上调用一个方法将改变服务器端同一对象的状态,因为在客户端和服务器之间没有任何共享对象。

5.3. Beware of Fine-Grained Interfaces

5.3.当心细粒度的接口

Invoking a method across network boundaries is significantly slower than invoking it on an object in the same process.

跨越网络边界调用一个方法要比在同一进程中的对象上调用要慢得多。

For this reason, it is usually a good practice to define services that should be remotely invoked with coarser grained interfaces that are able to complete business transactions requiring fewer interactions, even at the expense of a more cumbersome interface.

出于这个原因,通常的一个好做法是,用更粗粒度的接口来定义应该被远程调用的服务,这些服务能够完成需要较少交互的业务交易,甚至以更繁琐的接口为代价。

6. Conclusion

6.结论

With this example, we saw how it is easy with Spring Remoting to invoke a remote process.

通过这个例子,我们看到了Spring Remoting是如何轻松地调用一个远程进程的。

The solution is slightly less open than other widespread mechanisms like REST or web services, but in scenarios where all the components are developed with Spring, it can represent a viable and far quicker alternative.

与其他广泛的机制如REST或Web服务相比,该解决方案的开放性稍差,但在所有组件都用Spring开发的情况下,它可以代表一种可行的、更快速的替代方案。

As usual, you’ll find the sources over on GitHub.

像往常一样,你可以在GitHub上找到源代码