Categories
Teknik

Ladda Clojure-kod med java.util.ServiceLoader

I Crazy Snake-tävlingen laddade vi deltagarnas implementationer av ormhjärnor med hjälp av java.util.ServiceLoader. Här kan du se hur vi gav deltagarna möjlighet att implementera sin hjärna i Clojure.

I Crazy Snake-tävlingen laddade vi deltagarnas implementationer av ormhjärnor med hjälp av java.util.ServiceLoader. Här kan du se hur vi gav deltagarna möjlighet att implementera sin hjärna i Clojure.

Jag tänkte berätta lite om ett problem som vi stötte på under utveckling av spelmotorn till vår programmeringstävling Crazy Snake. Förhoppningsvis kan några av våra lärdomar kring klassladdning i Clojure och användandet av ServiceLoader att komma till användning för andra.

De tävlande lagen tillhandahöll var sin jar-fil med en ormhjärna (som implementerande Brain-interfacet i vårt API). Tanken var att ladda in de tävlandes jar-filer i runtime och instantiera en hjärna för varje lag. Hjärnan användes sedan för att skapa en orm som rörde sig på spelplanen. Således behövde vi använda dynamisk klassladdning för att ladda de olika lagens hjärnor.

Det vanligaste sättet att ladda klasser dynamiskt har tidigare varit att använda klassens namn och anropaClass.forName(). Eftersom vi inte ville styra vilket klassnamn de deltagande lagen skulle använda tyckte vi inte om den lösningen. I Java SE 6 infördes klassen ServiceLoader som ger möjlighet att ladda en klass baserad på dess typ istället för dess namn (som tidigare med Class.forName()). ServiceLoader läser filer i katalogen META-INF/services i sin classpath och instantierar en klass baserat på namnen som finns angivna i den filen. Dock måste klasserna implementera det interface som anges i ServiceLoader.load(). Namnet på filen med implementationsklasser måste även stämma överrens med det interface som klasserna implementerar. I vårt fall innehöll alltså alla deltagares jar-filer filen META-INF/services/se.citerus.crazysnake.Brain. Om denna förklaring inte var tydlig kan du titta i exempel-projekten här för att se hur det hela såg ut.

När vi implementerade spelmotorn använde vi alltså ServiceLoader för att ladda deltagarnas implementationer av ormhjärnor. Deltagarna packade in sin implementation av ormhjärnan i en jar-fil och vi laddade dem med ServiceLoadersom fick en referens till en URLClassLoader till deltagarens jar-fil. Koden såg ut ungefär så här:

  URL[] urls = {file.toURI().toURL()}; 
  ClassLoader classLoader = new URLClassLoader(urls); 
  ServiceLoader<Brain> services = ServiceLoader.load(Brain.class, classLoader);

Detta tillvägagångssätt fungerade för våra första test-hjärnor och vi började känna oss kanske nöjda. Vi ville dock även gärna ge möjligheten för deltagarna att implementera Brain-interfacet i andra JVM-stödda språk än Java och snabbt kom vi att tänka på Clojure som ett utmärkt alternativ. Detta visade sig dock vara något klurigare än vi först insåg.

Våra ormhjärnor skriva i ren Java kunde laddas utan problem (även Groovy och Scala gick bra), men när vi provade att ladda vår test-implementation som var skriven i Clojure stötte vi på patrull. Vi implementerade vår test-implementation i Clojure med :gen-class. En korkad exempel-orm som illustrerar detta ser ut såhär:

(ns se.citerus.crazysnake.brain.clojure-brain
  (:import
    [se.citerus.crazysnake Movement])
  (:gen-class
    :name se.citerus.crazysnake.brain.ClojureBrain
    :implements [se.citerus.crazysnake.Brain]
    :main false))

;-- Implementation of Brain interface

(defn -init [this participants meta])

(defn -getName [this]
  "ClojureBrain")

(defn -getNextMove [this state]
  Movement/FORWARD)

När vi skulle ladda vår Clojure-hjärna genom att iterera över vår ServiceLoader  så kastades ettServiceConfigurationError. Vi gjorde lite efterforskning och fann på att Clojure per default använder sig av kontextklassladdaren för den anropande tråden. Eftersom den anropande trådens klassladdare inte har kännedom om vår Clojure-kod fungerade detta inte. Som tur var kan man sätta om denna i Java och när vi istället satte trådens kontextklassladdare innan vi anropade ServiceLoader.load() fungerade det hela mycket bättre. Koden som vi slutligen använde i spelet såg ut ungefär så här:

File brainImplementingJarFile = new File(pathToJar);
URL[] urls = {file.toURI().toURL()};
ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
ClassLoader classLoader = new URLClassLoader(urls, contextLoader);
Thread.currentThread().setContextClassLoader(classLoader);
ServiceLoader<Brain> services = ServiceLoader.load(Brain.class, classLoader);

// Reset the context classloader    
Thread.currentThread().setContextClassLoader(contextLoader);

Detta är en väldigt liten, detaljerad inblick i vår implementation av spelet. Det var också något som vi lärde oss under resans gång och som vi gärna vill dela med oss av.

Hoppas det kommer till användning!

Leave a Reply

Your email address will not be published.