Ett användningsfall för JSR-223

JSR223

Ibland stöter man på en intressant Java-specifikation, men kan ha svårt att se praktiska användningsområden för den. Just det hade hänt en av våra kollegor innan han kom i kontakt med Java CMSet Magnolia.

I det här inlägget kommer vi gå igenom vad JSR-223 är, vad Magnolia CMS är och hur CMSet använder JSR-223 för att lösa ett verkligt problem.

För er som undrar vad JSR-223 är för en specifikation kan vi berätta att det är Java:s lösning för att köra skriptspråk i Java. Med skriptspråk menas språk som inte kompileras, till exempel JavaScript, Ruby, och Python. Historiskt har det varit en utmaning att blanda Java och skriptspråk. Vilket har gjort det svårt för personer som vill utnyttja styrkorna hos båda typerna av programmeringsspråk. Genom att ha ett standardiserat sätt att köra andra språk i Java erbjuds en lösning.

JSR-223

Skript körs genom en så kallad skriptmotor (javax.script.ScriptEngine). Den tar ett skript som indata, exekverar skriptet och om skriptet returnerar ett värde får Java-programmet tillbaka ett Object som representerar skriptspråkets returnerade värde. Notera att i regel så är inte en skriptmotor en wrapper för en fristående intrepretator utan ett gränssnitt mot en intrepretator kodad i Java.

Här ser du ett exempel på en exekvering av ett Ruby-skript.

ScriptEngine engine = new ScriptEngineManager().getEngineByName("jruby");
String script = "puts \"Hello, Exertus IT\"";
engine.eval(script); // Outputs "Hello, Exertus IT"

Du får tag på en skriptmotor genom JSR-223:s managerklass (javax.script.ScriptEngineManager). Manager:n använder sig av service provider-funktionen för att hitta tillgängliga skriptmotorer i classpath:en. Det behöver alltså inte finnas någon skriptmotor tillgänglig i din Java-miljö utan det beror på vilka JAR:s som är inkluderade. En standard Java-miljö har dock ha en implementation inkluderad – en JavaScript-motor som kallas Nashorn. Notera att Nashorn inte är en JavaScript-motor för en webbläsare utan en implementation av ECMA-script-specifikationen. Som sådan så saknas vissa API:er, till exempel setTimeout.

Nedan följer en lista med några exempel på JSR-223 implementationer.

Språk Implementation
Ruby JRuby (Site, Javadoc)
Python Jython (Site)
JavaScript Nashorn (Javadoc)

Här följer en förenklad översikt av klasserna i JSR-223. Klassen MyScriptEngine är inte med i specifikationen utan är med i översikten för att förtydliga kopplingen till de valbara interface:en.

JSR 223 overview

Som vi redan gått igenom får man tag på sin skriptmotor genom Manager-klassen. I översikten ser vi att detta sker genom en factory-klass som skapar upp skriptmotorn. Detta är inget du behöver veta om när du ska exekvera skript men det blir intressant om du ska utveckla din egen implementation av JSR-223. En skriptmotor kan implementera två valbara funktioner. Den första funktionen (Compilable) gör det möjligt att förkompilera ett givet skript så att det exekveras snabbare när du faktiskt anropar skriptet. Den andra funktionen (Invocable) gör det möjligt att, efter en skriptkörning, anropa funktioner som definierats i skriptet. Till exempel om ett JavaScript definierar en funktion i det globala context:et kan du anroppa den funktionen efter att skriptet lästs in.

Nedan följer ett exempel på ett skript som använder sig av Invocable-interface:et. Även detta skrivet i Ruby

public void exampleInvocation() throws ScriptException, NoSuchMethodException {
  ScriptEngine engine = new ScriptEngineManager().getEngineByName("jruby");
  
  String script = script(
    "$counter = 0",
      "def inc",
      "  $counter += 1",
      "  return $counter",
      "end",
      "def dec",
      "  $counter -= 1",
      "  return $counter",
      "end",
      "def set(value)",
      "  $counter = value",
      "  return $counter",
      "end"
  );
  
  engine.eval(script);
  
  Invocable invocable = (Invocable)engine;
  Object result;
  result = invocable.invokeFunction("inc");
  System.out.println("After 1 increase " + result);
  result = invocable.invokeFunction("inc");
  result = invocable.invokeFunction("inc");
  result = invocable.invokeFunction("inc");
  System.out.println("After 4 increases " + result);
  result = invocable.invokeFunction("dec");
  result = invocable.invokeFunction("dec");
  System.out.println("After 2 decreases: " + result);
  result = invocable.invokeFunction("set", 10);
  System.out.println("After set to 10: " + result);
}

private static String script(String... args) { 
  return Arrays.stream(args).collect(Collectors.joining("\n")); 
}

