Struts 2: REST resources in Java
Een tijdje terug heb ik samen met een klant nagedacht hoe een aantal lang lopende taken verdeeld kunnen worden over een aantal computers. Via een web interface moeten bezoekers makkelijk taken kunnen aanmaken zonder af te weten van het achterliggende computerpark. Na een aantal uur komt een computer met een rapport, wat weer te downloaden moet zijn voor de klant. Daarnaast moest de klant het computerpark kunnen beheren, dus computers uit het rekencluster halen of juist nieuwe toevoegen. De beheerschermen voor taken en computers (vanaf hier: workers) zijn uiteindelijk geïmplementeerd met behulp van Struts 2 waarover meer in deze blog.
De keuze voor Struts 2 is gemaakt om twee redenen. Ten eerste is de klant bekend met Java en niet bereid voor een dergelijk klein project een nieuwe taal te leren. Ten tweede moest de applicatie binnen enkele dagen werken. Struts 2 in combinatie met de REST plugin stelde me in staat de frontend van de applicatie binnen een dag te bouwen, waardoor er voldoende tijd over was om scheduling component te bouwen en de koppeling met de bestaande rekencomputers te realiseren.
De architectuur
Ik begin met een kort uitstapje naar Ruby on Rails. Dit framework staat positief bekend omdat het je in staat steld in zeer korte tijd een applicatie op te bouwen waarmee objecten (resources) via het web beheert kunnen worden. De eenvoud voor de programmeur volgt voor een groot deel uit de filosofie convention over configuration, oftewel ga uit van een zinnige standaard waarden die je alleen in uitzonderingsgevallen aanpast. Een goed voorbeeld is hoe Ruby on Rails urls en resources koppelt (de routers). De Struts 2 REST plugin heeft goed naar deze principes gekeken en streeft naar een compatibel aanpak (lees: een-op-een gekopieerd).
Verder hanteert Struts 2 een MVC aanpak. Het model bestaat uit POJOs, oftewel simpele Java beans. Als view kunnen JSPs gebruikt worden, of een van de andere template engines. Ook de controller is een simpele bean, en hoeft geen subklasse te zijn van het Struts 2 framework. URLs worden direct gemapt op methoden van de controller. Dat gebeurt via de volgende regels:
GET: /worker -> WorkerController.index()GET: /worker/1 -> WorkerController.show() met id=1GET: /worker/1/edit -> WorkerController.edit() met id=1GET: /worker/new -> WorkerController.editNew()POST: /worker -> WorkerController.create()PUT*: /worker/1 -> WorkerController.update() met id=1DELETE*: /worker/1 -> WorkerController.destroy() met id=1
* Merk op dat PUT en DELETE daar lang niet alle browsers ondersteund worden. Je kan dit omzeilen door de HTTP methode als normale variabele (genaamd: _method) mee te sturen.
Elke controller methode geeft via de return aan of, en zo ja naar welke view gedispatched moet worden. Het is voldoende om simpelweg de naam van de view terug te geven. Struts 2 zal daarna opzoek gaan naar de best passende view.
Als alternatief voor een view wordt trouwens ook XML en /worker/1.json.
Voorbeelden graag!
Om wat feeling te krijgen met hoe je Struts 2 in de praktijk toepast, wil ik wat code fragmenten uit het eerder genoemde (maar sterk vereenvoudigd) project toelichten. De directory layout van dit project is als volgt:
/my-app |-- src/main | |-- resources | | |-- struts.properties (Fragment 1) | |-- java | | |-- my.app.actions | | | |-- WorkerController.java (Fragment 2) | | |-- my.app.model | | | |-- Worker.java | |-- webapp/WEB-INF/content | | |-- worker-index.jsp (Fragment 3) | | |-- worker-editNew.jsp (Fragment 4) | | |-- ... etc ...
Project directory layout
De huidige versie van de REST plugin (2.1.6) laat wat steekjes vallen wat betreft de convention over configuration filosofie als je het mij vraagt. De volgende properties moeten gezet worden anders werkt de plugin niet. Hopelijk zal dit in toekomstige versies niet meer nodig zijn.
struts.convention.action.mapAllMatches=true struts.convention.action.suffix=Controller struts.convention.default.parent.package=rest-default
Fragment 1: Struts 2 configuratie in struts.properties
In de controller zijn de eerder genoemde REST acties geïmplementeerd. De regels voorzien van een markering zal ik hieronder nader uitleggen.
package my.app.actions; // 1 @Results({ // 2 @Result(name="success", type="redirect", location="worker/%{id}"), @Result(name="input", type="redirect", location="worker") }) public class WorkerController implements ModelDriven<worker> { // 3 @Inject private Session session; private Worker worker = new Worker(); private List<worker> workers; public Worker getModel() { return worker; } public List<worker> getWorkers() { return workers; } public void setId(Long id) { session.load(worker, id); } // 4 // 5 public String index() { workers = session.createQuery("from Worker").list(); return "index"; } public String show() { return "show"; } public String editNew() { return "editNew"; } public String create() { session.save(worker); return "success"; } public String edit() { return "edit"; } public String update() { session.update(worker); return "input"; } public String deleteConfirm() { return "deleteConfirm"; } public String destroy() { session.delete(worker); return "input"; } } </worker></worker></worker>
Fragment 2: De WorkerController
- 1:
- De package naam is van belang. Struts 2 zal alle klassen in packages met een speciale naam (o.a.
*.actions.*) scannen voor controllers. In de configuratie hebben we zojuist aangegeven dat deze te herkennen zijn aan de suffix Controller.
Subpackages worden ook gemapt. Zo zal de controllermy.app.actions.admin.UserControllerbeschikbaar zijn onder/admin/user. - 2:
- Sommige acties leiden niet in een dispatch. Zo wil je bijvoorbeeld na het opslaan van een object de bezoeker redirecten naar de show actie.
- 3:
- Controllers die de
ModelDriveninterface implementeren stellen impliciet een model object beschikbaar in de view. Dit model object zal ook gebruikt worden om meegegeven parameters in te bewaren. Denk hierbij aan de waarden die de gebruiker invult in het bewerkscherm van deze controller. - 4:
- De
setId()method wordt automatisch door Struts 2 aangeroepen, en geeft je de kans om de persistente staat van het model te laden. In dit voorbeeld wordt een geïnjecteerde Hibernate sessie gebruikt. - 5:
- De daadwerkelijke controller methoden. Merk op dat sommige methoden geen logica bevatten, maar alleen een dispatch naar de view doen. Wat mij betreft zijn deze methoden in een volgende versie niet meer nodig.
In de view kan je gebruik maken van een vrij intuïtieve tag library. Je vindt hierin de zelfde functionaliteit als in de JSTL, maar net iets beter uitgevoerd (zie ook fragment 3). Daarnaast beschik je over de OGNL expressie language, die net wat krachtiger is vergeleken met de standaard JSP EL.
<s:if test="workers.size > 0"> // 1
<ol>
<s:iterator value="workers"> // 2
<li><a href="worker/${id}">${address}</a></li>
// 3
</s:iterator></ol>
</s:if>
<s:else>
Er zijn geen workers!
</s:else>Fragment 3: worker-list.jsp
- 1:
- De Struts 2 tags accepteren OGNL expressies als argument.
- 2:
- De
workersworden uit de controller opgehaald. Naast het model is de controller een impliciet object in de view. Je kan deze benaderen via de normale Java bean specificatie. Hier wordt dus degetWorkers()methode aangeroepen. - 3:
- Binnen de iterator ligt het huidige element boven op de ValueStack.
De tag library ondersteund thema’s wat betreft de uitvoer. In onderstaand voorbeeld zal een compleet HTML formulier gegenereerd worden, inclusief de benodigde labels. Je kan zelf een thema schrijven, zodat de opbouw van je website mooi uniform blijft. Merk op dat er ook een thema simple bestaat, waarbij dit alles uitgeschakeld wordt.
<s:form method="post"> <s:textfield name="address" label="worker.address" csserrorclass="error"> <s:submit> </s:submit> </s:textfield></s:form>
Fragment 4: worker-editNew.jsp
De balans
Struts 2 is een zeer bruikbaar framework. De benodigde hoeveelheid boiler plate code voor een resource georiënteerd project wordt tot een minimum gereduceerd. Op het gebied van view en controller neemt Struts 2 het moeilijke werk van je over. Wat dat betreft komt Java met Struts heel dicht bij de gespecialiseerde frameworks zoals Ruby on Rails en Grails.
Toch laat Struts 2 een paar kleine puntjes vallen. Ik gaf eerder al wat suggesties om de benodigde configuratie verder in te perken. Ook de controllers kunnen nog iets beter wanneer de dispatch methoden zonder controller logica weggelaten kunnen worden.
Tot slot wil ik opmerken dat validatie van het model niet aan bod gekomen is in deze blog. Dit onderdeel is vrij matig uitgewerkt vind ik en zorgt er voor dat ik niet 100% positief kan zijn. Door middel van annotaties in je controller (waarom niet in het model?) of in losse XML bestanden (misschien nog wel erger anno 2009?) definieer je de validatieregels. Dat wordt snel onleesbaar bij een groot model. Groter nadeel is dat dezelfde validatieregels voor zowel create als update worden toegepast. Een gemiste kans zolang hier geen verbetering in komt…

