|
|
RESTful - что может быть проще? Только RESTful реализованный с помощью Java EE! Конечно, многие с этим не согласятся и приведут кучу весомых аргументов против, и будут по своему правы. Но поверьте, в Java мире проделана огромная работа, чтобы упростить воплощение ваших REST идей в жизнь. И в этой статье мне бы хотелось наглядно это продемонстрировать. |
Итак, самый простой способ объяснить что-то - показать это "что-то" в деле. Для примера, будет показан процесс реализации REST API для простого планировщика повседневных задач - так называемого TODO листа.
Что из себя представляет приложение TODO?
Демонстрационное приложение TODO будет позволять вести список задач. Каждая задача будет состоять из уникального
идентификатора, имени, описания и статуса выполнена/невыполнена:
Задачи будут представлены с помощью json. Пример Json представления задачи:
{
"uuid":"550e8400-e29b-41d4-a716-446655440000",
"name":"написать RESTful приложение",
"description":"чтобы полученные знания принесли пользу и не были забыты, ими необходимо воспользоваться в течении первых 72 часов после получения",
"completed":false
}
API приложения будет предусматривать следующие действия:
- Получение списка всех заданий
- Получение списка выполненых/невыполненных заданий
- Получение конкретного задания
- Добавление задания
- Создание нового задания/изменение существующего
- Редактирование отдельных параметров задания
- Удаление задания
Пример
специально максимально упрощен, чтобы не отвлекать читателя от API предоставляемой J2EE и быстро создать общую картину
процесса реализации RESTful приложений на java.Функциональные тесты
Статья не затрагивает тему написания клиента для разрабатываемого api. Вместо этого я предлагаю решение в виде функциональных тестов, написанное с помощью JUnit, Apache HttpComponents и Google gson.package ru.dokwork.todo;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.apache.http.HttpResponse;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.UUID;
import static ru.dokwork.todo.ResponseChecker.assertThat;
public class TaskServiceFT {
private static final String URL = "http://localhost:8080/todo/tasks/";
static final JsonObject jtask1;
static final JsonObject jtask2;
JsonParser parser;
TaskServiceClient todo;
static {
jtask1 = new JsonObject();
jtask1.addProperty("name", "First task");
jtask1.addProperty("description", "First task for test.");
jtask1.addProperty("completed", false);
jtask2 = new JsonObject();
jtask2.addProperty("name", "Second task");
jtask2.addProperty("description", "Second task for test.");
jtask2.addProperty("completed", false);
}
@Before
public void setup() throws Exception {
parser = new JsonParser();
todo = new TaskServiceClient(URL);
todo.removeAllExistingTasks();
}
@Test
public void testGetTask() throws Exception {
// Add task
HttpResponse response = todo.addNewTask(jtask1);
// Get uuid
String uuid = response.getFirstHeader("location").getValue().substring(URL.length());
// Get task
response = todo.getTask(UUID.fromString(uuid));
// Create expected json
JsonObject expectedObject = createJsonTask(uuid, jtask1);
// Check answer
assertThat(response).haveCode(200).haveContent(expectedObject);
}
@Test
public void testGetAllTasks() throws Exception {
// Add first task
HttpResponse response = todo.addNewTask(jtask1);
String uuid1 = response.getFirstHeader("location").getValue().substring(URL.length());
// Add second task
response = todo.addNewTask(jtask2);
String uuid2 = response.getFirstHeader("location").getValue().substring(URL.length());
// Get all tasks
response = todo.getAll();
// Create expected array
JsonArray array = new JsonArray();
array.add(createJsonTask(uuid1, jtask1));
array.add(createJsonTask(uuid2, jtask2));
// Check answer
assertThat(response).haveCode(200).haveContent(array);
}
@Test
public void testAddTask() throws Exception {
// Add task
HttpResponse response = todo.addNewTask(jtask1);
// Check answer
assertThat(response).haveCode(201).haveHeaders("location", 1);
// Get uuid
String uuid = response.getFirstHeader("location").getValue().substring(URL.length());
// Check new task
response = todo.getTask(UUID.fromString(uuid));
JsonObject addedTask = createJsonTask(uuid, jtask1);
assertThat(response).haveContent(addedTask);
}
@Test
public void testPutNewTask() throws Exception {
// Put new task
UUID uuid = UUID.randomUUID();
HttpResponse response = todo.putTask(uuid, jtask1);
// Check answer
assertThat(response).haveCode(201).haveHeaders("location", 1);
response.getFirstHeader("location").getValue().endsWith(uuid.toString());
// Check new task
response = todo.getTask(uuid);
JsonObject addedTask = createJsonTask(uuid.toString(), jtask1);
assertThat(response).haveContent(addedTask);
}
@Test
public void testPutExistingTask() throws Exception {
// Add task
HttpResponse response = todo.addNewTask(jtask1);
// Get uuid
String uuid = response.getFirstHeader("location").getValue().substring(URL.length());
// Change task
JsonObject editedTask = createJsonTask(uuid, jtask1);
editedTask.addProperty("name", "Edited task");
// Put edited task
response = todo.putTask(UUID.fromString(uuid), editedTask);
// Check answer
assertThat(response).haveCode(204).haveHeaders("location", 1);
response.getFirstHeader("location").getValue().endsWith(uuid.toString());
// Check edited task
response = todo.getTask(UUID.fromString(uuid));
assertThat(response).haveContent(editedTask);
}
@Test
public void testPatchTask() throws Exception {
// Add task
HttpResponse response = todo.addNewTask(jtask1);
// Get uuid
String uuid = response.getFirstHeader("location").getValue().substring(URL.length());
// Create patch
JsonObject patch = new JsonObject();
patch.addProperty("completed", true);
// Patch state for task
response = todo.patch(UUID.fromString(uuid), patch);
// Check answer
assertThat(response).haveCode(204).haveHeaders("location", 1);
response.getFirstHeader("location").getValue().endsWith(uuid);
// Check patched task
response = todo.getTask(UUID.fromString(uuid));
// Create expected json
JsonObject expectedObject = createJsonTask(uuid, jtask1);
expectedObject.addProperty("completed", true);
// Check task
assertThat(response).haveContent(expectedObject);
}
@Test
public void testFindTasks() throws Exception {
// Add first task
HttpResponse response = todo.addNewTask(jtask1);
// Get uuid for first task
String uuid1 = response.getFirstHeader("location").getValue().substring(URL.length());
// Get uuid for second task
String uuid2 = UUID.randomUUID().toString();
// Add second task
JsonObject jsonTask2 = createJsonTask(uuid2, jtask2);
jsonTask2.addProperty("completed", true);
todo.putTask(UUID.fromString(uuid2), jsonTask2);
// Get only completed tasks
response = todo.find(true);
// Create expected content
JsonArray expectedContent = new JsonArray();
expectedContent.add(createJsonTask(uuid2, jsonTask2));
// Check answer
assertThat(response).haveCode(200).haveContent(expectedContent);
}
@Test
public void testDeleteTask() throws Exception {
// Add task
HttpResponse response = todo.addNewTask(jtask1);
// Get uuid
String uuid = response.getFirstHeader("location").getValue().substring(URL.length());
// Delete task
int countBefore = todo.getTasksCount();
response = todo.delete(UUID.fromString(uuid));
// Check answer
assertThat(response).haveCode(204);
int countAfter = todo.getTasksCount();
Assert.assertEquals(1, countBefore - countAfter);
}
private JsonObject createJsonTask(String uuid, JsonObject pattern) {
JsonObject expectedObject = new JsonObject();
expectedObject.add("uuid", parser.parse(uuid));
expectedObject.add("name", pattern.get("name"));
expectedObject.add("description", pattern.get("description"));
expectedObject.add("completed", pattern.get("completed"));
return expectedObject;
}
}
Классы ResponseChecker и TaskServiceClient призваны сделать код функциональных тестов более наглядным.
ResponseChecker
TaskServiceClient
Обратите внимание на метод PATCH. Он не реализован (на момент написания статьи) в библиотеке Apache HttpComponents и его реализацией необходимо заняться самостоятельно. К счастью это совершенно не трудно:
public class HttpPatch extends HttpPost {
@Override
public String getMethod() {
return "PATCH";
}
public HttpPatch() {
super();
}
public HttpPatch(URI uri) {
super(uri);
}
public HttpPatch(String url) {
super(url);
}
}
Введение в JAX-RS API
Каждому ресурсу в rest-приложениях соответствует некоторый URI, а жизненный цикл ресурса управляется с помощью методов GET, POST, PUT и DELETE http протокола. JAX-RS предлагает очень прозрачную схему реализации такой идеи. Вы просто заводите сервисный класс, отвечающий за некоторую сущность и указываете базовый URL для этой сущности:
@Path(value="/tasks")
class TaskService {
}
Затем реализуете методы для управления этими сущностями и связываете их с методами http протокола:
@Path(value="/tasks")
class TaskService {
@GET
public List<Task> getTasks() {
}
@POST
public void addTask(Task task) {
}
}
Так,из коробки, вы получаете возможность сериализации/десериализации данных в популярные форматы, такие как JSON и XML с помощью аннотации
@Produces({"application/xml", "application/json"})
Что касается вопроса, откуда брать аргументы метода, то тут дефицита в вариантах нет. Возможными источниками могут выступать: параметры http запроса, значения веб-форм, часть пути url, значения куки, заголовки запроса и матрицы. Чтобы указать конкретный источник, вы должны пометить аргумент одной из аннотаций:
- @FormParam
- @PathParam
- @QueryParam
- @MatrixParam
- @HeaderParam
- @CookieParam
@FormParam привязывает значения параметров формы к значениям параметров метода:
@POST
@Path("/")
public Task addTask(@FormParam("name") String name,
@FormParam("description") String description) {
}
При большом количестве аргументов конструкция метода выглядит устрашающе. К счастью есть возможность этого избежать. Для этого вы можете повесить аннотации @FormParam прямо на поля класса и сделать этот класс аргументом метода:
class Task {
@FormParam("name")
String name;
@FormParam("description")
String description;
}
@Path(value="/tasks")
class TaskService {
@POST
@Path("/")
public Task addTask(@Form Task task) {
}
}
Важный нюанс такого решения: класс, с аннотированными полями, должен содержать конструктор поумолчанию!
@PathParam позволяет использовать в качестве аргументов часть пути:
@GET
@Path("/{id}")
public Task getTask(@PathParam("id") String id) {
}
@QueryParam позволяет использовать в качестве аргументов параметры HTTP запроса:
@GET
@Path("/task?id=123")
public Task getTask(@QueryParam("id") String id) {
}
URL для вызова такого метода может выглядеть так: http://todo.com/tasks/task?id=123
Чтобы избежать NullPointerException в случае, если в запросе будут пропущенные параметры, можно указать для аргумента значение поумолчанию:
@GET
@Path("/task")
public Task getTask(@DefaultValue("123") @QueryParam("id") String id) {
}
В качестве типов аргументов помеченных как @QueryParam или @PathParam могут выступать:
- Все примитивные типы кроме char
- Все классы-оболочки кроме Character
- Любой класс с конструктором, который принимает единственный аргумент типа String
- Любой класс со статическим методом valueOf(String)
- List<T>, Set<T>, или SortedSet<T>, где T - тип удовлетворяющий условиям выше
Но примитивными типами аргументы методов не ограничиваются. Выше уже было показано как с помощью параметров формы
принимать в методах целые классы. Или как легко превращать возвращаемые методами объекты в json или xml
представление. Справедливо было бы ожидать возможность так же легко и получать объекты, созданные из json или xml
запроса. И такая возможность есть! Для этого достаточно пометить метод аннотацией @Consumes и указать принимаемый
медиа тип:
@POST
@Consumes(MediaType.APPLICATION_JSON)
public void updateTask(Task task) {
}
POST /contacts HTTP/1.1
Content-Type: application/json
Content-Length: 32
Content-Type: application/json
Content-Length: 32
Привет мир!
И так, общее представление о JAX-RS уже должно было сложиться. Осталось опробывать его в деле. Как и у любой
спецификации в мире java, у JAX-RS есть несколько реализаций:
- Apache CXF
- Jersey
- RESTeasy
- Restlet
- Apache Wink
- WebSphere Application Server:Version 7.0, Version 8.0
- WebLogic Application Server
- Apache Tuscany
- Cuubez framework
О процессе инсталяции сервера я рассказывать не стану и буду ожидать, что с этим вы справитесь самостоятельно. Поэтому сразу перейду к разработке.
Создадим пустой maven проект:
mvn archetype:generate -DgroupId=ru.dokwork -DartifactId=todo -DinteractiveMode=false
Удалим лишние классы, которые были созданы автоматически, и настроим скрипт:
Освежим версию java:
Настроим плагин для сборки war архивов:
Упростим себе жизнь, настроив процесс локального разворачивания приложения прямо из скрипта (при работе из IDE это
может упростить жизнь, но я не настаиваю):
Пользоваться плагином достаточно просто:
Думаю имена команд говорят сами за себя и разъяснений не требуют. Более подробную информацию вы можете найти здесь:
https://docs.jboss.org/wildfly/plugins/maven/latest/usage.html
Теперь добавим зависимость RESTeasy:
И чтобы получить возможность преобразовывать возвращаемые объекты в json добавим реализацию соответствующего
провайдера:
Ну и наконец, добавим библиотеки для тестирования:
pom.xml
Там где сервлеты, там и web.xml. Согласно принятой в maven структуре каталогов, этот файл должен лежать в директории src/main/webapp/WEB-INF/web.xml:
Для включения библиотеки resteasy в web.xml указывается слушатель и специальный сервлет:
Далее указывается какие адреса должен обрабатывать сервлет:
Если url-pattern отличается от /*, вы должны явно указать используемый resteasy.servlet.mapping.prefix:
Все сервисные классы должны быть явно перечислены в этом конфигурационном файле. Но к счастью есть возможность
избежать этого с помощью автоматического сканирования:
web.xml
Приложение для хранения списка задач так и напрашивается на использование базы данных, но использование баз данных всегда сопрежено с мелкими нюансами, отвлекаться на которые в этой статье совершенно не хочется. Поэтому я отгорожусь от задачи хранения заданий с помощью интерфейса некоторого DAO класса:
И воспользуюсь простейшей его реализацией:
MockTaskDao
Само же приложение будет состоять из класса описывающего задачу и сервиса, эти задачи обслуживающего:
Task
TaskService
Здесь вы снова не найдете поддержки метода PATCH из коробки, и снова добавить ее самостоятельно не составляет труда:
Полный код рассмотренного в статье примера вы можете посмотреть здесь: http://bitbucket.org/dok/todo/
Освежим версию java:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.4</version>
</plugin>
<plugin>
<groupId>org.wildfly.plugins</groupId>
<artifactId>wildfly-maven-plugin</artifactId>
<version>1.0.2.Final</version>
<configuration>
<hostname>127.0.0.1</hostname>
<port>9990</port>
<username>admin</username>
<password>password</password>
</configuration>
</plugin>
mvn wildfly:deploy mvn wildfly:redeploy mvn wildfly:undeploy
Теперь добавим зависимость RESTeasy:
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<version>3.0.8.Final</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson-provider</artifactId>
<version>3.0.8.Final</version>
<scope>provided</scope>
</dependency>
Ну и наконец, добавим библиотеки для тестирования:
- Библиотека для модульного тестирования
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
- Библиотеки для работы с http:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>fluent-hc</artifactId>
<version>4.3.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.3.5</version>
<scope>test</scope>
</dependency>
- И библиотека для преобразования объектов из json:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.2.4</version>
<scope>test</scope>
</dependency>
Там где сервлеты, там и web.xml. Согласно принятой в maven структуре каталогов, этот файл должен лежать в директории src/main/webapp/WEB-INF/web.xml:
todo
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── ru
│ │ └── dokwork
│ │ └── todo
│ └── webapp
│ └── WEB-INF
│ └── web.xml
└── test
└── java
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── ru
│ │ └── dokwork
│ │ └── todo
│ └── webapp
│ └── WEB-INF
│ └── web.xml
└── test
└── java
Для включения библиотеки resteasy в web.xml указывается слушатель и специальный сервлет:
<listener>
<listener-class>
org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap
</listener-class>
</listener>
<servlet>
<servlet-name>resteasy-servlet</servlet-name>
<servlet-class>
org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>resteasy-servlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>resteasy-servlet</servlet-name>
<url-pattern>/resteasy/*</url-pattern>
</servlet-mapping>
<context-param>
<param-name>resteasy.servlet.mapping.prefix</param-name>
<param-value>/resteasy</param-value>
</context-param>
<context-param>
<param-name>resteasy.scan</param-name>
<param-value>true</param-value>
</context-param>
web.xml
Приложение для хранения списка задач так и напрашивается на использование базы данных, но использование баз данных всегда сопрежено с мелкими нюансами, отвлекаться на которые в этой статье совершенно не хочется. Поэтому я отгорожусь от задачи хранения заданий с помощью интерфейса некоторого DAO класса:
И воспользуюсь простейшей его реализацией:
MockTaskDao
Само же приложение будет состоять из класса описывающего задачу и сервиса, эти задачи обслуживающего:
Task
TaskService
Здесь вы снова не найдете поддержки метода PATCH из коробки, и снова добавить ее самостоятельно не составляет труда:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@HttpMethod("PATCH")
public @interface PATCH {
}
Полный код рассмотренного в статье примера вы можете посмотреть здесь: http://bitbucket.org/dok/todo/
Заключение
Обзор получился несколько скомканным, но если отвлечься от примеров кода (недаром они спрятаны поумолчанию), то понять общую концепцию реализации RESTful приложений на Java должно быть не трудно. Благодаря JAX-RS вам не придется заботиться о нудной работе с сервлетами, вы даже можете не замечать того, что пишете веб приложение. Всего десяток аннотаций помогут превратить ваши POJO классы в полноценные REST сервисы!
За рамками данной статьи осталась еще одна очень важная тема: реализация аутентификации и авторизации в REST приложениях. Надеюсь я найду время чтобы изучить и осветить этот вопрос в ближайших статьях.
За рамками данной статьи осталась еще одна очень важная тема: реализация аутентификации и авторизации в REST приложениях. Надеюсь я найду время чтобы изучить и осветить этот вопрос в ближайших статьях.
