JSON serialisointi: Pois Circular Reference – manalasta

En tiedä onko tuttu tilanne, mutta itselleni harmillisen usein tavattu. Otetaan oliorakenne, esim. Order -> OrderItem, eli tilaus ja tilausrivejä. Rakenne menisi näin:

class Order {

  List<OrderItem> orderItems;

}

class OrderItem {}

Tähän asti kaikki loistavasti. Nyt kuitenkin on useita syitä miksi haluaisimme myös linkin OrderItemistä Order-luokkaan, esim. jos item-riveissä on assosiaatioita muuallekin ja niistä pitäisi näppärästi päästä header-tietoihin kiinni. Tai jos käyttää serialisointiin JPA-tekniikkaa eikä halua tehdä turhia välitauluja (One-to-Many assosiaatiossa tieto assosiaatiosta on many-päässä eli OrderItem luokassa)

Pysyitkö mukana? Hyvä, muutamme siis rakenteen tällaiseksi:

class Order {

  long id;
  List<OrderItem> orderItems;
}

class OrderItem {
  long id;
  Order order;
}

Ja tästä päästääkin syklisten referenssien helvettiin. Tämä on oliorakenteena ihan kelvollinen ja mahdollistaa juuri edellämainitun navigoinnin molempiin suuntiin (bidirectional one-to-many association). Tähän voisi iloisesti läpsäyttää JPA annotaatiot ja antaa sen valua kantaan ja kannasta triviaalilla koodilla.

Ongelmia tulee siinä vaiheessa kun haluttaisiin serialisoida tätä rakennetta johonkin hierarkiseen puurakenteeseen, esim. XML tai JSON. Ongelma johtuu siitä että dynaaminen sarjallistaja, esim. JAXB tai Jackson, käy läpi olion ominaisuudet yksi kerrallaan, ja kutsuu gettereitä, kerää tiedot, ja muuttaa ne tekstimuotoiseksi siirtokelpoiseksi dataksi. Siinä käy siis näin:

  1. Tallennetaan order, hienoa. Order on oliorakenne, joka sisältää orderItems listan, käydään se läpi
  2. Käsitellään jokainen orderItem vuorollaan. OrderItem on oliorakenne, joka sisältää viittauksen Order olioon
  3. Käsitellään jokainen viitattu Order vuorollaan. Order on oliorakenne joka sisältää OrderItems listan

Ja niin edelleen. Ikiliikkuja on keksitty. Tästähän saa palkakseen yleensä jonkun hienon kaatumisen ja cyclic/circlar reference errorin. Tai jos hauskasti käy, kone puuskuttaa hetken ja antaa stack overflow errorin tai out of memory errorin.

Mitä sitten on tehtävissä? Tämä artikkeli koskee JSON vaihtoehtoa, jos olet vielä XML parissa, olet pysyvästi helvetissä vailla poispääsyä, pahoittelen.

Jos käytät tätä esim. Jacksonin puitteissa, vaikkapa REST-rajapinnassa, ratkaisutapoja on muutama (tosiasiassa osa näistä sopii XML hommiinkin, jos edellisestä kohdasta tuli paha mieli):

  1. Katkaise syklinen referenssiketju merkkaamalla jommassakummassa päässä referenssi ei-serialisoitavaksi. Tapoja tähän on monia, Jackson taitaa tukea esim. Javan transient avainsanaa, @JsonIgnore annotaatiota, ja luokkatasolla voi myös listata ohitettavat kentät @JsonIgnoreProperties-annotaatiolla
  2. On myös mahdollista merkitä master-dependant suhde Jackson annotaatioilla @JsonManagedReference ja @JsonBackReference
  3. Tehdään aina value/transfer object johon normalisoidaan kulloinkin tarvittavat tiedot
  4. Myös voi merkitä identity-kentät Jackson annotaatioilla, @JsonIdentityInfo kertoo mikä kenttä on uniikki avain, jonka jälkeen serialisoinnissa voidaan viitata vain id arvoon, ei käydä läpi koko sisältöä.
  5. @JsonView annotaation käyttö näkymien muodostamiseen