Quote: “Na een aantal uur komt een computer met een rapport”
Waarom duurt dat eigenlijk een aantal uur? Ben je een Cern LHC quantumchromocolissionsimulatie aan het draaien oid? Is dat niet het probleem wat eigenlijk aangepakt moet worden?
Maar zonder gekheid: kan je daar iets meer over vertellen? Dat illustreert het probleem wat…
Rikkert Koppes March 23, 2009 11:08
Dat is ook grappig.
Ik moet toevallig iets soortgelijks bouwen, maar dan veel kleinschaliger.
En gelukkig met Ruby on Rails en Crystal Reports.
Leuk om te zien hoe jij het aangepakt hebt.
Roy van der Meij March 23, 2009 18:00
Hallo Rikkert,
Irion, de klant, houdt zich bezig met taalanalyse. De applicatie waar deze front-end voor gebouwd is, crawlt, indexeert en classificeert een grote verzamelingen websites. Je moet hier denken in ordes van enkele duizenden tegelijk.
Crawlen van websites kan lang duren, omdat de tegenpartij nu eenmaal langzaam kan reageren. Ook is bandbreedte met deze volumes een probleem. Indexeren en classificeren aan de andere kant zijn behoorlijk CPU intensief. Vandaar dat gekozen is voor een offline aanpak.
De algoritmes om dit allemaal voor elkaar te krijgen, zijn al door Irion geïmplementeerd. Een goede front-end ontbrak echter, met als gevolg dat klanten van Irion direct met een worker moesten communiceren. De nieuwe applicatie zorgt er voor dat de klant één centraal aanspreekpunt heeft. Naast front-end gebaseerd op Struts2 acteert de applicatie ook als coördinator van het geheel. In de context van deze blog is dat gedeelte echter minder interessant.
Rob Schellhorn March 23, 2009 21:28