XML bindings with JAXB and JAX-RS
There are many tutorials and examples of using JAX-RS to create RESTful web services, but most fall short of explaining how to produce and consume complex object graphs using XML and JAXB. This article will show how easy it can be, several approaches to where you place the annotations, and how you can configure them.
Lets start with the root XML element. I chose to call mine GetOrdersResponse, and use it as a container for a collection of Order objects and a Customer object. You don't need to follow this convention.
Lets start with the root XML element. I chose to call mine GetOrdersResponse, and use it as a container for a collection of Order objects and a Customer object. You don't need to follow this convention.
01.
package
com.ryandelaplante.example;
02.
03.
import
java.util.ArrayList;
04.
import
java.util.List;
05.
import
javax.xml.bind.annotation.XmlElement;
06.
import
javax.xml.bind.annotation.XmlElementWrapper;
07.
import
javax.xml.bind.annotation.XmlRootElement;
08.
09.
@XmlRootElement
10.
public
class
GetOrdersResponse {
11.
private
Customer customer;
12.
private
List orders =
new
ArrayList();
13.
14.
@XmlElement
15.
public
Customer getCustomer() {
16.
return
customer;
17.
}
18.
19.
public
void
setCustomer(Customer customer) {
20.
this
.customer = customer;
21.
}
22.
23.
@XmlElement
24.
@XmlElementWrapper
(name =
"orders"
)
25.
public
List getOrders() {
26.
return
orders;
27.
}
28.
29.
public
void
setOrders(List orders) {
30.
this
.orders = orders;
31.
}
32.
}
JAXB's default naming convention is to use your class or bean getter name as-is, but starting with a lower case letter. In this example, once marshaled to XML the element names will be getOrdersResponse, customer, and orders. I will show an example of how to override the default naming later in this article.
Also notice that I placed the @XmlElement annotations on the getter methods instead of on the private fields. When placed on the private fields, JAXB will give you an error unless you add @XmlAccessorType(XmlAccessType.FIELD) at the class level. I will show an example of this later.
Finally, notice the @XmlElementWrapper annotation on the List collection. This makes JAXB wrap all of the order XML elements inside of an orders XML element. This annotation can be used with an array instead of a List too, which will be shown later.
01.
package
com.ryandelaplante.example;
02.
03.
import
javax.xml.bind.annotation.XmlElement;
04.
import
javax.xml.bind.annotation.XmlType;
05.
06.
@XmlType
(name =
"Customer"
)
07.
public
class
Customer {
08.
private
long
customerNumber;
09.
private
String firstName;
10.
private
String lastName;
11.
12.
@XmlElement
(name =
"CustomerNumber"
)
13.
public
long
getCustomerNumber() {
14.
return
customerNumber;
15.
}
16.
17.
public
void
setCustomerNumber(
long
customerNumber) {
18.
this
.customerNumber = customerNumber;
19.
}
20.
21.
22.
@XmlElement
(name =
"FirstName"
)
23.
public
String getFirstName() {
24.
return
firstName;
25.
}
26.
27.
public
void
setFirstName(String firstName) {
28.
this
.firstName = firstName;
29.
}
30.
31.
@XmlElement
(name =
"LastName"
)
32.
public
String getLastName() {
33.
return
lastName;
34.
}
35.
36.
public
void
setLastName(String lastName) {
37.
this
.lastName = lastName;
38.
}
39.
}
In the example above, we use @XmlType at the class level instead of @XmlRootElement because it is not the root element. Also notice the name parameter in each of the annotations. This is how you override JAXB's default element naming.
01.
package
com.ryandelaplante.example;
02.
03.
import
java.util.Date;
04.
import
javax.xml.bind.annotation.XmlAccessType;
05.
import
javax.xml.bind.annotation.XmlAccessorType;
06.
import
javax.xml.bind.annotation.XmlElement;
07.
import
javax.xml.bind.annotation.XmlElementWrapper;
08.
import
javax.xml.bind.annotation.XmlType;
09.
10.
@XmlType
(propOrder = {
"orderDate"
,
"orderNumber"
,
"lineItems"
} )
11.
@XmlAccessorType
(XmlAccessType.FIELD)
12.
public
class
Order {
13.
@XmlElement
14.
public
Date orderDate;
15.
16.
@XmlElement
17.
public
long
orderNumber;
18.
19.
@XmlElement
20.
@XmlElementWrapper
(name =
"lineItems"
)
21.
public
LineItem[] lineItems;
22.
23.
public
long
getOrderNumber() {
24.
return
orderNumber;
25.
}
26.
27.
public
void
setOrderNumber(
long
orderNumber) {
28.
this
.orderNumber = orderNumber;
29.
}
30.
31.
public
Date getOrderDate() {
32.
return
orderDate;
33.
}
34.
35.
public
void
setOrderDate(Date orderDate) {
36.
this
.orderDate = orderDate;
37.
}
38.
39.
public
LineItem[] getLineItems() {
40.
return
lineItems;
41.
}
42.
43.
public
void
setLineItems(LineItem[] lineItems) {
44.
this
.lineItems = lineItems;
45.
}
46.
}
In the example above I used the @XmlAccessorType(XmlAccessType.FIELD) to allow me to place the @XmlElement annotations on the private fields instead of on the getter methods.
Also notice the propOrder parameter of the @XmlType annotation. By default JAXB will order the elements alphabetically. Use the propOrder parameter to specify the order when marshaling to XML. The values are the bean names, not the overridden names in the @XmlElement(name = "overridenName") annotation.
Finally, notice the @XmlElementWrapper used on a LineItem[] array. It works with arrays and Lists.
01.
package
com.ryandelaplante.example;
02.
03.
import
javax.xml.bind.annotation.XmlElement;
04.
import
javax.xml.bind.annotation.XmlType;
05.
06.
@XmlType
(propOrder = {
"sku"
,
"description"
,
"quantity"
,
"unitPrice"
,
07.
"subTotal"
,
"tax"
,
"total"
} )
08.
public
class
LineItem {
09.
private
long
sku;
10.
private
String description;
11.
private
short
quantity;
12.
private
double
unitPrice;
13.
14.
@XmlElement
15.
public
long
getSku() {
16.
return
sku;
17.
}
18.
19.
public
void
setSku(
long
sku) {
20.
this
.sku = sku;
21.
}
22.
23.
@XmlElement
24.
public
String getDescription() {
25.
return
description;
26.
}
27.
28.
public
void
setDescription(String description) {
29.
this
.description = description;
30.
}
31.
32.
@XmlElement
33.
public
short
getQuantity() {
34.
return
quantity;
35.
}
36.
37.
public
void
setQuantity(
short
quantity) {
38.
this
.quantity = quantity;
39.
}
40.
41.
@XmlElement
42.
public
double
getUnitPrice() {
43.
return
unitPrice;
44.
}
45.
46.
public
void
setUnitPrice(
double
unitPrice) {
47.
this
.unitPrice = unitPrice;
48.
}
49.
50.
@XmlElement
51.
public
double
getSubTotal() {
52.
return
unitPrice * quantity;
53.
}
54.
55.
@XmlElement
56.
public
double
getTax() {
57.
return
getSubTotal() *
0
.15F;
58.
}
59.
60.
@XmlElement
61.
public
double
getTotal() {
62.
return
getSubTotal() + getTax();
63.
}
64.
}
In the example above there are no setter methods that correspond to getSubTotal, getTax and getTotal. Now lets create a JAX-RS RESTful web service that can return this object graph in the response.
01.
package
com.ryandelaplante.example;
02.
03.
import
java.util.Date;
04.
import
javax.ws.rs.GET;
05.
import
javax.ws.rs.PUT;
06.
import
javax.ws.rs.Path;
07.
import
javax.ws.rs.PathParam;
08.
import
javax.ws.rs.Produces;
09.
import
javax.ws.rs.core.MediaType;
10.
import
javax.ws.rs.core.Response;
11.
import
javax.ws.rs.core.Response.ResponseBuilder;
12.
13.
@Path
(
"/api/orders"
)
14.
public
class
OrderResource {
15.
@GET
16.
@Produces
(
"text/xml"
)
17.
public
GetOrdersResponse getOrders() {
18.
19.
GetOrdersResponse response =
new
GetOrdersResponse();
20.
Customer customer =
new
Customer();
21.
LineItem lineItem1;
22.
LineItem lineItem2;
23.
24.
// customer
25.
customer.setCustomerNumber(
12345
);
26.
customer.setFirstName(
"Ryan"
);
27.
customer.setLastName(
"de Laplante"
);
28.
response.setCustomer(customer);
29.
30.
// first order
31.
Order order1 =
new
Order();
32.
order1.setOrderNumber(
54321
);
33.
order1.setOrderDate(
new
Date());
34.
35.
lineItem1 =
new
LineItem();
36.
lineItem1.setSku(
77777
);
37.
lineItem1.setDescription(
"winning lottery ticket"
);
38.
lineItem1.setQuantity((
short
)
10
);
39.
lineItem1.setUnitPrice(
5
.00F);
40.
41.
lineItem2 =
new
LineItem();
42.
lineItem2.setSku(
12121212
);
43.
lineItem2.setDescription(
"Real World Java EE Patterns Rethinking "
+
44.
"Best Practices"
);
45.
lineItem2.setQuantity((
short
)
1
);
46.
lineItem2.setUnitPrice(
40
.40F);
47.
48.
order1.setLineItems(
new
LineItem[] { lineItem1, lineItem2 } );
49.
response.getOrders().add(order1);
50.
51.
// second order
52.
Order order2 =
new
Order();
53.
order2.setOrderNumber(
12345
);
54.
order2.setOrderDate(
new
Date());
55.
56.
lineItem1 =
new
LineItem();
57.
lineItem1.setSku(
787878
);
58.
lineItem1.setDescription(
"JavaServer Faces 2.0, The Complete "
+
59.
"Reference"
);
60.
lineItem1.setQuantity((
short
)
10
);
61.
lineItem1.setUnitPrice(
31
.49F);
62.
63.
lineItem2 =
new
LineItem();
64.
lineItem2.setSku(
1111111
);
65.
lineItem2.setDescription(
"Beginning Java EE 6 with GlassFish 3, "
+
66.
"Second Edition"
);
67.
lineItem2.setQuantity((
short
)
1
);
68.
lineItem2.setUnitPrice(
41
.73F);
69.
70.
order2.setLineItems(
new
LineItem[] { lineItem1, lineItem2 } );
71.
response.getOrders().add(order2);
72.
73.
return
response;
74.
}
75.
}
Notice the @Produces("text/xml") annotation, and that the method returns a GetOrdersResponse object. Since the GetOrdersResponse is annotated with JAXB annotations, JAX-RS will automatically marshal the response to XML.
Next lets add a method that takes part of the object graph as a request parameter. We'll start by creating an object to represent the XML root element:
01.
package
com.ryandelaplante.example;
02.
03.
import
javax.xml.bind.annotation.XmlElement;
04.
import
javax.xml.bind.annotation.XmlRootElement;
05.
06.
@XmlRootElement
07.
public
class
UpdateOrderRequest {
08.
private
Order order;
09.
10.
@XmlElement
11.
public
Order getOrder() {
12.
return
order;
13.
}
14.
15.
public
void
setOrder(Order order) {
16.
this
.order = order;
17.
}
18.
}
Now lets use this object in a PUT request:
01.
@PUT
02.
@Path
(
"{orderNumber}.xml"
)
03.
@Produces
(MediaType.TEXT_PLAIN)
04.
public
Response updateOrder(
@PathParam
(
"orderNumber"
) String orderNumber,
05.
UpdateOrderRequest request)
throws
OrderNotFoundException {
06.
07.
ResponseBuilder response;
08.
09.
if
(
"12345"
.equals(orderNumber)) {
10.
response = Response.status(Response.Status.ACCEPTED).entity(
11.
"Saved changes to order '"
+
12.
request.getOrder().getOrderNumber() +
"'."
);
13.
}
else
{
14.
throw
new
OrderNotFoundException(
"Order number '"
+ orderNumber +
15.
"' does not exist."
);
16.
}
17.
return
response.build();
18.
}
Since the UpdateOrderRequest is annotated with JAXB annotations, JAX-RS will automatically unmarshal it from XML.
This example returns a javax.ws.rs.core.Response built using javax.ws.rs.core.ResponseBuilder. You can use ResponseBuilder to set response headers, the status code, and many other things. The response body is called the entity, and you can place anything in it. For example, a String, or a JAXB annotated object graph.
This example also throws an OrderNotFoundException:
01.
package
com.ryandelaplante.example;
02.
03.
import
javax.ws.rs.WebApplicationException;
04.
import
javax.ws.rs.core.MediaType;
05.
import
javax.ws.rs.core.Response;
06.
import
javax.ws.rs.core.Response.Status;
07.
08.
public
class
OrderNotFoundException
extends
WebApplicationException {
09.
public
OrderNotFoundException(String message) {
10.
super
(Response.status(Status.NOT_FOUND).entity(message).type(
11.
MediaType.TEXT_PLAIN).build());
12.
}
13.
}
The custom exception extends the WebApplicationException in JAX-RS. There are many constructors in WebApplicationException. I chose to use the one that lets me provide the complete response data.
Another interesting issue is API versioning. When I created my first RESTful web service I included an API version number in the URL. For example, /rest/v1/orders. When it came to implementation, a number of thoughts came to mind:
- Some companies might deploy/install multiple copies of their service into production, each with a different context root. For example, https://api.company.com/v1/ and https://api.company.com/v2/ could be two separate copies of the same service/.war file, just different versions.
- Another approach might be to detect the version number in the URL, and process the request/response accordingly. There may be challenges such as returning an Order object in v1 has fewer fields than v2. Since the same object implementation is probably being used for both versions, is it important that v1 does not include the new v2 fields?
- Is there any benefit to distinguishing the API version number in the URL if you ensure backward compatibility in all releases?
I decided that the last option was best for me, so I do not include version numbers in the URI.
No comments:
Post a Comment