Einführung in Spring Remoting mit HTTP-Invokern

1. Übersicht

In einigen Fällen müssen wir ein System in mehrere Prozesse zerlegen, die jeweils die Verantwortung für einen anderen Aspekt unserer Anwendung übernehmen. In diesen Szenarien ist es nicht ungewöhnlich, dass einer der Prozesse synchron Daten von einem anderen abrufen muss.

Das Spring Framework bietet eine Reihe von Tools, die umfassend als Spring Remoting bezeichnet werden und es uns ermöglichen, Remotedienste so aufzurufen, als wären sie zumindest teilweise lokal verfügbar.

In diesem Artikel richten wir eine Anwendung ein, die auf dem HTTP-Aufrufer von Spring basiert und die native Java-Serialisierung und HTTP nutzt, um einen Remote-Methodenaufruf zwischen einem Client und einer Serveranwendung bereitzustellen.

2. Service-Definition

Nehmen wir an, wir müssen ein System implementieren, mit dem Benutzer eine Fahrt in einem Taxi buchen können.

Nehmen wir auch an, wir erstellen zwei unterschiedliche Anwendungen , um dieses Ziel zu erreichen:

  • eine Buchungsmaschine, um zu prüfen, ob eine Taxianfrage bearbeitet werden kann, und
  • Eine Front-End-Webanwendung, mit der Kunden ihre Fahrten buchen können, um sicherzustellen, dass ein Taxi verfügbar ist

2.1. Serviceschnittstelle

Wenn wir Spring Remoting mit HTTP-Aufrufer verwenden, müssen wir unseren remote aufrufbaren Dienst über eine Schnittstelle definieren, damit Spring sowohl auf Client- als auch auf Serverseite Proxys erstellen kann, die die technischen Details des Remote-Aufrufs kapseln. Beginnen wir also mit der Schnittstelle eines Dienstes, mit dem wir ein Taxi buchen können:

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

Wenn der Dienst ein Taxi zuweisen kann, gibt er ein Buchungsobjekt mit einem Reservierungscode zurück. Die Buchung muss serialisierbar sein, da der HTTP-Aufrufer von Spring seine Instanzen vom Server auf den Client übertragen muss:

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 }

Wenn der Dienst kein Taxi buchen kann , wird eine BookingException ausgelöst. In diesem Fall muss die Klasse nicht als serialisierbar markiert werden, da Exception sie bereits implementiert:

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

2.2. Service verpacken

Die Serviceschnittstelle muss zusammen mit allen benutzerdefinierten Klassen, die als Argumente, Rückgabetypen und Ausnahmen verwendet werden, sowohl im Klassenpfad des Clients als auch des Servers verfügbar sein. Eine der effektivsten Möglichkeiten, dies zu tun, besteht darin, alle in eine JAR- Datei zu packen , die später als Abhängigkeit in die pom.xml des Servers und des Clients aufgenommen werden kann .

Lassen Sie uns also den gesamten Code in ein dediziertes Maven-Modul namens "api" einfügen. In diesem Beispiel werden die folgenden Maven-Koordinaten verwendet:

com.baeldung api 1.0-SNAPSHOT

3. Serveranwendung

Lassen Sie uns die Booking Engine-Anwendung erstellen, um den Service mithilfe von Spring Boot verfügbar zu machen.

3.1. Maven-Abhängigkeiten

Zunächst müssen Sie sicherstellen, dass Ihr Projekt Spring Boot verwendet:

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

Die letzte Spring Boot-Version finden Sie hier. Wir brauchen dann das Web-Starter-Modul:

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

Und wir brauchen das Service-Definitionsmodul, das wir im vorherigen Schritt zusammengestellt haben:

 com.baeldung api 1.0-SNAPSHOT 

3.2. Service-Implementierung

Zunächst definieren wir eine Klasse, die die Schnittstelle des Dienstes implementiert:

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

Stellen wir uns vor, dass dies eine wahrscheinliche Implementierung ist. Mithilfe eines Tests mit einem zufälligen Wert können wir sowohl erfolgreiche Szenarien - wenn ein verfügbares Fahrerhaus gefunden und ein Reservierungscode zurückgegeben wurde - als auch fehlgeschlagene Szenarien reproduzieren, wenn eine BookingException ausgelöst wird, um anzuzeigen, dass kein Fahrerhaus verfügbar ist.

3.3. Den Service verfügbar machen

Wir müssen dann eine Anwendung mit einer Bean vom Typ HttpInvokerServiceExporter im Kontext definieren. Es wird dafür gesorgt, dass ein HTTP-Einstiegspunkt in der Webanwendung verfügbar gemacht wird, der später vom Client aufgerufen wird:

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

Es ist erwähnenswert, dass der HTTP- Aufrufer von Spring den Namen der HttpInvokerServiceExporter- Bean als relativen Pfad für die HTTP-Endpunkt-URL verwendet.

Wir können jetzt die Serveranwendung starten und weiter ausführen, während wir die Clientanwendung einrichten.

4. Client-Anwendung

Lassen Sie uns nun die Client-Anwendung schreiben.

4.1. Maven-Abhängigkeiten

Wir verwenden dieselbe Service-Definition und dieselbe Spring Boot-Version, die wir auf der Serverseite verwendet haben. Wir benötigen weiterhin die Webstarter-Abhängigkeit, aber da wir einen eingebetteten Container nicht automatisch starten müssen, können wir den Tomcat-Starter von der Abhängigkeit ausschließen:

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

4.2. Client-Implementierung

Lassen Sie uns den Client implementieren:

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

Die Lösung ist etwas weniger offen als andere weit verbreitete Mechanismen wie REST oder Webdienste. In Szenarien, in denen alle Komponenten mit Spring entwickelt werden, kann sie jedoch eine praktikable und weitaus schnellere Alternative darstellen.

Wie üblich finden Sie die Quellen auf GitHub.