Multiple Media Types in Java Microservices with RESTEasy

By: lyndseypadget
  |  March 18, 2023
Multiple Media Types in Java Microservices with RESTEasy

Today we’ll be talking about microservices in Java. While it’s true that Java EE has a robust platform for writing, deploying, and managing enterprise-level microservices, in this article I will create a RESTful microservice that is as slim as possible.

Don’t worry – we won’t be reinventing the wheel by marshaling our own data or anything. We’ll be using JBoss’ RESTEasy to take care of that! The objective in keeping things lightweight is to show how truly simple it can be to establish a RESTful interface in front of a new or existing Java microservice.

At the same time, I’ll illustrate the flexibility of such a service by supporting multiple media types, JSON and XML, and deploying it on Apache Tomcat rather than JBoss Enterprise Application Platform (EAP). Every tool has its place, but I think it’s helpful to explore technologies through the lens of the KISS principle first, then decide what kind of additional architectural features should be pursued depending on the long-term objectives and requirements of the software.

The code example in this article is available on GitHub, with “starter” and “final” branches. The following describes my environment, although your mileage may vary:

Technically speaking…

A microservice is a small, concise service whose objective is to “do one thing well”. It’s quite common to interact with microservices via some kind of interface. If that interface is accessible via the web (using HTTP) then it is a web service. Some web services are RESTful and others are not. It’s worth noting that not all microservices are web services, not all web services are RESTful, and not all RESTful web services are microservices!

not all web services are RESTful, and not all RESTful web services are microservices!

REST and XML… together?

If you’ve never encountered a RESTful web service that delivers content using one of the many media types other than JSON, you might think that these two things don’t belong together. But recall that REST is an architectural style for defining APIs, and that the popularity of REST and JSON happened to grow in parallel (not coincidentally, mind you). RESTful web services that accept and provide XML can be extremely useful for organizations who already have interconnected systems relying on that type of content, or for consumers who simply have more experience with XML. Of course, JSON would normally be the first choice because the message bodies are smaller, but sometimes XML is just an easier “sell”. Having a RESTful microservice that can do both is even better; not only is it concise and scalable from a deployment standpoint, but it’s also flexible enough to support different kinds of content to applications who wish to consume it.

Why RESTEasy?

RESTEasy is a framework by JBoss to help you build RESTful web services. With RESTEasy, it’s possible to build a RESTful web service that serves up both XML and JSON by depending on just four libraries:

  • resteasy-jaxrs, which implements JAX-RS 2.0 (Java API for RESTful Web Services)
  • resteasy-jaxb-provider, whose JAXB binding helps us support XML
  • resteasy-jettison-provider, which uses Jettison to convert XML to JSON
  • resteasy-servlet-initializer, for deploying to a Servlet 3.0 container (on Tomcat)

To start, we create a web service project with a pom.xml that looks something like this:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.lyndseypadget</groupId>
	<artifactId>resteasy</artifactId>
	<packaging>war</packaging>
	<version>0.0.1-SNAPSHOT</version>
	<name>resteasy</name>
	<repositories>
		<repository>
			<id>org.jboss.resteasy</id>
			<url>http://repository.jboss.org/maven2/</url>
		</repository>
	</repositories>
	<dependencies>
		<dependency>
			<groupId>org.jboss.resteasy</groupId>
			<artifactId>resteasy-jaxrs</artifactId>
			<version>3.1.4.Final</version>
		</dependency>
		<dependency>
			<groupId>org.jboss.resteasy</groupId>
			<artifactId>resteasy-jaxb-provider</artifactId>
			<version>3.1.4.Final</version>
		</dependency>
		<dependency>
			<groupId>org.jboss.resteasy</groupId>
			<artifactId>resteasy-jettison-provider</artifactId>
			<version>3.1.4.Final</version>
		</dependency>
		<dependency>
			<groupId>org.jboss.resteasy</groupId>
			<artifactId>resteasy-servlet-initializer</artifactId>
			<version>3.1.4.Final</version>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.0.2</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
		</plugins>
		<finalName>resteasy</finalName>
	</build>
</project>

All together, these libraries come in at ~830 KB. Of course, these are our direct dependencies and building the project with Maven will bring in a handful of transitive dependencies as well.

direct dependencies and building the project with Maven will bring in a handful of transitive dependencies as well.

Going forward, I’ll be building this project in the “Maven way” (i.e. classes under src/main/java, using Maven build commands, etc), but you can also download the RESTEasy jars directly from the download page if you prefer not to use Maven. If you go this route, don’t be alarmed by this popup on the RESTEasy site: JBoss is simply trying to steer you down a more “enterprise” path. You can click “Continue Download” and be on your way.

