Effectieve EAI met Apache Camel Veranderende requirements en integratie behoeften

Requirements in IT-projecten zijn gevoelig voor verandering, en dat geldt ook voor requirements aan de integratie met andere systemen. In staat zijn om snel te reageren op deze veranderingen kan cruciaal zijn voor een succesvol project. Gelukkig biedt Enterprise Application Integration (EAI) ons alle kennis, technologie en best practices, om uitbreidbare en onderhoudbare integratie oplossingen te bouwen op een productieve manier.

De meeste integratie oplossingen stellen ons echter wel voor een dilemma: terwijl ze vol met mogelijkheden zitten en heel geschikt zijn voor complexe projecten en een veeleisende omgeving, vereisen zij ook grote investeringen vooraf als het gaat om het leren van het systeem en het onderhouden ervan.

Om deze reden lijken ad-hoc oplossingen erg aantrekkelijk op het moment dat onze integratie eisen simpel zijn. Maar ze worden moeilijk te onderhouden en contraproductief indien de integratie behoeften groeien. Het toepassen van EAI best practices zou ons in staat kunnen stellen om een ad-hoc oplossing gaandeweg te laten groeien, maar dit vergt op zichzelf ook inspanning en kennis.

Hoe kunnen we dan productief zijn wanneer wij worden geconfronteerd met zowel eenvoudige als complexe integratie problemen, terwijl we grote investeringen vooraf vermijden? In dit artikel zal ik betogen dat Apache Camel een oplossing biedt. Ik zal laten zien dat Camel kan voldoen aan complexe integratie vraagstukken maar ook gemakkelijk is op te pakken en gemakkelijk te beheersen.

Bescheiden begin

Integratie begint vaak simpel. Zoals het ophalen van een bestand vanaf een FTP server om het lokaal op te slaan. In dit stadium lijkt de doe-het-zelf oplossing erg aantrekkelijk. Maar laten we dat eens nader bekijken.

Deze oplossing zou er als volgt uit kunnen zien:

 


			

