Introduction aux EIPs (suite) : mise en pratique avec Spring Integration

Passons à la mise en pratique avec Spring Integration !

Comme nous l’avons vu dans l’article précédent, Camel prends quelques libertés vis-à-vis des concepts des EIPs, les channels sont implicites et l’implémentation des routes modélisées par EIPs peuvent se révéler assez différentes. Spring Integration au contraire, est à la base une pure implémentation des EIPs, chaque élément retrouvé dans notre modélisation va être retranscrite point par point et enrichie :

Ainsi chaque endpoint et channel (aller comme retour) sont retranscrits dans Spring Integration, nous aurons donc :

  • Un endpoint http d’entrée
  • Un endpoint s’occupant de l’appel http au service Google
  • 2 endpoints transformant les messages
  • 4 channels liant les endpoints

Chacun de ces composants seront définis comme bean spring, on retrouvera donc une configuration et une architecture classique des applications spring, très modulaire facilitant la réutilisation.

Toute la configuration se fait par fichier spring XML, les endpoints et channels développés peuvent néanmoins être intégrés et configurés dans le conteneur Spring à l’aide d’annotations.

Les Channels

Contrairement à Camel, les channels sont explicites, ils peuvent être de type publish/subscribe, queue, direct… Nous allons utiliser seulement les directs dans notre exemple :

1
2
3
4
5
<!-- Inbound/Outbound Channels -->
<int:channel id="spellJsonRequest" />
<int:channel id="spellXmlRequest" />
<int:channel id="spellXmlResponse" />
<int:channel id="spellJsonResponse" />

Endpoint http : inbound-gateway

Afin de réaliser notre endpoint http, Spring Integration propose le endpoint http inbound gateway. C’est un endpoint producteur de messages. Evidemment, Spring se base sur Spring MVC pour créer ce endpoint :

1
2
3
4
5
6
7
<int-http:inbound-gateway id="inboundEmployeeSearchRequestGateway"
	supported-methods="GET" request-channel="spellJsonRequest"
	reply-channel="spellJsonResponse" mapped-request-headers="null"
	mapped-response-headers="Return-Status, Return-Status-Msg, HTTP_RESPONSE_HEADERS"
	path="/spell/{word}" reply-timeout="50000">
 <int-http:header name="word" expression="#pathVariables.word" />
</int-http:inbound-gateway>

Cette configuration créé un endpoint à l’adresse /spell qui est rajouté à la DispatcherServlet de Spring MVC. Le mapped-request-headers permet de spécifier quel headers http sont mappés sur les headers du message du flux. Les request et reply channel permettent de brancher le endpoint aux channels. Le header est enrichi du pathVariable pour la suite.

Template velocity

Afin de garder la même logique que pour l’implémentation Camel, la requête XML est générée à l’aide d’un template Velocity. Il n’existe pas de Transformer velocity natif sous Spring Integration, mais il est simple d’en implémenter un :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component("velocityTransformer")
public class VelocityTransformer {
	@Value("request.vm")
	private String templateLocation;
 
	@Autowired
	private VelocityEngine velocityEngine;
 
	@Transformer
	public String transform(@Header("word") String word)
			throws VelocityException, IOException {
		Map map = new HashedMap();
		map.put("word", word);
		return VelocityEngineUtils.mergeTemplateIntoString(velocityEngine,
				templateLocation, map);
	}
}

A noter l’utilisation des Annotations, la méthode @Transformer permet d’injecter directement dans ses paramètres d’appel des valeurs du header « word ».
Le transformer est ensuite branché sur la route :

1
2
<int:transformer input-channel="spellJsonRequest"
		output-channel="spellXmlRequest" ref="velocityTransformer" />

Endpoint http : outbound-gateway

Un endpoint permettant les appels http est disponible, il est basé sur le RestTemplate de Spring :

1
2
3
<int-http:outbound-gateway request-channel="spellXmlRequest"
		expected-response-type="com.sedona.simple.SpellResult" reply-channel="spellXmlResponse"
		url="http://www.google.com/tbproxy/spell?lang=en" http-method="POST" />

