Apache Camel, un framework pour les intégrer tous

le 28/03/2011 par Cédrick Lunven
Tags: Software Engineering

Depuis maintenant plusieurs années, architecture d'intégration rime avec Entreprise Service Bus. Nous leur avons déjà consacrés plusieurs articles comme ici ou encore là.. Bien que ces outils soient puissants ils restent très lourds à mettre en place.

Il existe aujourd'hui une alternative : des frameworks simples et légers ne nécessitant aucune d'installation , on parle de frameworks d'intégration ou encore de "lightweight ESB". Ils implémentent les Patterns d'Architecture de l'Intégration proposés dans l'ouvrage devenu référence Entreprise Integration Patterns. (ou EIP)

Apache Camel réalise la transformation, l'enrichissement,l'agrégation et ou encore le routage de messages entre applications. Il propose un (très) large panel de connecteurs afin de pouvoir s'interfacer avec de nombreux protocoles et/ou technologies.

Son utilisation est restée encore discrète probablement à cause d'une méconnaissance du périmètre qu'il peut couvrir ou de craintes par rapport au contexte exigent de l'Intégration. La parution en décembre 2010 du livre Apache Camel in Action, les différentes interventions des experts lors des rencontres JUG, ainsi que l'arrivée de nouveaux acteurs commerciaux proposant du support favoriserons certainement le déploiement plus large de la solution.

Nous vous proposons dans un premier temps de prendre en main l'outil au travers d'un exemple concret. Dans un seconde partie, nous approfondirons les éléments qui nous semblent pertinents dans un contexte d'intégration.

Premier contact

Nous allons réaliser le polling d'un répertoire : tout fichier déposé sera capté, interprété et déplacé dans les répertoires 'outA', 'outB', ou 'outDefault' en fonction des données qu'il contient. Il s'agit d'une implémentation du pattern EIP "Content Based Router".

Création du projet

L'environnement Camel, ou CamelContext, est un bean Spring que l'on peut déclarer dans le conteneur de son choix :web, ejb, eclipse RCP, batchs...... Commençons par initialiser un projet maven camel-example avec le POM suivant :

<project xmlns="http://maven.apache.org/POM/4.0.0"
	     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.octo.blog</groupId>
  <artifactId>camel-example</artifactId>
    <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <dependencies>
    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-core</artifactId>
      <version>2.6.0</version>
    </dependency>
    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-spring</artifactId>
      <version>2.6.0</version>
    </dependency>
    <dependency>
      <groupId>log4j</groupId> 
      <artifactId>log4j</artifactId> 
      <version>1.2.16</version> 
    </dependency>
  </dependencies>
  <build>
        <plugins>
           <plugin>
        <groupId>org.apache.camel</groupId>
        <artifactId>camel-maven-plugin</artifactId>
        <version>2.6.0</version>
      </plugin>
    </plugins>
  </build>
</project>

Ce qu'il faut noter : - la déclaration des dépendances pour travailler avec Spring : **camel-core** et **camel-spring**. - La présence d'un plugin camel-maven-plugin qui nous permet de tester directement le projet en ligne de commande.

Créons à la racine du projet le répertoire qui sera écouté "in" et préparons plusieurs fichiers XML à l'intérieur (en changeant la valeur de 'type'):

<demande>
  <id>1234</id>
  <type>A</type>
</demande>

Solution 1 : Configuration XML

