Komunikace v distribuovaných systémech: volání cest z aplikace

17. 2. 2021
Doba čtení: 7 minut

Sdílet

 Autor: Depositphotos
V předchozích dvou článcích jsem ukazoval komunikaci žadatelů o službu a jejích poskytovatelů pouze v rámci Camel cest. Ony jsou to spíše jenom školní ukázky. V praxi toto řešení postačuje jen zřídka.

Daleko častěji potřebuji volat cestu z prostředí vlastní Java aplikace.

Jedná se především o Camel cesty v roli žadatel. V případě komunikace typu zaslání zprávy nás nezajímá výsledek služby. Naopak v případě komunikace typu požadavek/odpověď nás výsledek zajímat bude. Rádi bychom jej dostali zpět do aplikace a nějak s ním dále pracovali.

V případě Camel cest, které plní roli poskytovatele, je tomu trochu jinak. Ty jsou iniciovány požadavkem, který přišel do jejich QUEUE nebo TOPIC. Tady jejich přímé volání z Java aplikace nepotřebujeme. 

Do příkladů pro tento článek a navazující články jsem zařadil webové rozhraní. To budu dále používat pro vyvolávání služeb a případně předání výsledků (jak jsem dříve sliboval, nebudu pro startování služeb dále používat  timer).

Příklady k tomuto článku je možné najít v package: example03

Předávané zprávy

Všechny vydefinované typy zpráv jsou Java Bean, jejíž definice jsou v package entity. Vzhledem k tomu, že se od předchozích článků nezměnily, nebudu je tady dále rozebírat.

Definované Camel cesty

Jak již je zvykem, tak nejdříve jejich ukázka a pak komentáře k nim:

@Component
public class CamelRoutes extends RouteBuilder {

    private static final Logger logger = LoggerFactory.getLogger(CamelRoutes.class);

    @Override
    public void configure() {

//      Applicant Route definitions ...
        from("direct:applicant01").routeId("applicant01")
            .to("activemq:queue:QUEUE-1");

        from("direct:applicant02").routeId("applicant02")
            .multicast()
                .aggregationStrategy(new GroupedBodyAggregationStrategy())
                .to("activemq:queue:QUEUE-1", "activemq:queue:QUEUE-2", "activemq:queue:QUEUE-3")
            .end();

//      Provider Route definitions ...
        from("activemq:queue:QUEUE-1").routeId("provider01")
            .process(exchange -> {
                Request request = exchange.getMessage().getBody(Request.class);
                logger.info("... {}", request);
                exchange.getMessage().setBody(new Response("provider01", new Date(), request.getValue() + 10));
            });

        from("activemq:queue:QUEUE-2").routeId("provider02")
            .process(exchange -> {
                Request request = exchange.getMessage().getBody(Request.class);
                logger.info("... {}", request);
                exchange.getMessage().setBody(new Response("provider02", new Date(), (request.getValue() + 10) * 2));
            });

        from("activemq:queue:QUEUE-3").routeId("provider03")
            .process(exchange -> {
                Request request = exchange.getMessage().getBody(Request.class);
                logger.info("... {}", request);
                exchange.getMessage().setBody(new Response("provider03", new Date(), (request.getValue() + 50) * request.getValue()));
            });
    }
}

Cesty pro roli žadatel

Mám vydefinované dvě cesty, které budou plnit roli žadatele. Jedná se o cestu applicant01 a aplicant02. Ta první oslovuje poskytovatele přes frontu QUEUE-1, no a ta druhá pak dělá multicast na poskytovatele reprezentované frontami QUEUE-1, QUEUE-2 a QUEUE-3.

Oproti předchozím příkladům již na úrovni cesty nerozlišuji vzor komunikace (zdali se jedná o jednosměrnou nebo obousměrnou). To se udělá na úrovni volání cesty z aplikace. 

Další změna je ve zdroji, odkud získává cesta zprávu. Jedná se o Camel komponentu direct.

Cesty pro roli poskytovatel

Tady se žádné překvapení nekoná. Mám vytvořené tři poskytovatele se svou frontou. Každý z nich příjme požadavek, zapíše jeho obsah do logu a vytvoří odpověď. Tu pak odešle zpět. A to je vše.

