Эта статья является частью нашего академического курса под названием « Разработка современных приложений с помощью 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 = jsonFormat 4 (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" : "[email protected]" }),HttpProtocol(HTTP /1 .1)) Response: Complete(HttpResponse(200 OK,List(),HttpEntity.Strict(application /json ,{ "id" :1, "email" : "[email protected]" }),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 { status shouldEqual Created header[Location] map { location = > 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 . Пусть радость и успех будут с вами по пути!
Полный исходный код доступен для скачивания .