Статьи

Ввод Java в REST

В прошлом месяце я дал вам введение в REST . Это была 100% теория, так что теперь пришло время увидеть немного REST в действии. Поскольку я в основном программист на Java, часть II этой серии будет посвящена написанию веб-сервисов RESTFul с использованием языка Java. REST не требует определенного клиентского или серверного фреймворка для написания ваших веб-сервисов. Все, что вам нужно, это клиент или сервер, который поддерживает протокол HTTP. В Java Land сервлеты являются хорошим инструментом для создания ваших распределенных приложений, но могут быть немного громоздкими и требовать связующего кода и конфигурации XML, чтобы все заработало. Итак, около полутора лет назад JSR-311JAX-RS был запущен в JCP для предоставления основанной на аннотациях инфраструктуры, которая поможет вам более продуктивно писать веб-сервисы RESTFul. В этой статье мы реализуем различные простые Web-сервисы, используя спецификацию JAX-RS.   

Простая служба JAX-RS

JAX-RS использует аннотации Java для сопоставления входящего HTTP-запроса с методом Java. Это не механизм RPC, а скорее способ легкого доступа к интересующим вас частям HTTP-запроса без большого количества стандартного кода, который вам пришлось бы писать, если вы использовали сырые сервлеты. Чтобы использовать JAX-RS, вы аннотируете свой класс аннотацией @Path, чтобы указать интересующий вас относительный путь URI, а затем аннотируете один или несколько методов вашего класса с помощью @GET , @POST , @PUT , @DELETE или @ HEAD, чтобы указать, какой HTTP-метод вы хотите отправить конкретному методу.

@Path("/orders")public class OrderEntryService {   @GET   public String getOrders() {...}}

Если бы мы указали наш браузер на http://somewhere.com/orders , JAX-RS отправил бы HTTP-запрос методу getOrders (), и мы бы вернули любое содержимое, которое вернул метод getOrders () .

JAX-RS имеет очень простую модель компонентов по умолчанию. Когда вы внедряете аннотированный класс JAX-RS в среду выполнения JAX-RS, он выделяет объект OrderEntryService для обслуживания одного конкретного HTTP-запроса и отбрасывает этот экземпляр объекта в конце HTTP-ответа. Эта модель для каждого запроса очень похожа на EJB без сохранения состояния. Большинство реализаций JAX-RS поддерживают интеграцию Spring, EJB и даже JBoss Seam. Я рекомендую вам использовать одну из этих компонентных моделей, а не модель по умолчанию JAX-RS, поскольку она очень ограничена, и вы быстро захотите использовать Spring, EJB или Seam для создания своих сервисов.

Доступ к параметрам запроса

Одна из проблем метода getOrders () нашего класса OrderEntryService заключается в том, что этот метод может возвращать тысячи заказов в нашей системе. Было бы неплохо иметь возможность ограничить размер набора результатов, возвращаемых из системы. Для этого клиент может отправить параметр запроса URI, чтобы указать, сколько результатов он хочет вернуть в ответе HTTP, т.е. http://somewhere.com/orders?size=50 . Чтобы извлечь эту информацию из HTTP-запроса, JAX-RS имеет аннотацию @QueryParam :

@Path("/orders")public class OrderEntryService {   @GET   public String getOrders(@QueryParam("size")                           @DefaultValue("50") int size)   {     ... method body ...   }}

@QueryParam будет автоматически пытаться тянуть «размер» параметр запроса из входящего URL и преобразовать его в целое число. @QueryParam позволяет вам вводить параметры запроса URL в любой примитивный тип, а также в любой класс, имеющий открытый метод static valueOf (String) или конструктор, имеющий один параметр String. @DefaultValue аннотаций является необязательной частью метаданных. Что он делает, так это сообщает среде выполнения JAX-RS, что если параметр запроса URI «size» не предоставлен клиентом, введите значение по умолчанию «50» .

Другие аннотации параметров

Существуют другие аннотации параметров, такие как @HeaderParam , @CookieParam и @FormParam, которые позволяют извлекать дополнительную информацию из HTTP-запроса для вставки в параметры вашего метода Java. Хотя @HeaderParam и @CookieParam говорят сами за себя, @FormParam позволяет извлекать параметры из тела запроса application / x-www-formurlencoded (форма HTML). Я не собираюсь тратить на них много времени в этой статье, так как они ведут себя почти так же, как @QueryParam .