L’url cible est simplement précisée avec la méthode http utilisée. L’expected-response-type permet de convertir directement la réponse XML en objet Java grâce à un mapping JaxB. Aucune configuration supplémentaire est nécessaire, seule la classe SpellResult est développée :

1
2
3
4
5
6
7
8
9
10
11
12
@XmlRootElement(name = "spellresult")
public class SpellResult {
	private String c;
 
	public String getC() {
		return c;
	}
 
	public void setC(String c) {
		this.c = c;
	}
}

 Transformation Objet vers Json

Un transformer natif est disponible à partir du moment où la librairie Jackson est disponible dans le classpath. Il suffit ensuite de le déclarer :

1
2
<int:object-to-json-transformer
	input-channel="spellXmlResponse" output-channel="spellJsonResponse" content-type="application/json" />

Les headers de réponse du service google étant par défaut mappés, attention à bien changer le content-type de xml à json.

Configuration et déploiement

La solution se basant sur du spring MVC, une configuration spring classique (ContextLoaderListener dans le web.xml) et un déploiement dans un conteneur de servlet (tomcat) sont nécessaires.

Conclusion

Comme déjà précisé, Spring Integration est plus fidèle aux EIPs, la route configurée correspond exactement à la conception de notre flux, l’implémentation est donc plus implicite et demande moins de connaissance du framework et de ses composants, on profite en plus des forces de ce framework (Spring MVC, système de conversion etc…).
Néanmoins Camel est plus pragmatique, ses composants sont plus riches et le développement et la configuration a été plus rapide qu’avec Spring.

{lang: 'fr'}

Introduction aux EIPs (suite) : mise en pratique avec Apache Camel

Suite de l’article précédent, la mise en pratique des EIPs se fera avec une mise en bouche de deux frameworks d’intégrations : Apache Camel et Spring Integration

Pour rappel, l’exemple de flux simple a été modélisé ainsi :

Deux implémentations vont être réalisées afin de présenter les deux frameworks, voici la première avec Apache Camel.

Apache Camel

Directement basé sur les EIPs, Camel permet de créer des routes et des règles de médiations. Nous verrons que ce framework comme Spring Integration, ne suit pas forcément à la lettre les design et modélisations des EIPs, certaines simplifications et extensions sont mises en place. Une des particularités de Camel est qu’il propose plusieurs moyen de définir des routes, comme par exemple par l’intermédiaire d’une API Java de type DSL ou par fichier de configuration Spring XML.

L’implémentations des EIPs se fait par l’utilisation de composants (qui peuvent être des endpoints comme des simple transformer) et de data format afin de faciliter la conversion de données.

Chaque composant possède sa propre URI,  qui déterminer à la fois le transport utilisé et des paramètres propres au composant. Par exemple, un endpoint http en entrée relié à un endpoint http de sortie donne en Java DSL :

1
from("jetty:http://localhost:9000/test").to("http://www.google.com");

Cette route indique que le composant Camel jetty est utilisé en entrée, écoute sur le port 9000 et transmet les requêtes en entrée vers google grâce au composant http de camel.
La liste des composants est disponibles ici. Ainsi le composant file permet de lire ou écrire un fichier, le composant cxf de faire appel à un web service cxf etc…
Un exemple de route en xml :

1
2
3
4
<route>
	<from uri="jetty:http://localhost:9000/test" />
	<to uri="http://www.google.com" />
</route>

Implémentation de la route

Implémenter notre route d’exemple se résume donc à choisir les composants camel à utiliser ou à en créer de nouveaux. L’implémentation choisie a finalement été :

  • Utilisation d’un web service rest CXF comme endpoint d’entrée
  • Utilisation d’un template velocity pour construire la requête XML nécessaire
  • Utilisation du composant http pour le endpoint vers google
  • Une transformation standard du xml vers json grâce à un data-format
