Er zijn een aantal plugins beschikbaar voor de Grails developer die het ontwikkelen van een webservice erg makkelijk zouden kunnen maken. Ik zeg “zouden kunnen” want een tijdens het uitproberen van enkele, liep ik tegen wat struikelblokken aan. Als we eens kijken in de Grails Plugins pagina bij de web services plugins, dan zien we o.a. de XFire plugin, Apache Axis2 en de Metro Grails plugin.

Ik ben aan de slag gegaan om vanaf scratch een Grails applicatie met enkele domain classes te exposen en met XFire als een webservice, en vanuit een client de service aanroepen.  Ik wilde wel eens weten in hoeverre ze out-of-the-box werkt en of ook iets lastigers dan de voorbeelden nog steeds makkelijk werken. Aan de slag met XFire!

Benodigdheden

Ik werk onder Windows XP en gebruik Java 1.5. Wat ik van te voren heb gedownload zijn de volgende dingen:

  • De laatste stable versie: Grails 1.0.4. Deze heb ik ergens geïnstalleerd en de omgevingsvariabele GRAILS_HOME er naar toe gewezen en de %GRAILS_HOME%\bin directory aan het PATH toegevoegd. Ter overvloede: zo kunnen we grails vanaf elke locatie runnen.
  • De laatste stable versie van Axis2, versie 1.4.1. Ik heb AXIS2_HOME gezet als omgevingsvariabele en %AXIS2_HOME%\bin toegevoegd aan het PATH. Bij deze distributie komt namelijk de tool wsdl2java, waarmee client Java stubs gegenereerd kunnen worden uit een WSDL beschrijving.

Verder gebruik ik Eclipse als favoriete IDE!

De stappen

Het enige wat ik met mijn applicatie wil is dat er personen bijgehouden worden die elk een lijstje van trainingen en cursussen heeft waaraan hij of zijn meegedaan heeft. Via de webservice wil ik een of meerdere personen kunnen teruggeven met zijn of haar details.

TestApp: de applicatie

  1. Ik ga in mijn favoriete workspace staan en maak een nieuwe, lege Grails applicatie:
    C:\Documents and Settings\Ted Vinke\Workspace>grails create-app Test
  2. Ik open/importeer project Test in Eclipse en maak 2 domain classes aan: Person en Course. Zie de code hieronder.
    class Person {
    
        String name
        static hasMany = [ courses:Course ]
    
        static constraints = {
            name ( blank: false, nullable: false, maxSize: 200 )
        }
    }
    class Course {
    
    	String name
    
    	static def belongsTo = [Person]
    	static hasMany = [persons:Person]
    
    	static constraints = {
    		name ( blank: false, nullable: false, maxSize: 200 )
    	}
    }
  3. In de BootStrap.groovy laat ik vervolgens wat wat voorbeeldpopulatie opslaan waar we wat mee kunnen doen.
    class BootStrap {
    
         def init = { servletContext ->
    
            def person = new Person(name: "Ted Vinke").save()
            assert person
    
            person.addToCourses(new Course(name: "Projectmanagement voor project- en teamleiders").save())
            person.addToCourses(new Course(name: "Sun Certified Programmer 1.5").save())
            person.addToCourses(new Course(name: "Hogere Informatica").save())
    
            println "Persoon gemaakt: ${person.name}"
            println "Gerelateerde cursussen: "
            person.courses.each { course ->
                println " " + course.name
            }
         }
         def destroy = {
         }
    }

Nu hebben we succesvol onze applicatie en enkele gegevens succesvol aanwezig: een persoon met een 3-tal cursussen/trainingen. Nu kunnen we kijken hoe we deze data benaderbaar kunnen maken via een webservice.

Webservice via de XFire plugin

  1. Ik installeer versie 0.7.3 van de XFire plugin volgens het standaard Grails install-plugin mechanisme.
  2. Volgens het voorbeeld maken we een nieuwe TestService aan: grails create-service Test. Deze hoeven we niet eens te annoteren; de XFire plugin doet al het werk. Het enige wat we bovenin neer hoeven te zetten is static expose=[‘xfire’]
  3. We passen de TestService aan voor onze eigen doeleinden. We willen namelijk personen teruggeven, dus onze TestService laat ik er zo uit zien:
    class TestService {
    
        static expose=['xfire']
    
        boolean transactional = true
    
        def getPersons() {
            return Person.list()
        }
    
        def getPerson(int id) {
            return Person.get(id)
        }
    }
  4. We kunnen nu de applicatie opstarten via grails run-app. Als nu willen weten welke WSDL gegenereerd wordt, dan kunnen we dat nu zien op http://localhost:8080/Test/services/test?wsdl.
    grails-xfire-wsdl-11
    Ziet er op zich goed uit.

