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:
- 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.
- 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).
- 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
nebocz.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:
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.