Jersey med JAXB

I denna artikel mixar vi Jersey med det XML-schema som Tommy Wassgren definierade i Serialisering med JAXB. Jersey är referensimplementationen för JAX-RS som är ett API för att bygga REST-tjänster.

I artikeln visar vi hur enkelt det är att dra nytta av Jerseys inbyggda stöd för att skicka bl.a. XML och JSON m.h.a. JAXB. Vi visar också hur du kan enhetstesta REST-applikationen. Koden hittar du på github: https://github.com/cygni-stacktrace/jaxbws.

Maven

Jag har utgått från pommen som Tommy Wassgren definierat. Här följer de viktigaste detaljerna för vad som behövs för att få allt att fungera. För att komma åt Jersey behöver vi lägga till Java.net:s Maven arkiv.

<repository>
    <id>maven2-repository.dev.java.net</id>
    <name>Java.net Repository for Maven</name>
    <url>http://download.java.net/maven/2/</url>
    <layout>default</layout>
</repository>

Ett gäng beroenden behövs också. Modulen jersey-servlet, grizzly-servlet-webserver och våra Jersey-annoterade klasser utgör grunden för vår webbtjänst. Utan några speciella handgrepp hanterar Jersey serialisering av XML via JAXB. För JSON via JAXB måste även modulen jersey-json finns med.

Jersey-client är en lättarbetad HTTP-klient. Även här stöds XML via JAXB direkt. Vill man ladda JSON behöver man förutom jersey-json även registrera den @Provider som sköter konverteringen. I denna artikel använder vi JacksonJaxbJsonProvider. Om du är intresserad av hur du kan lägga till egna format läs gärna Protobuf för serialisering del 2.

Vi presenterar också två typer av enhetstest (JUnit). Den första typen är baserad på en enkel Grizzly-server som kör i samma JVM. Den andra typen använder Jerseys testramverk som gör i princip samma sak fast under huven. Fördelen med den första är att den lämpar sig bättre ur en demo-synpunkt eftersom man får en bättre bild av hur Jersey används.

<dependency>
    <groupId>com.sun.grizzly</groupId>
    <artifactId>grizzly-servlet-webserver</artifactId>
    <version>1.9.18-i</version>
</dependency>
 
<dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-server</artifactId>
    <version>1.5</version>
</dependency>
 
<dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-json</artifactId>
    <version>1.5</version>
</dependency>
 
<dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-client</artifactId>
    <version>1.5</version>
</dependency>
 
<dependency>
    <groupId>com.sun.jersey.jersey-test-framework</groupId>
    <artifactId>jersey-test-framework-grizzly</artifactId>
    <version>1.5</version>
    <scope>test</scope>
</dependency>
 
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.8.2</version>
    <scope>test</scope>
</dependency>

Jersey-resurs

Vår webbservice returnerar en hårdkodad lista av användare i XML enligt det schema som Tommy Wassgren satte upp i sin artikel. Jersey och JAXB sköter omvandligen från Java-model till XML eller JSON beroende på accept-headern. Jersey skapar automatiskt ett JAXB-kontext för det returnerade objektets typ om det inte finns något redan.

@Path("/accounts")
public class UserAccountResource {
    @GET
    @Produces({ "application/xml", "application/json" })
    public UserAccounts getUserAccounts() {
        UserAccount account1 = createUserAccount(1, "account1",
                "account1@localhost", "news", "music");
 
        UserAccount account2 = createUserAccount(2, "account2",
                "account2@localhost", "news", "sports");

        UserAccounts accounts = new UserAccounts();
        accounts.getUserAccount().add(account1);
        accounts.getUserAccount().add(account2);
        return accounts;
    }

    private UserAccount createUserAccount(int id, String username,
            String email, String... serviceStrings) {
        UserAccount account = new UserAccount();
        account.setId(id);
        account.setUsername(username);
        account.setEmail(email);

        Services services = new Services();
        services.service = Arrays.asList(serviceStrings);
        account.setServices(services);
        return account;
    }
}

Grizzly webbserver

Grizzly är ett API för att skriva skalbara serverlösningar baserat på Java NIO. Från Jersey kan man enkelt skapa en Grizzly Servlet-container. Egenskapen com.sun.jersey.config.property.packages anger de paket som innehåller de Jersey-komponenter som ska driftsättas. Nedan har vi en Grizzly webbserver på port 8080. Vi testar den först med curl.

Map<String, String> params = new HashMap<String, String>();
params.put("com.sun.jersey.config.property.packages",
    "se.cygni.stacktrace.jaxbws");
SelectorThread ws = GrizzlyWebContainerFactory.create(
    new URI("http://localhost:8080/"), params);
System.out.println("Tryck return för att avsluta Jersey ...");
System.in.read();
ws.stopEndpoint();

Jersey lägger som standard ut en WADL-fil som beskriver den driftsatta tjänsten. WADL kan ses som RESTs motsvarighet till WSDL.

$ curl http://localhost:8080/application.wadl

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<application xmlns="http://research.sun.com/wadl/2006/10">
    <doc xmlns:jersey="http://jersey.dev.java.net/" jersey:generatedBy="Jersey: 1.5 01/14/2011 12:36 PM"/>
    <resources base="http://localhost:8080/">
        <resource path="/accounts">
            <method id="getUserAccounts" name="GET">
                <response>
                    <representation mediaType="application/xml"/>
                    <representation mediaType="application/json"/>
                </response>
            </method>
        </resource>
    </resources>
</application>

Responsformatet styrs genom att sätta t.ex. Accept: application/json. XML är default eftersom det är listat först. Flaggorna till curl, -sS, är tillför att förhindra att curl skriver ut statistik på stderr. tidy och json_xs är för att formatera XML- resp. JSON-responsen.

