Agile overgaan van JSF naar JS

Het liefst werken ontwikkelaars in een green field project. Lekker van de grond af aan werken. Geen legacy en de nieuwste technieken voor het oppakken. Helaas is dit meestal niet de realiteit…

Veel bedrijven maken nog gebruik van grote (legacy) systemen. Deze systemen vervangen kost vaak veel tijd en geld en staat new business development op korte termijn in de weg. Bovendien duren migratie trajecten vaak lang, worden de voordelen pas bij live-gang zichtbaar en leveren big-bang implementaties vaak onverwachte problemen op. Hoe kunnen we nu zorgen dat organisaties wel van de voordelen van nieuwe technieken profiteren en tegelijk de pijn van de migratie verzachten?

 

Agile migratie

In deze case gaan we uit van een uitgebreid portaal, gebouwd in JSF (met JBoss Seam 2), dat honderden klanten dagelijks informatie aanbiedt en ervoor zorgt dat zij hun administratie kunnen doen. Dit portaal was aan vernieuwing toe.

In JSF zagen wij vanuit de techniek weinig toekomst en de ontwikkeling van JBoss Seam is bovendien gestopt. Ook verwachten mensen tegenwoordig dat de user interface van webapplicaties werkt zoals Gmail. Daarnaast gaat ontwikkelen in een complexe server side architectuur als JSF niet bepaald snel en is er behoorlijk wat ijzer nodig om vele concurrent users aan te kunnen. Kortom, diverse redenen om de applicatie met andere technologie voort te zetten en dus te migreren.

In onze case willen we naar een single page HTML 5 front-end met een stateless REST back-end. Een RESTful API heeft onder meer als voordeel dat mobiele apps of andere applicaties hier ook gebruik van kunnen maken.

Tijdens de voorgenomen technische migratie was het voor de opdrachtgever wel noodzakelijk om nieuwe functionaliteit te blijven toevoegen op de bestaande omgeving en dat bugs snel opgelost konden worden. Kortom, business as usual. Het is dus nodig om deze applicatie agile te migreren, ofwel scherm voor scherm, en ondertussen de gebruikers een goede ervaring te blijven geven.

 

Architectuur

De ‘oude’ architectuur is dus een JSF applicatie. Er wordt in ons geval veel state bijgehouden op de server. De Hibernate domein objecten zijn direct gekoppeld aan de user interface en worden in Seam conversaties door de client gemuteerd. Dit maakt de transacties lang en complex.

In de ‘nieuwe’ architectuur hebben we te maken met twee gescheiden applicaties. Een Single Page Application geschreven in Javascript en een lichtgewicht Java back-end met een RESTful API. De enige state die de back-end heeft is een security context met daarin de ingelogde gebruiker en zijn rollen.

We lichten hieronder de nieuwe architectuur kort toe. Daarna beschrijven we onze aanpak van de migratie issues die een tijdelijke hybride situatie met zich meebrengt.

 

Single Page Applications

Zoals gezegd, de front-end is in de nieuwe architectuur een Single Page Application (SPA). Javascript wordt hier in niet alleen gebruikt om fancy user interface elementen te creeëren en zo de HTML wat ‘levendig’ te maken. Een SPA is een volwaardige applicatie waarin business logica is verwerkt. Deze applicatie wordt volledig ingeladen als de gebruiker voor het eerst de home page bezoekt. Hierna hoeft alleen data uit de database nog van de server gehaald te worden. De front-end is hierdoor extreem schaalbaar.

Als een gebruiker naar een andere pagina in de applicatie navigeert wordt er dus geen nieuwe HTML van de server opgehaald. De javascript past direct de DOM tree in de browser aan. Eventueel wordt nieuwe data met een AJAX request van de server gehaald en op het scherm getoond. Dit biedt een enorm goede gebruikerservaring.

 

Uitdagingen

Een tijdelijke hybride situatie in de overgang van een stateful JSF architectuur naar een stateless SPA architectuur brengt verschillende uitdagingen met zich mee. De SPA deelt bijvoorbeeld geen state met de backend. Maar je wilt natuurlijk geen schermen laten zien aan de gebruiker als deze niet ingelogd is op de backend (als er geen geauthenticeerde Principal in de Security Context is). Tevens moet het mogelijk zijn om vanuit de SPA frontend te navigeren naar JSF pagina’s die vanaf de backend geladen worden en vice versa.

 

Navigeren

De uitdaging bij het navigeren ligt in het feit dat zowel de JSF pagina’s als de SPA vanaf hetzelfde domein geserveerd moeten worden. Hierdoor heeft de client namelijk een geldige sessie cookie om beveiligde REST calls naar de backend te doen vanuit de browser of om JSF pagina’s te raadplegen. Om dit mogelijk te maken is de Apache webserver uitgerust met rewrite rules (zie listing 1).