Kahdessa ensimmäisessä kohdassa on yksi ongelma: Ne eivät salli talsimista edestakaisin, vaan vain yhteen suuntaan. Mutta ne ratkaisevat syklisen referenssipulman katkaisemalla rekursioketjun, eli sopivat moneen tilanteeseen. Kolmas kohta sisältää potentiaalisesti hurjan paljon virhealtista käsityötä ja myöhemmin ylläptoa ja en ole ollut koskaan kummankaan suuri fani. Neljäs kohta generoi kauheaa huttua serialisoinnista, ja en ole vielä löytänyt sille hyötykäyttöä. Neljäs kohta on näistä oma suosikkini. Se voisi olla vielä parempikin mutta sillä ainakin pääsee alkuun. Ja uusin Spring, Spring Boot, ja JAX-RS yhdistelmä tukee näitä ihanasti.

Homma toimi näin: Merkataan @JsonView annotaatiolla ne kentät, joita halutaan ehdollisesti serialisoida tai olla serialisoitamatta. Parametrina tulee tyypin nimi, joka on yleensä Java rajapinta. Esim. näin:

class View {

interface GimmeOrderRows {}

interface GimmeOrderHeader {}

}

Nyt voidaan muokata aiempia koodeja näin:

class Order {
  
  long id;
  
  @JsonView(View.GimmeOrderRows.class)
  List<OrderItem> orderItems;

}

class OrderItem {
  long id;

  @JsonView(View.GimmeOrderHeader.class)
  Order order;

}

Nyt pystyt hakemaan assosiaatiot on-demand periaatteisesti, eli voit navigoida kummasta päästä vaan. Jos et anna JsonView-annotaatiota, oletuksena saat kaiken. Heti jos annat yhdenkin @JsonView annotaation, saat kaikki kentät joihin se täsmää tai joita ei ole millään JsonView annotaatiolla varustettu. Eli jos meillä olisi tämän näköinen jax-rs palvelu…

@GET
@Path("order")
@JsonView(View.GimmeOrderRows.class)
public Order fetchOrderWithItems(long id) {
  return orderRepository.getOne(id);
}

… niin syklisen referenssin peikko pysyisi piilossa. Koska Jackson serialisoisi Orderin, ja sen sisältämät OrderItemit id-arvoineen, mutta ei seuraisi enää polkua niiden sisältämiin Order-instansseihin.

Vastaavasti nyt voisi huoletta hakea vaikkapa yhden OrderItem instanssin OrderHeadereineen ilman syklisiä referenssejä:

@GET
@Path("orderitem")
@JsonView(View.GimmeOrderHeader.class)
public OrderItem fetchOrderItemWithOrder(long id) {
  return orderItemRepository.getOne(id);
}

Samalla tekniikalla voi noutaa ehdollisesti esim. salasanatietoja, tai binäärisisältöä, tai muuten vain pitkiä kenttiä. Yhdessä kohtaa voi olla vain yksi JsonView-parametri, mutta koska niillä voi olla perintähierarkioita jotka tunnistetaan, rajoitus ei ole paha. On myös mahdollista säätää sellainen oletus, että mitään kenttää ei palauteta elleivät jsonviewt täsmää – ei myöskään niitä joista annotaatio puuttuu kokonaan.

Nyt kun vielä saisi Javaan luontevan suorastaan sisäänrakennetun JSON rajapinnan….

Glassfish 4.1, JAX-RS, Jersey, ja Jackson JSON-serializer

Jep, päivitin vähän servereitä Glassfish 4.1 versioon, ja vastaan tuli mielenkiintoinen bugi: JAX-RS palvelut lakkasivat toimimasta ja antoivat sensijaan kaikenlaisia herjoja, niistä ehkä mielenkiintoisin:

Severe: Error occurred when processing a response created from an already mapped exception.
Warning: StandardWrapperValve[com.qpr.entice.common.ApplicationConfig]: Servlet.service() for servlet com.mycompany.jaadajaada.ApplicationConfig threw exception
java.lang.ClassNotFoundException: com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector not found by com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider [129]

Glassfish käyttää oletuksena Moxya JSON serialisointiin, mutta itse käytän Jacksonia suorituskyvyn ja ominaisuuksien johdosta. Mielenkiintoista kyllä Glassfish toimitetaan osittaisin Jackson kirjastoin joten sen käyttöönotto on niinkin helppoa kuin aktivoida se config tiedostossa, tähän tapaan:

import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("rest")
public class ApplicationConfig extends ResourceConfig {

  public ApplicationConfig() {
    packages(true,
       "com.mycompany.jaadajaada",
       "com.wordnik.swagger.jersey.listing");
    register(JacksonFeature.class);
  }
}

