Introducción a Spring Remoting con invocadores HTTP

1. Información general

En algunos casos, necesitamos descomponer un sistema en varios procesos, cada uno asumiendo la responsabilidad de un aspecto diferente de nuestra aplicación. En estos escenarios, no es raro que uno de los procesos necesite obtener datos de forma síncrona de otro.

Spring Framework ofrece una gama de herramientas llamadas Spring Remoting que nos permite invocar servicios remotos como si estuvieran, al menos hasta cierto punto, disponibles localmente.

En este artículo, configuraremos una aplicación basada en el invocador HTTP de Spring , que aprovecha la serialización nativa de Java y HTTP para proporcionar una invocación de método remoto entre un cliente y una aplicación de servidor.

2. Definición de servicio

Supongamos que tenemos que implementar un sistema que permita a los usuarios reservar un viaje en un taxi.

Supongamos también que elegimos construir dos aplicaciones distintas para lograr este objetivo:

  • una aplicación de motor de reservas para comprobar si se puede atender una solicitud de taxi, y
  • una aplicación web de front-end que permite a los clientes reservar sus viajes, asegurando que se haya confirmado la disponibilidad de un taxi

2.1. Interfaz de servicio

Cuando usamos Spring Remoting con el invocador HTTP, tenemos que definir nuestro servicio que se puede llamar de forma remota a través de una interfaz para permitir que Spring cree proxies tanto en el lado del cliente como en el del servidor que encapsulan los aspectos técnicos de la llamada remota. Así que comencemos con la interfaz de un servicio que nos permite reservar un taxi:

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

Cuando el servicio puede asignar un taxi, devuelve un objeto Booking con un código de reserva. La reserva debe ser serializable porque el invocador HTTP de Spring debe transferir sus instancias del servidor al cliente:

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 }

Si el servicio no puede reservar un taxi, se lanza una BookingException . En este caso, no es necesario marcar la clase como serializable porque Exception ya la implementa:

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

2.2. Empaquetar el servicio

La interfaz de servicio junto con todas las clases personalizadas utilizadas como argumentos, tipos de devolución y excepciones deben estar disponibles en la ruta de clases del cliente y del servidor. Una de las formas más efectivas de hacerlo es empaquetarlos todos en un archivo .jar que luego se puede incluir como una dependencia en el pom.xml del servidor y del cliente .

Por lo tanto, coloquemos todo el código en un módulo dedicado de Maven, llamado "api"; Usaremos las siguientes coordenadas de Maven para este ejemplo:

com.baeldung api 1.0-SNAPSHOT

3. Aplicación de servidor

Construyamos la aplicación del motor de reservas para exponer el servicio usando Spring Boot.

3.1. Dependencias de Maven

Primero, deberá asegurarse de que su proyecto esté usando Spring Boot:

 org.springframework.boot spring-boot-starter-parent 2.2.2.RELEASE 

Puede encontrar la última versión de Spring Boot aquí. Luego, necesitamos el módulo de inicio web:

 org.springframework.boot spring-boot-starter-web 

Y necesitamos el módulo de definición de servicio que ensamblamos en el paso anterior:

 com.baeldung api 1.0-SNAPSHOT 

3.2. Implementación de servicios

Primero definimos una clase que implementa la interfaz del servicio:

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

Supongamos que esta es una implementación probable. Usando una prueba con un valor aleatorio, podremos reproducir ambos escenarios exitosos, cuando se ha encontrado un taxi disponible y se ha devuelto un código de reserva, y escenarios fallidos, cuando se lanza una BookingException para indicar que no hay ningún taxi disponible.

3.3. Exponiendo el servicio

Luego, necesitamos definir una aplicación con un bean de tipo HttpInvokerServiceExporter en el contexto. Se encargará de exponer un punto de entrada HTTP en la aplicación web que luego será invocado por el cliente:

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

 org.springframework.boot spring-boot-starter-web   org.springframework.boot spring-boot-starter-tomcat   

4.2. Client Implementation

Let's implement the client:

@Configuration public class Client { @Bean public HttpInvokerProxyFactoryBean invoker() { HttpInvokerProxyFactoryBean invoker = new HttpInvokerProxyFactoryBean(); invoker.setServiceUrl("//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.

5.1. Beware of Network Related Exceptions

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.

La solución es un poco menos abierta que otros mecanismos generalizados como REST o servicios web, pero en escenarios donde todos los componentes se desarrollan con Spring, puede representar una alternativa viable y mucho más rápida.

Como de costumbre, encontrará las fuentes en GitHub.