ProxyPass /backend-server balancer://jbosscluster stickysession=JSESSIONID|jsessionid nofailover=On
ProxyPassReverse /backend-server balancer://jbosscluster

listing 1

Alle calls die beginnen met ‘/backend-server/’ (de REST calls en JSF pagina’s) worden niet uit de webroot geserveerd, maar doorgestuurd naar de back-end server. Alle andere URL’s worden vanuit de webroot geserveerd. Het kan zijn dat een oude JSF pagina een bepaalde state of een lopende conversatie verwacht. Hiervoor is een ConversationHelper utility geschreven (zie listing 2).

 


@In
private FacesManager facesManager;
 
// make needed state available for JSF managed beans
@Out
private DetailsViewObject selectedRow;
...
public String makeTransitionFromSPAToJSF() {
       // rowId variable set by URL parameter
selectedRow = new DetailsViewObject(rowId);
       // begin Seam Conversation, needed for next JSF view
manager.beginConversation();
       // Go to the requested viewId, variable set by URL parameter
       return viewId;
}

listing 2

Deze gebruikt de JSF FacesManager om converstaties te starten en zet state die eventueel nodig is op een JSF managed bean. Dit kan een in de SPA geselecteerde rij zijn die nodig is voor een JSF details scherm.

 

Inloggen

De landingspagina is in de SPA gemaakt. Bij het bootstrappen van de SPA wordt er een REST call gedaan naar de backend. Voor deze call hoef je niet geauthenticeerd te zijn. Als antwoord geeft deze call een user object terug in de Json. Als je nog geen sessie hebt is dit object er niet en word je door de frontend naar de back-end server gestuurd om in te loggen. Na het inloggen wordt de gebruiker weer naar de SPA teruggestuurd. Deze keer is het user object er wel en wordt de applicatie verder geladen zodat de gebruiker op het eerste scherm komt.

afbeelding 1: Traditionele JSF applicatie vs. Single Page Application

Look & Feel

Een andere uitdaging is de Look & Feel. Voor de eindgebruiker moet het niet opvallen of hij op een JSF of javascript SPA pagina zit.

De HTML van de SPA wordt bij ons met Handlebars templates gemaakt. Onder andere de header, footer en het menu van de applicatie hebben we hierin gemaakt. Deze templates hebben we exact nagemaakt in JSF templates waarbij de CSS bestanden gedeeld worden. Hierdoor geldt voor beide applicaties dezelfde stijl.

 

Oude situatie vs nieuwe situatie

Wanneer je als Java developer over moet stappen van JSF naar JS word je geconfronteerd met nieuwe libraries en tools. Maar er blijken ook veel concepten in de JavaScript wereld overeen te komen met de Java wereld. Hieronder volgen een aantal concepten en hoe deze in de JSF wereld, dan wel de JS wereld werken.

 

Routing

JSF routing wordt op de server gedaan middels de navigation.xml (zie listing 3). Voor de routing in een SPA is gekozen voor Sammy.js, een veel gebruikte en eenvoudige javascript library. Je definieert simpelweg URL’s die afgevangen moeten worden, dit zijn zogenaamde routes in Sammy. Vervolgens voer je zelf de code uit om de DOM tree te verversen en zo een nieuw scherm te tonen. In ons geval gebruiken we een framework om volgens het MVC pattern te programmeren. Er wordt dus vanuit de route een model en een view geïnstantieerd. Het model haalt data van de server via een REST call. Vervolgens gooit het model een event waarop de view zich ververst. Tenslotte kan je een controller gebruiken om acties vanuit de HTML af te vangen en zo je model te manipuleren of REST calls naar de server uit te voeren.Navigeren naar een andere pagina werkt gewoon door een URL aan te roepen die door Sammy.js als route afgevangen wordt (zie listing 4).


<page view-id="/login.xhtml">
  <rewrite pattern="/Inloggen/{cid}" />
  <rewrite pattern="/Inloggen" />
 
  <navigation evaluate="#{identity.authorized}">
    <rule if-outcome="true">
      <redirect url="../"></redirect>
    </rule>
    <rule if-outcome="false">
      <render view-id="/generalError.xhtml" />
    </rule>
  </navigation>
</page>

listing 3


