1. Overview

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.

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.

2. Service Definition

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

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:

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:

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:

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

2.2. Packaging the Service

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.

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

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

3. Server Application

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

3.1. Maven Dependencies

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

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

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

<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

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.

3.3. Exposing the Service

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:

@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.

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

4. Client Application

Let’s now write the client application.

4.1. Maven Dependencies

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:

<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

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.

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.

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.

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.

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

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

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.

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.

5.2. Objects Are Transferred by Value, Not by Reference

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.

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

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

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

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.

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