1. Overview
This tutorial introduces Apache CXF as a framework compliant with the JAX-RS standard, which defines support of the Java ecosystem for the REpresentational State Transfer (REST) architectural pattern.
Specifically, it describes step by step how to construct and publish a RESTful web service, and how to write unit tests to verify a service.
This is the third in a series on Apache CXF; the first one focuses on the usage of CXF as a JAX-WS fully compliant implementation. The second article provides a guide on how to use CXF with Spring.
2. Maven Dependencies
The first required dependency is *org.apache.cxf:cxf-rt-frontend-*jaxrs. This artifact provides JAX-RS APIs as well as a CXF implementation:
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxrs</artifactId>
<version>3.1.7</version>
</dependency>
In this tutorial, we use CXF to create a Server endpoint to publish a web service instead of using a servlet container. Therefore, the following dependency needs to be included in the Maven POM file:
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http-jetty</artifactId>
<version>3.1.7</version>
</dependency>
Finally, let’s add the HttpClient library to facilitate unit tests:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
Here you can find the latest version of the cxf-rt-frontend-jaxrs dependency. You may also want to refer to this link for the latest versions of the org.apache.cxf:cxf-rt-transports-http-jetty artifacts. Finally, the latest version of httpclient can be found here.
3. Resource Classes and Request Mapping
Let’s start implementing a simple example; we’re going to set up our REST API with two resources Course and Student.
We’ll start simple and move towards a more complex example as we go.
3.1. The Resources
Here is the definition of the Student resource class:
@XmlRootElement(name = "Student")
public class Student {
private int id;
private String name;
// standard getters and setters
// standard equals and hashCode implementations
}
Notice we’re using the @XmlRootElement annotation to tell JAXB that instances of this class should be marshaled to XML.
Next, comes the definition of the Course resource class:
@XmlRootElement(name = "Course")
public class Course {
private int id;
private String name;
private List<Student> students = new ArrayList<>();
private Student findById(int id) {
for (Student student : students) {
if (student.getId() == id) {
return student;
}
}
return null;
}
// standard getters and setters
// standard equals and hasCode implementations
}
Finally, let’s implement the CourseRepository – which is the root resource and serves as the entry point to web service resources:
@Path("course")
@Produces("text/xml")
public class CourseRepository {
private Map<Integer, Course> courses = new HashMap<>();
// request handling methods
private Course findById(int id) {
for (Map.Entry<Integer, Course> course : courses.entrySet()) {
if (course.getKey() == id) {
return course.getValue();
}
}
return null;
}
}
Notice the mapping with the @Path annotation. The CourseRepository is the root resource here, so it’s mapped to handle all URLS starting with course.
The value of @Produces annotation is used to tell the server to convert objects returned from methods within this class to XML documents before sending them to clients. We’re using JAXB here as the default since no other binding mechanisms are specified.
3.2. Simple Data Setup
Because this is a simple example implementation, we’re using in-memory data instead of a full-fledged persistent solution.
With that in mind, let’s implement some simple setup logic to populate some data into the system:
{
Student student1 = new Student();
Student student2 = new Student();
student1.setId(1);
student1.setName("Student A");
student2.setId(2);
student2.setName("Student B");
List<Student> course1Students = new ArrayList<>();
course1Students.add(student1);
course1Students.add(student2);
Course course1 = new Course();
Course course2 = new Course();
course1.setId(1);
course1.setName("REST with Spring");
course1.setStudents(course1Students);
course2.setId(2);
course2.setName("Learn Spring Security");
courses.put(1, course1);
courses.put(2, course2);
}
Methods within this class that take care of HTTP requests are covered in the next subsection.
3.3. The API – Request Mapping Methods
Now, let’s go to the implementation of the actual REST API.
We’re going to start adding API operations – using the @Path annotation – right in the resource POJOs.
It’s important to understand that is a significant difference from the approach in a typical Spring project – where the API operations would be defined in a controller, not on the POJO itself.
Let’s start with mapping methods defined inside the Course class:
@GET
@Path("{studentId}")
public Student getStudent(@PathParam("studentId")int studentId) {
return findById(studentId);
}
Simply put, the method is invoked when handling GET requests, denoted by the @GET annotation.
Noticed the simple syntax of mapping the studentId path parameter from the HTTP request.
We’re then simply using the findById helper method to return the corresponding Student instance.
The following method handles POST requests, indicated by the @POST annotation, by adding the received Student object to the students list:
@POST
@Path("")
public Response createStudent(Student student) {
for (Student element : students) {
if (element.getId() == student.getId() {
return Response.status(Response.Status.CONFLICT).build();
}
}
students.add(student);
return Response.ok(student).build();
}
This returns a 200 OK response if the create operation was successful, or 409 Conflict if an object with the submitted id is already existent.
Also note that we can skip the @Path annotation since its value is an empty String.
The last method takes care of DELETE requests. It removes an element from the students list whose id is the received path parameter and returns a response with OK (200) status. In case there are no elements associated with the specified id, which implies there is nothing to be removed, this method returns a response with Not Found (404) status:
@DELETE
@Path("{studentId}")
public Response deleteStudent(@PathParam("studentId") int studentId) {
Student student = findById(studentId);
if (student == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
students.remove(student);
return Response.ok().build();
}
Let’s move on to request mapping methods of the CourseRepository class.
The following getCourse method returns a Course object that is the value of an entry in the courses map whose key is the received courseId path parameter of a GET request. Internally, the method dispatches path parameters to the findById helper method to do its job.
@GET
@Path("courses/{courseId}")
public Course getCourse(@PathParam("courseId") int courseId) {
return findById(courseId);
}
The following method updates an existing entry of the courses map, where the body of the received PUT request is the entry value and the courseId parameter is the associated key:
@PUT
@Path("courses/{courseId}")
public Response updateCourse(@PathParam("courseId") int courseId, Course course) {
Course existingCourse = findById(courseId);
if (existingCourse == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
if (existingCourse.equals(course)) {
return Response.notModified().build();
}
courses.put(courseId, course);
return Response.ok().build();
}
This updateCourse method returns a response with OK (200) status if the update is successful, does not change anything and returns a Not Modified (304) response if the existing and uploaded objects have the same field values. In case a Course instance with the given id is not found in the courses map, the method returns a response with Not Found (404) status.
The third method of this root resource class does not directly handle any HTTP request. Instead, it delegates requests to the Course class where requests are handled by matching methods:
@Path("courses/{courseId}/students")
public Course pathToStudent(@PathParam("courseId") int courseId) {
return findById(courseId);
}
We have shown methods within the Course class that process delegated requests right before.
4. Server Endpoint
This section focuses on the construction of a CXF server, which is used for publishing the RESTful web service whose resources are depicted in the preceding section. The first step is to instantiate a JAXRSServerFactoryBean object and set the root resource class:
JAXRSServerFactoryBean factoryBean = new JAXRSServerFactoryBean();
factoryBean.setResourceClasses(CourseRepository.class);
A resource provider then needs to be set on the factory bean to manage the life cycle of the root resource class. We use the default singleton resource provider that returns the same resource instance to every request:
factoryBean.setResourceProvider(
new SingletonResourceProvider(new CourseRepository()));
We also set an address to indicate the URL where the web service is published:
factoryBean.setAddress("http://localhost:8080/");
Now the factoryBean can be used to create a new server that will start listening for incoming connections:
Server server = factoryBean.create();
All the code above in this section should be wrapped in the main method:
public class RestfulServer {
public static void main(String args[]) throws Exception {
// code snippets shown above
}
}
The invocation of this main method is presented in section 6.
5. Test Cases
This section describes test cases used to validate the web service we created before. Those tests validate resource states of the service after responding to HTTP requests of the four most commonly used methods, namely GET, POST, PUT, and DELETE.
5.1. Preparation
First, two static fields are declared within the test class, named RestfulTest:
private static String BASE_URL = "http://localhost:8080/baeldung/courses/";
private static CloseableHttpClient client;
Before running tests we create a client object, which is used to communicate with the server and destroy it afterward:
@BeforeClass
public static void createClient() {
client = HttpClients.createDefault();
}
@AfterClass
public static void closeClient() throws IOException {
client.close();
}
The client instance is now ready to be used by test cases.
5.2. GET Requests
In the test class, we define two methods to send GET requests to the server running the web service.
The first method is to get a Course instance given its id in the resource:
private Course getCourse(int courseOrder) throws IOException {
URL url = new URL(BASE_URL + courseOrder);
InputStream input = url.openStream();
Course course
= JAXB.unmarshal(new InputStreamReader(input), Course.class);
return course;
}
The second is to get a Student instance given the ids of the course and student in the resource:
private Student getStudent(int courseOrder, int studentOrder)
throws IOException {
URL url = new URL(BASE_URL + courseOrder + "/students/" + studentOrder);
InputStream input = url.openStream();
Student student
= JAXB.unmarshal(new InputStreamReader(input), Student.class);
return student;
}
These methods send HTTP GET requests to the service resource, then unmarshal XML responses to instances of the corresponding classes. Both are used to verify service resource states after executing POST, PUT, and DELETE requests.
5.3. POST Requests
This subsection features two test cases for POST requests, illustrating operations of the web service when the uploaded Student instance leads to a conflict and when it is successfully created.
In the first test, we use a Student object unmarshaled from the conflict_student.xml file, located on the classpath with the following content:
<Student>
<id>2</id>
<name>Student B</name>
</Student>
This is how that content is converted to a POST request body:
HttpPost httpPost = new HttpPost(BASE_URL + "1/students");
InputStream resourceStream = this.getClass().getClassLoader()
.getResourceAsStream("conflict_student.xml");
httpPost.setEntity(new InputStreamEntity(resourceStream));
The Content-Type header is set to tell the server that the content type of the request is XML:
httpPost.setHeader("Content-Type", "text/xml");
Since the uploaded Student object is already existent in the first Course instance, we expect that the creation fails and a response with Conflict (409) status is returned. The following code snippet verifies the expectation:
HttpResponse response = client.execute(httpPost);
assertEquals(409, response.getStatusLine().getStatusCode());
In the next test, we extract the body of an HTTP request from a file named created_student.xml, also on the classpath. Here is content of the file:
<Student>
<id>3</id>
<name>Student C</name>
</Student>
Similar to the previous test case, we build and execute a request, then verify that a new instance is successfully created:
HttpPost httpPost = new HttpPost(BASE_URL + "2/students");
InputStream resourceStream = this.getClass().getClassLoader()
.getResourceAsStream("created_student.xml");
httpPost.setEntity(new InputStreamEntity(resourceStream));
httpPost.setHeader("Content-Type", "text/xml");
HttpResponse response = client.execute(httpPost);
assertEquals(200, response.getStatusLine().getStatusCode());
We may confirm new states of the web service resource:
Student student = getStudent(2, 3);
assertEquals(3, student.getId());
assertEquals("Student C", student.getName());
This is what the XML response to a request for the new Student object looks like:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Student>
<id>3</id>
<name>Student C</name>
</Student>
5.4. PUT Requests
Let’s start with an invalid update request, where the Course object being updated does not exist. Here is content of the instance used to replace a non-existent Course object in the web service resource:
<Course>
<id>3</id>
<name>Apache CXF Support for RESTful</name>
</Course>
That content is stored in a file called non_existent_course.xml on the classpath. It is extracted and then used to populate the body of a PUT request by the code below:
HttpPut httpPut = new HttpPut(BASE_URL + "3");
InputStream resourceStream = this.getClass().getClassLoader()
.getResourceAsStream("non_existent_course.xml");
httpPut.setEntity(new InputStreamEntity(resourceStream));
The Content-Type header is set to tell the server that the content type of the request is XML:
httpPut.setHeader("Content-Type", "text/xml");
Since we intentionally sent an invalid request to update a non-existent object, a Not Found (404) response is expected to be received. The response is validated:
HttpResponse response = client.execute(httpPut);
assertEquals(404, response.getStatusLine().getStatusCode());
In the second test case for PUT requests, we submit a Course object with the same field values. Since nothing is changed in this case, we expect that a response with Not Modified (304) status is returned. The whole process is illustrated:
HttpPut httpPut = new HttpPut(BASE_URL + "1");
InputStream resourceStream = this.getClass().getClassLoader()
.getResourceAsStream("unchanged_course.xml");
httpPut.setEntity(new InputStreamEntity(resourceStream));
httpPut.setHeader("Content-Type", "text/xml");
HttpResponse response = client.execute(httpPut);
assertEquals(304, response.getStatusLine().getStatusCode());
Where unchanged_course.xml is the file on the classpath keeping information used to update. Here is its content:
<Course>
<id>1</id>
<name>REST with Spring</name>
</Course>
In the last demonstration of PUT requests, we execute a valid update. The following is content of the changed_course.xml file whose content is used to update a Course instance in the web service resource:
<Course>
<id>2</id>
<name>Apache CXF Support for RESTful</name>
</Course>
This is how the request is built and executed:
HttpPut httpPut = new HttpPut(BASE_URL + "2");
InputStream resourceStream = this.getClass().getClassLoader()
.getResourceAsStream("changed_course.xml");
httpPut.setEntity(new InputStreamEntity(resourceStream));
httpPut.setHeader("Content-Type", "text/xml");
Let’s validate a PUT request to the server and validate a successful upload:
HttpResponse response = client.execute(httpPut);
assertEquals(200, response.getStatusLine().getStatusCode());
Let’s verify the new states of the web service resource:
Course course = getCourse(2);
assertEquals(2, course.getId());
assertEquals("Apache CXF Support for RESTful", course.getName());
The following code snippet shows the content of the XML response when a GET request for the previously uploaded Course object is sent:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Course>
<id>2</id>
<name>Apache CXF Support for RESTful</name>
</Course>
5.5. DELETE Requests
First, let’s try to delete a non-existent Student instance. The operation should fail and a corresponding response with Not Found (404) status is expected:
HttpDelete httpDelete = new HttpDelete(BASE_URL + "1/students/3");
HttpResponse response = client.execute(httpDelete);
assertEquals(404, response.getStatusLine().getStatusCode());
In the second test case for DELETE requests, we create, execute and verify a request:
HttpDelete httpDelete = new HttpDelete(BASE_URL + "1/students/1");
HttpResponse response = client.execute(httpDelete);
assertEquals(200, response.getStatusLine().getStatusCode());
We verify new states of the web service resource with the following code snippet:
Course course = getCourse(1);
assertEquals(1, course.getStudents().size());
assertEquals(2, course.getStudents().get(0).getId());
assertEquals("Student B", course.getStudents().get(0).getName());
Next, we list the XML response that is received after a request for the first Course object in the web service resource:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Course>
<id>1</id>
<name>REST with Spring</name>
<students>
<id>2</id>
<name>Student B</name>
</students>
</Course>
It is clear that the first Student has successfully been removed.
6. Test Execution
Section 4 described how to create and destroy a Server instance in the main method of the RestfulServer class.
The last step to make the server up and running is to invoke that main method. In order to achieve that, the Exec Maven plugin is included and configured in the Maven POM file:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0<version>
<configuration>
<mainClass>
com.baeldung.cxf.jaxrs.implementation.RestfulServer
</mainClass>
</configuration>
</plugin>
The latest version of this plugin can be found via this link.
In the process of compiling and packaging the artifact illustrated in this tutorial, the Maven Surefire plugin automatically executes all tests enclosed in classes having names starting or ending with Test. If this is the case, the plugin should be configured to exclude those tests:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<excludes>
<exclude>**/ServiceTest</exclude>
</excludes>
</configuration>
</plugin>
With the above configuration, ServiceTest is excluded since it is the name of the test class. You may choose any name for that class, provided tests contained therein are not run by the Maven Surefire plugin before the server is ready for connections.
For the latest version of Maven Surefire plugin, please check here.
Now you can execute the exec:java goal to start the RESTful web service server and then run the above tests using an IDE. Equivalently you may start the test by executing the command mvn -Dtest=ServiceTest test in a terminal.
7. Conclusion
This tutorial illustrated the use of Apache CXF as a JAX-RS implementation. It demonstrated how the framework could be used to define resources for a RESTful web service and to create a server for publishing the service.
The implementation of all these examples and code snippets can be found in the GitHub project.