Vous êtes sur la page 1sur 31

Best REST

Lessons learned from KNI Rest

Overview
Why REST? KNI Design Jersey/JAXB Patterns for creating your own REST projects TODO Q&A

Why REST?
Lightweight and simple -- just uses HTTP and URIs, and sends across the minimal amount of data. Stateless/Scalable

KNI Design

HTTP

KNI Web
(JSP, HTML, JS)

KNI Rest Commons

Example KNI Rest URIs


http://.../internal/customers/{id}? username=kelleybrown@officemax.com ../internal/customers/{id}/billtolocations?username=... ../internal/customers/{id}/billtolocations/{id}... ../internal/supplier/invoices/{id}... Can perform HTTP GET, POST, PUT, and DELETE requests. In CRUD terms, GET=Retrieve, POST=Create, PUT=Update, and DELETE=Delete.

Jersey
The Reference Implementation for JAX-RS, the JSR for building RESTful services with Java. Intended to be production quality. Key Features: Lets you easily surface resources. Easily extract path elements and query parameters from URIs. Works with JAXB to easily read/write both XML and JSON.

JAXB
Binds XML Schema to Java objects With the Jersey integration, it is easy to spit out XML and JSON from Java objects, and read XML and JSON into Java objects. We tried a few other things before settling on JAXB; much happier with JAXB.

Hello World with Jersey


Add java.net Maven repository to pom.xml
<repositories> <repository> <id>maven2-repository.java.net</id> <name>java.net</name> <url>http://download.java.net.maven/2/</url> <repository> </repositories>

Add Jersey dependencies to your pom.xml:


<dependencies> <dependency> <groupId>com.sun.jersey</groupId> <artifactId>jersey-server</artifactId> <version>1.0.3</version> <dependency> <dependency> <groupId>com.sun.jersey</groupId> <artifactId>jersey-json</groupId> <version>1.0.3</version> <dependency> <dependencies>

Hello World with Jersey


Update web.xml
<filter> <filter-name>Jersey Web Filter</filter-name> <filter-class>com.sun.jersey.spi.container.servlet.ServletContainer</filter-class> <init-param> <param-name>com.sun.jersey.config.property.resourceConfigClass</param-name> <param-value>com.sun.jersey.api.core.PackagesResourceConfig</param-value> </init-param> <init-param> <param-name>com.sun.jersey.config.property.packages</param-name> <param-value>com.ketera.rest.demo</param-value> </init-param> </filter> <filter-mapping> <filter-name>Jersey Web Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

Hello World with Jersey


Create a Resource
package com.ketera.rest.demo; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("me") public class MyResource { @GET @Path("{id: [0-9]+}") @Produces(MediaType.TEXT_PLAIN) public String sayHello(@PathParam("id") Long id) { return "Hello id " + id; } }

Deploy and Test with curl


>curl coverbeck-d820.ketera.com:8080/kdemo/me/1234 -->Hello id 1234 >curl coverbeck-d820.ketera.com:8080/kdemo/me/5678 -->Hello id 5678 >curl coverbeck-d820.ketera.com:8080/kdemo/me/a1234 -->HTTP 404

Adding JAXB
Add JAXB to pom.xml
<dependencies> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.1.10</version> <dependency> <dependencies>

Above is only necessary for JDK 5; JDK 6 bundles JAXB.

Adding JAXB
Add JAXB plugin to pom.xml
<plugin> <groupId>org.jvnet.jaxb2.maven2</groupId> <artifactId>maven-jaxb2-plugin</artifactId> <executions> <execution> <goals> <goal>generate</goal> </goals> </execution> </executions> <configuration> <generatePackage>com.ketera.rest.demo</generatePackage> </configuration> </plugin>

Create XSD
Create ../main/resources/demo.xsd
<?xml version="1.0" encoding="UTF-8"?> <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:jxb="http://java.sun.com/xml/ns/jaxb" jxb:version="2.0"> <xsd:complexType name="ContactType"> <xsd:all> <xsd:element name="name" type="xsd:string" maxOccurs="1" minOccurs="0" /> <xsd:element name="age" type="xsd:integer" maxOccurs="1" minOccurs="0" /> </xsd:all> </xsd:complexType> <xsd:element name="contact" type="ContactType" /> </xsd:schema>xsd:element name="contact" type="ContactType"/> </xsd:schema>