L’exemple ayant été implémenté sous maven, chaque composant est une dépendance maven à rajouter pour pouvoir l’utiliser, soient CXF, velocity, http et xmljson.
La route a été définie ainsi :
1
2
3
4
5
6
from("cxfrs:bean:spellCheckEndpoint")
		.to("velocity:request.xml")
		.setHeader(Exchange.HTTP_URI,
			simple("http://www.google.com/tbproxy/spell?lang=en"))
		.setHeader(Exchange.HTTP_METHOD, constant("POST"))
		.to("http://googleSpellCheck").marshal().xmljson();
L’équivalent en xml :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<route>
    <from uri="cxfrs:bean:spellCheckEndpoint"/>
    <to uri="velocity:request.xml"/>
    <setHeader headerName="CamelHttpUri">
        <simple>http://www.google.com/tbproxy/spell?lang=en</simple>
    </setHeader>
    <setHeader headerName="CamelHttpMethod">
        <constant>POST</constant>
    </setHeader>
    <to uri="http://googleSpellCheck"/>
    <marshal>
        <xmljson/>
    </marshal>
</route>

La route va être détaillée point par point.

CXF

Le endpoint d’entrée utilise le composant CXFRS. L’URI précise le bean spring à utiliser, son implémentation (JAX-RS) est simple :

1
2
3
4
5
6
7
8
9
@Path("/")
public class SpellCheck {
	@GET
	@Path("/{word}")
	@Produces("text/json")
	public String getSpellCheck(@PathParam("word") String word) {
		return word;
	}
}

Il retourne simplement le path param ‘word’ qui constituera donc le corps du message de la route.
Il est ensuite déclaré en tant que bean spring grâce à une notation spécifique afin de créer simplement le connecteur jetty associé :

1
2
<cxf:rsServer id="spellCheckEndpoint" address="http://0.0.0.0:9001/spell/"
		serviceClass="com.sedona.simple.SpellCheck" />

Template Velocity

Le composant velocity est ensuite utilisé pour dynamiser le requête xml qui sera envoyée à google :

1
2
3
4
<?xml version="1.0" encoding="utf-8" ?>
<spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">
    <text>${body[0]}</text>
</spellrequest>

Le path param précédemment récupéré par le service JAX-RS est donc inséré dans la balise text et le xml constitue désormais le payload du message.

Http

L’envoi de la requête http vers google se fait par le composant http. Comme précisé dans le précédent article les messages routés sont composés de headers et d’un payload. Afin de spécifier la destination réel du message et la méthode utilisée pour le requête (un Post), des headers sont précisés avant de faire appel au composant.

Réponse json

Enfin, la réponse est convertie grâce aux système data format de Camel, grâce à xmljson.
Par défaut, une route Camel est de type request/reply, le retour vers le endpoint d’entrée est implicite.

Dernières configurations

Si la notation java DSL est utilisée, le code java doit être dans la méthode configure() d’un classe héritant de RouteBuilder. Le fichier spring doit ensuite contenir une référence vers cette dernière :

1
2
3
4
5
6
7
8
9
10
11
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation=" http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans.xsd
	http://camel.apache.org/schema/spring
	http://camel.apache.org/schema/spring/camel-spring.xsd">
 
	<camelContext id="camel" xmlns="http://camel.apache.org/schema/spring">
		<package>com.sedona.simple</package>
	</camelContext>
</beans>

Pour la notation XML, il faut rajouter la définition dans la balise camelContext.
Pour tester, une classe Main standard camel est utilisée, se basant sur spring, elle charge automatiquement les fichiers spring positionnés dans le classpath dans META-INF/spring.

Une fois démarré, un simple test : http://localhost:9001/spell/herllo nous retourne un json avec des propositions de correction :

1
{"@error":"0","@clipped":"0","@charschecked":"6","@suggestedlang":"en","c":{"@o":"0","@l":"6","@s":"0","#text":"hello\tHelli\thell\tHeall\thallo"}}

Conclusion

