Introduzione a Spring Remoting con HTTP Invokers

1. Panoramica

In alcuni casi, dobbiamo scomporre un sistema in diversi processi, ognuno dei quali si assume la responsabilità di un aspetto diverso della nostra applicazione. In questi scenari non è raro che uno dei processi necessiti di ottenere dati in modo sincrono da un altro.

Spring Framework offre una gamma di strumenti denominati in modo completo Spring Remoting che ci consente di richiamare servizi remoti come se fossero, almeno in una certa misura, disponibili localmente.

In questo articolo, configureremo un'applicazione basata sull'invocatore HTTP di Spring , che sfrutta la serializzazione Java nativa e HTTP per fornire invocazione di metodi remoti tra un client e un'applicazione server.

2. Definizione del servizio

Supponiamo di dover implementare un sistema che consenta agli utenti di prenotare un passaggio in taxi.

Supponiamo anche di scegliere di costruire due applicazioni distinte per ottenere questo obiettivo:

  • un'applicazione del motore di prenotazione per verificare se una richiesta di taxi può essere servita e
  • un'applicazione web front-end che consente ai clienti di prenotare le loro corse, assicurando che sia stata confermata la disponibilità di un taxi

2.1. Interfaccia di servizio

Quando usiamo Spring Remoting con HTTP invoker, dobbiamo definire il nostro servizio richiamabile in remoto tramite un'interfaccia per consentire a Spring di creare proxy sia lato client che lato server che incapsulino gli aspetti tecnici della chiamata remota. Partiamo quindi dall'interfaccia di un servizio che ci permette di prenotare un taxi:

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

Quando il servizio è in grado di assegnare un taxi, restituisce un oggetto Prenotazione con un codice di prenotazione. La prenotazione deve essere serializzabile perché l'invocatore HTTP di Spring deve trasferire le sue istanze dal server al 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 }

Se il servizio non è in grado di prenotare un taxi, viene generata un'eccezione BookingException . In questo caso, non è necessario contrassegnare la classe come Serializable perché l' eccezione la implementa già:

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

2.2. Imballaggio del servizio

L'interfaccia del servizio insieme a tutte le classi personalizzate usate come argomenti, tipi restituiti ed eccezioni devono essere disponibili sia nel classpath del client che del server. Uno dei modi più efficaci per farlo è comprimerli tutti in un file .jar che può essere successivamente incluso come dipendenza nel pom.xml del server e del client .

Mettiamo quindi tutto il codice in un modulo Maven dedicato, chiamato “api”; useremo le seguenti coordinate di Maven per questo esempio:

com.baeldung api 1.0-SNAPSHOT

3. Applicazione server

Creiamo l'applicazione del motore di prenotazione per esporre il servizio utilizzando Spring Boot.

3.1. Dipendenze di Maven

Innanzitutto, devi assicurarti che il tuo progetto utilizzi Spring Boot:

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

Puoi trovare l'ultima versione di Spring Boot qui. Abbiamo quindi bisogno del modulo Web starter:

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

E abbiamo bisogno del modulo di definizione del servizio che abbiamo assemblato nel passaggio precedente:

 com.baeldung api 1.0-SNAPSHOT 

3.2. Implementazione del servizio

Per prima cosa definiamo una classe che implementa l'interfaccia del servizio:

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

Facciamo finta che questa sia una probabile implementazione. Utilizzando un test con un valore casuale saremo in grado di riprodurre sia gli scenari di successo - quando è stato trovato un taxi disponibile e restituito un codice di prenotazione - sia gli scenari di errore - quando viene generata un'eccezione BookingException per indicare che non ci sono taxi disponibili.

3.3. Esporre il servizio

Dobbiamo quindi definire un'applicazione con un bean di tipo HttpInvokerServiceExporter nel contesto. Si occuperà di esporre un punto di ingresso HTTP nell'applicazione web che verrà successivamente richiamato dal 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); } }

Vale la pena notare che l' invocatore HTTP di Spring utilizza il nome del bean HttpInvokerServiceExporter come percorso relativo per l'URL dell'endpoint HTTP.

Ora possiamo avviare l'applicazione server e mantenerla in esecuzione mentre configuriamo l'applicazione client.

4. Applicazione client

Scriviamo ora l'applicazione client.

4.1. Dipendenze di Maven

Useremo la stessa definizione di servizio e la stessa versione Spring Boot che abbiamo usato sul lato server. Abbiamo ancora bisogno della dipendenza web starter, ma poiché non abbiamo bisogno di avviare automaticamente un contenitore incorporato, possiamo escludere lo starter Tomcat dalla dipendenza:

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

4.2. Implementazione del cliente

Implementiamo il 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 soluzione è leggermente meno aperta rispetto ad altri meccanismi diffusi come REST o servizi web, ma in scenari in cui tutti i componenti sono sviluppati con Spring, può rappresentare un'alternativa praticabile e molto più rapida.

Come al solito, troverai i sorgenti su GitHub.