Статьи

Создание простого RESTful API с помощью Spark

Отказ от ответственности : Этот пост посвящен микро-фреймворку Java под названием Spark, а не движку обработки данных Apache Spark .

В этом посте мы увидим, как Spark можно использовать для создания простого веб-сервиса. Как упоминалось в заявлении об отказе от ответственности, Spark — это микро-фреймворк для Java, основанный на фреймворке Ruby Sinatra. Spark стремится к простоте и предоставляет только минимальный набор функций. Однако он предоставляет все необходимое для создания веб-приложения в несколько строк кода Java.

Начиная

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

1
2
3
4
5
6
7
8
public class User {
 
  private String id;
  private String name;
  private String email;
   
  // getter/setter
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
public class UserService {
 
  // returns a list of all users
  public List<User> getAllUsers() { .. }
   
  // returns a single user by id
  public User getUser(String id) { .. }
 
  // creates a new user
  public User createUser(String name, String email) { .. }
 
  // updates an existing user
  public User updateUser(String id, String name, String email) { .. }
}

Теперь мы хотим представить функциональность UserService как RESTful API (для простоты мы пропустим гипермедиа-часть REST). Для доступа, создания и обновления пользовательских объектов мы хотим использовать следующие шаблоны URL:

ПОЛУЧИТЬ / пользователей Получить список всех пользователей
ПОЛУЧИТЬ / пользователей / <идентификатор> Получить конкретного пользователя
ПОЧТА / пользователей Создать нового пользователя
ПОЛОЖИТЬ / пользователей / <идентификатор> Обновить пользователя

Возвращенные данные должны быть в формате JSON.

Для начала работы со Spark нам нужны следующие зависимости Maven:

01
02
03
04
05
06
07
08
09
10
<dependency>
  <groupId>com.sparkjava</groupId>
  <artifactId>spark-core</artifactId>
  <version>2.0.0</version>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-simple</artifactId>
  <version>1.7.7</version>
</dependency>

Spark использует SLF4J для ведения журнала, поэтому нам нужен механизм связывания SLF4J для просмотра сообщений журнала и сообщений об ошибках. В этом примере мы используем зависимость slf4j-simple для этой цели. Тем не менее, вы также можете использовать Log4j или любой другой переплет, который вам нравится Наличие slf4j-simple в classpath достаточно, чтобы увидеть вывод журнала в консоли.

Мы также будем использовать GSON для генерации вывода JSON и JUnit для написания простых интеграционных тестов. Вы можете найти эти зависимости в полном pom.xml .

Возвращение всех пользователей

Теперь пришло время создать класс, который отвечает за обработку входящих запросов. Мы начнем с реализации запроса GET / users, который должен возвращать список всех пользователей.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
import static spark.Spark.*;
 
public class UserController {
 
  public UserController(final UserService userService) {
     
    get("/users"new Route() {
      @Override
      public Object handle(Request request, Response response) {
        // process request
        return userService.getAllUsers();
      }
    });
     
    // more routes
  }
}

Обратите внимание на статический импорт spark.Spark. * В первой строке. Это дает нам доступ к различным статическим методам, включая get (), post (), put () и другие. В конструкторе метод get () используется для регистрации маршрута, который прослушивает GET-запросы для / users. Маршрут отвечает за обработку запросов. Всякий раз, когда делается запрос GET / users, вызывается метод handle (). Внутри handle () мы возвращаем объект, который должен быть отправлен клиенту (в данном случае это список всех пользователей).

Spark очень выигрывает от лямбда-выражений Java 8. Route — это функциональный интерфейс (он содержит только один метод), поэтому мы можем реализовать его с помощью лямбда-выражения Java 8. Используя лямбда-выражение, определение маршрута сверху выглядит так:

1
get("/users", (req, res) -> userService.getAllUsers());

Чтобы запустить приложение, мы должны создать простой метод main (). Внутри main () мы создаем экземпляр нашего сервиса и передаем его нашему недавно созданному UserController:

1
2
3
4
5
public class Main {
  public static void main(String[] args) {
    new UserController(new UserService());
  }
}

Если мы теперь запустим main (), Spark запустит встроенный сервер Jetty, который прослушивает порт 4567. Мы можем проверить наш первый маршрут, инициировав запрос GET http: // localhost: 4567 / users.

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

1
[com.mscharhag.sparkdemo.User@449c23fd, com.mscharhag.sparkdemo.User@437b26fe]

Очевидно, что это не тот ответ, который мы хотим.

Spark использует интерфейс ResponseTransformer для преобразования объектов, возвращаемых маршрутами, в фактический HTTP-ответ.
ReponseTransformer выглядит так:

1
2
3
public interface ResponseTransformer {
  String render(Object model) throws Exception;
}

ResponseTransformer имеет единственный метод, который принимает объект и возвращает строковое представление этого объекта. Реализация по умолчанию ResponseTransformer просто вызывает toString () для переданного объекта (который создает вывод, как показано выше).

Поскольку мы хотим вернуть JSON, нам нужно создать ResponseTransformer, который преобразует переданные объекты в JSON. Для этого мы используем небольшой класс JsonUtil с двумя статическими методами:

01
02
03
04
05
06
07
08
09
10
public class JsonUtil {
 