$ curl -Ss http://localhost:8080/accounts|tidy -xml -iq
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<UserAccounts xmlns="http://cygni.se/stacktrace/UserAccount">
  <UserAccount>
    <id>1</id>
    <username>account1</username>
    <email>account1@localhost</email>
    <services>
      <service>news</service>
      <service>music</service>
    </services>
  </UserAccount>
  <UserAccount>
    <id>2</id>
    <username>account2</username>
    <email>account2@localhost</email>
    <services>
      <service>news</service>
      <service>sports</service>
    </services>
  </UserAccount>
</UserAccounts>
$ curl -Ss http://localhost:8080/accounts \
-H 'Accept: application/json' |json_xs
{
   "UserAccount" : [
      {
         "email" : "account1@localhost",
         "services" : {
            "service" : [
               "news",
               "music"
            ]
         },
         "id" : "1",
         "username" : "account1"
      },
      {
         "email" : "account2@localhost",
         "services" : {
            "service" : [
               "news",
               "sports"
            ]
         },
         "id" : "2",
         "username" : "account2"
      }
   ]
}

NIO och IPv6 på Windows

En liten parentes angående Windows och IPv6 kan vara på sin plats här. Windows 7-maskinen som jag körde på var konfigurerad för IPv6, men trots det verkar Java binda till IPv4 som standard i Windows-versionen av Java NIO. Det går heller inte slå över till IPv6 eftersom det tydligen inte är korrekt implementerat för NIO förrens i JDK 7. Detta gör att om du kör en curl var som har stöd för IPv6 så kommer anropen att först gå mot IPv6-loopback (::1) för att där efter falla tillbaka till 127.0.0.1. Detta tillbakafall tar säkert en hel sekund(stön). Råkar du sitta i en liknande miljö kan du tvinga curl till IPv4 direkt med följande flagga -4.

Klienten

Jersey-klienten är ett lättarbetat högnivå-API ovanpå JDK:ns Http(s)URLConnection eller Apache HttpClient. Klienten gör det enkelt att jobba mot JAX-RS utan att det är ett krav. Jersey omvandlar automatiskt responsen till önskad klass, t.ex. resource.get(UserAccounts.class) för att låta Jersey sköta omvandlingen av HTTP-responskroppen till ett UserAccounts-objekt via JAXB. Behövs mer kontroll kan man få komma åt responskroppen som en sträng genom resource.get(String.class) eller hela responsen genom get(ClientResponse.class).

public class UserAccountResourceTest {
    private static SelectorThread server;
 
    @BeforeClass
    public static void beforeClass() throws Exception {
        Map<String, String> params = new HashMap<String, String>();
       params.put("com.sun.jersey.config.property.packages",
            "se.cygni.stacktrace.jaxbws");
        server = GrizzlyWebContainerFactory.create(
            new URI("http://localhost:8080/"), params);
    }

    @AfterClass
    public static void afterClass() {
        server.stopEndpoint();
    }

    @Test
    public void testXML() throws Exception {
        Client client = Client.create();
        WebResource resource =           client.resource("http://localhost:8080/accounts");
        UserAccounts accounts =           resource.accept("application/xml").get(UserAccounts.class);
        assertAccounts(accounts);
    }
 
    @Test
    public void testJSON() throws Exception {
        ClientConfig config = new DefaultClientConfig();
        config.getClasses().add(JacksonJaxbJsonProvider.class);
        Client client = Client.create(config);
        WebResource resource =            client.resource("http://localhost:8080/accounts");
        UserAccounts accounts =            resource.accept("application/json").get(UserAccounts.class);
        assertAccounts(accounts);
    }

    public static void assertAccounts(UserAccounts accounts) {
        Assert.assertNotNull(accounts);       Assert.assertFalse(accounts.getUserAccount().isEmpty());
        Assert.assertEquals(2, accounts.getUserAccount().size());
        UserAccount account1 = accounts.getUserAccount().get(0);
        Assert.assertEquals(1, account1.getId());
        Assert.assertEquals("account1@localhost", account1.getEmail());
        Assert.assertEquals(2, account1.getServices().getService().size());
        Assert.assertTrue(           account1.getServices().getService().contains("music"));
    }
}

JerseyTest

Jersey har ett testramverk som i princip gör vad vi visat ovan (jersey-test-framework-grizzly). Den kommer med en inbyggd Grizzly-support. Konstruktorn tar en varargs med namn på paket som ska skannas efter komponenter.

public class UserAccountResourceJerseyTestXML extends JerseyTest {
    public UserAccountResourceJerseyTestXML() {
        super("se.cygni.stacktrace.jaxbws");
    }

    @Test
    public void testXML() throws Exception {
        UserAccounts accounts = resource()
            .path("/accounts")
            .accept("application/xml")
            .get(UserAccounts.class);     
       
 UserAccountResourceTest.assertAccounts(accounts);
    } 
}

I konstruktorn kan man även skicka in en mer komplicerad konfiguration, men det blir lätt grötigt. Så det sista exemplet visar jag hur man kan åstadkomma detta genom att åsidosätta configure() i stället.

public class UserAccountResourceJerseyTestJSON extends JerseyTest {
    @Test
    public void testJSON() throws Exception {
        UserAccounts accounts = resource().path("/accounts")
                .accept("application/json").get(UserAccounts.class);
        UserAccountResourceTest.assertAccounts(accounts);
    }
 
    @Override
    protected AppDescriptor configure() {
        ClientConfig config = new DefaultClientConfig();
 
 config.getClasses().add(JacksonJaxbJsonProvider.class);
 
        return new WebAppDescriptor.Builder("se.cygni.stacktrace.jaxbws")
 
                .clientConfig(config).build();
 
    }
}