Webservice client

Nu wil ik graag met een ander Java programmaatje de webservice die we nu, binnen de Grails applicatie, up ‘n running hebben aanroepen. Hiervoor moet ik onder andere stubs genereren uit de WSDL die we kunnen aanroepen.

  1. Maak (bijv. in Eclipse) in een nieuw Java project, noem die bijv. “Client”.
  2. Ga in de root van dat project staan en roep de WSDL generator aan van het eerder gedownloade Axis framework. Onderstaande regel op de commandline geeft aan de gegenereerde XFire WSDL uitgelezen en stubs aangemaakt moeten worden. De package waarin e.e.a. terecht komt is stub. Als er niks speciaals wordt opgegeven dan worden deze volgens ADB (Axis Data Binding) gegenereerd.
    In C:\Documents and Settings\Ted Vinke\Workspace\Client doe

    wsdl2java -uri http://localhost:8080/Test/services/test?wsdl -p stub
  3. Er wordt nu in de /src directory een package stub aangemaakt met daarin de classes TestCallbackHandler en TestStub. Nu kunnen we een willekeuige class maken met een main method om iets van personen op te gaan vragen. Bijv. onderstaande code roept de getPersons() methode aan en print uit wat het resultaat is:
    public static void main(String[] args) throws Exception {
            TestStub client = new TestStub();
            GetPersonsResponse result = client.getPersons(new GetPersons());
            Object out = result.getOut();
            System.out.println("Resultaat is: " + out);
        }

Helaas krijgen we een foutmelding in de client:

Exception in thread “main” org.apache.axis2.AxisFault: Cannot create mapping for java.util.ArrayList, unspecified component type for class java.util.ArrayList
at org.apache.axis2.util.Utils.getInboundFaultFromMessageContext(Utils.java:512)
at org.apache.axis2.description.OutInAxisOperationClient.handleResponse(OutInAxisOperation.java:370)

en tevens in de Grails applicatie:

[4002875] binding.PostInvocationHandler org.codehaus.xfire.XFireRuntimeException: Cannot create mapping for java.util.ArrayList, unspecified component type for class java.util.ArrayList
[4002875] handler.DefaultFaultHandler Fault occurred!
org.codehaus.xfire.XFireRuntimeException: Cannot create mapping for java.util.ArrayList, unspecified component type for class java.util.ArrayList
at org.grails.xfire.aegis.type.DefaultTypeCreator.createCollectionType(DefaultTypeCreator.java:85)
at org.grails.xfire.aegis.type.XMLTypeCreator.createCollectionType(XMLTypeCreator.java:117)

We kunnen met deze melding alleen maar wat raden, maar schijnbaar kan een type object niet naar een ArrayList omgezet worden. Als we kijken naar de getPersons() methode (die we aanroepen) zou dit kunnen slaan op het return type van de Grails methode: def, ofwel “we zien run-time wel”. Alleen: de WSDL wordt wel al gegenereerd op basis van deze code en dus de def. Kan het wellicht zijn dat het niet duidelijk is hoe de lijst personen getypeerd worden?

Als we kijken naar de WSDL op http://localhost:8080/Test/services/test?wsdl en we zoeken het stukje op wat het resultaat beschrijft van de getPersons() aanroep, dan zien we het volgende:

grails-xfire-wsdl-snippet

Het resultaat is van het type xsd:anyType! Merk op dat xsd:anyType geen mapping specificeert in de specificatie, waardoor dit compleet door de onderliggende implementatie geregeld moet worden. Afgezien van dat het dus niet portable is, maar we in principe wel genoegen wilden nemen met een Object als returntype is er simpelweg dus geen mapping aanwezig.

