2013년 6월 3일 월요일

[developerWorks] Constructing REST services with WebSphere Application Server, Part 1: A simple RESTful implementation


JAX-RS is a Java™ API for RESTful web services. The JAX-RS API is a collection of interfaces and Java annotations that simplifies development of server-side REST applications. JAX-RS enables developers to specify RESTful web service behavior by annotating classes to respond to specific HTTP methods (such as GET or POST) and URI patterns. JAX-RS is an official feature of Java EE 6.
The most recent versions of IBM WebSphere Application Server provide support for JAX-RS. WebSphere Application Server V8.5 has support for JAX-RS built in; no extra installation is required. For WebSphere Application Server V8.0, JAX-RS features are provided in the Feature Pack for Web 2.0 and Mobile (Version 1.1.0), which an optional product extension installed separately.
Whichever of these configurations you use, WebSphere Application Server provides these features for JAX-RS:
  • JAX-RS 1.1 server runtime based on Apache Wink 1.1
  • Built-in POJO entity provider support provided by the Jackson JSON processor
  • Built-in IBM JSON4J entity provider support.
This article describes the design and implemention of a sample application scenario to help you understand how you can apply JAX-RS to a WebSphere Application Server application. A partial implementation of the design as a set of Eclipse projects, including a web module, EJB module, and a Derby database is included with this article. After completing this article and working with the sample application, you should be able to create a JAX-RS application with response providers that enable you to tailor the responses from your application.
The example we will use in this discussion is a sample Student Registration Application (SRA). The application's components and their high-level interactions are shown in Figure 1.

Figure 1. Student Registration Application example
Student                     Registration Application example 
The download materials included with this articleconsist of three projects exported from an Eclipse IDE: the EAR, EJB, and Web projects. If you use these files to follow along with the article, the persistence.xml file in the EJB project needs to be edited and the value of the openjpa.ConnectionURL property adjusted to point to a location in your environment where the Student database can be created the first time the application starts.
The student registration management functions provided in this sample SRA application enable you to:
  • Register new students
  • Edit registered student information
  • List registered students
  • Remove student registrations.
SRA consists of a web module that provides the RESTful services, an EJB module that provides the basic Create-Read-Update-Delete (CRUD) database logic, and the database where student records are stored.
The application configuration class shown in Figure 1 is used by the JAX-RS runtime to determine the classes in the application that are resource classes and provider classes. The StudentRegistrationApplication class in which resource and provider classes are registered is shown in Listing 1.

Listing 1. StudentRegistrationApplication class
public class StudentRegistrationApplication extends Application {
    public Set<Class<?>> getClasses() {
        Set<Class<?>> classes = new HashSet<Class<?>>();

        // Resources
        classes.add( com.dw.demo.rest.resources.StudentResource.class );

        // Providers

        return classes;
    }
}

The StudentRegistrationApplication class is a subclass of the JAX-RS provided Application class and provides a list of the REST resources and providers in SRA.
The web.xml file for the application is shown in Listing 2.

Listing 2. Student Registration Application web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
        http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">
    <display-name>demo.war</display-name>
    <servlet>
        <servlet-name>com.dw.demo.rest.StudentRegistrationApplication</servlet-name>
    </servlet>
    <servlet-mapping>
        <servlet-name>com.dw.demo.rest.StudentRegistrationApplication</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>
</web-app>

WebSphere Application Server provides a JAX-RS aware servlet container so the StudentRegistrationApplication class can be provided as the servlet name. The configuration in the web.xml file maps the "/rest/*" URL pattern to theStudentRegistrationApplication class, and the WebSphere Application Server servlet container automatically connects the dots so that references to the "/rest/*" URL pattern invoke the appropriate resource class.
The StudentRegistrationApplication class declares a single resource, the StudentResource class. The StudentResource class represents the entire collection of registered students. The initial implementation of the class is shown in Listing 3.

Listing 3. StudentResource class
@Path( "/students" )
@Stateless
public class StudentResource {

    @EJB
    IStudent studentService;

    @POST
    @Consumes( MediaType.APPLICATION_JSON )
    public Response create( @Context UriInfo uriInfo, Student student ) {

        Student createdStudent = studentService.createStudent( student );
        URI uri = uriInfo.getBaseUriBuilder().path( StudentResource.class )
            .path( createdStudent.getId() ).build();

        return Response.created( uri ).build();
    }