Eli rekisteröidään JacksonFeature – ja samantien käytössä on Jackson kirjastot. Aiemmassa Glassfish 4.0:ssa tämä pelasi hienosti – mutta 4.1 antaa ylläolevaa virheilmoitusta. Kokeilin myös jättää JacksonFeaturen pois ja testata pelkkää Moxya mutta sieltä tuli ihan omat herjansa. (Tuossa muuten näkyy myös Swagger joka automaattisesti dokumentoi REST APIt – ja käyttää Jackson kirjastoja)

Mielenkiintoista virheilmoituksessa on, että se viittaa puuttuvaan tiedostoon joka itselläkin oli kirjastopoluissa – mutta käy ilmi kaksi asiaa: se pitää olla Glassfishin alla, ja sen pitää olla oikea versio. Tiedosto joka uupuu on jackson-module-jaxb-annotations-2.3.2.jar, sen pitäisi olla glassfishin alla kansiossa modules, ja tosiaan versionumero 2.3.2, ei uusin. Tämän päälle kun putsaa osgi-cachen ja buuttaa serverin, saa taas REST JSON palvelut takaisin. 😉

Eli aika spesifi pulma. Aika noloa että Glassfishistä on hiljalleen turvonnut kohtuullisen monoliittinen mötikkä jossa on paljon kirjastoriippuvuuksia joita oma sovellus ei pysty helposti ylikirjoittamaan. JBOSS muistuu mieleen äärimmäisen modularisuutensa ansiosta, mutta muistelen kyllä sielläkin olleen aina uuden julkaisun kohdalla kirjastohaasteita. Mutta siellä on esim. web services implementaatio mukavan helposti vaihdettavissa.

Mutta, minä taidan digressoida 😉 Tässä joka tapauksessa virheilmoitus, ja korjaus, ja tuossa linkkiä keskusteluun samasta pulmasta:

https://java.net/jira/browse/GLASSFISH-21141

Teoriassa on mahdollista tehdä korjaus siistimminkin, autodeploy/bundles kansioon asennettavana paikkana joka ei muuta serverin toimintaa, mutta itse en saanut sitä vaihtoehtoa toimimaan.

JAX-RS ja Swagger: Helposti dokumentoity REST API

Hetken aikaa jo on tykyttänyt mietintämyssyn alla miten REST rajapintoja voisi helposti dokumentoida – WSDL kun sieltä uupuu (Ja WADL:ia ei kukaan halua). Kuitenkin pitäisi pystyä kertomaan ja rekisteröimään missä osoitteissa on palvelua, mitä ne tekevät, miten niitä kutsutaan, jne, muutenkin kuin testeillä ja esimerkeillä.

Törmäsin mukavaan jersey-yhteensopivaan laajennukseen nimeltä jersey-swagger. Sovitin tätä Glassfishin versioon, jossa pyörii Jersey 2, ja sainkin jotain aikaan. Tässä muistiinpanoja:

Käyttöönotto vaatii muutaman askelen: Ensiksi täytyy Mavenissä tai Gradlessa ottaa käyttöön swagger jersey versiolle kaksi. Samalla on tuikitärkeää määrittää pari exclusionia, tai serverille päätyy duplikaatteja hieman eri versioista ja tulee mukavia poikkeuksia logista. Tässä toimivaksi testattu dependency Glassfish neloselle:

 <dependency>
   <groupId>com.wordnik</groupId>
   <artifactId>swagger-jersey2-jaxrs_2.10</artifactId>
   <version>1.3.7</version>
   <exclusions>
     <exclusion>
       <groupId>org.glassfish.jersey.media</groupId>
       <artifactId>jersey-media-multipart</artifactId>
     </exclusion>
     <exclusion>
       <groupId>org.glassfish.jersey.containers</groupId>
       <artifactId>jersey-container-servlet-core</artifactId> 
     </exclusion>
   </exclusions>
 </dependency>

Hyvä tarkistaa tässä välissä että vanhat palvelut edelleen toimivat, jos niitä on. Seuraavaksi rekisteröidään Swagger servicet. Tämän voi tehdä xml:ssä – mutta itse käytän ResourceConfig-luokkaa ja annotaatioita:

@ApplicationPath("rest")
public class ApplicationConfig extends ResourceConfig {
public ApplicationConfig() {
packages("my.own.resources","com.wordnik.swagger.jersey.listing");
 }
}

