JAX-RS

Overview

JAX-RS, Jakarta RESTful Web Services, is an API for creating RESTful Java applications and Jersey is the reference implementation, Happy learning.

Resources

Resources are POJOs annotated with the Path annotation. Here is a resource that does nothing:

@Path("resources")
public class MyResource {
}

Paths and PathParams

Paths can have variables embed in the URIs, which makes them powerful. Following is an example where variable name is retrieved from the path:

@Path("resources")
public class MyResource {
    @GET
    @Path("greet/{name}")
    @Produces(MediaType.TEXT_PLAIN)
    public String hello(@PathParam("name") String name) {
        return "Hello " + name;
    }
}

HTTP Methods

Methods in resource classes are annotated with GET, POST, DELETE and so on for finding the matching method to accept the request. The example above is an example of a GET endpoint.

Producing and Consuming MediaTypes

Both annotations, Produces and Consumes can be applied at class and method levels, and used with a MediaType. These annotations can be used to narrow down the matching of methods and transforing DTOs to appropriate types by an appropriate MessageBodyWriter implementation or a MessageBodyReader implementation.

Working with XML

The following example works with XML out of the box with Jersey, without the need of any additional libraries.

Considering the following dependencies:

<dependencies>
    <dependency>
      <groupId>jakarta.ws.rs</groupId>
      <artifactId>jakarta.ws.rs-api</artifactId>
    </dependency>
    <dependency>
      <groupId>org.glassfish.jersey.core</groupId>
      <artifactId>jersey-server</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.glassfish.jersey.containers</groupId>
      <artifactId>jersey-container-servlet</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.glassfish.jersey.inject</groupId>
      <artifactId>jersey-hk2</artifactId>
      <scope>runtime</scope>
    </dependency>
</dependencies>

A very simple Employee modal:

@XmlRootElement
@Getter
@Setter
public class Employee {
    String name;
}

and an EmployeeResource:

@Path("employee")
public class EmployeeResource {
    
    private static Employee onlyEmployee;
  
    @POST
    @Consumes(MediaType.APPLICATION_XML)
    public void addEmployee(Employee employee) {
      onlyEmployee = employee;
    }
  
    @GET
    @Produces(MediaType.APPLICATION_XML)
    public Employee getEmployee() {
      return onlyEmployee;
    }

}

This works out of the box, since it seems like jersey-server has a dependency to jersey-media-jaxb which provides MessageBodyReaders and MessageBodyWriters for XML that implements JAXB. When I exclude this dependency:

<dependency>
    <groupId>org.glassfish.jersey.core</groupId>
    <artifactId>jersey-server</artifactId>
    <version>2.29.1</version>
    <scope>runtime</scope>
    <exclusions>
      <exclusion>
        <groupId>org.glassfish.jersey.media</groupId>
        <artifactId>jersey-media-jaxb</artifactId>
      </exclusion>
    </exclusions>
</dependency>

I started getting the following error:

MessageBodyWriter not found for media type=application/xml

Working with JSON

For whatever reason, Jersey seems to not include any writers that would work with JSON out of the box. However, once the following dependency is included:

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
    <version>2.29.1</version>
    <scope>runtime</scope>
</dependency>

the very same example starts working with JSON format by changing the MediaType to (MediaType.APPLICATION_JSON). At this point the XmlRootElement annotation is also not needed anymore.

A Note On MediaTypes

There is a lot more going on with MediaTypes, such as being able to consume or produce different MediaTypes from a single method, based on the preference of the client. For further details please see the official documentation.

Query Parameters

QueryParam annotation can be used to capture, well the query parameters from the URI. Here is an example:

@Path("query")
public class QueryResource {
    
    private static final String JOHN_DOE = "John Doe";
  
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String greet(@QueryParam("name") @DefaultValue(JOHN_DOE) String s) {
      return greetPerson(s.isEmpty() ? JOHN_DOE : s);
    }
  
    private String greetPerson(String s) {
      return String.format("Greetings %s\n", s);
    }

}

and a few runs:

kt$ curl -i http://localhost:8080/api/query?name=Koray
# HTTP/1.1 200 OK
# Date: Sun, 24 Nov 2019 02:52:26 GMT
# Content-Type: text/plain
# Content-Length: 16
# Server: Jetty(9.4.22.v20191022)
# 
# Greetings Koray

kt$ curl -i http://localhost:8080/api/query?name=
# HTTP/1.1 200 OK
# Date: Sun, 24 Nov 2019 02:52:50 GMT
# Content-Type: text/plain
# Content-Length: 19
# Server: Jetty(9.4.22.v20191022)

# Greetings John Doe

kt$ curl -i http://localhost:8080/api/query
# HTTP/1.1 200 OK
# Date: Sun, 24 Nov 2019 02:52:57 GMT
# Content-Type: text/plain
# Content-Length: 19
# Server: Jetty(9.4.22.v20191022)
# 
# Greetings John Doe