Параметры пути и @PathParam

Our current OrderEntryService has limited usefulness. While getting access to all orders in our system is useful, many times we will have web service client that wants access to one particular order. We could write a new getOrder() method that used @QueryParam to specify which order we were interested in. This is not good RESTFul design as we are putting resource identity within a query parameter when it really belongs as part of the URI path itself. The JAX-RS specification provides the ability to define named path expressions with the @Path annotation and access those matched expressions using the @PathParam annotation. For example, let’s implement a getOrder() method using this technique.

@Path("/orders")public class OrderEntryService {   @GET   @Path("/{id}")   public String getOrder(@PathParam("id") int orderId) {     ... method body ...   }}

The {id} string represents our path expression. What it initially means to JAX-RS is to match an incoming’s request URI to any character other than ‘/’. For example http://somewhere.com/orders/3333 would dispatch to the getOrder() method, but http://somewhere.com/orders/3333/entries would not. The «id» string names the path expression so that it can be referenced and injected as a method parameter. This is exactly what we are doing in our getOrder() method. The @PathParam annotation will pull in the information from the incoming URI and inject it into the orderId parameter. For example, if our request is http://somewhere.com/orders/111, orderId would get the 111 value injected into it.

More complex path expressions are also supported. For example, what if we wanted to make sure that id was an integer? We can use Java regular expressions in our path expression as follows:

@Path("{id: \\d+}")

Notice that a ‘:’ character follows «id». This tells JAX-RS there is a Java regular expression that should be matched as part of the dispatching process.

Content-Type

Our getOrder() example, while functional, is incomplete. The String passed back from getOrder() could be any mime type: plain text, HTML, XML, JSON, YAML. Since we’re exchanging HTTP messages, JAX-RS will set the response Content-Type to be the preferred mime type asked for by the client (for browsers its usually XML or HTML), and dump the raw bytes of the String to the response output stream.

You can specify which mime type the method return type provides with the @Produces annotation. For example, let’s say our getOrders() method actually returns an XML string.:

@Path("/orders")public class OrderEntryService {   @GET   @Path("{id}")   @Produces("application/xml")   public String getOrder(@PathParm("id") int orderId)   {      ...   }}

Using the @Produces annotation in this way would cause the Content-Type of the response to be set to «application/xml».

Content Negotiation

HTTP clients use the HTTP Accept header to specify a list of mime types they would prefer the server to return to them. For example, my Firefox browser sends this Accept header with every request:

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