    @DELETE
    @Path( "/{id}")
    public Response delete( @PathParam( "id" ) String id ) {
        studentService.delete( id );
        return Response.status( Status.NO_CONTENT ).build();
    }

    @GET
    @Produces( MediaType.APPLICATION_JSON )
    public Response getAll() {
        List<Student> students = studentService.getAll();
        return Response.ok( students ).build();
    }

    @GET
    @Path( "/{id}" )
    @Produces( MediaType.APPLICATION_JSON )
    public Response getById( @PathParam( "id" ) String id ) {
        Student student = studentService.findById( id );
        if ( student != null ) {
            return Response.ok( student ).build();
        } else {
            return Response.status( Status.NOT_FOUND ).build();
        }
    }

    @PUT
    @Consumes( MediaType.APPLICATION_JSON )
    public Response update( @Context UriInfo uriInfo, Student student ) {
        Student updatedStudent = studentService.update( student );
        return Response.ok( updatedStudent ).build();
    }
}

The "@Path( "/students" )" annotation means that requests to /demo/rest/students will be handled by the Student Resource class.
The resource class defines five methods:
  • create: create a new student record using HTTP POST.
  • delete: delete a particular student record using HTTP DELETE.
  • getAll: get all students using HTTP GET.
  • getById: get a particular student using their student ID using HTTP GET
  • update: update a student record using HTTP PUT.
A collection-wide delete method is specifically not implemented on the collection of students because you don't want to inadvertently delete all of the registered students.
The implementation of the StudentResource class is also annotated to either consume or produce JSON content depending on the method; for the purpose of this article, the supported media type is restricted to "application/json."
The StudentResource class uses an EJB to perform the required database operations; the @Stateless annotation at the class level tells WebSphere Application Server to treat the StudentResource class as an EJB so that the EJBs the class depends on – in this case, the StudentService EJB — will be injected automatically.
The methods shown in Listing 3 either accept or return instances (or a list of instances) of the Student class. The Student class is a POJO declared and implemented in the EJB module. The Student class is annotated as a JPA entity and is shown in Listing 4.

Listing 4. Student Entity class
@Entity
@Table( name = "STUDENT" )
public class Student implements Serializable {

    private static final long serialVersionUID = 1L;

    private String id;
    private String firstName;
    private String lastName;
    private String email;
    private String studentNumber;
    private Calendar registeredOn;
    private Gender gender;

    // Getters and setters omitted for brevity
}

Since the Student class is a POJO with well-defined getters and setters, the WebSphere Application Server JAX-RS runtime will use the Jackson JSON processer to automatically serialize and de-serialize the Student class, either a single instance of the Student class or a collection of instances of the Student class. No extra coding is required. Even the Gender enumeration used by the Student class is handled correctly by the Jackson JSON processor.
If you test the application by issuing a GET against /demo/rest/students, you will see that the application returns the JSON code shown in Listing 5.

Listing 5. JSON code returned by application
[ {
    "id" : "0b031e9b-6933-4a10-9ffc-c7fd2c604331",
    "firstName" : "Aaron",
    "lastName" : "Aaronson",
    "gender" : "MALE",
    "email" : "aaaronson@domain.net",
    "studentNumber" : "823-934",
    "registeredOn" : 1356480000000
}, …
{
    "id" : "864eb556-053a-42d7-a757-e24928fb19a3",
    "firstName" : "Pamela",
    "lastName" : "Peterson",
    "gender" : "FEMALE",
    "email" : "ppeterson@domain.net",
    "studentNumber" : "826-660",
    "registeredOn" : 1364256000000
} ]

By default, the Jackson JSON Processor serializes date objects ( java.util.Datejava.util.Calendar, and so on) as numeric timestamps (milliseconds since January 1, 1970, 00:00:00 GMT). For this sample application, you want the registeredOn value of Student entity serialized as an ISO8601 string.
In order to control serialization, you will create a JAX-RS provider class named JsonStudentProvider, as shown in Listing 6.

