1. Обзор
В некоторых случаях нам нужно разбить систему на несколько процессов, каждый из которых отвечает за разные аспекты нашего приложения. В этих сценариях нередко бывает, что одному из процессов необходимо синхронно получать данные от другого.
Spring Framework предлагает ряд инструментов, всесторонне называемых Spring Remoting
, которые позволяют нам вызывать удаленные службы, как если бы они были, по крайней мере, в некоторой степени, доступны локально.
В этой статье мы настроим приложение на основе HTTP-вызова Spring
, которое использует собственную сериализацию Java и HTTP для обеспечения удаленного вызова методов между клиентом и серверным приложением.
2. Определение услуги
Предположим, нам нужно внедрить систему, позволяющую пользователям заказать поездку в такси.
Давайте также предположим, что мы решили построить два разных приложения для достижения этой цели:
- приложение системы бронирования, чтобы проверить, может ли быть обслужен запрос на такси, и
- внешнее веб-приложение, которое позволяет клиентам бронировать свои поездки, гарантируя наличие такси.
2.1. Сервисный интерфейс
Когда мы используем Spring Remoting
с HTTP-вызовом,
мы должны определить нашу удаленно вызываемую службу через интерфейс, чтобы позволить Spring создавать прокси-серверы как на стороне клиента, так и на стороне сервера, которые инкапсулируют технические особенности удаленного вызова. Итак, начнем с интерфейса сервиса, который позволяет нам заказать такси:
public interface CabBookingService {
Booking bookRide(String pickUpLocation) throws BookingException;
}
Когда служба может выделить такси, она возвращает объект Booking
с кодом бронирования. Бронирование
должно быть сериализуемым, потому что HTTP-вызов Spring должен передавать свои экземпляры с сервера клиенту:
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
}
Если служба не может заказать такси, создается исключение BookingException
. В этом случае нет необходимости помечать класс как Serializable
, потому что Exception
уже реализует его:
public class BookingException extends Exception {
public BookingException(String message) {
super(message);
}
}
2.2. Упаковка услуги
Интерфейс службы вместе со всеми пользовательскими классами, используемыми в качестве аргументов, возвращаемых типов и исключений, должен быть доступен как в пути к классам клиента, так и сервера. Один из наиболее эффективных способов сделать это — упаковать их все в файл .jar , который впоследствии можно включить в качестве зависимости в
pom.xml
сервера и клиента .
Таким образом, давайте поместим весь код в специальный модуль Maven, называемый «api»; мы будем использовать следующие координаты Maven для этого примера:
<groupId>com.foreach</groupId>
<artifactId>api</artifactId>
<version>1.0-SNAPSHOT</version>
3. Серверное приложение
Давайте создадим приложение механизма бронирования для предоставления услуги с помощью Spring Boot.
3.1. Зависимости Maven
Во-первых, вам нужно убедиться, что ваш проект использует Spring Boot:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.1</version>
</parent>
Вы можете найти последнюю версию Spring Boot здесь . Затем нам понадобится стартовый веб-модуль:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
И нам нужен модуль определения службы, который мы собрали на предыдущем шаге:
<dependency>
<groupId>com.foreach</groupId>
<artifactId>api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
3.2. Реализация услуги
Сначала мы определяем класс, реализующий интерфейс службы:
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());
}
}
Предположим, что это вероятная реализация. Используя тест со случайным значением, мы сможем воспроизвести как успешные сценарии, когда свободное такси найдено и возвращен код бронирования, так и неудачные сценарии, когда выдается исключение BookingException, указывающее на отсутствие доступного такси.
3.3. Разоблачение службы
Затем нам нужно определить приложение с bean-компонентом типа HttpInvokerServiceExporter
в контексте. Он позаботится об открытии точки входа 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);
}
}
Стоит отметить, что HTTP
-вызов Spring использует имя bean-компонента HttpInvokerServiceExporter
в качестве относительного пути для URL-адреса конечной точки HTTP.
Теперь мы можем запустить серверное приложение и поддерживать его работу, пока мы настраиваем клиентское приложение.
4. Клиентское приложение
Теперь напишем клиентское приложение.
4.1. Зависимости Maven
Мы будем использовать то же определение службы и ту же версию Spring Boot, что и на стороне сервера. Нам по-прежнему нужна зависимость веб-стартера, но поскольку нам не нужно автоматически запускать встроенный контейнер, мы можем исключить стартер 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. Реализация клиента
Реализуем клиент:
@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"));
}
}
Аннотированный метод Invoker ( )
@Bean
создает экземпляр HttpInvokerProxyFactoryBean
. Нам нужно указать URL-адрес, по которому удаленный сервер отвечает, с помощью метода setServiceUrl()
.
``
Аналогично тому, что мы сделали для сервера, мы также должны предоставить интерфейс службы, которую мы хотим вызывать удаленно, с помощью метода setServiceInterface()
.
HttpInvokerProxyFactoryBean
реализует FactoryBean
Spring . FactoryBean определяется как bean-компонент, но контейнер Spring IoC будет внедрять созданный
им объект, а не саму фабрику. Более подробную информацию о FactoryBean
вы можете найти в нашей статье о фабричных компонентах .
Метод main()
загружает автономное приложение и получает экземпляр CabBookingService
из контекста. Под капотом этот объект является просто прокси, созданным HttpInvokerProxyFactoryBean
, который заботится обо всех технических деталях, связанных с выполнением удаленного вызова. Благодаря этому теперь мы можем легко использовать прокси, как если бы реализация сервиса была доступна локально.
Давайте запустим приложение несколько раз, чтобы выполнить несколько удаленных вызовов, чтобы проверить, как ведет себя клиент, когда кабина доступна, а когда нет.
5. Будьте осторожны
Когда мы работаем с технологиями, допускающими удаленные вызовы, есть некоторые подводные камни, о которых следует хорошо знать.
5.1. Остерегайтесь исключений, связанных с сетью
Мы всегда должны ожидать неожиданного, когда работаем с таким ненадежным ресурсом, как сеть.
Предположим, что клиент вызывает сервер, когда он недоступен — либо из-за проблемы с сетью, либо из-за того, что сервер не работает — тогда Spring Remoting вызовет RemoteAccessException
, которое является RuntimeException.
Тогда компилятор не будет заставлять нас включать вызов в блок try-catch, но мы всегда должны думать об этом, чтобы правильно управлять сетевыми проблемами.
5.2. Объекты передаются по значению, а не по ссылке
Spring Remoting HTTP
маршалирует аргументы метода и возвращаемые значения для их передачи по сети. Это означает, что сервер действует на копию предоставленного аргумента, а клиент действует на копию результата, созданного сервером.
Таким образом, мы не можем ожидать, например, что вызов метода для результирующего объекта изменит статус того же объекта на стороне сервера, потому что между клиентом и сервером нет общего объекта.
5.3. Остерегайтесь мелкозернистых интерфейсов
Вызов метода через границы сети значительно медленнее, чем вызов его для объекта в том же процессе.
По этой причине обычно рекомендуется определять службы, которые должны вызываться удаленно, с более грубыми интерфейсами, способными выполнять бизнес-транзакции, требующие меньшего количества взаимодействий, даже за счет более громоздкого интерфейса.
6. Заключение
В этом примере мы увидели, как легко с помощью Spring Remoting вызывать удаленный процесс.
Решение немного менее открыто, чем другие широко распространенные механизмы, такие как REST или веб-сервисы, но в сценариях, где все компоненты разрабатываются с помощью Spring, оно может представлять собой жизнеспособную и гораздо более быструю альтернативу.
Как обычно, вы найдете исходники на GitHub .