When you build, this generates a ContactType Java class.

New Jersey Resource


Add a Resource and method
@GET @Path("contact/{id : [0-9]+}") @Produces({MediaType.TEXT_XML, MediaType.APPLICATION_JSON}) public JAXBElement<ContactType> retrieveContact(@PathParam("id") Long id) { final ObjectFactory objectFactory = new ObjectFactory(); ContactType contactType = objectFactory.createContactType(); contactType.setName("Charles"); contactType.setAge(new BigInteger("21")); return objectFactory.createContact(contactType); }

Now we can get either XML or JSON

Outputting XML and JSON


>curl coverbeck-d820.ketera.com:8080/kdemo/me/contact/1234 --><?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <contact> <name>Charles</name> <age>21</age> </contact> >curl -H "Accept: application/json" coverbeck-d820.ketera.com:8080/kdemo/contact/1234 -->{"name":"Charles","age":"21"}

Inputting XML and JSON


@PUT @Path("{id: [0-9]+}") @Consumes({MediaType.TEXT_XML, MediaType.APPLICATION_JSON}) @Produces({MediaType.TEXT_XML, MediaType.APPLICATION_JSON}) public JAXBElement<ContactType> updateContact(ontactType contactType, C @PathParam("id") Long id) { final ObjectFactory objectFactory = new ObjectFactory(); return objectFactory.createContact(contactType); }

>curl -d "<contact><name>Joe</name><age>15</age></contact>" -X PUT -H "Accept: application/json" -H "Content-Type : text/xml" coverbeck-d820.ketera.com:8080/kdemo/contact/1234 {"name":"Joe","age":"15"} >curl -d @foo.json -X PUT -H "Accept: application/json" -H "Content-Type: application/json" coverbeck-d820.ketera.com:8080/kdemo/contact/1234 {"name":"John","age":"15"} foo.json: {"name":"John","age":"15"}

Validating Input
If we want to input validated against the schema, need to do a little extra work:
@Provider public class DemoContextResolver implements ContextResolver<Unmarshaller> private static Schema schema = SchemaFactory.newInstance(XMLConstants. W3C_XML_SCHEMA_NS_URI).newSchema( new StreamSource(DemoContextResolver.class .getResourceAsStream("/demo.xsd"))) ; ... public Unmarshaller getContext(Class<?> type) { Unmarshaller unmarshaller = getJAXBContext().createUnmarshaller(); unmarshaller.setSchema(schema); return unmarshaller; }

Exception Handler
package com.ketera.rest.demo; import import import import import javax.ws.rs.core.Response; javax.ws.rs.core.Response.Status; javax.ws.rs.ext.ExceptionMapper; javax.ws.rs.ext.Provider; com.sun.jersey.api.NotFoundException;

@Provider public class DemoExceptionMapper implements ExceptionMapper<Exception> { public Response toResponse(Exception exception) { if (exception instanceof NotFoundException) { NotFoundException nfe = (NotFoundException) exception; return Response.status(Status.NOT_FOUND).entity("Oops, bad URI: " + nfe.getNotFoundUri()).build(); } return Response.status(Status.BAD_REQUEST).entity("An error occurred: " exception.getMessage()).build(); } }

Response
Sometimes you don't want to return a simple entity that can be represented with an XML document; need to modify the header, status code, etc. Use a Response
@Produces(MediaType.APPLICATION_OCTET_STREAM) @GET public Response getImage(@PathParam("id") String id) { byte [] bytes = getBytes(); String filename = getFilename(); GenericEntity<byte[]> entity = new GenericEntity<byte[]>(bytes){} return Response.status(Status.OK).header("Content-Disposition", "inline; filename=\"" + filename + "\"").entity(entity).build(); }

Displaying JSP Content


