Hlavní navigace

Komunikace v distribuovaných systémech: serializace zpráv

9. 3. 2021
Doba čtení: 8 minut

Sdílet

 Autor: Depositphotos
V dnešním dílu článků zaměřených na komunikaci v distribuovaných systémech se podívám na serializaci zpráv před odesláním příjemci. Řekneme si, že jsme vlastně serializaci už dělali, ale jen implicitně.

Na co serializace, když jsme ji doposud nepotřebovali a fungovalo to? Popravdě řečeno, my jsme jí dělali, ale implicitně. Camel před odesláním zpráv do message brokeru sám provedl serializaci Java bean do proudu bytů, které předal dále jako obsah zprávy.

Ověřit si to můžete tak, že u předka všech posílaných zpráv, tedy u třídy Token, vyhodíte implementaci rozhraní Serializable. Pokud se pokusíte vyvolat nějakou službu, skončí to na výjimce java.lang.NullPointerException. To je proto, že Camel nenašel na vstupu žádný objekt, který by mohl převést na proud bytů, tak tam prostě nedal nic.

Takže nemohli bychom vystačit s implicitní Java serializací a dále se tím nezabývat?

Asi by to v některých případech stačilo, ale má to své zádrhele:

  1. Takto spolu mohou komunikovat pouze uzly napsané v Java. Pokud byste chtěli některý uzel napsat v nějakém jiném jazyku, pak byste měli problém.
  2. I s uzly v Java je problém, a sice s různými verzemi předávaných bean. Pokud komunikujících uzlů máme několik, pak nebude problém na všech udržet stejnou verzi software. V případě, že jich máme desítky nebo stovky, pak každý rozdíl v definici předávaných bean způsobí výjimku na straně příjemce (nebude umět převést proud bytů na bean).
  3. No a nakonec, s implicitní serializací bean má problém i klient ActiveMQ. Ten ji považuje za bezpečnostní riziko a nepovoluje ji. Musíte ho přesvědčit, aby se s tím smířil. To jsem udělal pomocí parametru spring.activemq.packages.trustAll nastaveném na hodnotu true. V případě, že hodnotu tohoto parametru změníte na false, zase komunikace přestane fungovat a skončí s výjimkou:

java.lang.ClassNotFoundException: Forbidden class cz.trp.distribguide.example06.entity.Request! This class is not trusted to be serialized as ObjectMessage payload.

Takže serializovat objekt před odesláním je dobrý nápad. Ale do jakého formátu zprávy převést? Možností je pochopitelně mnoho. Historicky oblíbená varianta je převod do XML. Mně se ovšem práce s XML moc nelíbí, takže dále ukážu převod do JSON. Je pochopitelně možné formáty dle služby a komunikujících uzlů různě kombinovat. To by ale bylo na ukázku zbytečně komplikované, tak raději zůstanu u jednoho formátu.

Formát mám vybrán. Jak se ale druhá strana dozví, co vlastně ve zprávě dostala a v jakém je to formátu?

Tady mohu s výhodou využít hlavičky, které putují s každou zprávou jako její metadata. Ke každé odeslané zprávě přidám dvě hlavičky:

  • kanonické jméno třídy objektu, který je ve zprávě zapsán (v mém případě to bude cz.dsw.distribguide.example06.entity.Request nebo cz.dsw.distribguide.example06.entity.Response)
  • ve druhé pak informace o formátu, do kterého byl objekt serializován (v mém případě to je „json“ nebo nic, což se rovná implicitní serializaci)

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

Předávané zprávy

Všechny vydefinované typy zpráv jsou Java Bean, jejíž definice jsou v package entity

V tomto článku již budu využívat to, že všechny předávané Java objekty mají jednoho společného abstraktního předka, a sice Token. Operace serializace a reverze budu dělat na úrovni Token bez ohledu na to, jaký reálný objekt se za ním skrývá.

Definované Camel cesty

Rovnou se tedy podíváme na definované cesty:

@Component
public class CamelRoutes extends RouteBuilder {

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

    private static final String HEADER_CLASS_NAME = "ObjectClassName";

    @Autowired
    private ObjectMapper jsonMapper;

    @Autowired
    private ProducerTemplate producerTemplate;

