Поддержка Apache CXF для веб-сервисов RESTful
1. обзор
В этом руководствеApache CXF рассматривается как структура, соответствующая стандарту JAX-RS, который определяет поддержку экосистемы Java для архитектурного шаблона REpresentational State Transfer (REST).
В частности, он описывает шаг за шагом, как создать и опубликовать веб-сервис RESTful, и как написать модульные тесты для проверки сервиса.
Это третий из серии, посвященной Apache CXF; the first one фокусируется на использовании CXF как полностью совместимой с JAX-WS реализации. second article содержит руководство по использованию CXF с Spring.
2. Maven Зависимости
Первая требуемая зависимость -org.apache.cxf:cxf-rt-frontend-jaxrs. This artifact provides JAX-RS APIs as well as a CXF implementation:
org.apache.cxf
cxf-rt-frontend-jaxrs
3.1.7
В этом руководстве мы используем CXF для создания конечной точкиServer для публикации веб-службы вместо использования контейнера сервлетов. Поэтому в файл Maven POM необходимо включить следующую зависимость:
org.apache.cxf
cxf-rt-transports-http-jetty
3.1.7
Наконец, давайте добавим библиотеку HttpClient для упрощения модульных тестов:
org.apache.httpcomponents
httpclient
4.5.2
3. Классы ресурсов и сопоставление запросов
Начнем с простого примера. мы собираемся настроить наш REST API с двумя ресурсамиCourse иStudent.
Мы начнем с простого и перейдем к более сложному примеру.
3.1. Ресурсы
Вот определение класса ресурсовStudent:
@XmlRootElement(name = "Student")
public class Student {
private int id;
private String name;
// standard getters and setters
// standard equals and hashCode implementations
}
Обратите внимание, что мы используем аннотацию@XmlRootElement, чтобы сообщить JAXB, что экземпляры этого класса должны быть маршалированы в XML.
Далее следует определение класса ресурсовCourse:
@XmlRootElement(name = "Course")
public class Course {
private int id;
private String name;
private List 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
}
Наконец, давайте реализуемCourseRepository, который является корневым ресурсом и служит точкой входа в ресурсы веб-службы:
@Path("course")
@Produces("text/xml")
public class CourseRepository {
private Map courses = new HashMap<>();
// request handling methods
private Course findById(int id) {
for (Map.Entry course : courses.entrySet()) {
if (course.getKey() == id) {
return course.getValue();
}
}
return null;
}
}
Обратите внимание на отображение с аннотацией@Path. CourseRepository здесь является корневым ресурсом, поэтому он сопоставлен для обработки всех URL-адресов, начинающихся сcourse.
Значение аннотации@Produces используется, чтобы сообщить серверу о необходимости преобразования объектов, возвращаемых из методов этого класса, в документы XML перед их отправкой клиентам. По умолчанию мы используем JAXB, поскольку другие механизмы привязки не указаны.
3.2. Простая настройка данных
Поскольку это простой пример реализации, мы используем данные в памяти вместо полноценного постоянного решения.
Имея это в виду, давайте реализуем простую логику настройки, чтобы заполнить систему некоторыми данными:
{
Student student1 = new Student();
Student student2 = new Student();
student1.setId(1);
student1.setName("Student A");
student2.setId(2);
student2.setName("Student B");
List 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);
}
Методы в этом классе, которые заботятся о HTTP-запросах, рассматриваются в следующем подразделе.
3.3. API - методы сопоставления запросов
Теперь перейдем к реализации самого REST API.
Мы собираемся начать добавлять операции API - используя аннотацию@Path - прямо в POJO ресурсов.
Важно понимать, что это существенное отличие от подхода в типичном проекте Spring - где операции API будут определяться в контроллере, а не в самом POJO.
Начнем с методов сопоставления, определенных внутри классаCourse:
@GET
@Path("{studentId}")
public Student getStudent(@PathParam("studentId")int studentId) {
return findById(studentId);
}
Проще говоря, метод вызывается при обработке запросовGET, что обозначается аннотацией@GET.
Обратил внимание на простой синтаксис отображения параметра путиstudentId из HTTP-запроса.
Затем мы просто используем вспомогательный методfindById, чтобы вернуть соответствующий экземплярStudent.
Следующий метод обрабатывает запросыPOST, обозначенные аннотацией@POST, путем добавления полученного объектаStudent в списокstudents:
@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();
}
Это возвращает ответ200 OK, если операция создания прошла успешно, или409 Conflict, если объект с отправленнымid уже существует.
Также обратите внимание, что мы можем пропустить аннотацию@Path, поскольку ее значение - пустая строка.
Последний метод обрабатывает запросыDELETE. Он удаляет элемент из спискаstudents,id которого является параметром полученного пути, и возвращает ответ со статусомOK (200). В случае отсутствия элементов, связанных с указаннымid, что означает, что удалять нечего, этот метод возвращает ответ со статусомNot Found (404):
@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();
}
Перейдем к методам сопоставления запросов классаCourseRepository.
Следующий методgetCourse возвращает объектCourse, который является значением записи в картеcourses, ключ которой является полученным параметром путиcourseId дляGET запрос. Внутренне метод отправляет параметры пути вспомогательному методуfindById для выполнения своей работы.
@GET
@Path("courses/{courseId}")
public Course getCourse(@PathParam("courseId") int courseId) {
return findById(courseId);
}
Следующий метод обновляет существующую запись картыcourses, где тело полученного запросаPUT является значением записи, а параметрcourseId является связанным ключом:
@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();
}
Этот методupdateCourse возвращает ответ со статусомOK (200), если обновление прошло успешно, ничего не меняет и возвращает ответNot Modified (304), если существующие и загруженные объекты имеют одинаковые значения полей. Если экземплярCourse с даннымid не найден на картеcourses, метод возвращает ответ со статусомNot Found (404).
Третий метод этого корневого класса ресурсов напрямую не обрабатывает HTTP-запрос. Вместо этого он делегирует запросы классуCourse, где запросы обрабатываются соответствующими методами:
@Path("courses/{courseId}/students")
public Course pathToStudent(@PathParam("courseId") int courseId) {
return findById(courseId);
}
Прямо перед этим мы показали методы в классеCourse, которые обрабатывают делегированные запросы.
4. Server Конечная точка
В этом разделе основное внимание уделяется созданию сервера CXF, который используется для публикации веб-службы RESTful, ресурсы которой изображены в предыдущем разделе. Первым шагом является создание экземпляра объектаJAXRSServerFactoryBean и установка корневого класса ресурсов:
JAXRSServerFactoryBean factoryBean = new JAXRSServerFactoryBean();
factoryBean.setResourceClasses(CourseRepository.class);
Затем в фабричном компоненте должен быть установлен поставщик ресурсов для управления жизненным циклом корневого класса ресурсов. Мы используем поставщик одноэлементных ресурсов по умолчанию, который возвращает один и тот же экземпляр ресурса для каждого запроса:
factoryBean.setResourceProvider(
new SingletonResourceProvider(new CourseRepository()));
Мы также установили адрес, чтобы указать URL, на котором опубликован веб-сервис:
factoryBean.setAddress("http://localhost:8080/");
ТеперьfactoryBean можно использовать для создания новогоserver, который начнет прослушивать входящие соединения:
Server server = factoryBean.create();
Весь приведенный выше код в этом разделе должен быть заключен в методmain:
public class RestfulServer {
public static void main(String args[]) throws Exception {
// code snippets shown above
}
}
Вызов этого методаmain представлен в разделе 6.
5. Тестовые случаи
В этом разделе описываются контрольные примеры, используемые для проверки веб-службы, которую мы создали ранее. Эти тесты проверяют состояние ресурсов службы после ответа на HTTP-запросы четырех наиболее часто используемых методов, а именноGET,POST,PUT иDELETE.
5.1. подготовка
Сначала в тестовом классе объявляются два статических поля с именемRestfulTest:
private static String BASE_URL = "http://localhost:8080/example/courses/";
private static CloseableHttpClient client;
Перед запуском тестов мы создаем объектclient, который используется для связи с сервером и затем уничтожает его:
@BeforeClass
public static void createClient() {
client = HttpClients.createDefault();
}
@AfterClass
public static void closeClient() throws IOException {
client.close();
}
Экземплярclient теперь готов для использования в тестовых примерах.
5.2. GET запросов
В тестовом классе мы определяем два метода для отправки запросовGET на сервер, на котором запущена веб-служба.
Первый способ - получить экземплярCourse с учетом егоid в ресурсе:
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;
}
Второй - получить экземплярStudent с учетомid курса и студента в ресурсе:
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;
}
Эти методы отправляют запросы HTTPGET к ресурсу службы, а затем демаршалируют ответы XML экземплярам соответствующих классов. Оба используются для проверки состояния ресурсов службы после выполнения запросовPOST,PUT иDELETE.
5.3. POST запросов
В этом подразделе представлены два тестовых примера для запросовPOST, иллюстрирующие операции веб-службы, когда загруженный экземплярStudent приводит к конфликту и когда он успешно создается.
В первом тесте мы используем объектStudent, немаршализованный из файлаconflict_student.xml, расположенный в пути к классам со следующим содержимым:
2
Student B
Вот как это содержимое преобразуется в тело запросаPOST:
HttpPost httpPost = new HttpPost(BASE_URL + "1/students");
InputStream resourceStream = this.getClass().getClassLoader()
.getResourceAsStream("conflict_student.xml");
httpPost.setEntity(new InputStreamEntity(resourceStream));
ЗаголовокContent-Type установлен, чтобы сообщить серверу, что тип содержимого запроса - XML:
httpPost.setHeader("Content-Type", "text/xml");
Поскольку загруженный объектStudent уже существует в первом экземпляреCourse, мы ожидаем, что создание завершится неудачно и будет возвращен ответ со статусомConflict (409). Следующий фрагмент кода проверяет ожидание:
HttpResponse response = client.execute(httpPost);
assertEquals(409, response.getStatusLine().getStatusCode());
В следующем тесте мы извлекаем тело HTTP-запроса из файла с именемcreated_student.xml, также находящегося в пути к классам. Вот содержимое файла:
3
Student C
Как и в предыдущем тестовом примере, мы создаем и выполняем запрос, а затем проверяем, что новый экземпляр успешно создан:
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());
Мы можем подтвердить новые состояния ресурса веб-сервиса:
Student student = getStudent(2, 3);
assertEquals(3, student.getId());
assertEquals("Student C", student.getName());
Вот как выглядит XML-ответ на запрос нового объектаStudent:
3
Student C
5.4. PUT запросов
Начнем с недопустимого запроса на обновление, когда обновляемый объектCourse не существует. Вот содержимое экземпляра, используемого для замены несуществующего объектаCourse в ресурсе веб-службы:
3
Apache CXF Support for RESTful
Этот контент хранится в файле с именемnon_existent_course.xml в пути к классам. Он извлекается и затем используется для заполнения тела запросаPUT с помощью приведенного ниже кода:
HttpPut httpPut = new HttpPut(BASE_URL + "3");
InputStream resourceStream = this.getClass().getClassLoader()
.getResourceAsStream("non_existent_course.xml");
httpPut.setEntity(new InputStreamEntity(resourceStream));
ЗаголовокContent-Type установлен, чтобы сообщить серверу, что тип содержимого запроса - XML:
httpPut.setHeader("Content-Type", "text/xml");
Поскольку мы намеренно отправили неверный запрос на обновление несуществующего объекта, ожидается получение ответаNot Found (404). Ответ подтвержден:
HttpResponse response = client.execute(httpPut);
assertEquals(404, response.getStatusLine().getStatusCode());
Во втором тестовом примере для запросовPUT мы отправляем объектCourse с теми же значениями полей. Поскольку в этом случае ничего не меняется, мы ожидаем, что будет возвращен ответ со статусомNot Modified (304). Весь процесс иллюстрируется:
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());
Гдеunchanged_course.xml - это файл в пути к классам, содержащий информацию, используемую для обновления. Вот его содержание:
1
REST with Spring
В последней демонстрации запросовPUT мы выполняем корректное обновление. Ниже приводится содержимое файлаchanged_course.xml, содержимое которого используется для обновления экземпляраCourse в ресурсе веб-службы:
2
Apache CXF Support for RESTful
Вот как запрос строится и выполняется:
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");
Давайте проверим запросPUT к серверу и подтвердим успешную загрузку:
HttpResponse response = client.execute(httpPut);
assertEquals(200, response.getStatusLine().getStatusCode());
Давайте проверим новые состояния ресурса веб-службы:
Course course = getCourse(2);
assertEquals(2, course.getId());
assertEquals("Apache CXF Support for RESTful", course.getName());
В следующем фрагменте кода показано содержимое XML-ответа, когда отправляется запрос GET для ранее загруженного объектаCourse:
2
Apache CXF Support for RESTful
5.5. DELETE запросов
Сначала попробуем удалить несуществующий экземплярStudent. Операция должна завершиться неудачно, и ожидается соответствующий ответ со статусомNot Found (404):
HttpDelete httpDelete = new HttpDelete(BASE_URL + "1/students/3");
HttpResponse response = client.execute(httpDelete);
assertEquals(404, response.getStatusLine().getStatusCode());
Во втором тестовом примере для запросовDELETE мы создаем, выполняем и проверяем запрос:
HttpDelete httpDelete = new HttpDelete(BASE_URL + "1/students/1");
HttpResponse response = client.execute(httpDelete);
assertEquals(200, response.getStatusLine().getStatusCode());
Мы проверяем новые состояния ресурса веб-службы с помощью следующего фрагмента кода:
Course course = getCourse(1);
assertEquals(1, course.getStudents().size());
assertEquals(2, course.getStudents().get(0).getId());
assertEquals("Student B", course.getStudents().get(0).getName());
Затем мы перечисляем ответ XML, полученный после запроса первого объектаCourse в ресурсе веб-службы:
1
REST with Spring
2
Student B
Понятно, что первыйStudent успешно удален.
6. Выполнение теста
В разделе 4 описано, как создать и уничтожить экземплярServer в методеmain классаRestfulServer.
Последний шаг к запуску сервера - это вызов методаmain. Для этого подключаемый модуль Exec Maven включен и настроен в файле POM Maven:
org.codehaus.mojo
exec-maven-plugin
1.5.0
com.example.cxf.jaxrs.implementation.RestfulServer
Последнюю версию этого плагина можно найти черезthis link.
В процессе компиляции и упаковки артефакта, показанного в этом руководстве, плагин Maven Surefire автоматически выполняет все тесты, заключенные в классы, имена которых начинаются или заканчиваются наTest. Если это так, то плагин должен быть настроен на исключение этих тестов:
maven-surefire-plugin
2.19.1
**/ServiceTest
В приведенной выше конфигурацииServiceTest исключается, поскольку это имя тестового класса. Вы можете выбрать любое имя для этого класса, при условии, что содержащиеся в нем тесты не запускаются плагином Maven Surefire до тех пор, пока сервер не будет готов к подключению.
Для последней версии плагина Maven Surefire проверьтеhere.
Теперь вы можете выполнить цельexec:java, чтобы запустить сервер веб-службы RESTful, а затем запустить вышеуказанные тесты с помощью IDE. Точно так же вы можете запустить тест, выполнив командуmvn -Dtest=ServiceTest test in a terminal.
7. Заключение
В этом руководстве показано использование Apache CXF в качестве реализации JAX-RS. Он продемонстрировал, как можно использовать среду для определения ресурсов для веб-службы RESTful и для создания сервера для публикации службы.
Реализацию всех этих примеров и фрагментов кода можно найти вthe GitHub project.