Listing 6. JsonStudentProvider
@Provider
@Produces( MediaType.APPLICATION_JSON )
public class JsonStudentProvider implements MessageBodyWriter<Object> {

    public long getSize( Object object, Class<?> type, Type genericType,
        Annotation[] annotations, MediaType mediaType ) {
        return -1;
    }

    public boolean isWriteable( Class<?> type, Type genericType,
        Annotation[] annotations, MediaType mediaType ) {
        boolean isWritable = false;
        if ( List.class.isAssignableFrom( type )
            && genericType instanceof ParameterizedType ) {
            ParameterizedType parameterizedType = (ParameterizedType) genericType;
            Type[] actualTypeArgs = ( parameterizedType.getActualTypeArguments() );
            isWritable = (
                actualTypeArgs.length == 1
                && actualTypeArgs[0].equals( Student.class ) );
        } else if ( type == Student.class ) {
            isWritable = true;
        }
        return isWritable;
    }

    public void writeTo( Object object, Class<?> type, Type genericType,
        Annotation[] annotations, MediaType mediaType,
        MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream )
        throws IOException {

        // Explicitly use the Jackson ObjectMapper to write dates in ISO8601 format
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure( SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS, false );
        mapper.writeValue( entityStream, object );
    }
}

The JsonStudentProvider class must be annotated with @Provider and must implement these methods declared by the JAX-RSMessageBodyWriter interface:
  • getSize: get the size of the serialized object, return -1 if the size is not known.
  • isWritable: if this method returns true, the JAX-RS framework will use this class to serialize the object.
  • writeTo: this method is invoked to serialize the supplied object to the supplied OutputStream instance.
The isWritable method tests if the supplied object is an instance of Student or a list of Student instances. If it is, then theJsonStudentProvider will be used to serialize the object.
The writeTo method creates a Jackson ObjectMapper instance, configures it to not write dates as timestamps, but instead to write dates as strings, and finally uses the ObjectMapper instance to serialize the supplied object to the supplied OutputStream.
Once the JsonStudentProvider class is created, it must be registered in the Student Registration Application class, shown in Listing 7.

Listing 7. Updated StudentRegistrationApplication class
public class StudentRegistrationApplication extends Application {
    public Set<Class<?>> getClasses() {
        Set<Class<?>> classes = new HashSet<Class<?>>();

        // Resources
        classes.add( dw.demo.rest.resources.StudentResource.class );

        // Providers
        classes.add( dw.demo.rest.providers.JsonStudentProvider.class );

        return classes;
    }
}

You need to make one small change to the getAll method in the Student Resource class to ensure that type information is correctly passed to the JsonStudentProvider when the user asks for a list of all students (Listing 8).

Listing 8. Updated Student Resource Class "getAll" Method
    @GET
    @Produces( MediaType.APPLICATION_JSON )
    public Response getAll() {
        List<Student> students = studentService.get();
        // This preserves type information for the provider
        GenericEntity<List<Student>> entity =
            new GenericEntity<List<Student>>( students ) {};
        return Response.ok( entity ).build();
    }

The JAX-RS GenericEntity class enables type information for the generic List interface to be available to theJsonStudentProvider at run time. Retesting the application by issuing a GET against /demo/rest/students, you will see that the application now returns the JSON code shown in Listing 9.

Listing 9. JSON code returned by application
[ {
    "id" : "0b031e9b-6933-4a10-9ffc-c7fd2c604331",
    "firstName" : "Aaron",
    "lastName" : "Aaronson",
    "gender" : "MALE",
    "email" : "aaaronson@domain.net",
    "studentNumber" : "823-934",
    "registeredOn" : "2012-12-26T00:00:00.000+0000"
}, …
{
    "id" : "864eb556-053a-42d7-a757-e24928fb19a3",
    "firstName" : "Pamela",
    "lastName" : "Peterson",
    "gender" : "FEMALE",
    "email" : "ppeterson@domain.net",
    "studentNumber" : "826-660",
    "registeredOn" : "2013-03-26T00:00:00.000+0000"
} ]

The values in the registeredOn property are now presented in ISO8601 format.
If invalid JSON is supplied to your application, a server error (HTTP 500) will result. Invalid JSON might be:
  • Badly formed JSON, such as missing property name/value delimiters, unbalanced braces, and so on.
  • JSON containing properties with no corresponding property in the Student class.