package com.ketera.rest.demo; import javax.ws.rs.GET; import javax.ws.rs.Path; import com.sun.jersey.api.view.Viewable; @Path("jsp") public class ShowJspResource { @GET public Viewable showJsp() { return new Viewable("/WEB-INF/jsp/foo.jsp", null); } }

Foo.jsp
Howdy from the JSP. >curl coverbeck-d820.ketera.com:8080/kdemo/jsp -->Howdy from the JSP.

Logging
Add this to web.xml, to the Jersey Filter:
<init-param> <param-name>com.sun.jersey.spi.container.ContainerRequestFilters</param-name> <param-value>com.sun.jersey.api.container.filter.LoggingFilter</param-value> </init-param> <init-param> <param-name>com.sun.jersey.spi.container.ContainerResponseFilters</param-name> <param-value>com.sun.jersey.api.container.filter.LoggingFilter</param-value> </init-param> <init-param> <param-name>com.sun.jersey.config.feature.logging.DisableEntitylogging</param-name> <param-value>true</param-value> </init-param>

Automated Testing
Generally separate "services" from resources
@GET @Path("{id}") public JAXBElement<CustomerType> getCustomer(@PathParam("id" String id) KniException { // Handle exception with exception mapper CustomerType customerType = CustomerService.getInstance().getCustomer(id); return objectFactory.createCustomer(customerType); } throws

Now you can just write regular unit tests against CustomerService.

Automated Testing, Part 2


To test resources, we are using embedded Jetty via Maven Cargo plug-in. These are considered "integration tests", because they can only be run once the .WAR is built and deployed. They need to be run later, not at the normal unit test phase. See KNI Rest pom.xml for how we are doing this. Tests can use HTTPClient or Jersey Client

WebAPI Filter
A servlet filter for RESTful apps that handles authentication, authorization, and throttle control. http://engwiki.ketera.com/index.php/Web_API_Filter Public URI (example only, not real): http://portal.ketera.com/kni/customers/123 Internal URI (example only, not real):
http://portal.ketera.com/internal/customers/123? username=kelleybrown@officemax.com

Pattern for Internal/External Resources


You want to expose both of these URIs: http://.../internal/customers/123 http://.../customers/123
public abstract class CustomerResource { @GET @Path("{id}") public JAXBElement<CustomerType> getCustomer(@PathParam("id").... ... @Path("customers") public static class PublicCustomerResource { } @Path("internal/customers") public static class InternalCustomerResource{ }

Reusability
Went through KNI, not too much is reusable. AbstractResource: Ties-in to Web API. Injects UriInfo, HttpServletRequest into the resource. FormatFilter, to allow specifying document format via query parameter :
http://.../customers/1234?alt=json http://../customers/1234?alt=xml

Unresolved Issues/TODO
Documentation XSD published on site Jersey has a .WADL http://.../kdemo/applicaton.wadl Need to generate .WADL as part of site

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.1.5.1 03/10/2010 02:33 PM"/> <resources base="http://coverbeck-d820.ketera.com:8080/kdemo/"> <resource path="jsp"> <method name="GET" id="showJsp"> <response> <representation mediaType="*/*"/> </response> </method> </resource> <resource path="contact"> <resource path="{id: [0-9]+}"> <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:long" style="template" name=" id"/> <method name="GET" id="retrieveContact"> <response> <representation mediaType="text/xml"/> <representation mediaType="application/json"/> </response> </method> <method name="PUT" id="updateContact">

Unresolved Issues/TODO, cont.


Create a separate module with base functionality? JSON validation Batch operations Jersey uses JDK logging, which goes to StdErr, causing misleading levels in JBoss logs. Bug http://bugs.ketera.
com/show_bug.cgi?id=40314

Links
THE Book: Restful Web Services, by Richardson and Ruby CVS Module: kni/rest Sample Project:
https://docs.google.com/a/ketera.com/leaf?id=0B605PbiG2YIYzI1ZTI3NDctOTVmZC00ZmIxLWE0ZjgtZGU3ZjY1ZDU4YWMx&hl=en

Hudson: http://cvs.ketera.com:8080/hudson/job/KNI-RestContinuous/ Jersey website: https://jersey.dev.java.net/

Vous aimerez peut-être aussi