Рубрики
Без рубрики

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

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

Автор оригинала: baeldung.

1. Обзор

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

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

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

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

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


    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

Здесь вы можете найти последнюю версию зависимости cxf-rt-frontend-jaxrs . Вы также можете обратиться к этой ссылке для получения последних версий org.apache.cxf:cxf-rt-transports-http-jetty artifacts. Наконец, последнюю версию 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 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 . Репозиторий Course является здесь корневым ресурсом, поэтому он сопоставлен для обработки всех URL-адресов, начинающихся с course .

Значение @Products annotation используется для указания серверу преобразовать объекты, возвращаемые методами этого класса, в 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 – прямо в POJOs ресурса.

Важно понимать, что это существенное отличие от подхода в типичном весеннем проекте, где операции API будут определены в контроллере, а не в самом POJO.

Давайте начнем с методов отображения, определенных внутри класса Course :

@GET
@Path("{studentId}")
public Student getStudent(@PathParam("studentId")int studentId) {
    return findById(studentId);
}

Проще говоря, метод вызывается при обработке запросов GET , обозначаемых аннотацией @GET .

Заметил простой синтаксис отображения параметра student Id path из 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();
}

Давайте перейдем к методам requestmapping класса Course Repository .

Следующий метод getsource возвращает объект Course , который является значением записи в карте courses , ключом которой является полученный параметр CourseID path запроса 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. Конечная точка сервера

Этот раздел посвящен построению сервера CXF, который используется для публикации веб-службы RESTful, ресурсы которой описаны в предыдущем разделе. Первым шагом является создание экземпляра объекта JAXRSServerFactoryBean и установка корневого класса ресурсов:

JAXRSServerFactoryBean factoryBean = new JAXRSServerFactoryBean();
factoryBean.setResourceClasses(CourseRepository.class);

Затем поставщик ресурсов должен быть установлен на factory bean для управления жизненным циклом корневого класса ресурсов. Мы используем поставщик одноэлементных ресурсов по умолчанию, который возвращает один и тот же экземпляр ресурса для каждого запроса:

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

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

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

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

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

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

private static String BASE_URL = "http://localhost:8080/baeldung/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 запросов на сервер, на котором работает веб-служба.

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

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


    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. УДАЛЕНИЕ запросов

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

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

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

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

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


    org.codehaus.mojo
    exec-maven-plugin
    3.0.0
    
        
          com.baeldung.cxf.jaxrs.implementation.RestfulServer
        
    

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

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


    maven-surefire-plugin
    2.19.1
    
    
        **/ServiceTest
    
    

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

Для получения последней версии плагина Maven Surefire, пожалуйста, проверьте здесь .

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

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

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

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