Эта статья является частью нашего академического курса под названием « Разработка современных приложений с помощью Scala» .
В этом курсе мы предоставляем среду и набор инструментов, чтобы вы могли разрабатывать современные приложения Scala. Мы охватываем широкий спектр тем: от сборки SBT и реактивных приложений до тестирования и доступа к базе данных. С нашими простыми учебными пособиями вы сможете запустить и запустить собственные проекты за минимальное время. Проверьте это здесь !
1. Введение
Как часто в наши дни вы слышите фразы типа «Веб-API пожирают мир» ? Действительно, спасибо Марку Андреессену за хорошее обобщение этого, но API становятся все более и более важными для поддержки разговоров между предприятиями или между потребителями , особенно в веб-вселенной.
Для многих предприятий наличие веб-API является обязательным конкурентным преимуществом и часто является вопросом выживания.
Содержание
2. Быть ОТДЫХОМ (фул)
В сегодняшней сети HTTP — король. За прошедшие годы было предпринято много разных попыток придумать стандартный, унифицированный протокол высокого уровня для предоставления услуг над ним. SOAP был, вероятно, первым, который получил широкую популярность (особенно в корпоративном мире) и на протяжении ряда лет служил де-факто спецификацией для разработки веб-сервисов и API. Тем не менее, его многословие и формализм часто служили источником дополнительной сложности, и это является одной из причин возникновения передачи представительского состояния (или просто REST ).
В наши дни большинство веб-сервисов и API-интерфейсов разрабатываются в соответствии с архитектурными стилями REST и поэтому называются REST (ful) . Для создания веб-сервисов и API, соответствующих принципам и ограничениям REST (ful), существует множество отличных инфраструктур, но в мире Scala есть несомненный лидер: Akka HTTP .
3. От спрея до Акки Http
Многие из вас могут быть знакомы с Akka HTTP с помощью его потрясающего предшественника, очень популярного Spray Framework . Это все еще довольно широко используется в дикой природе, но с прошлого года или около того, Spray Framework активно мигрирует под зонтиком Akka HTTP и будет в конечном итоге прекращен. На момент написания статьи последняя стабильная версия Akka HTTP (распространяемая как часть любимого Akka Toolkit ) была 2.4.11 .
Akka HTTP стоит на плечах Actor Model (предоставленной ядром Akka ) и Akka Streams и, таким образом, полностью охватывает парадигму реактивного программирования . Однако, пожалуйста, обратите внимание, что некоторые части Akka HTTP все еще носят экспериментальный ярлык, так как миграция Spray Framework в новый дом продолжается, и некоторые контракты с большой вероятностью изменятся.
4. Оставаться на сервере
В общем, когда мы говорим о веб-сервисах и API, в нем участвуют как минимум две стороны: сервер (поставщик) и клиент (потребитель). Неудивительно, что Akka HTTP имеет поддержку обоих, поэтому давайте начнем с самой интересной части, со стороны сервера.
Akka HTTP предлагает довольно много уровней API, от довольно низкоуровневой обработки запросов / ответов до красивых DSL . В этой части руководства мы собираемся использовать только DSL-сервер Akka HTTP , поскольку это самый быстрый (и самый красивый) способ начать создание веб-сервисов REST (ful) и API в Scala .
4.1. Маршруты и директивы
В основе HTTP- серверов Akka лежит маршрутизация, которая в контексте HTTP- коммуникаций может быть описана как процесс выбора наилучших путей для обработки входящих запросов. Маршруты составляются и описываются с использованием директив : строительные блоки серверного DSL -сервера Akka HTTP .
Например, давайте разработаем простой веб-API REST (ful) для управления пользователями, несколько функционально похожий на тот, который мы сделали в разделе «Веб-приложения с Play Framework» . Напомню, что вот как выглядит наш класс User дел (неудивительно, что он используется везде):
|
1
2
|
case class User(id: Option[Int], email: String, firstName: Option[String], lastName: Option[String]) |
Вероятно, первый маршрут, который может понадобиться нашему веб-API для обработки, — это вернуть список всех пользователей, поэтому пусть это будет нашей отправной точкой:
|
1
2
3
4
5
6
7
|
val route: Route = path("users") { pathEndOrSingleSlash { get { complete(repository.findAll) } }} |
Так просто! Каждый раз, когда клиент отправляет запрос GET на конечную точку /users (согласно path("users") и get директивы), мы собираемся извлечь всех пользователей из основного хранилища данных и вернуть их обратно (согласно complete директиве). В действительности маршруты могут быть довольно сложными, требующими извлечения и проверки различных параметров, но, к счастью, в DSL маршрутизации есть все, что встроено, например:
|
1
2
3
4
5
6
7
8
9
|
val route: Route = pathPrefix("users") { path(IntNumber) { id => get { rejectEmptyResponse { complete(repository.find(id)) } } } } |
Давайте внимательнее посмотрим на этот фрагмент кода. Как вы уже могли догадаться, мы предоставляем веб-API для извлечения пользователя по его целочисленному идентификатору, который предоставляется в качестве параметра пути URI (с использованием директивы path(IntNumber) ), например /users/101 . Однако пользователь с таким идентификатором может существовать или не существовать (вообще говоря, repository.find(id) возвращает Option[User] ). В этом случае наличие директивы rejectEmptyResponse указывает нашей конечной точке возвращать код состояния HTTP 404 (а не пустой ответ) в случае, если пользователь не был найден в хранилище данных.
Выглядит очень просто и лаконично, но любопытным читателям может быть интересно, какой формат данных будет использоваться для представления пользователей?
4.2. Маршаллинг и демаршаллинг
Akka HTTP использует процесс маршалинга и демаршаллинга для преобразования объектов в представление, передаваемое по проводам. Но практически, в большинстве случаев, когда речь шла о веб-API REST (ful) , мы говорим о формате JSON , и Akka HTTP имеет превосходную поддержку для этого.
|
1
2
3
4
|
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._import spray.json.DefaultJsonProtocol._implicit val userFormat = jsonFormat4(User) |
Использование предопределенной функции jsonFormat4 вводит неявную поддержку для маршаллинга User класса case в представление JSON и отмены маршалинга его обратно User из JSON , например:
|
1
2
3
4
5
6
7
8
9
|
val route: Route = pathPrefix("users") { pathEndOrSingleSlash { post { entity(as[User]) { user => complete(Created, repository.insert(user)) } } }} |
Выглядит неплохо, но практики веб-API REST (ful) могут утверждать, что семантика действия POST должна включать заголовок Location указывающий на вновь созданный ресурс. Давайте исправим это, потребуется немного реструктурировать реализацию, введя директивы onSuccess и respondWithHeader .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
val route: Route = pathPrefix("users") { pathEndOrSingleSlash { post { entity(as[User]) { user => onSuccess(repository.insert(user)) { u => respondWithHeader(Location(uri.withPath(uri.path / u.id.mkString))) { complete(Created, u) } } } } }} |
Отлично, что не менее важно, есть специальная поддержка потоковой передачи ответов HTTP в формате JSON , особенно когда задействованы потоки Akka . В разделе «Доступ к базе данных с помощью Slick» мы уже узнали, как транслировать результаты из хранилища данных с использованием потрясающей библиотеки Slick , поэтому этот фрагмент кода должен выглядеть очень знакомым.
|
1
2
3
4
5
6
7
|
class UsersRepository(val config: DatabaseConfig[JdbcProfile]) extends Db with UsersTable { ... def stream(implicit materializer: Materializer) = Source .fromPublisher(db.stream( users.result.withStatementParameters(fetchSize = 10))) ... } |
Source [T, _] может быть напрямую возвращен из директивы complete , при условии, что T (в нашем случае это User ) имеет неявную поддержку маршалинга JSON , например:
|
1
2
3
4
5
6
7
8
9
|
implicit val jsonStreamingSupport = EntityStreamingSupport.json()val route: Route = pathPrefix("users") { path("stream") { get { complete(repository.stream) } }} |
Хотя это может отличаться от других фрагментов кода, которые мы видели до сих пор, в этом случае Akka HTTP будет использовать кодирование передачи по частям для доставки ответа.
Также обратите внимание, что поддержка JSON в настоящее время находится на переходном этапе и все еще находится в пакетах Spray Framework . Надеемся, что возможные абстракции HTTP Akka будут очень похожи, и миграция будет зависеть от изменения пакета (скрестив пальцы).
4,3. Директивы в глубине
Директивы в Akka HTTP способны выполнять практически все с запросом или ответом, и существует действительно впечатляющий список предопределенных . Чтобы лучше понять механику, мы рассмотрим еще два примера: ведение журнала и безопасность.
Директива logRequestResult становится очень logRequestResult если вам необходимо устранить неполадки веб-API HTTP Akka путем проверки полных снимков запросов и ответов.
|
1
2
3
|
val route: Route = logRequestResult("user-routes") { ... } |
Вот лишь краткая иллюстрация того, как эта информация может выглядеть в выводе журнала при выполнении запроса POST конечной точки /users :
|
1
2
3
|
[akka.actor.ActorSystemImpl(akka-http-webapi)] user-routes: Response for Request : HttpRequest(HttpMethod(POST),http://localhost:58080/users,List(Host: localhost:58080, User-Agent: curl/7.47.1, Accept: */*, Timeout-Access: ),HttpEntity.Strict(application/json,{"email": "a@b.com"}),HttpProtocol(HTTP/1.1)) Response: Complete(HttpResponse(200 OK,List(),HttpEntity.Strict(application/json,{"id":1,"email":"a@b.com"}),HttpProtocol(HTTP/1.1))) |
Обратите внимание, что все заголовки запросов и ответов вместе с полезной нагрузкой включены. В случае передачи конфиденциальной информации (например, паролей или учетных данных) было бы неплохо внедрить какую-либо фильтрацию или маскировку сверху.
Другой интересный пример связан с защитой веб-API Akka HTTP с использованием семейства authenticateXxx . На данный момент поддерживаются только два потока аутентификации: HTTP Basic Auth и OAuth2 . В качестве примера, давайте введем еще одну конечную точку в наш веб-API, чтобы разрешить пользовательскую модификацию (обычно это делается с помощью запроса PUT ).
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
val route: Route = pathPrefix("users") { path(IntNumber) { id => put { authenticateBasicAsync(realm = "Users", authenticator) { user => rejectEmptyResponse { entity(as[UserUpdate]) { user => complete( repository.update(id, user.firstName, user.lastName) map { case true => repository.find(id) case _=> Future.successful(None) } ) } } } } }} |
Эта конечная точка защищена с помощью HTTP Basic Auth и будет проверять, существует ли уже электронная почта пользователя в хранилище данных. Для простоты пароль всегда жестко запрограммирован на "password" (пожалуйста, никогда не делайте этого в реальных приложениях).
|
1
2
3
4
5
6
7
|
def authenticator(credentials: Credentials): Future[Option[User]] = { credentials match { case p @ Credentials.Provided(email) if p.verify("password") => repository.findByEmail(email) case _ => Future.successful(None) }} |
Действительно приятно, но, вероятно, лучшая часть дизайна Akka HTTP — это расширяемость, встроенная прямо в ядро: очень легко ввести свои собственные директивы, если нет ничего предопределенного для удовлетворения ваших потребностей.
4.4. Когда дела идут плохо
Конечно, большую часть времени ваши веб-API будут работать отлично, обслуживая счастливых клиентов. Однако время от времени случаются плохие вещи, и лучше быть готовыми к ним. По сути, причин сбоя может быть много: нарушение бизнес-ограничений, подключение к базе данных, недоступность внешних зависимостей, сборка мусора и т. Д. Под капотом мы можем классифицировать их по двум различным сегментам: исключения и продолжительность выполнения.
В свете разрабатываемого нами веб-API типичным примером исключительной ситуации может быть создание пользователя с дублирующимся адресом электронной почты. На уровне хранения данных это приведет к уникальному нарушению ограничений и требует особой обработки. Как и следовало ожидать, для этого есть специальная директива handleExceptions , например:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
val route: Route = pathPrefix("users") { pathEndOrSingleSlash { post { handleExceptions(exceptionHandler) { extractUri { uri => entity(as[User]) { user => onSuccess(repository.insert(user)) { u => complete(Created, u) } } } } } }} |
Директива handleExceptions принимает ExceptionHandler в качестве аргумента, который отображает конкретное исключение в ответ. В нашем случае это будет SQLException, сопоставленное с HTTP- кодом ответа 409 , что указывает на конфликт.
|
1
2
3
|
val exceptionHandler = ExceptionHandler { case ex: SQLException => complete(Conflict, ex.getMessage)} |
Это здорово, но как насчет времени выполнения? К счастью, Akka HTTP поддерживает различные таймауты для защиты ваших веб-API. Тайм-аут запроса — один из тех, который позволяет ограничить максимальное количество времени, которое может занять маршрут для возврата ответа. Если это время превышено, будет возвращен код ответа HTTP 503 , сигнализирующий о том, что веб-API в данный момент недоступен. Все эти тайм-ауты могут быть настроены с использованием глобальных настроек в application.conf или с использованием директив timeout , например:
|
1
2
3
4
5
6
7
8
9
|
val route: Route = pathPrefix("users") { pathEndOrSingleSlash { post { withRequestTimeout(5 seconds) { ... } } }} |
Возможность использовать такой детализированный элемент управления является действительно мощным вариантом, поскольку не все веб-API одинаково важны и ожидания выполнения могут отличаться.
4,5. Запуск / выключение
Akka HTTP использует механизмы расширения Akka и обеспечивает реализацию расширения для поддержки Http . Существует довольно много способов его инициализации и использования, но самый простой из них — использование функции bindAndHandle .
|
1
2
3
4
5
|
implicit val system = ActorSystem("akka-http-webapi")implicit val materializer = ActorMaterializer()val repository = new UsersRepository(config)Http().bindAndHandle(new UserApi(repository).route, "0.0.0.0", 58080) |
Процедура выключения несколько сложна, но в двух словах состоит из двух этапов: открепление расширения Http и выключение базовой системы акторов, например:
|
1
2
3
4
|
val f = Http().bindAndHandle(new UserApi(repository).route, "0.0.0.0", 58080)f.flatMap(_.unbind) onComplete { _ => system.terminate} |
Вероятно, вы редко сталкивались бы с необходимостью использовать программное завершение, но, тем не менее, полезно знать механизм этого.
4,6. Безопасный HTTP (HTTPS)
Наряду с обычным HTTP , Akka HTTP готов к производственным развертываниям и поддерживает безопасную связь через HTTPS . Как и в том, что мы узнали в разделе учебника «Веб-приложения с Play Framework» , нам понадобится хранилище ключей Java с импортированными сертификатами. Для целей разработки создание самозаверяющих сертификатов является достаточно хорошим вариантом для начала:
|
01
02
03
04
05
06
07
08
09
10
11
|
keytool -genkeypair -v -alias localhost -dname "CN=localhost" -keystore src/main/resources/akka-http-webapi.jks -keypass changeme -storepass changeme -keyalg RSA -keysize 4096 -ext KeyUsage:critical="keyCertSign" -ext BasicConstraints:critical="ca:true" -validity 365 |
Однако для настройки поддержки HTTPS на стороне сервера требуется написать немало кода, но, к счастью, он хорошо документирован . Черта SslSupport служит примером такой конфигурации.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
trait SslSupport { val configuration = ConfigFactory.load() val password = configuration.getString("keystore.password").toCharArray() val https: HttpsConnectionContext = managed(getClass.getResourceAsStream("/akka-http-webapi.jks")).map { in => val keyStore = KeyStore.getInstance("JKS") keyStore.load(in, password) val keyManagerFactory = KeyManagerFactory.getInstance("SunX509") keyManagerFactory.init(keyStore, password) val tmf = TrustManagerFactory.getInstance("SunX509") tmf.init(keyStore) val sslContext = SSLContext.getInstance("TLS") sslContext.init(keyManagerFactory.getKeyManagers, tmf.getTrustManagers, new SecureRandom) ConnectionContext.https(sslContext) }.opt.get } |
Теперь мы можем использовать переменную контекста соединения https для создания привязки HTTPS , передав ее функции bindAndHandle в качестве аргумента, например:
|
1
2
|
Http().bindAndHandle(new UserApi(repository).route, "0.0.0.0", 58083, connectionContext = https) |
Обратите внимание, что у вас может быть поддержка HTTP, поддержка HTTPS или и то и другое одновременно, Akka HTTP дает вам полную свободу в выборе такого рода.
4,7. тестирование
Существует множество различных подходов к тестированию веб-API. Неудивительно, что, как и любой другой модуль Akka , Akka HTTP имеет специальные тестовые леса, полностью интегрированные со структурой ScalaTest (к сожалению, specs2 пока не поддерживается «из коробки»).
Давайте начнем с очень простого примера, чтобы убедиться, что конечная точка /users будет возвращать пустой список пользователей при отправке запроса GET .
|
1
2
3
4
5
6
7
|
"Users Web API" should { "return all users" in { Get("/users") ~> route ~> check { responseAs[Seq[User]] shouldEqual Seq() } }} |
Очень простой и легкий, тестовый пример выглядит близко к совершенству! И эти тестовые примеры выполняются на удивление быстро, потому что они запускаются с определением маршрута, без начальной загрузки полноценного экземпляра HTTP- сервера.
Немного более сложным сценарием будет разработка тестового примера для модификации пользователя, который требует, чтобы пользователь был создан до того, как были переданы учетные данные базовой аутентификации HTTP .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
"Users Web API" should { "create and update user" in { Post("/users", User(None, "a@b.com", None, None)) ~> route ~> check { status shouldEqual Created header[Location] map { location => val credentials = BasicHttpCredentials("a@b.com", "password") Put(location.uri, UserUpdate(Some("John"), Some("Smith"))) ~> addCredentials(credentials) ~> route ~> check { status shouldEqual OK responseAs[User] should have { 'firstName (Some("John")) 'lastName (Some("Smith")) } } } } }} |
Конечно, если вы практикуете TDD или производные методологии (и я искренне верю, что все должны), вы бы оценили надежность и краткость лесов тестирования Akka HTTP . Кажется, все сделано правильно.
5. Жизнь как клиент
Я надеюсь, что в этот момент мы по-настоящему оценили мощную серверную поддержку, которую Akka HTTP предоставляет для разработки REST (ful) веб-сервисов и API. Но часто веб-API, которые мы сами создали, становятся клиентами для других внешних веб-сервисов и API.
Как мы уже упоминали ранее, Akka HTTP имеет отличную клиентскую поддержку для взаимодействия с внешними веб-сервисами и API-интерфейсами на основе HTTP . Как и на стороне сервера, доступно несколько уровней клиентских API , но, вероятно, самый простой в использовании — это API уровня запросов .
|
1
2
3
4
|
val response: Future[Seq[User]] = Http() .flatMap { response => Unmarshal(response).to[Seq[User]] } |
В этом простом примере есть только один запрос к нашей конечной точке /users и результаты передаются из JSON в Seq[User] . Однако в большинстве реальных приложений стоит постоянно платить цену за установление HTTP- соединений, поэтому использование пулов соединений является предпочтительным и эффективным решением. Полезно знать, что в Akka HTTP также есть все необходимые строительные блоки для таких сценариев, например:
|
01
02
03
04
05
06
07
08
09
10
|
val pool = Http().superPool[String]() val response: Future[Seq[User]] = .via(pool) .runWith(Sink.head) .flatMap { case (Success(response), _) => Unmarshal(response).to[Seq[User]] case _ => Future.successful(Seq[User]()) } |
Конечные результаты этих двух фрагментов кода абсолютно одинаковы. Но последний использует пул и еще раз показывает, что Akka Streams постоянно поддерживается Akka HTTP и весьма удобен при обработке HTTP- сообщений на стороне клиента. Однако есть одна важная деталь: после завершения пулы соединений должны быть отключены вручную, например:
|
1
|
Http().shutdownAllConnectionPools() |
Если связь между клиентом и сервером основывается на протоколе HTTPS , API-интерфейс клиента Akka HTTP поддерживает шифрование TLS из коробки.
6. Выводы
Если вы создаете или думаете о создании REST (ful) веб-сервисов и API в Scala, обязательно попробуйте Akka HTTP . Его DSL маршрутизации — прекрасный и элегантный способ описания семантики ваших ресурсов REST (ful) и включения реализации для их обслуживания. По сравнению с Play Framework , на самом деле есть несколько совпадений, но для чистых проектов веб-API Akka HTTP определенно победитель. Присутствие некоторых экспериментальных модулей может наложить некоторые риски на принятие Akka HTTP сразу, однако с каждым выпуском все ближе и ближе, чтобы обеспечить стабильные и сжатые контракты.
7. Что дальше
Это действительно грустно признать, но наш учебник подходит к концу. Но я искренне верю, что для большинства из нас это станет лишь началом увлекательного путешествия в мир языка программирования и экосистемы Scala . Пусть радость и успех будут с вами по пути!
Полный исходный код доступен для скачивания .