JLupin Next Server – komunikacja między usługami – podstawy
Na początek link do repozytorium GH gdzie leży sobie kod.
Dziś zajmiemy się komunikacją pomiędzy usługami oraz podstawami testowania z wykorzystaniem JLNS.
Konfigurowanie zależności
Jak wspomniałem w poprzednim wpisie, usługi natywne są wewnętrznie podzielone na interfejsy i implementacje. Wykorzystując mavena, możemy uzależniać się od interfejsów, bez konieczności dociągania implementacji. Ma to dwie zalety. Po pierwsze ogranicza ilość kodu jaki, zostanie dodany do naszego finalnego artefaktu. Po drugie pozwala na kulturalną współpracę pomiędzy zespołami. Pośrednikiem będzie tu repozytorium mavena. Miodzio.
Zależności gateway
Gateway udostępnia nam wybrane API. Na tym poziomie musimy zatem mieć zależności do procesów biznesowych. Dlatego
w ./AccessLayer/gateway-access/implementation/pom.xml
musimy dodać zależności do serwisów z warstwy biznesowej:
Listing 1. pom.xml w gateway
<dependencies>
<dependency>
<groupid>pl.koziolekweb.jlns.bankster</groupid>
<artifactid>account-business-logic-interfaces</artifactid>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupid>pl.koziolekweb.jlns.bankster</groupid>
<artifactid>customer-business-logic-interfaces</artifactid>
<version>1.0-SNAPSHOT</version>
</dependency>
...
</dependencies>
Tu objawia się kolejna mała niedoróbka domyślnej struktury projektu JLupinowego. W takiej strukturze ciężko w jakiś sensowny sposób zarządzać wersjami
usług. Z jednej strony można wszystko po prostu zapisać jako zmienną i podstawiać wszędzie gdzie trzeba. Rzecz w tym, że tych zmiennych może być dużo.
Drugim sposobem może być wykorzystanie parametru ${parent.version}
, ale zadziała to tylko w przypadku natywnych mikroserwisów.
Mikroserwisy servletowe jako „rodzica” mają ustawionego SpringBoota w wersji 1.5, co niestety nie jest parametryzowane z poziomu kreatora i jest
kolejną małą niedoróbką. Przy czym problemu wersjonowania usług w sensowny sposób to jeszcze nikt dobrze nie wymyślił, a JLNS tak naprawdę deleguje
problem do użytkownika.
A co z dostępem do usług ogarniających dane? Moim zdaniem, to ważne, nie należy ich linkować bezpośrednio do gatewaya. Wynika to ze sposobu, w jaki publikowane są usługi i może spowodować, że ujawnimy za dużo. Nawet jeżeli na poziomie warstwy biznesowej serwis pośredniczący będzie tępą rurą bez logiki, to nadal daje nam to separację modelu danych na niskim poziomie od eksponowanego modelu.
Zależności w serwisach biznesowych
Tu zasada jest ta sama. Z tą małą różnicą, że zależność dodajemy tylko do pom-a w module z implementacją np.
Listing 2. zależność w ./BusinessLogicLayer/customer-business-logic/implementation/pom.xml
<dependencies>
<dependency>
<groupid>pl.koziolekweb.jlns.bankster</groupid>
<artifactid>customer-storage-data-interfaces</artifactid>
<version>1.0-SNAPSHOT</version>
</dependency>
...
</dependencies>
I tak oto mamy już ogarniętą konfigurację na poziomie zależności. Czas na zabawę w kodzie.
Jak korzystać z usług?
Proces konfiguracji zależności już za nami. Teraz mamy dostęp do interfejsów, a JLNS zrobi z nich interfejsy zdalne. Tyle tylko, że trzeba mu trochę w
tym pomóc. Zrobimy to na przykładzie usługi customer-storage
. Wystawimy z niej prosty interfejs `
StorageCustomerBasicInformationService`, który wygląda następująco:
Listing 3. Interfejs StorageCustomerBasicInformationService
public interface StorageCustomerBasicInformationService {
Set<BasicCustomerInformation> all();
Option<BasicCustomerInformation> byId(UUID customerId);
}
Klasa CustomerBasicInformationService
znajduje się w module common-pojo
, a to dlatego, że jest to minimalny zbiór informacji
o kliencie. Coś w rodzaju VO klienta. Będziemy z tej klasy korzystać w wielu miejscach i można przyjąć, że na poziomie systemu to element naszego
języka domenowego (Podstawowe Dane Klienta, czyli PaDaKa). Sam interfejs znajduje się w module interfaces
.
Kolejnym krokiem jest implementacja tego interfejsu w module implementation. Oczywiście nie mam zamiaru tego pisać z palca. W InteliJ rozwijamy moduł
implementation i odnajdujemy pakiet COŚTAM.service.impl
, naciskami alt+insert
i:
Następnie podajemy nazwę serwisu bez przyrostka Service
, ten zostanie dodany automatycznie.
W efekcie zostaną wygenerowane dwa pliki (interfejs i klasa):
Upublicznienie usługi
Teraz należy jeszcze usługę upublicznić. Generator nie tylko stworzył pliki, ale też zmodyfikował klasę CustomerStorageSpringConfiguration
:
Listing 4. Klasa CustomerStorageSpringConfiguration
@Configuration
@ComponentScan("pl.koziolekweb.jlns.bankster")
public class CustomerStorageSpringConfiguration {
@Bean
public JLupinDelegator getJLupinDelegator() {
return JLupinClientUtil.generateInnerMicroserviceLoadBalancerDelegator(PortType.JLRMC);
}
@Bean(name = "jLupinRegularExpressionToRemotelyEnabled")
public List getRemotelyBeanList() {
List<String> list = new ArrayList<>();
list.add("storageCustomerBasicInformationService");
return list;
}
}
JLupin na podstawie listy nazwanej jLupinRegularExpressionToRemotelyEnabled
odszuka i opublikuje wybrane serwisy. Tu bardzo ważnym
elementem jest nadanie naszym springowym beanom nazw. Co prawda, istnieje konwencja nazewnicza, dzięki której możemy wykorzystać domyślne nazwy, ale
może być ona źródłem różnych pomyłek. Zatem nasza implementacja wygląda w następująco:
Listing 5. Interfejs StorageCustomerBasicInformationServiceImpl
@Service(value = "storageCustomerBasicInformationService")
public class StorageCustomerBasicInformationServiceImpl implements StorageCustomerBasicInformationService {
@Override
public Set<BasicCustomerInformation> all() {
return HashSet.empty();
}
@Override
public Option<BasicCustomerInformation> byId(UUID customerId) {
return Option.none();
}
}
Używam tu Vavr, bo chcę mieć serializowane kontenery. Dlaczego? O tym za chwilę. Przejdźmy do konfiguracji po stronie klienta.
Konfiguracja kliencka
Klientem naszej usługi jest serwis customer-business-logic
. Dlatego też udamy się teraz do klasy konfiguracyjnej CustomerSpringConfiguration
:
Listing 6. Klasa CustomerSpringConfiguration
@Configuration
@ComponentScan("pl.koziolekweb.jlns.bankster")
public class CustomerSpringConfiguration {
@Bean
public JLupinDelegator getJLupinDelegator() {
return JLupinClientUtil.generateInnerMicroserviceLoadBalancerDelegator(PortType.JLRMC);
}
@Bean(name = StorageCustomerBasicInformationService.name)
public CustomerBasicInformationService getStorageCustomerBasicInformationService() {
return JLupinClientUtil.generateRemote(getJLupinDelegator(),
"customer-storage",
StorageCustomerBasicInformationService.name,
StorageCustomerBasicInformationService.class);
}
@Bean(name = "jLupinRegularExpressionToRemotelyEnabled")
public List getRemotelyBeanList() {
List<String> list = new ArrayList<>();
return list;
}
}
Uwaga! Po drodze wyrzuciłem jeszcze nazwę serwisu, którą używamy w nazwie beana na poziom interfejsu. Taka mała rzecz, a cieszy i daje trochę większą kontrolę nad tym, co się dzieje.
Metoda getStorageCustomerBasicInformationService
produkuje nam proxy, które opakowuje JLupinowy mechanizm wywołań zdalnych. W naszym
kodzie możemy później użyć takiego wywołania tak, jak byśmy wywoływali zwykłą, lokalną, metodę. Bardziej doświadczeni czytelnicy rozpoznają tu zapewne
mechanizm RMI obecny w EJB. Podstawowa różnica pomiędzy tymi rozwiązaniami leży w sposobie ich implementacji. To, czy dany interfejs jest dostępny
zdalnie, czy też nie jest definiowane na poziomie konfiguracji, a nie interfejsu. Nie ma ty adnotacji Local
czy Remote
, bo
nie są do niczego potrzebne. Co prawda można by dodać adnotację, która by wyszukiwała za nas implementacje, ale to szczegół. Zresztą i tak musimy
przepchnąć nasze obiekty dalej do gateway-a
. Powtórzymy zatem proces tworzenia serwisu na poziomie warstwy usług biznesowych i
skonfigurujmy usługę tak, by można było jej używać z zewnątrz. Nasza implementacja będzie wyglądać następująco:
Listing 7. Klasa CustomerSpringConfiguration
@JLupinService
@Service(value = CustomerBasicInformationService.name)
public class CustomerBasicInformationServiceImpl implements CustomerBasicInformationService {
private final StorageCustomerBasicInformationService delegate;
@Autowired
public CustomerBasicInformationServiceImpl(StorageCustomerBasicInformationService delegate) {
this.delegate = delegate;
}
@Override
public Set<BasicCustomerInformation> all() {
return delegate.all();
}
@Override
public Option<BasicCustomerInformation> byId(UUID customerId) {
return delegate.byId(customerId);
}
}
Testy integracyjne
Na tym etapie wystarczyłoby zrobić mvn clean install
i po problemie. Zanim jednak tak zrobimy, musimy jeszcze trochę rzeczy związanych z
testowaniem. W zasadzie testowanie integracyjne w JLNS nie różni się zasadniczo od zwykłych testów integracyjnych. W module integration-test
dodajmy
sobie prosty test naszego CustomerBasicInformationService
Listing 8. Klasa CustomerBasicInformationServiceITest
public class CustomerBasicInformationServiceITest extends BaseTest {
@Test
public void exampleTest() {
CustomerBasicInformationService service = JLupinClientUtil.generateRemote(getJLupinDelegator(),
"customer",
CustomerBasicInformationService.name,
CustomerBasicInformationService.class);
Assertions.assertThat(service.all()).isEmpty();
}
}
Klasa BaseTest
przygotowuje nam prostego klienta, który będzie komunikować się z serwerem na localhost.
Uruchomienie
Pozostało nam to wszystko uruchomić. Wystarczy zatem uruchomić serwer i następnie odpalić mvn clean install
. Poniżej nagrałem ten
proces (koniecznie full screen):
Górna połowa to oczywiście maven. Dolna to podgląd konsoli JLNS. Ładnie widać jak wstają kolejne serwisy.
Podsumowanie
Podstawowa komunikacja pomiędzy serwisami w ramach JLNS jest bardzo prosta, a dzięki kilku „sztuczkom ze Springiem” można to ładnie ogarnąć. Fajne, prawda?