REST API aplikace

Tak to je nová věc, o které jsem se zatím nezmínil. V rámci SpringBoot je jako komponenta přidáno webové uživatelské rozhraní. Jako implementace je použita vložená knihovna Jetty.

Aplikační rozhraní se nastartuje společně se startem aplikace a běží na URL:  http://localhost:8080

Dále je do aplikace doplněna komponenta implementující REST Controller. Najdete ji v package rest.

Opět nejdříve ukážu, a pak nějaké komentáře:

@RestController
public class ServiceController {

    @Autowired
    private ProducerTemplate producerTemplate;

    @RequestMapping(value = "/mesg/appl01")
    public void sendApplicant01(@RequestParam(value = "value") long value) {
        Request request = new Request("mesg-applicant01", new Date(), value);
        producerTemplate.sendBody("direct:applicant01", request);
    }

    @RequestMapping(value = "/mesg/appl02")
    public void sendApplicant02(@RequestParam(value = "value") long value) {
        Request request = new Request("mesg-applicant02", new Date(), value);
        producerTemplate.sendBody("direct:applicant02", request);
    }

    @RequestMapping(value = "/call/appl01")
    public String callApplicant01(@RequestParam(value = "value") long value) {
        Request request = new Request("call-applicant01", new Date(), value);
        Response response = producerTemplate.requestBody("direct:applicant01", request, Response.class);
        return response.toString();
    }

    @RequestMapping(value = "/call/appl02")
    public String callApplicant02(@RequestParam(value = "value") long value) {
        Request request = new Request("call-applicant02", new Date(), value);
        List<Response> responses = producerTemplate.requestBody("direct:applicant02", request, List.class);
        return responses.stream().map(Response::toString).collect(Collectors.joining("\n"));
    }

    @RequestMapping(value = "/rest/appl01")
    public ResponseEntity<Response> restApplicant01(@RequestBody Request request) {
        if (request.getName() == null)
            request.setName("rest-applicant01");
        request.setTs(new Date());

        Response response = producerTemplate.requestBody("direct:applicant01", request, Response.class);
        if (response != null) {
            return ResponseEntity.ok(response);
        } else {
            return ResponseEntity.notFound().build();
        }
    }

    @RequestMapping(value = "/rest/appl02")
    public ResponseEntity<List<Response>> restApplicant02(@RequestBody Request request) {
        if (request.getName() == null)
            request.setName("rest-applicant02");
        request.setTs(new Date());

        List<Response> response = producerTemplate.requestBody("direct:applicant02", request, List.class);
        if (response != null) {
            return ResponseEntity.ok(response);
        } else {
            return ResponseEntity.notFound().build();
        }
    }
}

Mám vytvořeny tři typy REST služeb:

začíná prefixem /mesg
jedná se o jednosměrnou komunikaci typu odeslání zpráv
začíná prefixem /call
tady jde o obousměrnou komunikaci typu požadavek/odpověď
začíná prefixem /rest
opět jde o obousměrnou komunikaci, ale rozhraní komunikuje formou JSON objektů

Abych se mohl napojit na cesty definované v Camel kontextu, potřebuji objekt implementující rozhraní ProducerTemplate. Pokud nemám nějaké speciální přání, mohu využít instanci vytvořenou v rámci SpringBoot (zajištěno anotací @Autowired).

Jak si to mohu vyzkoušet

Jednosměrná komunikace – odesílání zpráv

Hodnotu, kterou budu zadávat do požadavku, získám z parametru volání služby. Vytvořím požadavek, který předám do Camel cesty tímto voláním:

producerTemplate.sendBody("direct:applicant01", request);

V prvém parametru je URL identifikující cestu, ve druhém parametru pak bean předaný na vstup cesty. 

Že se jedná o vzor jednosměrné komunikace, se určí použitou metodou, což je v tomto případě  sendBody.

Příklad volání první služby, kdy se zpráva předá jednomu příjemci:

[raska@localhost ~]$ curl -s http://localhost:8080/mesg/appl01?value=11

A tohle by se mělo objevit v logu jako záznam o přijetí zprávy od poskytovatele:

2020-12-13 18:34:36.815  INFO: ... Request{value=11, Token{name='mesg-applicant01', ts=Sun Dec 13 18:34:36 CET 2020}}

Jako další příklad je odeslání zprávy více příjemcům:

[raska@localhost ~]$ curl -s http://localhost:8080/mesg/appl02?value=22

A takto by se to mělo projevit v logu:

2020-12-13 18:39:47.479  INFO: ... Request{value=22, Token{name='mesg-applicant02', ts=Sun Dec 13 18:39:47 CET 2020}}
2020-12-13 18:39:47.480  INFO: ... Request{value=22, Token{name='mesg-applicant02', ts=Sun Dec 13 18:39:47 CET 2020}}
2020-12-13 18:39:47.500  INFO: ... Request{value=22, Token{name='mesg-applicant02', ts=Sun Dec 13 18:39:47 CET 2020}}

Obousměrná komunikace – požadavek/odpověď 

Způsob získání hodnoty a vytvoření požadavku je stejný jako v předchozích příkladech. Nicméně v případě obousměrné komunikace cestu vyvolám takto:

Response response = producerTemplate.requestBody("direct:applicant01", request, Response.class);

V prvém parametru je URL identifikující cestu, ve druhém parametru bean předaný na vstup cesty, a ve třetím je třída, jakou by měl mít očekávaný výsledek. Ten pak předám jako výsledek volání webové služby.

Že se jedná o vzor obousměrné komunikace, se určí použitou metodou, což je v tomto případě  requestBody.

Jako první příklad může posloužit:

[raska@localhost ~]$ curl -s http://localhost:8080/call/appl01?value=11
Response{result=21, Token{name='provider01', ts=Sun Dec 13 18:45:21 CET 2020}}

Vrátila se mně jedna odpověď od poskytovatele provider01.

Nebo druhý příklad, kdy oslovím více poskytovatelů:

[raska@localhost ~]$ curl -s http://localhost:8080/call/appl02?value=22
Response{result=32, Token{name='provider01', ts=Sun Dec 13 18:45:29 CET 2020}}
Response{result=64, Token{name='provider02', ts=Sun Dec 13 18:45:29 CET 2020}}
Response{result=1584, Token{name='provider03', ts=Sun Dec 13 18:45:29 CET 2020}}

No a můžete si ještě zkontrolovat obsah logů. Opět by tam měly být záznamy o přijatých požadavcích.

Obousměrná komunikace jako REST služba

Jedná se o variaci na obousměrnou komunikaci. Rozdíl není ve způsobů interakce s Camel cestami, ale ve způsobu předávání parametrů a zobrazení výsledků.

Parametry pro vyvolání služby zde předávám v těle HTTP dotazu jako JSON objekt. Výsledek služby je pak předán v těle HTTP odpovědi, a to opět jako JSON objekt.

Takže další příklady bez dalších zbytečných komentářů.

Volání jednoho poskytovatele:

bitcoin školení listopad 24

[raska@localhost ~]$ curl -s -d '{ "value":"11", "name": "REQUESTED by TRPASLIK" }' -H 'Content-Type: application/json' http://localhost:8080/rest/appl01 | jq .
{
  "name": "provider01",
  "ts": "2020-12-13T17:51:48.022+00:00",
  "result": 21
}

Volání více poskytovatelů:

[raska@localhost ~]$ curl -s -d '{ "value": "22", "name": "REQUESTED by TRPASLIK" }' -H 'Content-Type: application/json' http://localhost:8080/rest/appl02 | jq .
[
  {
    "name": "provider01",
    "ts": "2020-12-13T17:52:07.547+00:00",
    "result": 32
  },
  {
    "name": "provider02",
    "ts": "2020-12-13T17:52:07.566+00:00",
    "result": 64
  },
  {
    "name": "provider03",
    "ts": "2020-12-13T17:52:07.606+00:00",
    "result": 1584
  }
]

V dalším pokračování tohoto seriálu budu používat pro vyvolání služeb již pouze REST rozhraní.

Autor článku

Jiří Raška pracuje na pozici IT architekta. Poslední roky se zaměřuje na integrační a komunikační projekty ve zdravotnictví. Mezi jeho koníčky patří také paragliding a jízda na horském kole.