Ce petit exemple a permis de présenter un aperçu de Camel, une page présente comment les différents EIPs sont intégrés mais c’est surtout la richesse de ses composants et connecteurs qui font la force de Camel, il a était simple de réaliser cette route avec un minimum de développement.

Prochain article : même exercice avec Spring Integration !

{lang: 'fr'}

Introduction aux Entreprise Integration Patterns

Les design patterns, on en connaît à la pelle ! Mais ce billet est une petite introduction à ces patterns particuliers que sont les Entreprise Integration Patterns ou EIP.

 

Les EIPs ? Qu’est ce que c’est ?

Nous connaissons tous les patterns Gof, les MVC, les IOC qui concernent la programmation orientée objet, mais les EIPs, comment leur nom l’indique, concernent une toute autre problématique, l’intégration d’entreprise ou plus simplement les flux de données dans un système d’information. La moindre application faisant partie d’un SI est amenée à échanger des informations avec d’autres applications, par transfert de fichier, partage de base de données, web services, système de messaging… Les EIPs proposent de modéliser ces échanges et d’identifier les patterns les plus courants utilisés lors de ces flux, ne se basant pas sur des diagrammes de classes, ils proposent une toute autre représentation graphique. Principalement orientés autour de systèmes de messaging, nous verront que leur application peut être plus large et plus simple et que même l’intégration d’un fichier plat peut être modélisé sous forme d’échange de message.

Car c’est bien là la base des EIPs, elles modélisent tout échange d’information comme un transfert de message entre deux Endpoints par l’intermédiaire d’un channel. Un message est composé d’un header (qui contient des informations liées à l’infrastructure et le mode de transfert) et d’un payload, la données en elle même, qui peut être de n’importe quelle nature.

Le channel est tout simplement la tuyauterie et la manière dont est transféré de le message (direct point à point, publish/subscribe, message bus). Un Endpoint est un producteur et/ou un consommateur de messages, comme un connecteur à votre application ou un système.

Enfin à cela peut se rajouter :

  • Un traducteur / convertisseur du message (message translator ou transformer)
  • Un routeur, comme un choix sur le channel à emprunter selon le contenu du message
  • Un filtre…
Par exemple, un échange entre deux systèmes (endpoints), demandant une transformation selon le type de données peut être modélisé simplement :
A partir de ces éléments de base, tout une série de patterns peuvent créée, comme par exemple, le normalizer, qui selon le message en entrée, procédera à une ou l’autre des transformations disponibles grâce à un router :
Qui possède son propre schema :
Ce billet n’a pas vocation de présenter tous les patterns, vous pouvez en avoir un aperçu ici (En effet, cette notation et ces patterns sont tirés du livre Entreprise Integration Patterns), mais plusieurs seront présentés au cours de la petite série de billets sur le sujet.

Essayons de mettre en pratique cette notation afin de modéliser un traitement simple, la mise en place d’un web service Rest/Json à partir d’un web service Rest/XML hébergé chez google. Les deux aspects fondamentaux sont :

  1.  Nous avons affaire à un flux request/reply, en effet le endpoint producteur (de type http/rest donc) du message a besoin de retourner une réponse
  2. Le traitement du flux est essentiellement une conversion du message de la request d’entrée et de la réponse de retour du service google.
C’est tout simplement un aller retour avec une transformation de message comme vu précédemment, soit :
Voilà pour une rapide introduction à cette notation et les premiers patterns, suite au prochain article, avec deux exemples d’implémentations de cette échange suivant deux frameworks, Apache Camel et Spring Integration.
{lang: 'fr'}

JSoup

Vous connaissez JSoup ?
Il s’agit d’une librairie très utile. Elle vous permettra de lire et surtout de parcourir, manipuler, traverser un DOM et son contenu avec des sélecteurs html/css. Elle est en plus compatible Html 5.
Un exemple:

1
2
Document doc = Jsoup.connect("http://en.wikipedia.org/").get(); 
Elements newsHeadlines = doc.select("#mp-itn b a");

ou à partir d’un fichier:

1
2
File input = new File("/tmp/input.html"); 
Document doc = Jsoup.parse(input, "UTF-8", "http://example.com/");

La configuration Maven:

1
2
3
4
5
<dependency>
  <groupId>org.jsoup</groupId>
  <artifactId>jsoup</artifactId>
  <version>1.7.2</version>
</dependency>

JSoup.org

{lang: 'fr'}

Créer un cache avec google guava

javaPour des raisons de tenue à la charge, il est souvent nécessaire de créer des caches mémoire dans les applications. Le plus souvent, le choix se porte sur ehcache, ou une ConcurrentHashMap.

Il y a maintenant une autre possibilité : utiliser le CacheBuilder de google guava. Grace à une petite api de type DSL, vous pourrez configurer un cache en quelques lignes de code.(tirées de la javadoc)

Cache graphs = CacheBuilder.newBuilder()
       .concurrencyLevel(4)
       .weakKeys()
       .maximumSize(10000)
       .expireAfterWrite(10, TimeUnit.MINUTES)
       .build(
           new CacheLoader() {
             public Graph load(Key key) throws AnyException {
               return createExpensiveGraph(key);
             }
           });

Et voila on dispose d’un cache finement configuré (weak keys, taille limitée, temps d’expiration de 10min) sans avoir à le coder soit même comme avec une concurrentHashMap et sans avoir à intégrer tout ehcache. En bonus un objet CacheStats est même tout prêt à être exposé via JMX pour monitorer la pertinence de ce cache.

Pour l’intégrer facilement à votre projet : la dependence maven à déclarer :

5
6
7
8
9
<dependency>
     <groupId>com.google.guava</groupId>
     <artifactId>guava</artifactId>
     <version>10.0</version>
 </dependency>
{lang: 'fr'}

Obtenir le userId dans un ModelListener Liferay

Lorsqu’on est dans un ModelListener, nous n’avons pas la request et donc à priori aucun moyen d’obtenir le userId. Pourtant, en regardant la classe com.liferay.portal.service.base.PrincipalBean (dont tous les services liferay héritent) on aperçoit la méthode getUserId() ci-dessous.

    public long getUserId() throws PrincipalException {
    String name = PrincipalThreadLocal.getName();
 
    if (name == null) {
    throw new PrincipalException();
    }
 
    if (Validator.isNull(name)) {
    throw new PrincipalException("Principal cannot be null");
    }
    else {
    for (int i = 0; i < ANONYMOUS_NAMES.length; i++) {
    if (name.equalsIgnoreCase(ANONYMOUS_NAMES[i])) {
    throw new PrincipalException(
    "Principal cannot be " + ANONYMOUS_NAMES[i]);
    }
    }
    }
 
    return GetterUtil.getLong(name);
    }

On a donc juste à réutiliser le code de cette méthode dans un ModelListener pour récupérer le userId courant.

{lang: 'fr'}

Liferay Clusterlink : Synchronisation des indexs Lucene

La synchronisation entre les différents noeuds d’un cluster (réel ou simple load-balancing) Liferay est devenue maintenant très simple grâce à une nouvelle fonctionnalité : Le ClusterLink.
Il n’est actuellement (version 6.0CE) utilisé que pour la synchro des indexes lucene mais son utilisation sera étendu à la synchronisation des caches (http://www.liferay.com/about-us/news/-/blogs/5229206/maximized).
Trois propriétés sont à rajouter dans le portal-ext.properties :

cluster.link.enabled=true
# Set this property to autodetect the default outgoing IP address so that
# JGroups can bind to it. The property must point to an address that is
# accessible to the portal server, www.google.com or your local gateway.
cluster.link.autodetect.address=192.X.X.X:XX <-- à remplacer par une adresse connue de tous les noeuds
lucene.replicate.write=true

Rajouter la ligne suivante pour vérifier que tout marche bien au démarrage :

cluster.executor.debug.enabled=true

(par contre, pas de log pendant son fonctionnement)

{lang: 'fr'}