For example, if the request body of a POST to the /demo/rest/students URL is:
{ "bad":"property" }
The response shown in Listing 10 is returned by the server.

Listing 10. Server response
HTTP/1.1 500 Internal Server Error
X-Powered-By: Servlet/3.0
Content-Type: text/html;charset=ISO-8859-1
$WSEP:
Content-Language: en-CA
Content-Length: 377
Connection: Close
Date: Tue, 02 Apr 2013 07:40:06 GMT
Server: WebSphere Application Server/8.0

Error 500: javax.servlet.ServletException: 
org.codehaus.jackson.map.exc.UnrecognizedPropertyException: 
Unrecognized field &quot;bad&quot; &#40;Class com.dw.demo.entities.Student&#41;, 
not marked as ignorable at 
[Source: com.ibm.ws.webcontainer.srt.http.HttpInputStream@5370a0&#59; 
line: 1, column: 10] &#40;through reference chain: com.dw.demo.entities.Student
[&quot;bad&quot;]&#41;

The exception is issued because the Student class has no property named "bad."
What you would like is for the server to return an HTTP 400 (Bad Request) response with some diagnostic text if the application is provided with invalid JSON. To do this, create a new provider class, as shown in Listing 11.

Listing 11. JsonProcessingExceptionMapper class
@Provider
public class JsonProcessingExceptionMapper implements
    ExceptionMapper<JsonProcessingException> {

    @Override
    public Response toResponse( JsonProcessingException e ) {
        String message = e.getMessage();
        return Response
            .status( Status.BAD_REQUEST )
            .type( MediaType.TEXT_PLAIN )
            .entity( "The supplied JSON was not well formed: " + message )
            .build();
    }
}

The JsonProcessingExceptionMapper class implements the JAX-RS ExceptionMapper interface, which has one method:toResponse. The toResponse method takes a Jackson JsonProcessingException, and creates an HTTP 400 response with a plain text payload.
Once the JsonProcessingExceptionMapper class is created, it must be registered in the StudentRegistrationApplication class, as shown in Listing 12.

Listing 12. Updated StudentRegistrationApplication class
public class StudentRegistrationApplication extends Application {
    public Set<Class<?>> getClasses() {
        Set<Class<?>> classes = new HashSet<Class<?>>();

        // Resources
        classes.add( dw.demo.rest.resources.StudentResource.class );

        // Providers
        classes.add( dw.demo.rest.providers.JsonStudentProvider.class );
        classes.add( dw.demo.rest.providers.JsonProcessingExceptionMapper.class );

        return classes;
    }
}

Now, if the request body of a POST to the /demo/rest/students URL is: { "bad":"property" } the response in Listing 13 is returned by the server.

Listing 13. Server response
HTTP/1.1 400 Bad Request
X-Powered-By: Servlet/3.0
Content-Type: text/plain
Content-Language: en-CA
Transfer-Encoding: chunked
Connection: Close
Date: Tue, 02 Apr 2013 07:35:30 GMT
Server: WebSphere Application Server/8.0

10f
The supplied JSON was not well formed: Unrecognized field "bad" 
(Class com.dw.demo.entities.Student), not marked as ignorable
 at [Source: com.ibm.ws.webcontainer.srt.http.HttpInputStream@5370a0; 
line: 1, column: 10] (through reference chain: com.dw.demo.entities.Student["bad"])
0

This article described how to create a RESTful service to manage student registrations. The service uses a custom serializer to ensure student records represented in JSON contain dates in the correct format, and a custom exception mapper to report invalid input JSON with an HTTP 400 error code instead of an HTTP 500 error code.
Part 2 will show how you can integrate your RESTful service with a client-side UI framework, Dojo.

Download
DescriptionNameSizeDownload method
Code sampledemo.zip41 KBHTTP

Learn
Get products and technologies
Rick Gunderson is an IT Specialist in Application Integration and Middleware in the IBM Software Group. In his 15 years at IBM, Rick has worked on web-based projects for clients in a variety of industries, including Healthcare, Telecommunications, Agriculture and Energy.

댓글 없음:

댓글 쓰기