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ło by 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?