The official Jersey documentation has very clear and detailed information on the rest of the lesser used possibitiles such as MatrixParam, HeadParam, CookieParam, FormParam, and my personal favorite BeanParam.

Sub-Resources

Sub-resources can be used to further organize the design, if there are several method-level resources in a resource class that all are a part of a particular path.

@Path("myResource")
public class MyResource {
    @Path("sub")
    public MySubResource greet() {
      return new MySubResource();
    }
}

public class MySubResource {
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
      return "Hello from SubResource!\n";
    }
}

And here is a sample invocation:

kt$ curl -i http://localhost:8080/api/myResource/sub
# HTTP/1.1 200 OK
# Date: Sun, 24 Nov 2019 03:11:22 GMT
# Content-Type: text/plain
# Content-Length: 24
# Server: Jetty(9.4.22.v20191022)
# 
# Hello from SubResource!

Representations and Responses

So far we have been returning 200 OK in our responses, but we can do better. Following is an example that returns a 201 Created:

@Path("employee")
public class EmployeeResource {

    static List<Employee> employees = new ArrayList<>();

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Path("{name}")
    public Response createEmployee(@PathParam("name") String name) {
        Employee e = new Employee(employees.size() + 1, name);
        employees.add(e);
        return Response.status(Response.Status.CREATED).entity(e).build();
    }

}

And an example invocation:

kt$ curl -i -X POST http://localhost:8080/api/employee/koray
# HTTP/1.1 201 Created
# Date: Sun, 24 Nov 2019 23:18:05 GMT
# Content-Type: application/json
# Content-Length: 23
# Server: Jetty(9.4.22.v20191022)
# 
# {"id":1,"name":"koray"}

Response building provides other functionality such as adding header values in responses, setting the entity tag and last modified date of the representation.

Response Builder Shortcuts and the Location Header

The Response class has some shortcuts, here is an example:

@Path("employee")
public class EmployeeResource {

    static List<Employee> employees = new ArrayList<>();

    @POST
    @Path("{name}")
    public Response createEmployee(@PathParam("name") String name,
                                   @Context UriInfo uriInfo) {
        Employee e = new Employee(employees.size() + 1, name);
        employees.add(e);
        UriBuilder path = uriInfo
                .getBaseUriBuilder()
                .path(EmployeeResource.class)
                .path(valueOf(e.getId()));
        return Response.created(path.build()).build();
    }

    @GET
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    // We could have returned the Employee directly instead of a Response
    // The default code is 200 already
    public Response employee(@PathParam("id") int id) {
        return Response
                .ok(employees.get(id - 1))
                .build();
    }

}

with a sample run:

kt$ curl -i -X POST  http://localhost:8080/api/employee/koray
# HTTP/1.1 201 Created
# Date: Mon, 25 Nov 2019 02:21:45 GMT
# Location: http://localhost:8080/api/employee/1
# Content-Length: 0
# Server: Jetty(9.4.22.v20191022)

kt$ curl -i http://localhost:8080/api/employee/1
# HTTP/1.1 200 OK
# Date: Mon, 25 Nov 2019 02:21:54 GMT
# Content-Type: application/json
# Content-Length: 23
# Server: Jetty(9.4.22.v20191022)

# {"id":1,"name":"koray"}

Things to note in the example above:

Make sure to check other shortcuts such as: accepted and noContent.

JAX-RS in Client Side

JAX-RS also provides an API for the client side, i.e. for making requests. The following example demonstrates several features of JAX-RS in client side including:

public class RestApiClient {

    static String BASE_URL = "http://localhost:8080/api";
    
    static WebTarget BASE_TARGET = ClientBuilder.newClient().target(BASE_URL);

    static WebTarget MESSAGES = BASE_TARGET.path("messages");

    static WebTarget BY_ID = MESSAGES.path("{id}");

    public static void main(String[] args) {
        Message message = new Message();
        message.setAuthor("koraytugay");
        message.setContent("From JAX-RS client!");

        Response response = MESSAGES.request().post(Entity.json(message));
        response = MESSAGES.request(MediaType.APPLICATION_JSON).get();

        // Handling Generic Types, such as List<Message>
        List<Message> messages = 
            response.readEntity(new GenericType<List<Message>>() {});
        
        // resolveTemplate example
        message = BY_ID.resolveTemplate("id", 1)
                       .request(MediaType.APPLICATION_JSON)
                       .get(Message.class);
    }
}    

Invocations

JAX-RS also has a concept that is called invocations: where you prepare an object that is ready to make the request, that includes all the requiered information, which can be invoked at any time.

Here is an example:

public Invocation getMessageInvocation(int id) {
    Invocation byId = BY_ID.resolveTemplate("id", 1)
                           .request(MediaType.APPLICATION_JSON)
                           .buildGet();
    return byId;  // Call anytime with byId.invoke();
}

Further Topics To Study

I have examples to above topics in JAX-RS Playground listed in the References section.

References and Useful Resources


🏠