Hlavní navigace

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:

CS24_early

[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í.

Byl pro vás článek přínosný?

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.