Apache Camel med Scala
Apache Camel är ett javabaserat integrationsramverk som innehåller en mängd komponenter. När man konfigurerar kan man använda Spring xml, annoteringar och en Java DSL. Allt är väl beskrivet på Camels hemsida. Eftersom Scala är utvecklat för att enkelt kunna integrera med Java och Javas ramverk är det inte konstigt att är väldigt enkelt att integrera Scala-komponenter i Camel. Det finns även en Scala DSL som kan användas för att konfigurera Camel-routar med. Jag har skrivit ett litet Scala-Camel projekt, WeirdTranslator för att visa på hur Scala i Camel kan fungera. WeirdTranslator är en variant på viskleken, man tar en mening och översätter den mellan ett antal språk och avslutar med att översätta till ursprungsspråket. I detta fallet finns det två vägar att få in och ut text antingen via GTalk, XMPP, och direkt med en TCP socket.
Vi börjar med att titta på hur routarna sätts upp:
package se.cygni.weirdtranslator
import org.apache.camel.Exchange
import org.apache.camel.scala.dsl.builder.RouteBuilder
trait CamelRouteBuilder extends ChainedTranslatorComponent {
def createRouteBuilder(minaPort: Int, xmppAddress: String): RouteBuilder = new RouteBuilder {
val xmpp = "xmpp://" + xmppAddress
val mina = "mina:tcp://localhost:%s?textline=true&sync=true&encoding=UTF-8".format(minaPort)
xmpp bean(chainedTranslator) to xmpp
mina process ((exchange: Exchange) => {
val translated = chainedTranslator.translate(exchange.in.toString)
exchange.out = translated
})
}
}
Vi ser att rad 8 sätter upp en route som börjar med ett meddelande mottaget från Camels XMPP komponent, det går vidare till bönan chainedTranslator och därefter ut via XMPP igen.
För Tcp-socketfallet, rad 9 till 12 använder vi Apache Mina som finns som en färdig Camel komponent. I detta fallet är meddelandet av typen InOut och vi behöver sätt exchange.out till det översatta meddelandet. Vi använder då DSL komponenten process som tar en funktion (Exchange)->Unit som parameter.
Själva översättandet görs av chainedTranslator som mixas in via ChainedTranslatorComponent traitet. Detta är ett sätt att göra Dependency Injection utan ramverk. Vi använder enbart scalaspråket och denna varianten kallas ”Cake-pattern”, se till exempel Jonas Bonérs bloggpost för mer information.
Hur hade detta blivit i java? Huvudsyftet med denna artikel är inte att jämföra Java med Scala men vi gör det för denna klass.
package se.cygni.weirdtranslator;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.builder.RouteBuilder;
public class JavaRouteBuilder {
final ChainedTranslatorComponent.ChainedTranslator chainedTranslator;
public JavaRouteBuilder(ChainedTranslatorComponent.ChainedTranslator chainedTranslator) {
this.chainedTranslator = chainedTranslator;
}
public RouteBuilder createRouteBuilder(int minaPort, String xmppAddress) {
final String xmpp = "xmmp://" + xmppAddress;
final String mina = String.format("mina:tcp://localhost:%s?textline=true&sync=true&encoding=UTF-8", minaPort);
return new RouteBuilder() {
@Override
public void configure() throws Exception {
from(xmpp).bean(chainedTranslator).to(xmpp);
from(mina).process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
String translated = chainedTranslator.translate(exchange.getIn().toString());
exchange.getOut().setBody(translated);
}
});
}
};
}
}
Vi ser att javakoden blir ungefär dubbelt så många rader. Skillnaden ligger i att vi i Scala skriver på en högre nivå med ett mer avancerat DSL-bibliotek. Eftersom Scala är funktionellt kan vi enkelt skicka in en funktion till process. I Java tvingas vi använda en anonym klass. I Scala DSL:en behöver vi inte skriva *from(xmpp) *för att börja en ny route, utan det räcker med en sträng, i vårt fall variabel xmpp. I Scala DSL:en finns funktionen stringToRoute som är en implicit funktion. Det vill säga om scalakompilatorn hittar en sträng när den skulle behöva en SRouteDefinition så kommer stringToRoute automagiskt att anropas.
implicit def stringToRoute(target: String) : SRouteDefinition = new SRouteDefinition(builder.from(target), this)
Scala får ibland kritik för att vara komplicerat, sanningen är Scala kan vara komplicerat. Skriver man bibliotek så använder man ofta de mer avancerade scalakonstruktionerna. Det som händer är att man kan flytta komplexitet till bibliotekskoden, kvar i tillämpningskoden blir enklare kod som dock kräver god förståelse av de bibliotek man använder. I slutändan tycker jag scalakoden blir mer lättläst. Jämför de markerade raderna i Scala- och Javafallen ovan.
Vi kommer nu till bönan ChainedTranslator:
package se.cygni.weirdtranslator
trait ChainedTranslatorComponentImpl extends ChainedTranslatorComponent {
this: TranslatorComponent =>
val chainedTranslator = new ChainedTranslatorImpl
class ChainedTranslatorImpl extends ChainedTranslator{
def translate(text: String): String = {
if (text == null || text.isEmpty) return null
val first = translator.identifyLang(text)
val translatePairs = createLanguagePairs(first, languages)
translatePairs.foldLeft(text)((t, p) => translator.translateText(t, p))
}
}
val languages = List("it", "pl", "de", "fr", "es")
def createLanguagePairs(first: String, languages: List[String]): List[LanguagePair] = {
def pairs(first: String, tail: List[String]): List[LanguagePair] = tail match {
case Nil => Nil
case head :: tail => LanguagePair(first, head) :: pairs(head, tail)
}
languages match {
case Nil => List.empty[LanguagePair]
case list => pairs(first, list :+ first)
}
}
}
Vi ser att på rad 3 att komponenten WeirdTranslator själv använder en TranslatorComponent och metoden som anropas från Camel routarna är translate definierad på rad 6 till 11. Här ser vi först att vi kollar om argumentet är *null *och**som scalaprogrammerare blir man nu fundersam. Null brukar man inte använda, man föredrar ju Option. Det finns dock ett fall när man måste använda null och det är när man interagerar med javakod, vilket är precis vad vi gör i detta fallet. När vi nu vet att vi har en text så börjar vi med att identifiera vilket språk den är skrivet på, genom att anropa translator.identifiyLang. Därefter skapar vi med hjälp av funktionen createPairs en lista på vilka språk vi ska översätta till. CreatePairs är en rekursiv funktion som börjar exekvera på rad 19 och i vårt fall kommer vi sedan fortsätta med rad 21. Där ropar vi på den inre funktionen pairs med det identifierade språket som kommer från parametern *first. *Vi lägger också till det språket sist i listan. Funktionen pairs kommer sedan skapa ett LanguagePair med *first *och det huvudet i listan det vill säga ”it” samt anropa sig själv rekursivt med ”it” och List[”pl”, ”de”, ”fr”, ”es”, first]. Till slut får vi en lista med de översättningar vi önskar.
Det sista vi gör är att använda den högre ordningens funktion *foldLeft*på översättningslistan. Här ser vi hur funktionell programmering kan bli otroligt kompakt och tämligen svårläst för den oinvigde.**Snabbversion för den oinvigde som dessutom inte vill följa länken ovan: FoldLeft börjar med ett startvärde, i vårt fall text. Därefter anropas den funktionen vi skickar med som andra parameter (t, p) => translator.translateText(t, p), där t binds till text och p till det första elementet i listan. Resultatet av anropet till translator.translateText(t, p) används sedan som t för nästa element i listan och så vidare för alla elementen. Slutligen returneras resultatet av den sista översättningen.
Jag kommer inte att gå igenom själva Translator-komponenten utan nöjer mig med att säga att det är Googles Translate API som gör jobbet och hänvisar till koden för GoogleTranslatorComponent.
Slutligen knyter vi ihop det hela med en main-klass som plockar tre argument från kommandoraden och startar camel-contexten. Argumentet är vilken xmpp-användare vi ska skicka meddelande till och användare och lösenord vi vill ansluta till XMPP med, det vill säga vår avsändare.
package se.cygni.weirdtranslator
import org.apache.camel.impl.DefaultCamelContext
object Main extends CamelRouteBuilder with ChainedTranslatorComponentImpl with GoogleTranslatorComponent {
val xmppAddressTemplate = "talk.google.com:5222/%s?serviceName=%s&user=%s&password=%s"
def main(args: Array[String]) = {
if (args.length != 3) {
println("Usage: toUser fromUser password")
exit
}
val (to, domain) = extractUserAndDomain(args(0))
val (from, _) = extractUserAndDomain(args(1))
val password = args(2)
val xmppAddress = xmppAddressTemplate.format(to, domain, from, password)
val minaPort = 6666
val context = new DefaultCamelContext()
context.addRoutes(createRouteBuilder(minaPort, xmppAddress))
context.start
while (true) {
Thread.sleep(10);
}
}
def extractUserAndDomain(arg: String): (String, String) = {
val defaultDomain = "gmail.com"
arg.split("@") match {
case Array(user) =& (user + "@" + defaultDomain, defaultDomain)
case Array(user, domain) =& (arg, domain)
}
}
}
Själva camelhanteringen sker på de markerade raderna, resten är kod för att ta fram argumenten.
Vi ser att det går utmärkt att använda Camel i ett scalaprojekt, det enda man märker av är att man behöver arbeta med null.
I detta exemplet har jag valt att inte använda mig av Spring eller Guice för att knyta ihop olika delar utan istället enbart Scala. Jag vill bara nämna att det går utmärkt att använda Scala tillsammans med Spring/Guice.