JBoss is simply trying to steer you down a more “enterprise” path. You can click “Continue Download” and be on your way.

The project layout

This service is going to be extremely simple to illustrate some basic concepts. You’ll need five classes, organized like this:

You’ll need five classes, organized like this:

FruitApplication is the entry point for the microservice.  FruitService provides the main endpoint (/fruits), and it also serves as the router. Apple and Fruit are the models; Fruit has some abstract functionality and Apple will concretely extend it.

As you can imagine, FruitComparator helps us compare fruits. If you’re unfamiliar with Java comparators, you can learn about object equality and comparison in this article, where I’m using Strings instead. While FruitComparator isn’t a model, I prefer to keep comparators close to the type of object it is intended to compare.

The models

Let’s start with the Fruit class:

package com.lyndseypadget.resteasy.model;
import javax.xml.bind.annotation.XmlElement;
public abstract class Fruit {
    private String id;
    private String variety;
    @XmlElement
    public String getId() {
        return id;
    }
    
    public void setId(String id) {
        this.id = id;
    }
    @XmlElement
    public String getVariety() {
        return variety;
    }
    
    public void setVariety(String variety) {
        this.variety = variety;
    }
}

And the Apple class that extends it:

package com.lyndseypadget.resteasy.model;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name = "apple")
public class Apple extends Fruit {
    
    private String color;
    @XmlElement
    public String getColor() {
        return color;
    }
    
    public void setColor(String color) {
        this.color = color;
    }
}

This isn’t particularly earth-shattering code here – it’s a simple example of Java inheritance. However, the important parts are the annotations @XmlElement and @XmlRootElement, which define what the XML apple structure will look like:

<apple>
	<id>1</id>
	<variety>Golden delicious</variety>
	<color>yellow</color>
</apple>

There’s also something else going on here that’s more subtle since no constructor is explicitly provided: Java uses an implicit, no-arg default constructor. This no-arg constructor is actually necessary for the JAXB magic to work (this article explains why that is, and how you can work around it with XMLAdapter if necessary).

Now we have our object, an apple, defined. It has three properties: id, variety and color.

The service

The FruitService class serves as the primary endpoint (/fruits) we’ll use to interact with the microservice. In this case, I’ve defined the first route, /fruits/apples, directly in this class using the @Path annotation. As your RESTful microservice grows, you’ll likely want to define each final endpoint (i.e. /apples, /bananas, /oranges) in its own class.

package com.lyndseypadget.resteasy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import com.lyndseypadget.resteasy.model.Apple;
import com.lyndseypadget.resteasy.model.FruitComparator;
@Path("/fruits")
public class FruitService {
	private static Map<String, Apple> apples = new TreeMap<String, Apple>();
	private static Comparator comparator = new FruitComparator();
	@GET
	@Path("/apples")
	@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
	public List getApples() {
		List retVal = new ArrayList(apples.values());
		Collections.sort(retVal, comparator);
		return retVal;
	}
}

The apples map helps us keep track of our apples by id, thus simulating some kind of persistence layer. The getApples method returns the values of that map. The GET /apples route is defined with the @GET and @Path annotations, and it can produce content of media type XML or JSON.

This method needs to return a List<Apple> object, and we use the comparator to sort that list by the variety property.

The FruitComparator looks like this:

package com.lyndseypadget.resteasy.model;
import java.util.Comparator;
public class FruitComparator implements Comparator {
	public int compare(F f1, F f2) {
		return f1.getVariety().compareTo(f2.getVariety());
	}
}

Note that if we wanted to sort by a property that is Apple-specific, such as color, we’d have to create a different looking implementation of Comparator instead, and name it something like AppleComparator.

The application

As of RESTEasy version 3.1.x, you’ll need to define a class that extends Application. RESTEasy example documentation suggests for this to be a singleton registry, like so:

package com.lyndseypadget.resteasy;
import javax.ws.rs.core.Application;
import java.util.HashSet;
import java.util.Set;
public class FruitApplication extends Application
{
   HashSet singletons = new HashSet();
   public FruitApplication()
   {
      singletons.add(new FruitService());
   }
   @Override
   public Set<Class> getClasses()
   {
      HashSet<Class> set = new HashSet<Class>();
      return set;
   }
   @Override
   public Set getSingletons()
   {
      return singletons;  
   }
}