De WSDL geeft niet expliciet aan wat verwacht wordt, omdat de Grails TestService webservice dat niet duidelijk aangeeft.

  1. We passen onze getPersons() methode (en dan ook maar meteen de getPerson()) aan zodat er expliciet een (array van) Person wordt teruggeven.
    class TestService {
    
        static expose=['xfire']
    
        boolean transactional = true
    
        Person[] getPersons() {
            return Person.list()
        }
    
        Person getPerson(int id) {
            return Person.get(id)
        }
    }
  2. We herstarten de Grails applicatie voor de zekerheid en bekijken de WSDL.
    grails-xfire-wsdl-snippet-2
    en we zien dat het resultaat van getPersons een een complex type ArrayOfPerson wordt welke een hoeveelheid types van Person kan bevatten. Dit ziet er al beter uit; nu eens kijken wat hieruit weer opnieuw gegenereerd wordt door de wsdl2java tool.
  3. Run de wsdl2java tool opnieuw met de -or parameter om de al eerder gegenereerde bestanden te overschrijven.
    wsdl2java -uri http://localhost:8080/Test/services/test?wsdl -p stub -or
  4. De clientcode passen we aan, want er wordt nu daadwerkelijk een ArrayOfPerson object teruggegeven die ons een array van Person geeft. De aanpassingen zijn de volgende:
    public static void main(String[] args) throws Exception {
            TestStub client = new TestStub();
            GetPersonsResponse result = client.getPersons(new GetPersons());
            ArrayOfPerson out = result.getOut();
            System.out.println("Resultaat is: ");
            for (Person p : out.getPerson()) {
                System.out.println("Persoon " + p.getName());
            }
        }

Als we dit runnen krijgen we het volgende in de Grails app:

Server running. Browse to http://localhost:8080/Test
2009-03-03 17:38:47.378::WARN:  Error for /Test/services/test
java.lang.StackOverflowError
at sun.reflect.GeneratedMethodAccessor110.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.grails.xfire.aegis.type.basic.BeanType.readProperty(BeanType.java:450)
at org.grails.xfire.aegis.type.basic.BeanType.writeObject(BeanType.java:385)
at org.grails.xfire.aegis.type.basic.ArrayType.writeValue(ArrayType.java:298)
at org.grails.xfire.aegis.type.collection.CollectionType.writeObject(CollectionType.java:103)
at org.grails.xfire.aegis.type.basic.BeanType.writeObject(BeanType.java:400)
at org.grails.xfire.aegis.type.basic.ArrayType.writeValue(ArrayType.java:298)
at org.grails.xfire.aegis.type.collection.CollectionType.writeObject(CollectionType.java:103)
at org.grails.xfire.aegis.type.basic.BeanType.writeObject(BeanType.java:400)
at org.grails.xfire.aegis.type.basic.ArrayType.writeValue(ArrayType.java:298)

Zucht, het lijkt erop dat GRAILSPLUGINS-233 en GRAILSPLUGINS-462 mij beletten gebruik te maken van een referentie vanuit Course wellicht terug naar Person. Hoe kunnen we dit testen?

Zet de referentie naar Person in commentaar:

class Course {

	String name

	//static def belongsTo = [Person]
	//static hasMany = [persons:Person]

	static constraints = {
		name ( blank: false, nullable: false, maxSize: 200 )
	}
}

Als we nu de applicatie herstarten, de wsdl tool draaien en onze client uitvoeren, dan zien we het volgende:

Resultaat is:
Persoon Ted Vinke

Dit is uiteraard niet wat we willen! We hebben nu ons model van de applicatie aangepast, puur om de output van een webservice te laten werken – een workaround voor XFire specifieke bug – terwijl we in de client alleen de naam van een persoon nodig hebben.  We zouden nu niet meer vanuit een Course bij zijn personen kunnen komen, dus we moeten nu iets anders verzinnen.

De meest logische oplossing die voor de hand ligt is het gebruiken van het data transfer object pattern (DTO) – we geven niet het daadwerkelijke domain object terug maar DTO object welke puur een container is voor de data met getters om deze data eruit te halen. Dit container object noemen we een DTO en een voor de hand liggende naam is PersonDTO, welke we aanmaken in de /src/groovy folder van ons project.