Noin, sieltä aktivoituu tosiaan pari resurssiluokkaa, automaattisesti /rest/api-docs osoitteella. Seuraavaksi rekisteröidään Swagger servlet. Tämänkin voi tehdä annotaatioilla mutta kun sattui olemaan vielä web.xml (se on ainoa paikka jossa voi tehdä distributable-setin ha klusteriin) – tein sen siellä näin:

 <servlet>
 <servlet-name>JerseyJaxrsConfig</servlet-name>
 <servlet-class>com.wordnik.swagger.jersey.config.JerseyJaxrsConfig</servlet-class>
 <init-param>
 <param-name>api.version</param-name>
 <param-value>1.0.0</param-value>
 </init-param>
 <init-param>
 <param-name>swagger.api.basepath</param-name>
 <param-value>http://localhost:8080/rest/api-docs</param-value>
 </init-param>
 <load-on-startup>2</load-on-startup>
 </servlet>

Nyt alkaa jo melkein tapahtumaan. Seuraavaksi pitää varustaa ainakin yksi palvelu @Api-annotaatiolla, metodeille voi pistää @ApiOperation, ja parametreille ja paluuarvoille @ApiModel annotaatioita. Jotta saat jotain näkyviin tarvitset ainakin yhden @Api-annotaatiolla varustetun palvelun, jolla on ainakin yksi @ApiOperation annotaatiolla varustettu operaatio.

Tähän tapaan:

@Path("/pet")
@Api(value = "/pet", description = "Operations about pets")
@Produces({"application/json", "application/xml"})
public class PetResource {
  ...
@GET
@Path("/{petId}")
@ApiOperation(value = "Find pet by ID", notes = "More notes about this method", response = Pet.class)
@ApiResponses(value = {
  @ApiResponse(code = 400, message = "Invalid ID supplied"),
  @ApiResponse(code = 404, message = "Pet not found") 
})
public Response getPetById(
    @ApiParam(value = "ID of pet to fetch", required = true) @PathParam("petId") String petId)
    throws WebApplicationException {
@ApiModel(value = "A pet is a person's best friend")
@XmlRootElement(name = "Pet")
public class Pet {
  @XmlElement(name = "status")
  @ApiModelProperty(value = "Order Status", required=true, allowableValues = "placed,approved,delivered")
  public void setStatus(String status) {
    this.status = status;
  }
  public String getStatus() {
    return status;
  }

Sitten voitkin käynnistellä serverin, ja kokeilla mitä löytyy kontekstin alta osoitteella /api-docs, /api-docs/myservicename, jne – tässä tietysti myservicename on joku palvelu johon olet noita Swagger-annotaatioita ripotellut.

Tuolta löytyy myös esimerkkiä live-sisällöstä, niin kauan kuin se tietysti on pystyssä:

http://petstore.swagger.wordnik.com/api/api-docs

Näyttäisi tämmöiseltä:

{"apiVersion":"1.0.0","swaggerVersion":"1.2","apis":[{"path":"/pet","description":"Operations about pets"},{"path":"/user","description":"Operations about user"},{"path":"/store","description":"Operations about store"}],"authorizations":{"oauth2":{"type":"oauth2","scopes":[{"scope":"write:pets","description":"Modify pets in your account"},{"scope":"read:pets","description":"Read your pets"}],"grantTypes":{"implicit":{"loginEndpoint":{"url":"http://petstore.swagger.wordnik.com/oauth/dialog"},"tokenName":"access_token"},"authorization_code":{"tokenRequestEndpoint":{"url":"http://petstore.swagger.wordnik.com/oauth/requestToken","clientIdName":"client_id","clientSecretName":"client_secret"},"tokenEndpoint":{"url":"http://petstore.swagger.wordnik.com/oauth/token","tokenName":"auth_code"}}}}},"info":{"title":"Swagger Sample App","description":"This is a sample server Petstore server.  You can find out more about Swagger \n    at <a href=\"http://swagger.wordnik.com\">http://swagger.wordnik.com</a> or on irc.freenode.net, #swagger.  For this sample,\n    you can use the api key \"special-key\" to test the authorization filters","termsOfServiceUrl":"http://helloreverb.com/terms/","contact":"apiteam@wordnik.com","license":"Apache 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.html"}}

Törmäsin myös toiseen aika metkaan standardiin – ALPS. Sitä näyttäisi osaavan Spring Framework hyvin, mutta ainakan nyky-jerseystä ei löytynyt helppoa ratkaisua siihen. Pidän tätäkin kuitenkin silmällä. Looking good!

http://www.dzone.com/links/r/spring_data_rest_now_comes_with_alps_metadata.html

http://alps.io/

Näistä linkeistä löytyy vähän lisää ideoita. Varovaisuutta vanhemman Swagger-version kanssa!

https://github.com/wordnik/swagger-core/wiki/Java-JAXRS-Quickstart

 

 

Google App Engine pilvipalvelu ja Java EE 6, osa 4

Silmäilin mielenkiinnolla miten Java EE 7 tulee koskemaan pääteemana pilvipalveluita; Tekisi hyvää saada yhteistä rajapintaa myös tämän osalta, jolloin ainakin teoriassa pilvipalveluista tulisi vapaammin siirettäviä ja kilpailutettavia, ja vältettäisiin nykyinen vahva vendor lock-in.. Aika näyttää miten tässä käy.

Itse päädyin tekemään muutoksen oman pilvipalveluni arkkitehtuuriin. Siinä missä homma alkoi Solakka Java-periaatteella, totesin että vaikka JSF 2 on paljon parannettu versio aiemmasta, tekniikkademosta saa paljon mielenkiintoisemman jos korvaan sen RESTful web service tekniikalla ja suomalaisella Vaadin AJAX frameworkillä.

Valitsin toteutusalustaksi referenssitoteutuksen eli Jersey JAX-RS alustan. Sain sen käyttöön Mavenin  pom.xml:ssä näin:

<!-- Turn on Jersey JAX-RS -->
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-server</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-client</artifactId>
<version>1.4</version>
</dependency>
 
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-json</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>com.sun.jersey.contribs</groupId>
<artifactId>jersey-multipart</artifactId>
<version>1.4</version>
</dependency>

Eli muutama dependency lisää. Lisäksi web.xml:ssä piti ottaa Jersey servlet käyttöön ja kertoa mitä pakettia skannata resurssien osalta, tähän tapaan:

<!-- JERSEY support -->
<servlet>
<servlet-name>Jersey Web Application</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>com.sun.jersey.config.property.packages</param-name>
<param-value>fi.tieturi.pilvenveikko.services</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Jersey Web Application</servlet-name>
<url-pattern>/resources/*</url-pattern>
</servlet-mapping>

Ja siitä se sitten lähti. REST filosofian mukaan muokkaan palveluita CRUD tyyliin joten jatkossa voi luoda tätä kautta WorkUnit, Person, ja Goal yksiköitä, hakea ja päivittää ja poistaakin niitä, http url osoitteilla.

Pari pikku knoppiakin tuli vastaan. Google App Enginen JPA toteutus ei haekaan tietoja aina heti vaan käyttää lazy load periaatetta. Näin on tarpeen varmistaa että kaikki tiedot on kannasta saatu ennen kuin niitä xml:ksi marshalloidaan . Tämän voi hoitaa vaikkapa size-metodia kutsumalla. Tässä maistiainen Resource luokastani:

package fi.tieturi.pilvenveikko.services;
import fi.tieturi.pilvenveikko.domain.WorkUnit;
import fi.tieturi.pilvenveikko.util.EMF;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
@Path("/workUnit")
public class WorkUnitResources {
  @GET
@Path("/findAll")
@Produces("text/xml")
public List<WorkUnit> getWorkUnits() {
EntityManager em = null;
try {
em = EMF.get().createEntityManager();
Query q = em.createQuery("SELECT wu FROM WorkUnit wu ORDER BY wu.workDate");
q.setMaxResults(200); // limit to max 200 results
List<WorkUnit> results = q.getResultList();
results.size(); // this is a hack to wait until results are all there
return results;
} finally {
if (em != null) {
em.close();
}
}
 }

@GET
@Path("/read/{workUnitId}")
@Produces("text/xml")
public WorkUnit getWorkUnitById(@javax.ws.rs.PathParam(value="workUnitId") long workUnitId) {
EntityManager em = null;
try {
em = EMF.get().createEntityManager();
WorkUnit result = em.find(WorkUnit.class, workUnitId);
return result;
} finally {
if (em != null) {
em.close();
}
}
}
}

Ja siinä se. Ensi kerralla voisin jutella vaikkapa Vaadin frameworkin käyttöönotosta ja piirteistä ja pilvivirityksistä.