We won’t need to do much with this class for the purpose of this example, but we will need to wire it up in our web.xml file, described in the “A bit of web service wiring” section later.

Structuring collections of objects

As written, the GET /apples call will return data like this:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<collection>
    <apple>
        <id>1</id>
        <variety>Golden delicious</variety>
        <color>yellow</color>
    </apple>
</collection>
[
    {
        "apple": {
            "id": 1,
            "variety": "Golden delicious",
            "color": "yellow"
        }
    }
]

However, it is possible to change the data to look a bit different, like this:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<apples>
    <apple>
        <id>1</id>
        <variety>Golden delicious</variety>
        <color>yellow</color>
    </apple>
</apples>
{
    "apples": {
        "apple": {
            "id": 1,
            "variety": "Golden delicious",
            "color": "yellow"
        }
    }
}

The second option looks a bit nicer in XML, but affects the JSON in a potentially undesirable way. If you prefer this structure, you can wrap the List<Apple> in its own type and modify the FruitService.getApples method to return this type:

package com.lyndseypadget.resteasy.model;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "apples")
public class Apples {
	private static Comparator comparator = new FruitComparator();
	
	@XmlElement(name = "apple", type = Apple.class)
	private List apples;
	public List getApples() {
		Collections.sort(apples, comparator);
		return apples;
	}
	
	public void setApples(Collection apples) {
		this.apples = new ArrayList(apples);
	}
}

These annotations effectively “relabel” the root element, which is the collection/list. You can experiment with this and different XML Schema mapping annotations by reading the javadocs for javax.xml.bind.annotation. Of course, it is possible to write different methods – one for XML and one for JSON – if you can’t settle on a common method signature.

A bit of web service wiring

Since I’m deploying this service to Tomcat, I’ll need a web application deployment descriptor file at src/main/webapp/WEB-INF/web.xml. Its contents will look like the following:

<?xml version="1.0"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
    <display-name>resteasy</display-name>
    <context-param>
        <param-name>javax.ws.rs.core.Application</param-name>
        <param-value>com.lyndseypadget.resteasy.FruitApplication</param-value>
    </context-param>
       
    <context-param>
        <param-name>resteasy.servlet.mapping.prefix</param-name>
        <param-value>/v1</param-value>
    </context-param>
    
    <listener>
        <listener-class>
            org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap
        </listener-class>
    </listener>
    <servlet>
        <servlet-name>Resteasy</servlet-name>
        <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Resteasy</servlet-name>
        <url-pattern>/v1/*</url-pattern>
    </servlet-mapping>
</web-app>

The servlet-name value indicates (you guessed it) the servlet (aka service) name: Resteasy. The servlet-mapping url-pattern (/v1/*) tells Tomcat to route incoming requests containing that pattern to our Resteasy service. For more information about how to construct this file, as well as the different options available, check out Tomcat’s Application Developer documentation.

Build and deploy

From your project’s root directory, you can run the following to build the WAR (web application resource) file:

mvn clean install

This will create a new folder in that directory called target, containing the WAR file. While you can use Maven or other deployment-specific tools to deploy this file, I just use a simple copy command. As a reminder, each time you redeploy a war to Tomcat, you should first stop Tomcat and delete the service application folder (in this case, <tomcatDirectory>/webapps/resteasy) and the old war file (<tomcatDirectory>/webapps/resteasy.war).

[sudo] cp target/resteasy.war <tomcatDirectory>/webapps/resteasy.war

If Tomcat is already running, it will deploy the web service immediately. If it’s not, it will be deployed the next time you start. Then, you’ll be able to reach the web service at http://<tomcatHost>:<tomcatPort>/resteasy/v1/fruits/apples. In my case, this is http://localhost:8080/resteasy/v1/fruits/apples.

[adinserter block=”33″]

Leveraging content negotiation to test the service

Content negotiation is the mechanism that makes it possible to serve different representations of a resource (a URI). At a basic level, this means is that you can:

  • specify the Accept header to indicate what kind of content you are willing to accept from the service, and/or
  • specify the Content-Type header to indicate what kind of content you are sending to the service

For further information about what you can do with content negotiation and headers, see sections 12 and 14 of RFC 2616. For the purpose of this example, all you really need to know is:

  • the @Produces annotation indicates what kind of content the method is able to produce (this will attempt to match the Accept header on the request), and
  • the @Consumes annotation indicates what kind of content the method is able to consume (this will attempt to match on the Content-Type header of the request)

If you attempt to make an HTTP call to a valid endpoint but the content can’t be negotiated – meaning no @Produces matches the Accept, or no @Consumes matches the Content-Type – you’ll get HTTP status code 415: Unsupported media type.

GET calls that return common media types can actually be entered directly into the browser. In the case of GET /apples, you’ll get XML by default:

GET calls that return common media types can actually be entered directly into the browser. In the case of GET /apples, you’ll get XML by default:

It’s more helpful, though, to use a tool like Postman, explicitly specifying the Accept header as application/xml:

It’s more helpful, though, to use a tool like Postman, explicitly specifying the Accept header as application/xml:

Both of these return some valid yet underwhelming XML – namely, an empty list of apples. But here’s something cool… Change the Accept header to application/json, and voilà! JSON just works:

Change the Accept header to application/json, and JSON just works

Beyond the read operation

You’ll tend to find many examples of RESTful web services that are Read-only, but some may not go further to show you how to handle Create, Update and Delete operations, too. While we have the skeleton of our web service in place right now, an empty list that we can’t change isn’t particularly useful. Let’s add some other methods so we can add and remove apples to the list.

package com.lyndseypadget.resteasy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import com.lyndseypadget.resteasy.model.Apple;
import com.lyndseypadget.resteasy.model.FruitComparator;
@Path("/fruits")
public class FruitService {
	private static Comparator comparator = new FruitComparator();
	private static Map apples = new TreeMap();
	private static int appleCount = 0;
	@GET
	@Path("/apples")
	@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
	public List getApples() {
		List retVal = new ArrayList(apples.values());
		Collections.sort(retVal, comparator);
		return retVal;
	}
	@GET
	@Path("/apples/{id}")
	@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
	public Response getApple(@PathParam("id") String id) {
		Apple found = apples.get(id);
		if(found == null) {
			return Response.status(404).build();
		}
		return Response.ok(found).build();
	}
	
	@DELETE
	@Path("/apples/{id}")
	public Response deleteApple(@PathParam("id") String id) {
		apples.remove(id);
		return Response.status(200).build();
	}
	
	@POST
	@Path("/apples")
	@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
	public Response createApple(Apple apple) {
		String newId = Integer.toString(++appleCount);
	
		apple.setId(newId);
		apples.put(newId, apple);
		
		return Response.status(201).header("Location", newId).build();
	}
}

We’ve added the ability to:

  • retrieve an apple by its id (return 404 if not found in the map)
  • delete an apple by its id
  • create a new apple (return a 201 if successful)

These methods provide enough functionality to ensure that the service is working as intended. Implementing the ability to update an apple (using @PUT and/or @PATCH) – as well as more endpoints, logic, and persistence – are left as exercises for the reader.

If we build and deploy again (see “Build and deploy” above if using the assumed Maven/Tomcat setup), we’ll see that we can now create, retrieve and delete apples from our service. Calls can alternate between XML and JSON without any reconfigurations on the server.

Creating an apple with a Content-Type of “application/json” and a JSON body:

Creating an apple with a Content-Type of “application/json” and a JSON body

Another example: creating an apple with a Content-Type of “application/xml” and an XML body:

creating an apple with a Content-Type of “application/xml” and an XML body

Retrieving all apples in XML:

getAllApples.png

Retrieving apple 2 by id, in JSON:

Retrieving apple 2 by id, in JSON

Deleting apple 1 by id:

Deleting apple 1 by id

Retrieving all apples in JSON:

getAllApplesJSON.png

Conclusion

We’ve explored how RESTEasy can help you seamlessly support both XML and JSON in a Java web service. I also explained the technical differences between REST, media types, web services and microservices, as there tends to be a lot of grey area between these terms.

The example we built here is a bit contrived; I’ve never really needed to work with fruit data, but then again, I’ve never worked in the grocery industry! That said, I think it helps illustrate the right “size” for a microservice, as you can imagine how other microservices such as vegetables, canned goods or seafood, in this example, could collectively comprise a food distribution system. Food distribution in the real world is actually extremely complicated; a system attempting to model it would have to account for concepts such as sales, coupons, expiration dates, nutrition information, and so on.

Of course, there are different ways you could slice this, but RESTEasy is a handy tool to have in your toolbox when you need to support multiple media types in a quick and lightweight fashion.

Don’t forget to continually improve your Java application by writing better code with Stackify Prefix, the free dynamic code profiler, and Stackify Retrace, the only full lifecycle APM.

Improve Your Code with Retrace APM

Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.

Learn More

Want to contribute to the Stackify blog?

If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]