Developing RESTful Web Services with JAX-RS (Jersey)
Java, Maven, Jersey, JAX-RS, REST, Java EE
The REST architectural style describes six constraints. These constraints, applied to the architecture, were originally communicated by Roy Fielding in his doctoral dissertation and defines the basis of RESTful-style.
Introduction
REST stands for REpresentational State Transfer. The REST architectural style describes six constraints. These constraints, applied to the architecture, were originally communicated by Roy Fielding in his doctoral dissertation and defines the basis of RESTful-style.
The six constraints are: (click on the constraint below to read more)
JAX-RS (JSR-339) as a specification defines a set of Java APIs for the development of Web services built according to the REpresentational State Transfer (REST) architectural style.
Jersey RESTful Web Services framework is an open source, production quality, framework for developing RESTful Web Services in Java that provides support for JAX-RS APIs and serves as a JAX-RS (JSR 311 & JSR 339) Reference Implementation.
Project Structure
At the end of this guide our folder structure will look similar to the following:
.
|__src/
| |__main/
| | |__java/
| | | |__com/
| | | | |__juliuskrah
| | | | | |__App.java
| | | | | |__Resource.java
| | | | | |__ResourceService.java
|__pom.xml
Prerequisites
To follow along this guide, you should have the following set up:
Optional
Creating Project Template
To create the project structure, we would use the Maven Archetype
to generate a project skeleton. In the directory
where you want to generate your project, run the following commands:
mvn archetype:generate
You will get a list of archetypes to select from. Select 1002: org.apache.maven.archetypes:maven-archetype-quickstart (An archetype which contains a sample Maven project.)
maven quickstart archetype. This is a simple maven project with just one class.
...
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): 1002:
Type 1002
and hit enter.
...
Choose org.apache.maven.archetypes:maven-archetype-quickstart version:
1: 1.0-alpha-1
2: 1.0-alpha-2
3: 1.0-alpha-3
4: 1.0-alpha-4
5: 1.0
6: 1.1
Choose a number: 6:
Hit enter to accept the default of 1.1
.
...
Define value for property 'groupId':
Type group Id e.g. com.juliuskrah
and hit enter
...
Define value for property 'artifactId':
Type artifact Id e.g. rest-example
and hit enter
...
Define value for property 'version' 1.0-SNAPSHOT: :
Hit enter to accept 1.0-SNAPSHOT
and proceed
...
Define value for property 'package' com.juliuskrah: :
Hit enter to accept default and proceed
...
Confirm properties configuration:
groupId: com.juliuskrah
artifactId: rest-example
version: 1.0-SNAPSHOT
package: com.juliuskrah
Y: :
Review your selection and hit enter to proceed. At this stage your project is generated in rest-example
.
Setting up dependencies
To build a RESTful webservice with Jersey, we must add the Jersey dependencies to our pom.xml
:
file:
pom.xml
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.glassfish.jersey</groupId>
<artifactId>jersey-bom</artifactId>
<version>2.26-b07</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-netty-http</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
</dependency>
</dependencies>
In the snippet above we have specified the dependencyManagement
so that we can omit the version of
jersey-container-netty-http
.
What we will Create
HTTP Method | URI | Action |
---|---|---|
GET | /api/v1.0/resources/ |
Retrieve list of resources |
GET | /api/v1.0/resources/[resource_id] |
Retrieve a resource |
POST | /api/v1.0/resources/ |
Create a new resource |
PUT | /api/v1.0/resources/[resource_id] |
Update an existing resource |
DELETE | /api/v1.0/resources/[resource_id] |
Delete a resource |
The HTTP GET method is used to read (or retrieve) a representation of a resource. In the “happy”
(or non-error)
path, GET returns a representation in XML
or JSON
and an HTTP response code of 200
(OK). In an error case, it
most often returns a 404
(NOT FOUND) or 400
(BAD REQUEST).
According to the design of the HTTP specification, GET (along with HEAD) requests are used only to read data and not
change it. Therefore, when used this way, they are considered safe. That is, they can be called without risk of data
modification or corruption — calling it once has the same effect as calling it 10 times, or none at all. Additionally,
GET (and HEAD) is idempotent, which means that making multiple identical requests ends up having the same result as a
single request.
Do not expose unsafe operations via GET — it should never modify any resources on the server.
Examples:
- GET http://www.example.com/resources/1
- GET http://www.example.com/resources/1/items
The POST verb is most-often utilized to create new resources. In particular, it’s used to create subordinate
resources. That is, subordinate to some other (e.g. parent) resource. In other words, when creating a new resource,
POST to the parent and the service takes care of associating the new resource with the parent, assigning an ID (new
resource URI), etc.
On successful creation, return HTTP status 201
, returning a Location
header with a link to the newly-created
resource with the 201 HTTP status.
POST is neither safe nor idempotent. It is therefore recommended for non-idempotent resource requests. Making two
identical POST requests will most-likely result in two resources containing the same information.
Examples:
- POST http://www.example.com/resources
- POST http://www.example.com/resources/1/items
PUT is most-often utilized for update capabilities, PUT-ing to a known resource URI with the request body
containing the newly-updated representation of the original resource.
However, PUT can also be used to create a resource in the case where the resource ID is chosen by the client instead
of by the server. In other words, if the PUT is to a URI that contains the value of a non-existent resource ID. Again,
the request body contains a resource representation. Many feel this is convoluted and confusing. Consequently, this
method of creation should be used sparingly, if at all.
Alternatively, use POST to create new resources and provide the client-defined ID in the body
representation—presumably to a URI that doesn’t include the ID of the resource (see POST above).
On successful update, return 200
(or 204
if not returning any content in the body) from a PUT. If using PUT for
create, return HTTP status 201
on successful creation. A body in the response is optional — providing one consumes
more bandwidth. It is not necessary to return a link via a Location header in the creation case since the client
already set the resource ID.
PUT is not a safe operation, in that it modifies (or creates) state on the server, but it is idempotent. In other
words, if you create or update a resource using PUT and then make that same call again, the resource is still there
and still has the same state as it did with the first call.
If, for instance, calling PUT on a resource increments a counter within the resource, the call is no longer
idempotent. Sometimes that happens and it may be enough to document that the call is not idempotent. However, it’s
recommended to keep PUT requests idempotent. It is strongly recommended to use POST for non-idempotent requests.
Examples:
- PUT http://www.example.com/resources/1
- PUT http://www.example.com/resources/1/items/1
DELETE is pretty easy to understand. It is used to delete a resource identified by a URI.
On successful deletion, return HTTP status 200
(OK) along with a response body, perhaps the representation of the
deleted item (often demands too much bandwidth), or a wrapped response. Either that or return HTTP status 204
(NO
CONTENT) with no response body. In other words, a 204
status with no body, or the JSEND-style response and HTTP
status 200
are the recommended responses.
HTTP-spec-wise, DELETE operations are idempotent. If you DELETE a resource, it’s removed. Repeatedly calling DELETE
on that resource ends up the same: the resource is gone. If calling DELETE say, decrements a counter (within the
resource), the DELETE call is no longer idempotent. As mentioned previously, usage statistics and measurements may be
updated while still considering the service idempotent as long as no resource data is changed. Using POST for
non-idempotent resource requests is recommended.
There is a caveat about DELETE idempotence, however. Calling DELETE on a resource a second time will often return a
404
(NOT FOUND) since it was already removed and therefore is no longer findable. This, by some opinions, makes
DELETE operations no longer idempotent, however, the end-state of the resource is the same. Returning a 404 is
acceptable and communicates accurately the status of the call.
Examples:
- DELETE http://www.example.com/resources/1
- DELETE http://www.example.com/resources/1/items
Building Resources
We will create a POJO
to represent our REST resource.
file:
src/main/java/com/juliuskrah/Resource.java
@XmlRootElement
public class Resource {
private Long id;
private String description;
private LocalDateTime createdTime;
private LocalDateTime modifiedTime;
// Default constructor
// All Args constructor
// Getters and Setters omitted for brevity
}
The Resource POJO
above consists of self explanatory feilds. The @XmlRootElement
is put on the class to instruct
jersey on how to represent the resource as XML.
The next thing is to wire up a ResourceService
.
file:
src/main/java/com/juliuskrah/ResourceService.java
@Path("/api/v1.0/resources")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public class ResourceService {
private static List<Resource> resources = null;
static {
resources = new ArrayList<>();
resources.add(new Resource(1L, "Resource One", LocalDateTime.now(), null));
resources.add(new Resource(2L, "Resource Two", LocalDateTime.now(), null));
resources.add(new Resource(3L, "Resource Three", LocalDateTime.now(), null));
resources.add(new Resource(4L, "Resource Four", LocalDateTime.now(), null));
resources.add(new Resource(5L, "Resource Five", LocalDateTime.now(), null));
resources.add(new Resource(6L, "Resource Six", LocalDateTime.now(), null));
resources.add(new Resource(7L, "Resource Seven", LocalDateTime.now(), null));
resources.add(new Resource(8L, "Resource Eight", LocalDateTime.now(), null));
resources.add(new Resource(9L, "Resource Nine", LocalDateTime.now(), null));
resources.add(new Resource(10L, "Resource Ten", LocalDateTime.now(), null));
}
/**
* GET /api/v1.0/resources : get all resources.
*
* @return the {@code List<Resource>} of resources with status code 200 (OK)
*/
@GET
public List<Resource> getResources() {
return resources;
}
...
}
In an actual production ready application, you will connect to a database. For the purpose of this tutorial, we will use a
static
list.
In the ResourceService
we have specified the root context path we are going to access the service
(/api/v1.0/resources
).
In the same service class we have also created a GET
endpoint which returns a list of all resources available
on the server. The resource will be represented as JSON
or XML
identified by
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
.
We will setup a Netty
server to serve our requests.
file:
src/main/java/com/juliuskrah/App.java
public class App {
public static void main(String... cmd) throws IOException, InterruptedException {
URI baseUri = UriBuilder.fromUri("http://localhost/").port(8080).build();
ResourceConfig resourceConfig = new ResourceConfig().packages("com.juliuskrah");
Channel server = NettyHttpContainerProvider.createServer(baseUri, resourceConfig, false);
System.out.println("Press ENTER to terminate...");
System.in.read();
server.close().await();
}
}
With the server setup we can test our REST resource. Start the server by running the following command:
mvn clean compile exec:java -Dexec.mainClass="com.juliuskrah.App"
With the Netty server up and running, open another shell window and execute the following cURL
command:
> curl -i -H "Accept: application/json" http://localhost:8080/api/v1.0/resources
HTTP/1.1 200 OK
Content-Type: application/json
content-length: 991
connection: keep-alive
[
{
"id": 1,
"description": "Resource One",
"createdTime": "2017-07-13T22:36:28.384",
"modifiedTime": null
},
{
"id": 2,
"description": "Resource Two",
"createdTime": "2017-07-13T22:36:28.384",
"modifiedTime": null
},
{
"id": 3,
"description": "Resource Three",
"createdTime": "2017-07-13T22:36:28.384",
"modifiedTime": null
},
{
"id": 4,
"description": "Resource Four",
"createdTime": "2017-07-13T22:36:28.384",
"modifiedTime": null
},
{
"id": 5,
"description": "Resource Five",
"createdTime": "2017-07-13T22:36:28.384",
"modifiedTime": null
},
{
"id": 6,
"description": "Resource Six",
"createdTime": "2017-07-13T22:36:28.384",
"modifiedTime": null
},
{
"id": 7,
"description": "Resource Seven",
"createdTime": "2017-07-13T22:36:28.384",
"modifiedTime": null
},
{
"id": 8,
"description": "Resource Eight",
"createdTime": "2017-07-13T22:36:28.384",
"modifiedTime": null
},
{
"id": 9,
"description": "Resource Nine",
"createdTime": "2017-07-13T22:36:28.384",
"modifiedTime": null
},
{
"id": 10,
"description": "Resource Ten",
"createdTime": "2017-07-13T22:36:28.384",
"modifiedTime": null
}
]
We can request for the same resource represented as XML
:
> curl -i -H "Accept: application/xml" http://localhost:8080/api/v1.0/resources
HTTP/1.1 200 OK
Content-Type: application/xml
content-length: 1288
connection: keep-alive
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<resources>
<resource>
<createdTime>2017-07-13T22:36:28.384</createdTime>
<description>Resource One</description>
<id>1</id>
</resource>
<resource>
<createdTime>2017-07-13T22:36:28.384</createdTime>
<description>Resource Two</description>
<id>2</id>
</resource>
<resource>
<createdTime>2017-07-13T22:36:28.384</createdTime>
<description>Resource Three</description>
<id>3</id>
</resource>
<resource>
<createdTime>2017-07-13T22:36:28.384</createdTime>
<description>Resource Four</description>
<id>4</id>
</resource>
<resource>
<createdTime>2017-07-13T22:36:28.384</createdTime>
<description>Resource Five</description>
<id>5</id>
</resource>
<resource>
<createdTime>2017-07-13T22:36:28.384</createdTime>
<description>Resource Six</description>
<id>6</id>
</resource>
<resource>
<createdTime>2017-07-13T22:36:28.384</createdTime>
<description>Resource Seven</description>
<id>7</id>
</resource>
<resource>
<createdTime>2017-07-13T22:36:28.384</createdTime>
<description>Resource Eight</description>
<id>8</id>
</resource>
<resource>
<createdTime>2017-07-13T22:36:28.384</createdTime>
<description>Resource Nine</description>
<id>9</id>
</resource>
<resource>
<createdTime>2017-07-13T22:36:28.384</createdTime>
<description>Resource Ten</description>
<id>10</id>
</resource>
</resources>
Let us write the REST operation for getting a specific resource /api/v1.0/resources/[resource_id]
:
file:
src/main/java/com/juliuskrah/ResourceService.java
...
/**
* GET /api/v1.0/resources/:id : get the resource specified by the identifier.
*
* @param id the id to the resource being looked up
* @return the {@code Resource} with status 200 (OK) and body or status 404 (NOT FOUND)
*/
@GET
@Path("{id: [0-9]+}")
public Resource getResource(@PathParam("id") Long id) {
Resource resource = new Resource(id, null, null, null);
// Search the Resource list for a resource with the given id and return its index in the list
int index = Collections.binarySearch(resources, resource, Comparator.comparing(Resource::getId));
if (index >= 0)
return resources.get(index);
else
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
The @Path
annotation takes a variable (denoted by {
and }
) passed by the client, which is interpreted by Jersey
in the @PathParam
and cast to a Long
as id
. The : [0-9]+
is a regular expression which constraint the client
to use only positive whole numbers otherwise return 404
to the client.
If the client passes the path parameter in the format the server accepts, the id
of the resource will be searched
from within the static
resources field. If it exist return the response to the client or else return a 404
.
Test this resource by running:
> curl -i -H "Accept: application/json" http://localhost:8080/api/v1.0/resources/1
HTTP/1.1 200 OK
Content-Type: application/json
content-length: 96
connection: keep-alive
{
"id":1,
"description": "Resource One",
"createdTime": "2017-07-14T23:55:18.76",
"modifiedTime": null
}
Now let us write our POST
method that creates a new resource:
file:
src/main/java/com/juliuskrah/ResourceService.java
...
/**
* POST /api/v1.0/resources : creates a new resource.
*
* @param resource the resource being sent by the client as payload
* @return the {@code Resource} with status 201 (CREATED) and no -content or status
* 400 (BAD REQUEST) if the resource does not contain an Id or status
* 409 (CONFLICT) if the resource being created already exists in the list
*/
@POST
public Response createResource(Resource resource) {
if (Objects.isNull(resource.getId()))
throw new WebApplicationException(Response.Status.BAD_REQUEST);
int index = Collections.binarySearch(resources, resource, Comparator.comparing(Resource::getId));
if (index < 0) {
resource.setCreatedTime(LocalDateTime.now());
resources.add(resource);
return Response
.status(Response.Status.CREATED)
.location(URI.create(String.format("/api/v1.0/resources/%s", resource.getId())))
.build();
} else
throw new WebApplicationException(Response.Status.CONFLICT);
}
What is happening in the above snippet is a POST
request that returns a 201
status code and a Location
header
with the location of the newly created resource.
If the resource being created already exists on the server an error code of 409
is returned by the server.
> curl -i -X POST -H "Content-Type: application/json" -d "{ \"id\": 87, \"description\": \"Resource Eighty-Seven\"}" http://localhost:8080/api/v1.0/resources
HTTP/1.1 201 Created
Location: http://localhost:8080/api/v1.0/resources/87
content-length: 0
connection: keep-alive
For those not using Windows, you should omit the escape \
.
The remaining two methods of our webservice is shown below:
file:
src/main/java/com/juliuskrah/ResourceService.java
...
/**
* PUT /api/v1.0/resources/:id : update a resource identified by the given id
*
* @param id the identifier of the resource to be updated
* @param resource the resource that contains the update
* @return the {@code Resource} with a status code 204 (NO CONTENT) or status code
* 404 (NOT FOUND) when the resource being updated cannot be found
*/
@PUT
@Path("{id: [0-9]+}")
public Response updateResource(@PathParam("id") Long id, Resource resource) {
resource.setId(id);
int index = Collections.binarySearch(resources, resource, Comparator.comparing(Resource::getId));
if (index >= 0){
Resource updatedResource = resources.get(index);
updatedResource.setModifiedTime(LocalDateTime.now());
updatedResource.setDescription(resource.getDescription());
resources.set(index, updatedResource);
return Response
.status(Response.Status.NO_CONTENT)
.build();
} else
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
/**
* DELETE /api/v1.0/resources/:id : delete a resource identified by the given id
*
* @param id the identifier of the resource to be deleted
* @return the {@code Response} with a status code of 204 (NO CONTENT) or status code
* 404 (NOT FOUND) when there is no resource with the given identifier
*/
@DELETE
@Path("{id: [0-9]+}")
public Response deleteResource(@PathParam("id") Long id) {
Resource resource = new Resource(id, null, null, null);
int index = Collections.binarySearch(resources, resource, Comparator.comparing(Resource::getId));
if (index >= 0) {
resources.remove(index);
return Response
.status(Response.Status.NO_CONTENT)
.build();
} else
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
Test the PUT
endpoint with the following command:
> curl -i -X PUT -H "Content-Type: application/json" -d "{\"description\": \"Resource One Modified\"}" http://localhost:8080/api/v1.0/resources/1
HTTP/1.1 204 No Content
content-length: 0
connection: keep-alive
Test DELETE
endpoint with the following command:
> curl -i -X DELETE http://localhost:8080/api/v1.0/resources/1
HTTP/1.1 204 No Content
content-length: 0
connection: keep-alive
Deploying to Heroku
To deploy this application to Heroku, we must ensure we have a Heroku account and Heroku CLI
. Navigate to
the root directory of your application and execute the following command from your terminal:
> heroku create
Creating app... done, murmuring-shore-59920
https://murmuring-shore-59920.herokuapp.com/ | https://git.heroku.com/murmuring-shore-59920.git
This creates a heroku app called murmuring-shore-59920
. Heroku defines an environment variable $PORT
which is
the HTTP port exposed over firewall for HTTP traffic. We will modify our source file to read this variable:
file:
src/main/java/com/juliuskrah/App.java
public static void main(String... cmd) throws IOException, InterruptedException {
int port = Objects.nonNull(System.getenv("PORT")) ?
Integer.valueOf(System.getenv("PORT")) : 8080;
URI baseUri = UriBuilder.fromUri("http://localhost/").port(port).build();
ResourceConfig resourceConfig = new ResourceConfig().packages("com.juliuskrah");
NettyHttpContainerProvider.createServer(baseUri, resourceConfig, false);
System.out.printf("Application running on %s\n", baseUri.toURL().toExternalForm());
}
Heroku needs to be able to run our web service with all the dependencies. We add the maven dependency plugin to our project:
file:
pom.xml
...
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<includeScope>compile</includeScope>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
Next create a Procfile
. This is a plain text file called Procfile
not procfile
or
procfile.txt
but just Procfile
.
In this file we will create a web
process type. The web
process type is a special process type that listens for
HTTP traffic.
file:
Procfile
web: java -cp target/classes:target/dependency/* com.juliuskrah.App
The application is ready to be deployed to Heroku with git add . && git push heroku master
. But first let us
test the application locally to make sure everything works. To test locally create an .env
file:
file:
.env
PORT=2222
This creates an environment variable for local testing:
> mvn clean package
> heroku local web
Now that we are confident everything works, we will deploy to heroku:
> git add .
> git commit -am "Added heroku app"
> git push heroku master
Counting objects: 137, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (93/93), done.
Writing objects: 100% (137/137), 72.61 KiB | 0 bytes/s, done.
Total 137 (delta 42), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Java app detected
remote: -----> Installing OpenJDK 1.8... done
remote: -----> Executing: mvn -DskipTests clean dependency:list install
...
remote: [INFO] ------------------------------------------------------------------------
remote: [INFO] BUILD SUCCESS
remote: [INFO] ------------------------------------------------------------------------
remote: [INFO] Total time: 13.305 s
remote: [INFO] Finished at: 2017-08-01T23:39:57Z
remote: [INFO] Final Memory: 31M/342M
remote: [INFO] ------------------------------------------------------------------------
remote: -----> Discovering process types
remote: Procfile declares types -> web
remote:
remote: -----> Compressing...
remote: Done: 82.4M
remote: -----> Launching...
remote: Released v3
remote: https://murmuring-shore-59920.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/murmuring-shore-59920.git
* [new branch] origin -> master
We scale up a dyno with the following command:
> heroku ps:scale web=1
Scaling dynos... done, now running web at 1:Free
> heroku open
The last command opens the heroku app in your default browser. If you get a 404
, that is normal because you
haven’t mapped any resource to /
. Just append the url with /api/v1.0/resources
to start hacking.
Conclusion
In this post we learned REST and touched a little on the constraints. We learned how it is HTTP
based.
We also talked about the basic methods
in HTTP and how to leverage them in a RESTful application. We also learned
how to deploy a REST application to Heroku.
As usual you can find the full example to this guide in the github repository. Until the next post, keep doing cool things .