Créons le fichier /src/main/resources/META-INF/spring/camel-context.xml. Les noms des configurations Spring ne sont pas imposés mais cela permet au camel-maven-plugin de retrouver le fichier.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:camel="http://camel.apache.org/schema/spring"
	xsi:schemaLocation="
         http://www.springframework.org/schema/beans  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
         http://camel.apache.org/schema/spring        http://camel.apache.org/schema/spring/camel-spring-2.0.0.xsd">
	<camelContext id="xml-1" xmlns="http://camel.apache.org/schema/spring">
	 <endpoint id="dossierin" uri="file:in"         ></endpoint>
	 <endpoint id="dossierOutA" uri="file:outA"      ></endpoint>
	 <endpoint id="dossierOutB" uri="file:outB"       ></endpoint>
	 <endpoint id="dossierOutDefault" uri="file:outDefault"></endpoint>
	 <route>
	  <from ref="dossierin" />
	  <choice>
	   <when>
	     <xpath>/demande/type = 'A'</xpath>
	     <to ref="dossierOutA" />
	    </when>
	    <when>
	     <xpath>/demande/type = 'B'</xpath>
	     <to ref="dossierOutB" />
	    </when>
	    <otherwise>
	     <to ref="dossierOutDefault" />
	    </otherwise>
	   </choice>
	 </route>	
	</camelContext>
</beans>

Exécutons la commande mvn camel:run. Les fichiers sont déplacés.

Explications :

Le CamelContext englobe toutes les déclarations (connecteurs, transformations, routage). C'est un bean simple qui n'est pas nécessairement unique dans le contexte Spring.

