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 .