  public static String toJson(Object object) {
    return new Gson().toJson(object);
  }
 
  public static ResponseTransformer json() {
    return JsonUtil::toJson;
  }
}

toJson () — это универсальный метод, который преобразует объект в JSON с помощью GSON. Второй метод использует ссылки метода Java 8 для возврата экземпляра ResponseTransformer. ResponseTransformer снова является функциональным интерфейсом, поэтому его можно удовлетворить, предоставив соответствующую реализацию метода (toJson ()). Поэтому всякий раз, когда мы вызываем json (), мы получаем новый ResponseTransformer, который использует наш метод toJson ().

В нашем UserController мы можем передать ResponseTransformer в качестве третьего аргумента методу Spark get ():

01
02
03
04
05
06
07
08
09
10
11
import static com.mscharhag.sparkdemo.JsonUtil.*;
 
public class UserController {
   
  public UserController(final UserService userService) {
     
    get("/users", (req, res) -> userService.getAllUsers(), json());
     
    ...
  }
}

Снова обратите внимание на статический импорт JsonUtil. * В первой строке. Это дает нам возможность создать новый ResponseTransformer, просто вызвав json ().

Наш ответ теперь выглядит так:

1
2
3
4
5
6
7
8
9
[{
  "id""1866d959-4a52-4409-afc8-4f09896f38b2",
  "name""john",
  "email""[email protected]"
},{
  "id""90d965ad-5bdf-455d-9808-c38b72a5181a",
  "name""anna",
  "email""[email protected]"
}]

У нас все еще есть небольшая проблема. Ответ возвращается с неправильным Content-Type . Чтобы это исправить, мы можем зарегистрировать Фильтр, который устанавливает JSON Content-Type:

1
2
3
after((req, res) -> {
  res.type("application/json");
});

Фильтр снова является функциональным интерфейсом и поэтому может быть реализован с помощью короткого лямбда-выражения. После того как запрос обработан нашим маршрутом, фильтр изменяет тип содержимого каждого ответа на приложение / json. Мы также можем использовать before () вместо after () для регистрации фильтра. Затем Фильтр будет вызван до того, как Маршрут обработает запрос.

GET / запрос пользователей должен работать сейчас!

Возвращение определенного пользователя

Чтобы вернуть конкретного пользователя, мы просто создаем новый маршрут в нашем UserController:

1
2
3
4
5
6
7
8
9
get("/users/:id", (req, res) -> {
  String id = req.params(":id");
  User user = userService.getUser(id);
  if (user != null) {
    return user;
  }
  res.status(400);
  return new ResponseError("No user with id '%s' found", id);
}, json());

С помощью req.params («: id») мы можем получить параметр пути: id из URL. Мы передаем этот параметр нашему сервису для получения соответствующего пользовательского объекта. Мы предполагаем, что сервис возвращает ноль, если ни один пользователь с переданным идентификатором не найден. В этом случае мы меняем код состояния HTTP на 400 (неверный запрос) и возвращаем объект ошибки.

ResponseError — это небольшой вспомогательный класс, который мы используем для преобразования сообщений об ошибках и исключений в JSON. Это выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class ResponseError {
  private String message;
 
  public ResponseError(String message, String... args) {
    this.message = String.format(message, args);
  }
 
  public ResponseError(Exception e) {
    this.message = e.getMessage();
  }
 
  public String getMessage() {
    return this.message;
  }
}

Теперь мы можем запросить одного пользователя с таким запросом:

GET / users / 5f45a4ff-35a7-47e8-b731-4339c84962be

Если пользователь с таким идентификатором существует, мы получим ответ, который выглядит примерно так:

1
2
3
4
5
{
  "id""5f45a4ff-35a7-47e8-b731-4339c84962be",
  "name""john",
  "email""[email protected]"
}

Если мы используем неверный идентификатор пользователя, объект ResponseError будет создан и преобразован в JSON. В этом случае ответ выглядит так:

1
2
3
{
  "message""No user with id 'foo' found"
}

Создание и обновление пользователей

Создание и обновление пользователей снова очень просто. Как и возвращение списка всех пользователей, это делается с помощью одного сервисного вызова:

01
02
03
04
05
06
07
08
09
10
post("/users", (req, res) -> userService.createUser(
    req.queryParams("name"),
    req.queryParams("email")
), json());
 
put("/users/:id", (req, res) -> userService.updateUser(
    req.params(":id"),
    req.queryParams("name"),
    req.queryParams("email")
), json());

Чтобы зарегистрировать маршрут для HTTP-запросов POST или PUT, мы просто используем статические методы Spark для post () и put (). Внутри маршрута мы можем получить доступ к параметрам HTTP POST с помощью req.queryParams ().

По причинам простоты (и чтобы показать другую особенность Spark) мы не проводим никакой проверки внутри маршрутов. Вместо этого мы предполагаем, что служба выдаст исключение IllegalArgumentException, если мы передадим недопустимые значения.

Spark дает нам возможность зарегистрировать ExceptionHandlers. ExceptionHandler будет вызываться, если при обработке маршрута выдается исключение. ExceptionHandler — это еще один интерфейс одного метода, который мы можем реализовать с помощью лямбда-выражения Java 8:

1
2
3
4
exception(IllegalArgumentException.class, (e, req, res) -> {
  res.status(400);
  res.body(toJson(new ResponseError(e)));
});

Здесь мы создаем ExceptionHandler, который вызывается, если выбрасывается IllegalArgumentException. Пойманный объект Exception передается в качестве первого параметра. Мы устанавливаем код ответа 400 и добавляем сообщение об ошибке в тело ответа.

Если служба выдает исключение IllegalArgumentException, когда параметр электронной почты пуст, мы можем получить ответ, подобный следующему:

1
2
3
{
  "message""Parameter 'email' cannot be empty"
}

Полный исходный код контроллера можно найти здесь .

тестирование

Из-за простой природы Spark очень легко написать интеграционные тесты для нашего примера приложения.

Давайте начнем с этой базовой настройки теста JUnit:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public class UserControllerIntegrationTest {
 
  @BeforeClass
  public static void beforeClass() {
    Main.main(null);
  }
 
  @AfterClass
  public static void afterClass() {
    Spark.stop();
  }
   
  ...
}

В beforeClass () мы запускаем наше приложение, просто запустив метод main (). После завершения всех тестов мы вызываем Spark.stop (). Это останавливает встроенный сервер, на котором работает наше приложение.

После этого мы можем отправлять HTTP-запросы в тестовых методах и проверять, что наше приложение возвращает правильный ответ. Простой тест, который отправляет запрос на создание нового пользователя, может выглядеть так:

1
2
3
4
5
6
7
8
9
@Test
public void aNewUserShouldBeCreated() {
  TestResponse res = request("POST""/users?name=john&[email protected]");
  Map<String, String> json = res.json();
  assertEquals(200, res.status);
  assertEquals("john", json.get("name"));
  assertEquals("[email protected]", json.get("email"));
  assertNotNull(json.get("id"));
}

request () и TestResponse — две небольшие самодельные утилиты для тестирования. request () отправляет HTTP-запрос на переданный URL-адрес и возвращает экземпляр TestResponse. TestResponse — это просто небольшая оболочка для некоторых HTTP-данных ответа. Источник request () и TestResponse включен в полный тестовый класс, найденный на GitHub.

Вывод

По сравнению с другими веб-фреймворками Spark предоставляет лишь небольшое количество функций. Тем не менее, это так просто, что вы можете создавать небольшие веб-приложения в течение нескольких минут (даже если вы раньше не использовали Spark). Если вы хотите взглянуть на Spark, вы должны четко использовать Java 8, что сокращает объем кода, который вам приходится много писать.

  • Вы можете найти полный источник примера проекта на GitHub .