I överblicken ser vi även ScriptContext och Bindings. Dessa interface används för kommunikation mellan skriptet och Java-applikationen. Dels kan Java-applikationen skicka in data till skriptet och dels så kan Java-applikationen läsa ut värden från skripet. ScriptContext exponerar även readers och writers för att skicka data in och ut från skriptet (ala System.out och System.err).

Magnolia CMS

Magnolia är ett Java-baserat CMS som främst riktar sig till större/komplexa sajter. CMS:et är byggt runt JCR, med både konfiguration och redaktörsinnehåll sparat som JCR-innehåll. För att presentera innehållet för slutanvändaren måste ett GUI (HTML, CSS, osv) utvecklas. Traditionellt paketeras denna frontend-kod i en JAR-fil som Magnolia laddar in som en modul. Nackdelen med detta upplägg är att frontend-utvecklarna måste använda Java-byggverktyg samt att en kodändring kräver en omstart av CMS:et. Detta är inte populärt bland frontend-utvecklare, som är vana vid hot-reload och i regel ogillar allt vad Java heter. Detta har lett Magnolia-teamet till att leta efter en alternativ lösning för sajtutveckling.

De kallar sin lösning light modules och lösningen bygger på att CMS:et laddar in filer direkt från disk istället för från JCR/classpath:en. Det som tidigare var konfiguration i JCR:en specificeras nu i YAML-filer och andra resurser (till exempel vyfiler) hämtas direkt från disk. Ändringar på disk upptäckts automatiskt och ingen omstart krävs. I denna artikel ska vi inte djupdyka i light modules-konceptet men det är en del i light modules-lösningen som är av intresse när vi kollar på JSR-223.

I den traditionella Magnolia-lösningen byggs en vy upp av en Freemarker-fil som genererar HTML och vid behov initieras en modell-klass (Java). I light module-lösningen används fortfarande Freemarker, de är inte knutna till Java så det finns inget behov att byta ut dem, men modellen kan inte vara en Java-klass. Det är här JSR-223 kommer in i bilden. Istället för Java används JavaScript, genom Nashorn, för att skapa modellklassen.

Magnolia + JSR-223

När Mangolia ska rendrera en vy kollar den om det finns en modell för den givna vyn. Om det finns och det är en JavaScript-modell exekverar den skriptet med Nashorn. Magnolia förväntar sig att skriptet ska returnera en JavaScript-klass. Klassen ska exponera en metod – execute. Nedan visas ett exempel på en enkel modellklass.

function DemoPage() {
  this.execute = function() {
    // Logic goes here
  };
}

new DemoPage(); // The result of the last executed command becomes the return value in Nashorn 

Objektet som skapas av new DemoPage() blir tillgängligt i Freemarker-filen som dess modellklass. Magnolia exponerar ett gäng Java-API:er som modellklassen kan använda för att förbereda data till vyn. Detta är möjligt genom en funktion Nashorn erbjuder som omsluter Java-klasser i JavaScript-objekt. Magnolia skickar in ett antal klasser med hjälpfunktioner som du kan använda i JavaScript:et som om det vore vanliga JavaScript-objekt. Se till exempel nedan där vi läser ut ett innehåll från JCR:en och exponerar ett av dess property:s i modellklassen.

var newsListContent = cmsfn.contentByPath("/demo-page/news-list");
var newsListLink = null;
if (newsListContent !== null) {
  newsListLink = cmsfn.link(newsListContent);
}

function DemoPage() {
  this.newsListLink = newsListLink;
  this.execute = function() {};
}

new DemoPage(); 

I exemplet ovan är cmsfn en instans av Java-klassen info.magnolia.templating.functions.TemplatingFunctions som Magnolia lägger in i Nashhorn:s skriptkontext. Vilken är en hjälpklass för att komma åt data som relaterar till CMS:et (sidor, länkar, osv).

Eftersom det är JavaScript-filer behövs det ingen kompilering för att göra en ändring och frontend-utvecklaren kan använda sina vanliga verktyg för att koda och bearbeta JavaScript-filen, som sedan läses in direkt av Magnolia (dvs, hot reload). Vi kan inte uttala oss om gemene frontend-utvecklare låter sig luras av denna maskering av Java-utveckling men vi gillar det i alla fall. Det snabbar definitivt upp utvecklingstempot till skillnad från att jobba med modeller i Java.

Verkar det spännande att få jobba på uppdrag där man stöter på saker som detta? Om ja, hör då av dig till oss via formuläret nedan!

Vi har mottagit ditt meddelande och återkommer inom kort.
Hoppsan! Något gick dessvärre fel, vänligen verifiera att du inte är en robot eller ladda om sidan och försök igen.
Vi vill göra dig uppmärksam på att vi behandlar dina uppgifter i strikt enlighet med vår Integritetspolicy. Allt för att du ska känna dig trygg i att vi värnar om din integritet.