Les Endpoints réalisent l'interfaçage avec les applications. Ils sont décrits par une URI qui renseigne sur le protocole utilisé et l'adresse du composant. Ils peuvent être déclarés implicitement (ex : <from uri="uri="file:in" />) mais dans la version actuelle de Camel seules les balises <endpoint> permettent une externalisation des constantes dans des fichiers properties (au moyen des PropertyPlaceHolderConfigurer). Chaque URI peut contenir des paramètres supplémentaires permettant d'affiner le comportement : (ex : jms:queue:octoqueue?jmsMessageType=Text;receiveTimeout=5000;)

Les Routes constituent les canaux de communication (channels) entre deux endpoints ou plus. Chacune possède un seul point d'entrée (from) et une ou plusieurs sorties. (to). Il est possible d'intercaler des composants réalisant des traitements (les Processors) mais sans nécessairement implémenter une interface particulière, le framework réalise de l'introspection et est capable d'injecter le body des messages.

Chaque message est constitué d'un header (HashMap de propriétés) et d'un body (tout objet java sérialisable)Les Messages qui transitent sont encapsulés dans un objet Exchange comme le montre la figure ci-dessous.

Solution 2 : Configuration du composant en Java avec le DSL

Démarrer par un exemple XML nous a permis de nous familiariser avec le vocabulaire. Nous allons maintenant reproduire l'exemple en déclarant les routes avec le Domain Specific Language de Camel.

Le RouteBuilder est unique dans tout le camelContext, c'est lui qui permet de construire toutes les routes pour construire notre composant il faut en hériter.

package com.octo.blog.camel.sample;
import org.apache.camel.builder.RouteBuilder;

/**
 * Exemple de RouteBuilder.
 *
 * @author <a href="mailto:clunven@octo.com">C&eacute;drick LUNVEN</a>
 */
public class CustomRouteBuilder extends RouteBuilder {
    
    /** Constante a externaliser. */
    private static final String FOLDER_IN           = "file:in2";
    /** Constante a externaliser. */
    private static final String FOLDER_OUT_A        = "file:outA";
    /** Constante a externaliser. */
    private static final String FOLDER_OUT_B        = "file:outB";
    /** Constante a externaliser. */
    private static final String FOLDER_OUT_DEFAULT  = "file:outDefault";
        
    /** {@inheritDoc} */
    public void configure() throws Exception {
        from(FOLDER_IN).choice()
            .when(xpath("/demande/type = 'A'").booleanResult()).to(FOLDER_OUT_A)
            .when(xpath("/demande/type = 'B'").booleanResult()).to(FOLDER_OUT_B)
            .otherwise().to(FOLDER_OUT_DEFAULT);
    }
}

On déclare de nouveaux beans dans le fichier camel-context.xml.

<bean id="monRouter" class="com.octo.blog.camel.sample.CustomRouteBuilder" />

<camelContext id="java-1"  xmlns="http://camel.apache.org/schema/spring">
  <camel:routeBuilder ref="monRouter"/>
</camelContext>

Créons le répertoire **"in2"** à la racine du projet et déposons nos fichiers XML. Lançons la commande mvn camel:run. Les fichiers sont à nouveaux déplacés.

Cet exemple illustre la simplicité et la puissance du DSL de Camel. La complétion automatique fournit par un IDE comme eclipse nous permet d'écrire très facilement les règles de routage. Avec le langage JAVA il reste aisé d'externaliser la configuration des endpoints dans des fichiers de propriétés.

Maintenant que vous êtes familier avec le vocabulaire et le fonctionnement général nous allons détailler les fonctionnalités proposées par l'outil.

Passons le chameau sur le grill....

Le support et la documentation

Certains reprochent aux solutions open source leur manque de documentation, de support, de training ou encore de stabilité dans les versions.

Camel fournit une documentation riche sur son site avec un grand nombre d'exemples ainsi qu'un manuel de l'utilisateur détaillé. La communauté est active et reste à l'écoute des utilisateurs grâce à son forum. Plusieurs sociétés comme FuseSource, OpenLogic ou plus récemment Talend proposent un support commercial, des formations ainsi que des certifications.

La première release du framework remonte à 2007 et le composant est toujours en cours d'évolution rapide. Depuis la 2.4.0 (juillet 2010) on ressent une stabilisation. Les nouvelles versions s'accompagnent de nouveaux protocoles et de plus de simplicité mais le coeur n'est plus retouché. Le rythme des releases reste encore rapide. (2.4.0 juillet 2010, 2.5.0 octobre 2010, 2.6.0 janvier 2011).

La richesse des connecteurs :

Ce qu'il y a sans doute de plus frappant lorsque l'on découvre Camel est le nombre très important de connecteurs. (il y en a plus d'une centaine). Attardez-vous quelques secondes sur cette page qui en présente une liste exhaustive.

Cette richesse permet souvent plusieurs solutions pour chaque problème. Pour accéder à un service REST, on peut par exemple :

- Réaliser un simple appel HTTP en utilisant le module camel-http. - Utiliser camel-cxf avec le protocole spécialement dédié CXFR. C'est la solution mise en avant, n'oublions pas que c'est un framework de la communauté Apache. - Essayer camel-restlet pour intégrer le framework RESTLet.

Les facilités de développement :

Notre exemple illustre la puissance et la simplicité d'utilisation. La prise en main est rapide grâce notamment à la présence du Domain Specific Language et nécessite peu de connaissances préalables (Spring, Maven).

FuseSource propose depuis le mois dernier un IDE sous forme de plugin eclipse en version beta qui reprend la charte graphique EIP pour définir les routes camel. Etant donné la simplicité de la syntaxe XML cela ne semble pas indispensable pour les développements. Elle apporte en revanche de la valeur ajoutée pour disposer de rendus graphiques des flux modélisés.

Les tests :

Chez Octo, le Test est une valeur que nous défendons. Nous avons vu précédemment que le camel-maven-plugin permet de se familiariser avec l'outil mais cela ne constitue pas un test de validation des développements. Camel propose plusieurs protocoles adaptés aux tests ainsi qu'un module spécifiquement dédié : camel-test.

Protocoles de tests :

"mock:*" permet de bouchonner l'un des endpoint lorsque ce dernier n'est pas encore disponible.

"seda:*" est un gestionnaire de message JMS intégralement hébergé en mémoire. En cas de coupure de la JVM les messages ne sont pas persistés, cela permet de tester le comportement de l'application sans nécessiter une intégration à un système de messaging. Au delà de l'aspect test, le protocole SEDA permet une gestion avancée du parallélisme et un volet d'optimisation des performances.

"direct:*" est un endpoint prévu pour être appelé directement par un composant Java. On peut imaginer créer une route destiner à l'injection de messages depuis Java.

Module Camel-Test:

Le framework Camel repose sur Spring et reprend nombre de ses idées. Une classe ProducerTemplate (par association aux composants Spring comme JdbcTemplate ou JmsTemplate) sert de client et permet facilement d'envoyer des messages vers le contexte.

Le module de test propose un jeu de superclasses et d'annotations facilitant la mise en place de tests unitaires JUnit notamment (CamelSpringTestSupport). Nous allons illustrer par un exemple.

Ajoutons une dépendance vers le module de test :

<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-test</artifactId>
    <version>2.6.0</version>
 </dependency>

Définissons un nouveau fichier de définition context-camel2.xml

<camelContext id="test-1" xmlns="http://camel.apache.org/schema/spring">
  <route>
   <from uri="direct:testmulti" />
    <multicast>
     <to  uri="mock:destA"  />
     <to  uri="mock:destB"  />
    </multicast>
   </route>
</camelContext>

Ecrivons une classe de test :

/**
 * Exemple de test unitaire.
 *
 * @author <a href="mailto:clunven@octo.com">C&eacute;drick LUNVEN</a>
 */
public class CamelContextTest extends CamelSpringTestSupport {
    
    /** logger. */
    private Logger logger = Logger.getLogger(this.getClass());
    
    @EndpointInject(uri = "mock:destA")
    protected MockEndpoint endPointA;
    
    @EndpointInject(uri = "mock:destB")
    protected MockEndpoint endPointB;
    
    @EndpointInject(uri = "direct:testmulti")
    protected Endpoint testMulti;
    
    @Override
    protected AbstractXmlApplicationContext createApplicationContext() {
        return new ClassPathXmlApplicationContext("classpath:META-INF/spring/camel-context2.xml");
    }

    @Test
    public void testCamel() throws Exception {
        template.sendBody(testMulti, "Bonjour A et B");
        
        // Temps laissé pour initialisation du contexte
        Thread.sleep(1000);
        
        logger.info("A a recu '" + endPointA.getReceivedCounter() + "' élément(s)");
        logger.info("B a recu '" + endPointB.getReceivedCounter() + "' élément(s)");
        
        // 0 est ici l'index d'un tableu et commence à 0.
        endPointA.assertExchangeReceived(0);
        endPointB.assertExchangeReceived(0);
        
        List < Exchange > messagesA = endPointA.getExchanges();
        for (Exchange exchange : messagesA) {
            logger.info("Messages A :  " + exchange.getIn().getBody());
        }
        List < Exchange > messagesB = endPointB.getExchanges();
        for (Exchange exchange : messagesB) {
            logger.info("Messages B :  " + exchange.getIn().getBody());
        }
    }
}

On notera ici : - la présence du template permettant de générer des messages - l'injection de EndPoint par annotation et les opérations de contrôles (assert*).

La sécurité :

Les solutions d'intégration transportent de grandes quantités de données sensibles, la sécurité est au coeur du système. L'éditeur lui consacre une page entière. En substance il existe 4 niveaux de sécurité possibles :

EndPoint Security : La sécurité native inhérente aux systèmes que l'on interface (JAAS, SSL, HTTP....) ` Route Security : la route est encapsulée par une "policy" nécessitant des habilitations particulières. Payload Security : permettant d'encrypter le contenu des messages. Configuration Security : Crypter les données de configuration externalisées comme les mots de passe par exemple.

Ces éléments sont susceptibles de ralentir fortement les traitements et il convient de les utiliser avec parcimonie.

Reporting et Monitoring :

Le module **camel-bam** répond au besoin de tracer les échanges entre applications. Il propose au travers d'un modèle de quelques entités JPA de persister les données dans un SGBD.

La persistance ne devrait pas être réalisée en synchrone par rapport au reste des opérations : cela ralentit les échanges. Définir un proxy Spring AOP qui pousse des éléments d'information dans une file de reporting paraît une solution plus performante. Un second composant (une seconde route Camel par exemple) dépilerait les messages pour alimenter la base de données.

Dans l'idéal, un framework d'intégration ne devrait pas persister de données pour s'en servir à des fins de routage : ce n'est pas son rôle, il devrait rester stateless. Des outils plus adaptés comme les moteurs de Business Process Management ou les frameworks de Complex Event Process (ex : Esper) pourraient prendre en charge cette fonctionnalité.

Une console d'administration est disponible et permet d'administrer les routes "à chaud" grâce notamment à des composants JMX.

Scalabilité et Performance :

Toute brique d'intégration peut être amener à travailler sur de gros volumes de données. Elles se doit d'être performante et souple. En tant que framework léger Camel s'adapte au conteneur dans lequel il est déployé, il ne peut "voir" au delà.

Scalabilité verticale (serveur unique) :

Afin d'optimiser les performances on peut être amené à augmenter la puissance d'une machine (CPU), à ajouter de la mémoire (RAM) ou encore à sizer la JVM mais dans aucun de ces cas on ne configure le framework. Il est malgré tout possible d'agir sur la velocité du composant, notamment au niveau des pollers. On peut redéfinir le nombre de threads d'écoute ou diminuer les temps de latence entre deux pollings. (Ces paramètres sont configurables dans l'écriture des URI des endpoints.)

Scalabilité horizontale (redondance) :

Les Runtime Camel peuvent être redondés dans plusieurs machines ou plusieurs conteneurs mais ils sont cloisonnés, ils ne se connaissent pas et en ce sens, il ne s'agit pas de vrai clustering. Si deux runtimes Camel pointent sur le même endPoint (la même application) ils sont susceptibles de consommer à tort deux fois le même message. Il est nécessaire de réaliser la répartition de charge en amont en utilisant, par exemple, le clustering du système de messaging (JMS, MOM).

Robustesse :

L'un des enjeux forts en terme d'intégration est la non perte de message, Il est vital d'anticiper les éventuelles défaillances.

Camel propose un système de LoadBalancing (roundrobin ou masses pondérées) mais également une tolérance aux pannes (**FailOver**) directement au niveau des endpoints :

<route errorHandlerRef="myErrorHandler">
      <from uri="direct:junit-error"/>
      <loadBalance>
          <failover>
              <exception>java.io.IOException</exception>
              <exception>com.octo.blog.ToutEstCasseException</exception>
          </failover>
          <to uri="mock:destA" />
          <to uri="mock:destB" />
      </loadBalance>
    </route>

Notez la déclaration d'un **errorHandler** en charge du traitement des messages en cas d'erreur.

Même si Camel implémente le pattern Transactional Client les échanges entre deux applications ne sont pas forcément transactionnels. Dans le cas où les endPoints sont compatibles (technologies JMS ou JDBC...) il est possible d'encapsuler les échanges dans un **TransactionManager** Spring. S'il n'existe pas de système de transactions global (two-phase-commit) out-of-the-box, il est possible de s'en sortir avec des moteurs de transactions tiers comme Atomikos.

Conclusion

Apache Camel est une solution à envisager pour vos nouveaux besoins d'intégration. Son DSL, la richesse des protocoles, et ses capacités de test en font un framework de développement à la fois simple et très puissant. Il ne nécessite aucune installation tout en offrant un large spectre de possibilités en terme d'exploitation et constitue ainsi une très bonne alternative à la mise en place d' architectures lourdes à base d'ESB. La communauté très active, la parution d'ouvrage de référence ainsi les offres de supports commerciaux très sérieuses devraient contribuer à sa diffusion à plus grande échelle dans nos DSI.

Pour en savoir plus notez dès maintenant dans vos agendas les prochains rendez-vous Apache Camel : - Le 14 avril 2011, présentation par Stéphane Kay au JUG de Lausanne - Le 10 mai 2011, présentation de Charles Moulliard lors de la Soirée Apache Camel et ServiceMIX au Paris JUG