public class FTPFetch { public static void main(String[] args) { FTPClient ftp = new FTPClient(); try { ftp.connect("host"); //probeer te verbinden if (!ftp.login("camel", "apache")){ //log in op de server ftp.disconnect(); return; } int reply = ftp.getReplyCode();

if (!FTPReply.isPositiveCompletion(reply)) { ftp.logout(); ftp.disconnect(); return; } ftp.changeWorkingDirectory("folder"); //maak een stream aan naar het bestemmingsbestand file.xml OutputStream output = new FileOutputStream("data/outbox/file.xml"); ftp.retrieveFile("file.xml", output); //doe de data overdracht output.close(); ftp.logout(); ftp.disconnect(); } catch (Exception ex) { ex.printStackTrace(); } finally { if (ftp.isConnected()) { try { ftp.disconnect(); } catch (IOException ioException) { ioException.printStackTrace(); } } } } }

 

 

Deze oplossing maakt gebruik van de FTPClient klasse van Apache Commons. Omdat het maar een client is en niets meer, moeten we het opzetten van een FTP verbinding en foutafhandeling zelf regelen. Maar wat als het bestand op de FTP server later verandert? Om dergelijke veranderingen te kunnen detecteren, zouden we dan zelf code moeten schrijven om bijvoorbeeld dit bestand periodiek binnen te halen.

Laten we nu eens kijken naar Apache Camel. Camel is een framework voor applicatie integratie en ontworpen om dit soort problemen op te lossen door het volgen van EAI best practices. Camel kan  worden gezien als een gereedschapskist van kant en klare integratie componenten. Maar het is ook een runtime die voor specifieke behoeften kan worden aangepast door deze componenten aan te vullen en te combineren.

Dit is hoe we het probleem hierboven zouden oplossen met Camel:

 


			

public class CamelRunner{ public static void main(String args[]) throws Exception { Main camelMain = new Main(); camelMain.enableHangupSupport(); //ctrl-c shutdown camelMain.addRouteBuilder(new RouteBuilder() { public void configure() { from( "ftp://host/folder?username=camel&password=apache&fileName=file.xml&delay=360000" ) .to("file:data/outbox"); } }); camelMain.run(); //Camel blijft onbeperkt draaien } }

                 

Let op de ‘from’ en ‘to’ methoden. Camel noemt dit een 'route': het pad dat wordt doorlopen door de gegevens vanaf de bron naar de bestemming. Bericht, bronnen en bestemmingen worden 'endpoints' genoemd, en het is door hen dat Camel data ontvangt en verzendt. Endpoints worden opgegeven met een URI geformatteerde string zoals te zien in de argumenten voor de ‘from’ en ‘to’ methoden.  Op deze manier worden de routes declaratief aangemaakt en geregistreerd bij Camel, die deze informatie gebruikt om runtime te weten wat het moet doen.

De rest is enkel boilerplate code die wordt hergebruikt als meer routes worden toegevoegd, en is veel eenvoudiger dan rechtstreeks babbelen met een FTP server. Camel zal ook zorg dragen voor de lastige FTP details en zal zelfs het periodiek benaderen van de server voor zijn rekening nemen voor het geval het bestand verandert, aangezien we Camel hebben geconfigureerd om voor onbepaalde tijd te blijven draaien in de laatste regel.

De ‘from’ en ‘to’ methoden zijn onderdeel van de Camel DSL, een Domain Specific Language waarvan het 'domein' EAI is. Dat betekent dat, in tegenstelling tot andere oplossingen, er geen vertaling hoeft te worden gemaakt tussen de EAI taal en de Camel taal: beide zijn vrijwel gelijk. Dit helpt om de leercurve te beperken en het instappunt laag te houden. Dus als je eenmaal je probleem in EAI termen hebt doorgrond, is het een relatief kleine stap om het uit te voeren met Camel. Het resultaat is compacte en effectieve code.

Maar de code die je schrijft is niet het enige dat compact is: alles wat nodig is om dit draaiende te krijgen is camel-core.jar en camel-ftp.jar (plus afhankelijkheden), samen slechts een paar MB. De main klasse CamelRunner kan dan worden uitgevoerd vanaf de commandline. Dus hoewel je Camel prima kunt uitrollen in een volwaardig J2EE application server zoals JBoss, word je er niet toe gedwongen. Je doet het dus wanneer het jou uitkomt, niet omdat het van je EAI vendor moet. Dus het kiezen van een doe-het-zelf-oplossing om de enkele reden dat frameworks  veel complexiteit toevoegen is niet geldig: Camel is eenvoudig te begrijpen, eenvoudig te gebruiken en eenvoudig uit te rollen.

Vertrouwen op Camel

We hebben net kunnen zien dat Camel weinig reden overlaat om op een doe-het-zelf manier integratie problemen aan te pakken. Om dit punt te maken is echter een erg simpel probleem behandeld. De vraag is dan wel of Camel nog steeds een geschikte keuze is voor complexere situaties. Moeten we dan alsnog een van de zwaardere concurrenten kiezen?

Dat is mijns inziens niet nodig. Met zijn uitgebreide ondersteuning voor allerlei transport mechanismen zoals JMS, HTTP, SMTP, en nog veel meer, is het mogelijk om met een veelheid aan systemen te koppelen. Integratie vraagstukken met complexe logica zijn met de Camel DSL op doeltreffende wijze op te lossen, omdat de DSL nauw aansluit op het integratie domein. Camel kent ook voorzieningen voor zelfgemaakte uitbreidingen, zodat je als ontwikkelaar de zaak in eigen hand kan nemen mochten de standaard Camel bouwstenen niet toereikend zijn. Bijzonder aan Camel is ook de ondersteuning voor het maken van unittests, tegenwoordig een onmisbaar wapen in het arsenaal van iedere ontwikkelaar.

De vorige paragraaf heeft een aantal manieren geschetst waarmee Camel ons helpt complexiteit van welk omvang ook het hoofd te bieden. Een EAI systeem kan echter voor uitdagingen van een compleet andere aard komen te staan, en ook aan die uitdagingen willen we kunnen voldoen. Om die te begrijpen moeten we echter een stapje terug doen zodat we het geheel waarin Camel opereert in ogenschouw kunnen nemen.

Een belangrijk kenmerk van integratie oplossingen is het feit dat ze een tussenstation vormen tussen systemen. Daarmee is het echter zelf een single point of failure. En naarmate meer en meer systemen onderling verbonden worden door middel van het centrale EAI systeem, zullen storingen, gegevensverlies en performance daling steeds minder aanvaardbaar worden (voor zover ze het al waren). En dat terwijl het volume aan data alleen maar toeneemt.

Camel kan niet op zichzelf dergelijke eisen volledig adresseren, daar is het simpelweg niet voor gemaakt. Camel is echter wel een centraal onderdeel in een dergelijke oplossing, want het bevat alle logica voor het verplaatsen van gegevens en het verbinden van alle andere systemen. Het is dus belangrijk om te weten dat Camel zijn taken kan blijven vervullen, ook in deze veeleisende omstandigheden.

Laten we een concreet voorbeeld bekijken om te zien hoe aan zulke eisen doorgaans wordt voldaan. In dit voorbeeld is er een inkomende JMS queue waar berichten worden geplaatst door externe systemen. Camels taak zal zijn om de berichten op te pakken, te verwerken, en ze vervolgens te plaatsen in een uitgaande JMS queue. Laten we er vanuit gaan dat de verwerking vrij complex en rekenintensief zou kunnen zijn, maar voor de rest geheel op de node zelf plaatsvindt.  

JMS queues kennen hun eigen mechanismen om persistent te worden gemaakt en ‘high available’, wat betekent dat ze een hoge mate van beschikbaarheid hebben. Met dit gegeven gaan we ervan uit dat externe systemen 'altijd' berichten op de inkomende queue kunnen plaatsen. Totdat het vol raakt natuurlijk, hetgeen zal gebeuren indien Camel de verwerking niet snel genoeg kan uitvoeren.

Ons doel is dan om Camel bestand te maken tegen systeem storingen zodat het ook high available wordt, en de prestaties te verhogen. Dat doen we door het inzetten van meerdere servers (nodes), die elk een eigen Camel instantie runnen die aangesloten is op dezelfde eindpunten. Zie ook de afbeelding hieronder:

Dit heeft twee consequenties: ten eerste worden de berichten uit de queue door meerdere Camel instanties opgepakt en parallel verwerkt, zodat het systeem sneller wordt. Een bericht dat door een instantie wordt verwerkt is niet langer beschikbaar voor de andere. Berichten worden dus slechts eenmaal verwerkt. Ten tweede wordt het werk van een eventueel uitgevallen node automatisch overgenomen door de andere, nog draaiende nodes. De werklast wordt bovendien verdeeld over de nodes die berichten oppakken: snellere nodes kunnen berichten in een hoger tempo verwerken en nemen daarmee vanzelf meer werk voor hun rekening dan langzame nodes. Op deze manier wordt de coördinatie bereikt die nodig is om de verwerking van alle berichten correct en efficiënt te laten verlopen. 

Er is echter een element dat ontbreekt: mocht er namelijk een node uitvallen tijdens het verwerken van een bericht, dan moet een andere die verwerking overnemen want anders gaat dat bericht verloren. En mochten alle nodes uitvallen, dan moeten berichten die op dat moment in verwerking zijn ook niet verloren gaan.

Om dat te kunnen bereiken maken we gebruik van transacties. Met transacties zal de JMS queue wachten op een bevestiging van de node die het bericht opnam alvorens het echt weg te gooien. Mocht de node uitvallen tijdens de verwerking dan komt die bevestiging nooit, en uiteindelijk zal er een rollback van de transactie plaatsvinden, waardoor het bericht opnieuw terugkomt in de queue en weer beschikbaar zal zijn voor de nodes die nog wel actief zijn. Als er geen nodes actief zijn, blijft het bericht gewoon daar tot er een node uiteindelijk weer in de lucht komt.

Voor Camel betekent dit dat de routes transactioneel moeten worden gemaakt zodat ze mee kunnen doen in een transactie. Camel heeft op zichzelf geen ondersteuning voor transacties, maar in plaats daarvan maakt het gebruik van 3rd party oplossingen. Dat houdt Camel eenvoudig, terwijl gebruik kan worden gemaakt van bewezen technologieën terwijl het wisselen van implementaties wordt vergemakkelijkt.

Als voorbeeld zullen we nu Camel met transacties configureren binnen een Spring container, zodat we de Spring JmsTransactionManager kunnen gebruiken. Merk op dat aangezien we binnen Spring draaien, het praktischer is om de Spring XML versie van de Camel DSL te gebruiken in plaats van de Java versie. De Camel DSL bestaat in meerdere varianten, niet alleen Spring/Java maar bijvoorbeeld ook Scala of Groovy.

Het veranderen van DSL's tijdens een project veroorzaakt uiteraard rework, dus het is belangrijk om te migreren op een geschikt moment. Gelukkig draait de Spring DSL ook in een unittest, zodat unittests kunnen helpen om veilig de overgang te maken, ongeacht welk DSL type wordt gebruikt.

 


			

<beans //namespace declaraties verwijderd > //connectie naar jms server opzetten <jee:jndi-lookup id="jmsConnectionFactory" jndi-name="ConnectionFactory"> <jee:environment> java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory java.naming.factory.url.pkgs=org.jboss.naming.client java.naming.provider.url=jnp://localhost:1099 </jee:environment> </jee:jndi-lookup> //configuratie voor de transactionele jms client <bean id="jmsConfig" class="org.apache.camel.component.jms.JmsConfiguration"> <property name="connectionFactory" ref="jmsConnectionFactory"/> <property name="transactionManager" ref="jmsTransactionManager"/> <property name="transacted" value="true"/> <property name="acknowledgementModeName" value="TRANSACTED"/> <property name="cacheLevelName" value="CACHE_NONE"/> <property name="transactionTimeout" value="5"/> </bean> //registreer camel jms component bean <bean id="jboss" class="org.apache.camel.component.jms.JmsComponent"> <property name="configuration" ref="jmsConfig" /> </bean> //registreer spring transactionmanager bean <bean id="jmsTransactionManager" class="org.springframework.jms.connection.JmsTransactionManager"> <property name="connectionFactory" ref="jmsConnectionFactory"/> </bean> <camelContext xmlns="http://camel.apache.org/schema/spring"> <route> <from uri="jboss:queue:incoming"/> <transacted/> <log loggingLevel="INFO" message="processing started." /> <!— tijdrovende verwerking hier --> <to uri="jboss:queue:outgoing?exchangePattern=InOnly" /> </route> </camelContext> </beans>

 

Met de <transacted/> tag wordt de route gemarkeerd als transactioneel, zodat Camel alle stappen van die route binnen een transactie zal uitvoeren. Dit zorgt ervoor dat in het geval van een storing tijdens de verwerking van deze route, er een rollback van de transactie plaatsvindt en dat het bericht dus terug komt in de inkomende berichten queue.

Conclusie

Apache Camel heeft een gemakkelijke leercurve en is licht in gebruik en uitrol zodat investeringen vooraf klein zijn. Zelfs in vrij eenvoudige gevallen kan het gebruiken van Camel een snellere weg naar integratie zijn dan doe-het-zelf oplossingen. Camel biedt dus zonder meer laagdrempelige toegang tot de best practices van EAI. Maar Camel is ook prima inzetbaar in meer veeleisende omgevingen, bijvoorbeeld als onderdeel van een oplossing met hoge eisen ten aanzien van beschikbaarheid.

Over het geheel genomen is Camel een uitstekende optie voor integratie van vrijwel elke omvang en complexiteit: je kunt klein en eenvoudig beginnen met een minimale investering vooraf in de wetenschap dat mochten integratie behoeften complexer worden, Camel je niet teleur zal stellen. In de tussentijd kun je productief blijven, terwijl je de vruchten plukt van een volwaardig integratie framework.

In dit artikel hebben we maar een paar kanten van Camel kunnen belichten, er is nog veel meer dat Camel bijzonder en de moeite waard maakt zoals de DSL en unittest support. Ik raad dan ook iedereen aan die een effectieve en flexibele integratie oplossing zoekt om zich wat verder in Camel te verdiepen.