app.mapRoutes([
      // startpagina
      [ 'get', '/', m.mainmenu ],
      [ 'get', '/menu/:name', m.submenu ],
 
      [ 'get', '/logout', function () { this.logout(); } ],
...

listing 4

 

Templating

Voor JSF templating worden facelets gebruikt. Dit zijn XHTML pagina’s die gerendered worden op de server

(zie listing 5).


<h:panelGroup rendered="#{rendered}">
    <h:form prependId="false">
      <s:div rendered="#{showExportButtons}" styleClass="export">
        <h:commandLink styleClass="noBlock" title="#{messages['export.pdf']}">

listing 5

Handlebars templates worden tijdens de build omgezet naar javascript en zijn hierdoor client side beschikbaar. Javascript code en HTML worden compleet van elkaar gescheiden door het gebruik van templates. In deze logic-less templates schrijf je HTML en kan je de variabelen gebruiken die op het model beschikbaar zijn. Simpele if-then en loop constructies zijn mogelijk in de templates. Het is echter niet mogelijk om javascript zelf vanuit je template aan te roepen. Een template ziet er als volgt uit (zie listing 6).


{{#if fileAvailable}}
  <a class="download" rel="tooltip" title="Bestand downloaden">
    <i class="icon-file"></i>
  </a>
{{else}}
  <i class="icon-file file-unavailable" title="Bestand niet beschikbaar."></i>
{{/if}}

listing 6

 

Dependency/module management

In Java hebben we diverse Dependency Injection frameworks tot onze beschikking, zoals Seam. Omdat er aan de client side ook business logica geimplementeerd wordt willen we hier ook de verantwoordelijkheden scheiden middels DI principes. De frameworks voor dependency management en injection zijn ook hier aanwezig. Voor onze applicatie hebben we gekozen voor RequireJS.

RequireJS is een javascript file loader die tijdens het ontwikkelen de afhankelijke files asynchroon kan inladen. Iedere file wordt middels een ajax call van de webserver opgehaald. Dit is dus alleen tijdens het ontwikkelen.

 

Build tooling (Grunt)

Met simpele grunt taken (soort ant voor javascript) is het mogelijk om snel een lokale live-reload webserver in de lucht te brengen. Hierop worden al je javascript, HTML en templates automatisch uitgevoerd. Bij elke wijziging in de code wordt de pagina gerefreshed en zie je direct het resultaat van je code en is de ontwikkelervaring geweldig. Dit geldt ook voor de Mocha unit tests die je zo live in de browser kan draaien. Deze lokale server om de SPA te draaien kan zowel in lokale als in proxy modus gestart worden. In lokale modus wordt de Json data van het filesysteem gehaald. In proxy modus worden de REST calls naar de back-end server gestuurd.

Ook worden de grunt taken eenvoudig door de Continuous Integration omgeving (Jenkins) uitgevoerd, voor het maken van builds, het draaien van unit tests na een commit en voor het maken van releases. De release taak zorgt ervoor dat alle javascript en templates gecomprimeerd en samengevoegd worden tot 1 bestand.

 

Package management NPM en Bower

De Node Package Manager (NPM) en Bower worden gebruikt om dependencies op diverse javascript libraries te managen. NPM wordt vooral gebruikt om de development dependencies te managen. Een voorbeeld hiervan is Grunt.

Bower wordt voor de runtime dependencies gebruikt. Deze worden dus met de Javascript code opgeleverd. Handlebars is hier een voorbeeld hiervan.

 

Conclusies

Onze ervaring is dat het zeker de moeite loont om een JSF applicatie te migreren naar een SPA met een simpele RESTful back-end.

Eindgebruikers ervaren een veel betere User Experience. De RESTful API is herbruikbaar voor externe (mobiele) applicaties, wat in ons geval een belangrijke wens was. De ontwikkelervaring van een javascript SPA is boven verwachting goed, met name door tools als grunt en de live-reload server. Code wijzigingen waarvan je binnen een seconde het resultaat ziet, zonder dat je een zware Enterprise Java server moet draaien. Bovendien zorgt de duidelijke scheiding van de REST API voor een heldere architectuur. Zonder lang draaiende transacties en schermen die onderling state delen zijn bugs vele malen sneller te vinden en op te lossen.

Bovendien is het met de door ons gekozen aanpak heel goed mogelijk om deze migratie geleidelijk en gecontroleerd te doen, zonder ingrijpende aanpassingen aan de bestaande JSF applicatie te hoeven doorvoeren. Het vergt natuurlijk wel een extra investering om in een hybride situatie te kunnen draaien, maar deze was in ons geval niet erg groot. Alleen al gezien het feit dat we zeer flexibel op wensen van de business in kunnen blijven spelen tijdens de migratie maakt het zeer de moeite waard!

afbeelding 2: Integratie van oude en nieuwe architectuur.

Referenties                    

  • Handlebars for templating (Mustache): http://handlebarsjs.com/
  • Sammy for routing and application context: http://sammyjs.org/
  • RequireJS for AMD module loading: http://requirejs.org/
  • JQuery for ajax requests and animations: http://jquery.com/
  • Bootstrap for responsive ui and components: http://getbootstrap.com/
  • Mocha to run javascript unit tests: http://visionmedia.github.io/mocha/
  • Grunt task runner for build automation/execution: http://gruntjs.com/
  • Bower package manger for web applications: http://bower.io/
  • Node Package Manager: http://nodejs.org/