    @Override
    public void configure() {

//      Serialization Route definitions ...
        from("direct:object-mapping").routeId("object-mapping")
            .choice()
                .when(simple("${header.ObjectMapping?.toLowerCase()} == 'json'"))
                    .process(exchange -> {
                        Token token = exchange.getMessage().getBody(Token.class);
                        exchange.getMessage().setHeader(HEADER_CLASS_NAME, token.getClass().getCanonicalName());
                        String json = jsonMapper.writeValueAsString(exchange.getMessage().getBody());
                        exchange.getMessage().setBody(json, String.class);
                        logger.info("SERIALIZED OBJECT: {}", json);
                    })
                .otherwise()
                    .process(exchange -> logger.info("No object mapping applied on the message."))
            .end();

        from("direct:reverse-mapping").routeId("reverse-mapping")
            .choice()
                .when(simple("${header.ObjectMapping?.toLowerCase()} == 'json'"))
                    .process(exchange -> {
                        String className = exchange.getMessage().getHeader(HEADER_CLASS_NAME, String.class);
                        Token token = (Token) jsonMapper.readValue(exchange.getMessage().getBody(String.class), Class.forName(className));
                        exchange.getMessage().setBody(token);
                        logger.info("DESERIALIZED OBJECT: {}", exchange.getMessage().getBody(String.class));
                    })
                .otherwise()
                    .process(exchange -> logger.debug("No reverse mapping applied on the message."))
            .end();

//      Applicant Route definitions ...
        from("direct:applicant01").routeId("applicant01")
            .to("direct:object-mapping")
            .multicast()
                .aggregationStrategy((oldExchange, newExchange) -> {
                    Exchange result;

                    producerTemplate.send("direct:reverse-mapping", newExchange);

                    List<Response> list;
                    if (oldExchange != null) {
                        list = oldExchange.getIn().getBody(List.class);
                        result = oldExchange;
                    }
                    else {
                        list = new ArrayList<>();
                        result = newExchange;
                    }
                    Response resp = newExchange.getMessage().getBody(Response.class);
                    list.add(newExchange.getMessage().getBody(Response.class));
                    result.getMessage().setBody(list, List.class);
                    return result;
                })
                .to("activemq:queue:QUEUE-1", "activemq:queue:QUEUE-2", "activemq:queue:QUEUE-3")
            .end();

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

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

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

Nejdříve k poskytovatelům služeb. Ty mám opět definované tři: provider01, provider02 a provider03.

Žadatele mám pouze jednoho, a to applicant01. Ten osloví všechny tři poskytovatele, spojí odpovědi do seznamu a předá zpět REST službě.

Doposud tedy stále nic nového.

Přibyly dvě cesty dostupné v rámci Camel kontextu přes jejich URL:

  • direct:object-mapping   – provede serializaci obsahu zprávy
  • direct:reverse-mapping – provede zpětný převod serializované zprávy do Java objektu

Obě cesty jsou volány v rámci žadatele i poskytovatelů.

U žadatele se nejdříve provede serializace požadavku. Ten je následně rozeslán všem poskytovatelům a očekávají se odpovědi. Spojení všech odpovědí do jednoho výsledku provádím v rámci vlastní agregační strategie, kdy prvním krokem je zpětný převod zprávy na Java bean.

V případě poskytovatelů je to přesně opačně. Nejdříve se serializovaný objekt převede do bean, vytvoří se odpověď, která je před odesláním serializována.

Obě cesty kontrolují obsah hlavičky ObjectMapping. Podle jejího obsahu provede serializaci do JSON nebo žádnou.

Postup serializace do JSON:

  • vytáhnu si token z obsahu zprávy
  • nastavím hlavičku ObjectClassName na kanonické jméno třídy token
  • převedu obsah token do JSON pomocí ObjectMapper (instance je v rámci SpringBoot k dispozici, takže stačí udělat @Autowired na rozhraní)
  • a nakonec se vše vloží do obsahu zprávy

Postup reverzního převodu z JSON:

  • vytáhnu si hlavičku ObjectClassName z přijaté zprávy
  • zpětný převod dělám opět pomocí ObjectMapper
  • výsledný objekt vložím do obsahu zprávy

REST API aplikace

V tomto případě se jedná o jednoduchou službu. Nic zvláštního na ní vidět není:

@RestController
public class ServiceController {

    private static final String HEADER_OBJECT_MAPPING = "ObjectMapping";

    @Autowired
    private ProducerTemplate producerTemplate;

    @RequestMapping(value = "/rest/appl01")
    public ResponseEntity<List<Response>> restApplicant01(
            @RequestBody Request request,
            @RequestParam(value = "mapping", required = false) String objectMapping) {
        if (request.getName() == null)
            request.setName("rest-applicant01");
        request.setTs(new Date());

        Map<String, Object> headers = new HashMap<>();
        if (objectMapping != null && objectMapping.length() > 0)
            headers.put(HEADER_OBJECT_MAPPING, objectMapping.toLowerCase());

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

Služba přijímá jeden parametr, a sice typ požadované serializace. V mém případě dává smysl hodnota parametru json nebo nic.

Požadovaný typ serializace se před voláním Camel cesty vloží do hlavičky ObjectMapping.

Jak si to vyzkoušet

Takto to vypadá, pokud zavolám službu s požadovanou serializací do JSON:

[raska@localhost ~]$ curl -s -d '{ "value": "1234", "name": "REQUESTED by TRPASLIK" }' -H 'Content-Type: application/json' 'http://localhost:8080/rest/appl01?mapping=json' | jq .
[
  {
    "name": "provider01",
    "ts": "2021-01-02T18:49:39.182+00:00",
    "result": 1244
  },
  {
    "name": "provider02",
    "ts": "2021-01-02T18:49:39.337+00:00",
    "result": 2488
  },
  {
    "name": "provider03",
    "ts": "2021-01-02T18:49:39.435+00:00",
    "result": 1584456
  }
]

a v logu by se mělo objevit něco takového:

Hacking tip

2021-01-02 19:49:39.037  INFO: SERIALIZED OBJECT: {"name":"REQUESTED by TRPASLIK","ts":"2021-01-02T18:49:38.983+00:00","value":1234}
2021-01-02 19:49:39.180  INFO: DESERIALIZED OBJECT: Request{value=1234, Token{name='REQUESTED by TRPASLIK', ts=Sat Jan 02 19:49:38 CET 2021}}
2021-01-02 19:49:39.188  INFO: SERIALIZED OBJECT: {"name":"provider01","ts":"2021-01-02T18:49:39.182+00:00","result":1244}
2021-01-02 19:49:39.226  INFO: DESERIALIZED OBJECT: Response{result=1244, Token{name='provider01', ts=Sat Jan 02 19:49:39 CET 2021}}
2021-01-02 19:49:39.332  INFO: DESERIALIZED OBJECT: Request{value=1234, Token{name='REQUESTED by TRPASLIK', ts=Sat Jan 02 19:49:38 CET 2021}}
2021-01-02 19:49:39.338  INFO: SERIALIZED OBJECT: {"name":"provider02","ts":"2021-01-02T18:49:39.337+00:00","result":2488}
2021-01-02 19:49:39.383  INFO: DESERIALIZED OBJECT: Response{result=2488, Token{name='provider02', ts=Sat Jan 02 19:49:39 CET 2021}}
2021-01-02 19:49:39.435  INFO: DESERIALIZED OBJECT: Request{value=1234, Token{name='REQUESTED by TRPASLIK', ts=Sat Jan 02 19:49:38 CET 2021}}
2021-01-02 19:49:39.436  INFO: SERIALIZED OBJECT: {"name":"provider03","ts":"2021-01-02T18:49:39.435+00:00","result":1584456}
2021-01-02 19:49:39.468  INFO: DESERIALIZED OBJECT: Response{result=1584456, Token{name='provider03', ts=Sat Jan 02 19:49:39 CET 2021}}

A takto by to mělo vypadat, pokud nepožaduji žádnou serializaci:

[raska@localhost ~]$ curl -s -d '{ "value": "1234", "name": "REQUESTED by TRPASLIK" }' -H 'Content-Type: application/json' 'http://localhost:8080/rest/appl01' | jq .
[
  {
    "name": "provider01",
    "ts": "2021-01-02T18:52:02.272+00:00",
    "result": 1244
  },
  {
    "name": "provider02",
    "ts": "2021-01-02T18:52:02.287+00:00",
    "result": 2488
  },
  {
    "name": "provider03",
    "ts": "2021-01-02T18:52:02.307+00:00",
    "result": 1584456
  }
]

no a v logu informace, že k žádnému převodu nedošlo:

2021-01-02 19:52:02.227 INFO: No object mapping applied on the message.
2021-01-02 19:52:02.273 INFO: No object mapping applied on the message.
2021-01-02 19:52:02.287 INFO: No object mapping applied on the message.
2021-01-02 19:52:02.308 INFO: No object mapping applied on the message.

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.