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

Поддержка 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

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

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.