Перейти к основному содержимому

Поддержка Apache CXF для веб-служб RESTful

· 13 мин. чтения

1. Обзор

В этом учебном пособии Apache CXF представлен как платформа, совместимая со стандартом JAX-RS, который определяет поддержку экосистемы Java для архитектурного шаблона REpresentational State Transfer (REST).

В частности, в нем шаг за шагом описывается, как создать и опубликовать веб-службу RESTful, а также как написать модульные тесты для проверки службы.

Это третья статья из серии об Apache CXF; первый фокусируется на использовании CXF как реализации, полностью совместимой с JAX-WS. Вторая статья содержит руководство по использованию CXF с Spring.

2. Зависимости Maven

Первая необходимая зависимость — org.apache.cxf:cxfr- rt -frontend- jaxrs . Этот артефакт предоставляет API-интерфейсы JAX-RS, а также реализацию CXF:

<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxrs</artifactId>
<version>3.1.7</version>
</dependency>

В этом руководстве мы используем CXF для создания конечной точки сервера для публикации веб-службы вместо использования контейнера сервлетов. Поэтому в файл Maven POM необходимо включить следующую зависимость:

<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http-jetty</artifactId>
<version>3.1.7</version>
</dependency>

Наконец, давайте добавим библиотеку HttpClient для облегчения модульных тестов:

<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>

Здесь вы можете найти последнюю версию зависимости cxf-rt-frontend-jaxrs . Вы также можете обратиться к этой ссылке для получения последних версий артефактов org.apache.cxf:cxf-rt-transports-http-jetty . Наконец, последнюю версию httpclient можно найти здесь .

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<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

}

Наконец, давайте реализуем CourseRepository , который является корневым ресурсом и служит точкой входа в ресурсы веб-сервиса:

@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;
}
}

Обратите внимание на сопоставление с аннотацией @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<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);
}

Методы этого класса, отвечающие за 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 в список студентов :

@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 , если объект с отправленным идентификатором уже существует.

Также обратите внимание, что мы можем пропустить аннотацию @Path, поскольку ее значением является пустая строка.

Последний метод обрабатывает запросы DELETE . Он удаляет элемент из списка студентов , идентификатор которого является полученным параметром пути, и возвращает ответ со статусом 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 , являющийся значением записи в карте курсов , ключом которой является полученный параметр пути courseId запроса GET . Внутри метод отправляет параметры пути вспомогательному методу findById для выполнения своей работы.

@GET
@Path("courses/{courseId}")
public Course getCourse(@PathParam("courseId") int courseId) {
return findById(courseId);
}

Следующий метод обновляет существующую запись карты курсов , где тело полученного запроса 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), если существующие и загруженные объекты имеют одинаковые значения полей. Если экземпляр курса с заданным идентификатором не найден на карте курсов , метод возвращает ответ со статусом « Не найдено» (404).

Третий метод этого корневого класса ресурсов напрямую не обрабатывает какие-либо HTTP-запросы. Вместо этого он делегирует запросы классу Course , где запросы обрабатываются соответствующими методами:

@Path("courses/{courseId}/students")
public Course pathToStudent(@PathParam("courseId") int courseId) {
return findById(courseId);
}

Прямо перед этим мы показали методы в классе Course , обрабатывающие делегированные запросы.

4. Конечная точка сервера

В этом разделе основное внимание уделяется созданию сервера 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 = factoryBean.create();

Весь код, приведенный выше в этом разделе, должен быть обернут в метод main :

public class RestfulServer {
public static void main(String args[]) throws Exception {
// code snippets shown above
}
}

Вызов этого основного метода представлен в разделе 6.

5. Тестовые случаи

В этом разделе описываются тестовые примеры, используемые для проверки веб-службы, которую мы создали ранее. Эти тесты проверяют состояние ресурсов службы после ответа на HTTP-запросы четырех наиболее часто используемых методов, а именно GET , POST , PUT и DELETE .

5.1. Подготовка

Во-первых, в тестовом классе объявляются два статических поля с именем RestfulTest :

private static String BASE_URL = "http://localhost:8080/foreach/courses/";
private static CloseableHttpClient client;

Перед запуском тестов мы создаем клиентский объект, который используется для связи с сервером, а затем уничтожаем его:

@BeforeClass
public static void createClient() {
client = HttpClients.createDefault();
}

@AfterClass
public static void closeClient() throws IOException {
client.close();
}

Экземпляр клиента теперь готов к использованию тестовыми примерами.