PersonDTO bevat alleen de velden waarin we geinteresseerd zijn. Tevens heb ik een convenience methode gemaakt die als parameter een Person meekrijgt en een PersonDTO retourneert. Merk op dat een van de technieken om een DTO te maken is, om het simpelweg af te leiden van het originele object – we hebben automatisch alle properties tot onze beschikking.

Zo ziet /src/groovy/PersonDTO eruit:

class PersonDTO extends Person {

	static PersonDTO load(Person p) {
		PersonDTO dto = new PersonDTO()
		dto.id = p.id
		dto.name = p.name
		// rest van evt. properties
		return dto
	}
}

We runnen de wsdl tool opnieuw en merken op dat de client code lichtelijk aangepast moet worden omdat er nu uiteraard een PersonDTO wordt teruggegeven. De nieuwe code:

	public static void main(String[] args) throws Exception {
		TestStub client = new TestStub();
		GetPersonsResponse result = client.getPersons(new GetPersons());
		ArrayOfPersonDTO out = result.getOut();
		System.out.println("Resultaat is: ");
		for (PersonDTO p : out.getPersonDTO()) {
			System.out.println("Persoon " + p.getName());
		}
	}

Uitvoeren en het werkt!

Resultaat is:
Persoon Ted Vinke

Als we nu nog een enkel persoon willen ophalen, dan passen we de client code zo aan:

		GetPerson gp = new GetPerson();
		gp.setIn0(1);
		GetPersonResponse result = client.getPerson(gp);
		PersonDTO person = result.getOut();
		System.out.println("Enkel persoon: " + person.getId() + " " + person.getName());

met als resultaat wat we verwachten:

Enkel persoon: 1 Ted Vinke

Conclusie

We hebben nu succesvol de XFire plugin binnen onze test Grails applicatie ingezet om een webservice te exposen en met een standaard Java client (via Axis2) deze service ‘geconsumeert’.

Voordelen van de XFire plugin die gebleken zijn:

  • Minimale code nodig om een service als een webservice te laten gedragen! Een static expose=[‘xfire’] is voldoende, waardoor automatisch alle methoden in deze service aan te roepen zijn. De WSDL defintitie wordt door de plugin automatisch gegenereerd op basis van deze methoden, input parameters en output typen – vanuit een client kun je in no-time stubs genereren.

Enkele zaken om rekening mee te houden als je de XFire plugin zelf gaat gebruiken zijn we tegengekomen:

  • def gebruiken om een return object of lijst mee te typeren werkt goed in Grails, maar de XFire plugin genereert er (terecht) xsd:anyType voor. Hier konden XFire run-time niks mee want we kregen een exceptie om onze oren! Niet doen dus. Gebruik als returntype GEEN def, maar specificeer waar je zeker weet wat het moet gaan worden alles met naam en toenaam.
  • Circulaire referenties zijn nog buggy in de XFire plugin op moment van schrijven met de gebruikte versie. Bij onverklaarbaar gedrag, check de openstaande issues in het Codehaus JIRA Grails Plugins project – hier kwamen we erachter dat de referentie vanuit Course naar Person terug roet in het eten gooide. XFire verslikte zich in deze bi-directionele relatie, liep in een rondje en eindigde met een java.lang.StackOverflowError. Uiteindelijk konden we een persoon succesvol als een PersonDTO teruggeven, maar is niet ideaal – hoewel deze oplossing werkt ben je dus wel genoodzaakt om in een wat grotere applicatie flink om deze bug(s) heen te moeten werken zolang XFire stuk gaat op circulaire referenties.

Het gaat me er niet om te demonstreren of het gebruik van def en het DTO pattern uberhaupt goed gebruik is, maar om het feit hoe goed een plugin als XFire daadwerkelijk out-of-the-box werkt in een Grails applicatie. We zagen dat als we verder gingen dan de standaard ‘retourneer-een String-of boolean’ methode doorwat standaard Grails webapplicatie gedrag te benaderen door enkele domain classes te introduceren met een relatie ertussen, we enkele kuilen in de weg moesten dichtgooien of er omheen moesten rijden. Wetende welke issues je kunt verwachten, zal XFire redelijk rap voor simpele webservices ingezet kunnen worden zonder zelf allerlei handwerk te hoeven verrichten en is een goede aanvulling in het Grails landschap.

Het ophalen van bijbehorende courses voor een persoon laat ik als oefening over voor de lezer 🙂