The server should interpret this string as that the client prefers html or xhtml, but would accept raw XML second, and any other content type third. The mime type parameter «q» in our example identifies that «application/xml» is our 2nd choice and «*/*» (anything) is our third choice (1.0 is the default «q» value if «q» is not specified).

JAX-RS understands the Accept header and will use it when dispatching to JAX-RS annotated methods. For example, let’s add two new getOrder() methods that return HTML and JSON.

@Path("/orders")public class OrderEntryService {   @GET   @Path("{id}")   @Produces("application/xml")   public String getOrder(@PathParm("id") int orderId)  {...}   @GET   @Path("{id}")   @Produces("text/html")   public String getOrderHtml(@PathParm("id") int orderId) {...}   @GET   @Path("{id}")   @Produces("application/json")   public String getOrderJson(@PathParm("id") int orderId) {...}}

If we pointed our browser at our OrderEntryService, it would dispatch to the getOrderHtml() method as the browser prefers HTML. The Accept header and each method’s @Produces annotation is used to determine which Java method to dispatch to.

Content Marshalling

Our getOrder() method is still incomplete. It still will have a bit of boilerplate code to convert a list of orders to an XML string that the client can consume. Luckily, JAX-RS allows you to write HTTP message body readers and writers that know how to marshall a specific Java type to and from a specific mime type. The JAX-RS specification has some required built-in marshallers. For instance, vendors are required to provide support for marshalling JAXB annotated classes (JBoss RESTEasy also has providers for JSON, YAML, and other mime types). Let’s expand our example:

@XmlRootElement(name="order")public class Order {   @XmlElement(name="id")   int id;   @XmlElement(name="customer-id")   int customerId;   @XmlElement("order-entries")   List<OrderEntry> entries;...}@Path("/orders")public class OrderEntryService {   @GET   @Path("{id}")   @Produces("application/xml"   public Order getOrder(@PathParm("id") int orderId) {...}}

JAX-RS will see that your Content-Type is application/xml and that the Order class has a JAXB annotation and will automatically using JAXB to write the Order object to the HTTP output stream. You can plug in and write your own marshallers using the MessageBodyReader and Writer interfaces, but we will not cover how to do this in this article.

Response Codes and Custom Responses

The HTTP specification defines what HTTP response codes should be on a successful request. For example, GET should return 200, OK and PUT should return 201, CREATED. You can expect JAX-RS to return the same default response codes.

Sometimes, however, you need to specify your own response codes, or simply to add specific headers or cookies to your HTTP response. JAX-RS provides a Response class for this.

@Path("/orders")public class OrderEntryService {   @GET   @Path("{id}")   public Response getOrder(@PathParm("id") int orderId)   {      Order order = ...;      ResponseBuilder builder = Response.ok(order);      builder.expires(...some date in the future);      return builder.build();   }}

In this example, we still want to return a JAXB object with a 200 status code, but we want to add an Expires header to the response. You use the ResponseBuilder class to build up the response, and ResponseBuilder.build() to create the final Response instance.

Exception Handling

JAX-RS has a RuntimeException class, WebApplicationException, that allows you to abort your JAX-RS service method. It can take an HTTP status code or even a Response object as one of its constructor parameters. For example

@Path("/orders")public class OrderEntryService {   @GET   @Path("{id}")   @Produces("application/xml")   public Order getOrder(@PathParm("id") int orderId) {      Order order = ...;      if (order == null) {         ResponseBuilder builder = Response.status(Status.NOT_FOUND);         builder.type("text/html");         builder.entity("<h3>Order Not Found</h3>");         throw new WebApplicationException(builder.build();      }      return order;   }}

In this example, if the order is null, send a HTTP response code of NOT FOUND with a HTML encoded error message.

Beyond WebApplicationException, you can map non-JAXRS exceptions that might be thrown by your application to a Response object by registering implementations of the ExceptionMapper class:

public interface ExceptionMapper<E extends Throwable>{   Response toResponse(E exception);}

For example, lets say we were using JPA to locate our Order objects. We could map javax.persistence.EntityNotFoundException to return a NOT FOUND status code.

@Providerpublic class EntityNotFoundMapper     implements ExceptionMapper<EntityNotFoundException>{   Response toResponse(EntityNotFoundException exception)   {      return Response.status(Status.NOT_FOUND);   }}

You register the ExceptionMapper using the Application deployment method that I’ll show you in the next section.

Deploying a JAX-RS Application

Although the specification may expand on this before Java EE 6 goes final, it provides only one simple way of deploying your JAX-RS applications into a Java EE environment. First you must implement a javax.ws.rs.core.Application class.

public abstract class Application{   public abstract Set<Class<?>> getClasses();   public Set<Object>getSingletons()}

The getClasses() method returns a list of classes you want to deploy into the JAX-RS environment. They can be @Path annotated classes, in which case, you are specifying that you want to use the default per-request component model. These classes could also be a MessageBodyReader or Writer (which I didn’t go into a lot of detail), or an ExceptionMapper. The getSingletons() method returns actual instances that you create yourself within the implementation of your Application class. You use this method when you want to have control over instance creation of your resource classes and providers. For example, maybe you are using Spring to instantiate your JAX-RS objects, or you want to register an EJB that uses JAX-RS annotations.

You tell the JAX-RS runtime to use the class via a <context-param> within your WAR’s web.xml file

<context-param>      <param-name>javax.ws.rs.core.Application</param-name>      <param-value>com.smoke.MyApplicationConfig</param-value></context-param>

Other JAX-RS Features

There’s a bunch of JAX-RS features I didn’t go into detail with or mention. There’s a few helper classes for URI building and variant matching as well as classes for encapsulating HTTP specification concepts like Cache-Control. Download the specification and take a look at the Javadocs for more information on this stuff.

Test Drive JAX-RS

You can test drive JAX-RS through JBoss’s JAX-RS implementation, RESTEasy available at http://jboss.org/resteasy.

About the Author

Bill Burke is an engineer and Fellow at JBoss, a division of Red Hat. He is JBoss’s representative on JSR-311, JAX-RS and lead on JBoss’s RESTEasy implementation.