5.2. ПОЛУЧИТЬ запросы

В тестовом классе мы определяем два метода для отправки запросов GET на сервер, на котором запущена веб-служба.

Первый способ — получить экземпляр курса по его идентификатору в ресурсе:

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 с учетом идентификаторов курса и студента в ресурсе:

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;
}

Эти методы отправляют запросы HTTP GET к ресурсу службы, а затем демаршалируют ответы XML экземплярам соответствующих классов. Оба используются для проверки состояний ресурсов службы после выполнения запросов POST , PUT и DELETE .

5.3. POST- запросы

В этом подразделе представлены два тестовых примера для POST -запросов, иллюстрирующих работу веб-службы, когда загруженный экземпляр Student приводит к конфликту и когда он успешно создается.

В первом тесте мы используем неупорядоченный объект Student из файла конфликта_student.xml , расположенный в пути к классам, со следующим содержимым:

<Student>
<id>2</id>
<name>Student B</name>
</Student>

Вот как этот контент преобразуется в тело запроса 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 , который также находится в пути к классам. Вот содержимое файла:

<Student>
<id>3</id>
<name>Student C</name>
</Student>

Как и в предыдущем тестовом примере, мы создаем и выполняем запрос, а затем проверяем, успешно ли создан новый экземпляр:

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 :

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Student>
<id>3</id>
<name>Student C</name>
</Student>

5.4. PUT- запросы

Начнем с недопустимого запроса на обновление, в котором обновляемый объект « Курс » не существует. Вот содержимое экземпляра, используемого для замены несуществующего объекта Course в ресурсе веб-службы:

<Course>
<id>3</id>
<name>Apache CXF Support for RESTful</name>
</Course>

Этот контент хранится в файле с именем 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());

Где неизмененный_курс.xml — это файл в пути к классам, содержащий информацию, используемую для обновления. Вот его содержание:

<Course>
<id>1</id>
<name>REST with Spring</name>
</Course>

В последней демонстрации запросов PUT мы выполняем действительное обновление. Ниже приведено содержимое файла change_course.xml , содержимое которого используется для обновления экземпляра курса в ресурсе веб-службы:

<Course>
<id>2</id>
<name>Apache CXF Support for RESTful</name>
</Course>

Вот как создается и выполняется запрос:

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 для ранее загруженного объекта курса :

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Course>
<id>2</id>
<name>Apache CXF Support for RESTful</name>
</Course>

5.5. УДАЛИТЬ Запросы

Сначала попробуем удалить несуществующий экземпляр 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 в ресурсе веб-службы:

<?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>

Понятно, что первый Студент успешно удален.

6. Выполнение теста

В разделе 4 описано, как создать и уничтожить экземпляр сервера в методе main класса RestfulServer .

Последним шагом для запуска сервера является вызов этого основного метода. Для этого подключаемый модуль Exec Maven включен и настроен в файле Maven POM:

<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0<version>
<configuration>
<mainClass>
com.foreach.cxf.jaxrs.implementation.RestfulServer
</mainClass>
</configuration>
</plugin>

Последнюю версию этого плагина можно найти по этой ссылке .

В процессе компиляции и упаковки артефакта, показанного в этом руководстве, подключаемый модуль Maven Surefire автоматически выполняет все тесты, заключенные в классы, имена которых начинаются или заканчиваются на Test . Если это так, плагин должен быть настроен на исключение этих тестов:

<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<excludes>
<exclude>**/ServiceTest</exclude>
</excludes>
</configuration>
</plugin>

В приведенной выше конфигурации ServiceTest исключается, поскольку это имя тестового класса. Вы можете выбрать любое имя для этого класса при условии, что содержащиеся в нем тесты не будут запущены подключаемым модулем Maven Surefire до того, как сервер будет готов для соединений.

Актуальную версию плагина Maven Surefire можно найти здесь .

Теперь вы можете выполнить цель exec:java , чтобы запустить сервер веб-службы RESTful, а затем запустить приведенные выше тесты с помощью IDE. Точно так же вы можете запустить тест, выполнив команду mvn -Dtest=ServiceTest test в терминале.

7. Заключение

В этом руководстве показано использование Apache CXF в качестве реализации JAX-RS. Он продемонстрировал, как эту структуру можно использовать для определения ресурсов для веб-службы RESTful и для создания сервера для публикации службы.

Реализацию всех этих примеров и фрагментов кода можно найти в проекте GitHub .