Статьи

Учебник. Начало работы со Scala и Scalatra. Часть IV


Новая Refcard: последние несколько месяцев мы с готовностью готовили релизы этой карты, и, наконец, широко запрашиваемая
Scala Refcard уже здесь. Скачайте
Scala: The Scalable JVM Language прямо сейчас! —DZ Кураторы

Добро пожаловать в последнюю часть этой серии уроков по скале и скалатре. В этой части мы рассмотрим, как вы можете использовать Akka для обработки ваших запросов с помощью асинхронного диспетчера, как использовать subcut для внедрения зависимостей и, наконец, как вы можете запустить полный API в облаке. В этом примере я использовал openshift из JBoss для запуска API на сервере приложений JBoss 7.1. Теперь, что мы видели в предыдущих уроках:

Примеры в этом уроке предполагают, что вы завершили предыдущие три урока. Мы не будем показывать все детали, но сосредоточимся на добавлении новых функций в существующее приложение (из части III). Чтобы быть точным в этом примере, мы покажем вам следующие шаги:

  1. Сначала мы введем subcut для приложения для внедрения зависимостей
  2. Далее мы сделаем наши запросы асинхронными, используя фьючерсы Akka.
  3. Наконец, мы включим CORS, упакуем приложение и развернем его в openshift.

И у нас будет API, который мы можем вызвать в облаке openshift.

Dev HTTP Client_0.png

Начнем с подреза

Добавление внедрения зависимостей в приложение

В Java есть много структур внедрения зависимостей. Большинство людей слышали о Spring и Guice, и внедрение зависимостей даже имеет свои собственные JSR и спецификации. В Scala, однако, это не так. Было много разговоров о том, нужно ли scala-приложению инфраструктуру DI, поскольку эти концепции также могут быть применены с использованием стандартных языковых конструкций Scala. Когда вы начнете исследовать внедрение зависимостей для Scala, вы быстро столкнетесь с шаблоном тортов ( здесь приведено очень подробное объяснение). Я не буду вдаваться в подробности, почему вы должны или не должны использовать шаблон тортов, но лично для меня это было похоже на то, что он ввел слишком много бессмысленного кода и я хотел чего-то более простого. Для этой статьи я собираюсь использовать subcut, Subcut — это действительно небольшой и простой в использовании фреймворк, который делает использование DI в scala очень простым и ненавязчивым.

Ничто не работает как примеры. Итак, что вам нужно сделать, чтобы subcut управлял вашими зависимостями. Во-первых, нам, конечно, нужно красиво отделить нашу реализацию от нашего интерфейса / черты. В части III мы создаем набор репозиториев, которые мы использовали непосредственно из маршрутов скалатры, создавая их как переменные класса:

  // repo stores our items
  val itemRepo = new ItemRepository;
  val bidRepo = new BidRepository;

Проблема в том, что это связывает наши маршруты непосредственно с реализацией, чего мы не хотим. Итак, сначала давайте расширим репозитории, определив черту для этих репозиториев.

trait BidRepo {
 
  def get(bid: Long, user: String) : Option[Bid]
  def create(bid: Bid): Bid 
  def delete(user:String, bid: Long) : Option[Bid] 
}
 
trait ItemRepo {
  def get(id: Number) : Option[Item]
  def delete(id: Number) : Option[Item]
}
 
trait KeyRepo {
  def validateKey(key: String, app:String, server: String): Boolean
}

Ничего необычного. Мы используем эту черту из наших реализаций, как показано ниже, и все готово.

class BidRepository extends RepositoryBase with BidRepo {
 ...
}

Теперь, когда мы определили наши черты, мы можем начать использовать subcut для управления нашими зависимостями. Для этого нам понадобится пара вещей:

  1. Какая реализация связана с какой чертой
  2. В какие классы нужно вводить ресурсы
  3. Загрузите «корневой» объект с нашей конфигурацией

Прежде чем мы начнем. Сначала нам нужно обновить наш build.sbt зависимостью subcut и добавить правильный репозиторий.

libraryDependencies ++= Seq(
  "com.escalatesoft.subcut" %% "subcut" % "2.0-SNAPSHOT",
  "org.scalaquery" %% "scalaquery" % "0.10.0-M1",
  "postgresql" % "postgresql" % "9.1-901.jdbc4",
  "net.liftweb" %% "lift-json" % "2.4",
  "org.scalatra" % "scalatra" % "2.1.0",
  "org.scalatra" % "scalatra-scalate" % "2.1.0",
  "org.scalatra" % "scalatra-specs2" % "2.1.0",
  "org.scalatra" % "scalatra-akka" % "2.1.0",
  "ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime",
  "org.eclipse.jetty" % "jetty-webapp" % "8.1.5.v20120716" % "container",
  "org.eclipse.jetty" % "test-jetty-servlet" % "8.1.5.v20120716" % "test",
  "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar"))
)
 
resolvers ++= Seq("Scala-Tools Maven2 Snapshots Repository" at "https://oss.sonatype.org/content/groups/public/",
                  "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/")

Это не только добавляет зависимости subcut, но также и akka, которые мы увидим далее в этой статье.

Привязать реализации к черте

Привязки в subcut определяются в модуле привязки. Таким образом, расширяя модуль, вы создаете конфигурацию для своего приложения. Например, вы можете определить конфигурацию для тестирования, одну для обеспечения качества и другую для производства.

    // this defines which components are available for this module
    // for this example we won't have that much to inject. So lets
    // just inject the repositories.
    object ProjectConfiguration extends NewBindingModule(module => {
      import module._   // can now use bind directly
 
      // in our example we only need to bind to singletons, all bindings will
      // return the same instance.
      bind [BidRepo] toSingle new BidRepository
      bind [ItemRepo] toSingle new ItemRepository
 
      // with subcut however, we have many binding option as an example we bind the keyrepo and want a new
      // instance every time the binding is injected. We'll use the toProvider option for this
      bind [KeyRepo] toProvider {new KeyRepository}
    }
 )

Не погружаясь слишком глубоко в подрезать. В этом фрагменте кода мы делаем то, что привязываем реализацию к признаку. Мы делаем это для всех ресурсов, которые мы хотим внедрить, поэтому subcut знает, какую реализацию создать, когда встречает определенный интерфейс. Если мы хотим внедрить различные реализации определенной черты, мы также можем добавить идентификатор в привязку, чтобы мы могли однозначно ссылаться на них.

Сконфигурируйте классы, в которые нужно вводить ресурсы

Теперь, когда у нас есть набор характеристик, связанных с реализацией, мы можем позволить subcut вводить ресурсы. Для этого нам нужно сделать две вещи. Сначала нам нужно добавить неявный val в класс HelloScalatraServlet.

class HelloScalatraServlet(implicit val bindingModule: BindingModule) extends ScalatraServlet with Authentication
                                                   with RESTRoutes {
 ....
}

Это должно быть добавлено ко всем классам, которые хотят, чтобы ресурсы там вводились с помощью subcut. С этим неявным значением subcut имеет доступ к конфигурации и может использовать ее для внедрения зависимостей. Мы определили наши маршруты в признаке RESTRoutes, поэтому давайте посмотрим, как мы настроим эту особенность для работы с subcut:

trait RESTRoutes extends ScalatraBase with Injectable {
 
    // simple logger
  val logger = Logger(classOf[RESTRoutes]);
 
  // This repository is injected based on type. If no type can be found an exception is thrown
  val itemRepo = inject[ItemRepo]
  // This repo is injected optionally. If none is provided a standard one will be created
  val bidRepo = injectOptional[BidRepo] getOrElse {new BidRepository};
 
  ...
}

Мы добавили черту Injectable из подреза, чтобы мы могли использовать функции инъекции (из которых есть несколько вариантов). В этом примере itemRepo внедряется с использованием функции inject. Если подходящей реализации не найдено, выдается сообщение об ошибке. И bidRepo вводится с помощью injectOptional. Если ничто не было связано, используется значение по умолчанию. Поскольку этот признак используется только что увиденным сервлетом (с неявным модулем привязки), он имеет доступ к конфигурации привязки, и подрез будет вводить необходимые зависимости.

Загрузите «корневой» объект с нашей конфигурацией

Все, что нам нужно сделать сейчас, это сообщить нашему корневому объекту (сервлету), какую конфигурацию он должен использовать, и все будет соединено вместе. Мы делаем это из сгенерированного слушателя Scalatra, где добавляем следующее:

   ...
  override def init(context: ServletContext) {
 
    // reference the project configuation, this is implicatly passed into the 
    // helloScalatraServlet
    implicit val bindingModule = ProjectConfiguration
 
    // Mount one or more servlets, this will inject the projectconfiguration
    context.mount(new HelloScalatraServlet, "/*")
  }
 
  ...

Здесь мы создаем модуль связывания, который неявно передается в конструктор HelloScalatraServlet. И все, когда вы сейчас запустите приложение, subcut определит, какую зависимость нужно внедрить. Вот и все. Если мы сейчас запустим приложение, то subcut обработает зависимости Если все идет хорошо и все зависимости найдены, приложение запустится успешно. Если одна из зависимостей не может быть найдена, будет выдано сообщение об ошибке:

15:05:51.112 [main] WARN  o.eclipse.jetty.webapp.WebAppContext - Failed startup of context o.e.j.w.WebAppContext{/,file:/Users/jos/Dev/scalatra/firststeps/hello-scalatra/src/main/webapp/},src/main/webapp
org.scala_tools.subcut.inject.BindingException: No binding for key BindingKey(org.smartjava.scalatra.repository.ItemRepo,None)
	at org.scala_tools.subcut.inject.BindingModule$class.inject(BindingModule.scala:66) ~[subcut_2.9.1-2.0-SNAPSHOT.jar:2.0-SNAPSHOT]

К следующему пункту в списке, Akka.

Добавить асинхронную обработку с Akka

Akka предоставляет вам полную среду Actor, которую вы можете использовать для создания масштабируемых многопоточных приложений. Scalatra поддерживает Akka из коробки, поэтому заставить его работать очень легко. Просто добавьте правильную черту, оберните функции с помощью функции Future, и вы почти закончили. Все действия происходят в признаке RESTRoutes, где мы определили наши маршруты. Позволяет нескольким из этих методов использовать Akka.

trait RESTRoutes extends ScalatraBase with Injectable with AkkaSupport{
 
   ...
 
  /**
   * Handles get based on items id. This operation doesn't have a specific
   * media-type since we're doing a simple GET without content. This operation
   * returns an item of the type application/vnd.smartbid.item+json
   */
  get("/items/:id") {
    // set the result content type
	contentType = "application/vnd.smartbid.item+json"
 
	// the future can't access params directly, so extract them first
	val id = params("id").toInt;  
 
    Future {
	    // convert response to json and return as OK
	    itemRepo.get(id) match {
	      case Some(x) => Ok(write(x));
	      case None => NotFound("Item with id " + id + " not found");
	    }
    }
  }
 
  /**
   * Delete the specified item
   */
  delete("/items/:id") {
   val id = params("id").toInt;
 
    Future {
       itemRepo.delete(id) match {
        case Some(x) => NoContent();
        case None => NotFound("Item with id " + id + " not found");
      }
    }
  }
  ...
}

Не слишком, чтобы увидеть здесь. Мы просто добавили черту AkkaSupport и обернули тело нашего метода функцией Future. Это запустит блок кода асинхронно. Скалатра будет ждать, пока этот блок не будет сделан, и вернет результат. Здесь нужно отметить одну вещь: у вас нет доступа к переменным контекста запроса, предоставляемым scalatra. Поэтому, если вы хотите установить тип содержимого ответа, вам нужно сделать это вне будущего. То же самое касается, например, доступа к параметрам или телу запроса.
Все, что вам нужно сделать сейчас, это установить Akka ActorSystem. Самый простой способ сделать это — просто использовать систему актеров по умолчанию. См. Документацию Akka для дополнительных параметров.

class HelloScalatraServlet(implicit val bindingModule: BindingModule) extends ScalatraServlet with Authentication
                                                   with AkkaSupport
                                                   with RESTRoutes {
 
  // create a default actor system. This is used from the futures in the web routes
  val system = ActorSystem()
}

Теперь, когда вы запустите контейнер сервлета, вы будете использовать фьючерсы Akka для обработки запросов.

Добавьте CORS и разверните в облаке

В качестве последнего шага давайте добавим CORS . с CORS вы можете открыть свой API для использования из других доменов. Это устраняет необходимость в JSONP. Использование этого в скалатре удивительно просто. Просто добавьте черту CorsSupport и все готово. Когда вы запустите приложение, вы увидите нечто подобное:

15:31:28.505 [main] DEBUG o.s.scalatra.HelloScalatraServlet - Enabled CORS Support with:
allowedOrigins:
	*
allowedMethods:
	GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH
allowedHeaders:
	Cookie, Host, X-Forwarded-For, Accept-Charset, If-Modified-Since, Accept-Language, X-Forwarded-Port, Connection, X-Forwarded-Proto, User-Agent, Referer, Accept-Encoding, X-Requested-With, Authorization, Accept, Content-Type

Вы можете точно настроить то, что вы поддерживаете, используя набор параметров инициализации, описанных
здесь .

Теперь осталось только упаковать все и развернуть в openshift . Если вы еще этого не сделали, зарегистрируйтесь на openshift (это бесплатно). Для моего примера я использую стандартное приложение «JBoss Application Server 7.1» без каких-либо картриджей.

OpenShift от Red Hat.png

Я не хотел настраивать postgresql, поэтому я создал фиктивную реализацию репо:

class DummyBidRepository extends BidRepo{
 
  val dummy = new Bid(Option(10l),10,10,20,"FL",10l,12345l, List());
 
  def get(bid: Long, user: String) : Option[Bid] = {
    Option(dummy);
  }
  def create(bid: Bid): Bid = {
    dummy;
  }
  def delete(user:String, bid: Long) : Option[Bid] = {
    Option(dummy);
  }
}

И использовал subcut для внедрения этого, вместо репозитория, который требует базы данных:

      bind [BidRepo] toSingle new DummyBidRepository

С этим небольшим изменением мы можем использовать sbt для создания файла войны.

[email protected]:~/Dev/scalatra/firststeps/hello-scalatra$ sbt package && cp target/scala-2.9.1/hello-scalatra_2.9.1-0.1.0-SNAPSHOT.war ~/dev/git/smartjava/deployments/
[info] Loading project definition from /Users/jos/Dev/scalatra/firststeps/hello-scalatra/project
[info] Set current project to hello-scalatra (in build file:/Users/jos/Dev/scalatra/firststeps/hello-scalatra/)
[info] Compiling 2 Scala sources to /Users/jos/Dev/scalatra/firststeps/hello-scalatra/target/scala-2.9.1/classes...
[info] Packaging /Users/jos/Dev/scalatra/firststeps/hello-scalatra/target/scala-2.9.1/hello-scalatra_2.9.1-0.1.0-SNAPSHOT.war ...
[info] Done packaging.
[success] Total time: 7 s, completed Oct 5, 2012 1:57:12 PM

И используйте git для развертывания его в openshift

[email protected]:~/git/smartjava/deployments$ git add hello-scalatra_2.9.1-0.1.0-SNAPSHOT.war && git commit -m 'update' && git push
[master b1c6eae] update
 1 files changed, 0 insertions(+), 0 deletions(-)
Counting objects: 7, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 11.16 KiB, done.
Total 4 (delta 3), reused 0 (delta 0)
remote: Stopping application...
remote: Done
remote: ~/git/smartjava.git ~/git/smartjava.git
remote: ~/git/smartjava.git
remote: Running .openshift/action_hooks/pre_build
...
remote: Emptying tmp dir: /var/lib/stickshift/3bc81f5b0d7c48ad84442698c9da3ac4/smartjava/jbossas-7/standalone/tmp/work
remote: Running .openshift/action_hooks/deploy
remote: Starting application...
remote: Done
remote: Running .openshift/action_hooks/post_deploy
To ssh://[email protected]/~/git/smartjava.git/
   a45121a..b1c6eae  master -> master

Вы, вероятно, увидите нечто подобное, и теперь все готово. Или, по крайней мере, почти сделано. Причина, что происходит, когда вы получаете доступ к ресурсу:

Dev HTTP Client-1.png

Хм .. что-то пошло не так. Это сообщение, которое нам интересно:

java.lang.IllegalStateException: The servlet or filters that are being used by this request do not support async operation

Хммм … очевидно JBoss AS работает с сервлетами немного отличающимися от Jetty. Причина, по которой мы видим это сообщение, заключается в том, что по умолчанию, согласно спецификации сервлета 3.0, сервлеты не поддерживают асинхронные операции. Поскольку в результате мы используем Akka Futures для наших маршрутов, нам нужна эта асинхронная поддержка. Обычно вы включаете эту поддержку в файле web.xml или используете аннотации в сервлете. В нашем случае, однако, наш сервлет запускается из слушателя:

  override def init(context: ServletContext) {
 
    // reference the project configuation, this is implicatly passed into the 
    // helloScalatraServlet
    implicit val bindingModule = ProjectConfiguration
 
    // Mount one or more servlets, this will inject the projectconfiguration
    context.mount(new HelloScalatraServlet, "/*")
  }

 

Context.mount — это удобный метод, предоставляемый scalatra, который регистрирует сервлет. Однако это не включает асинхронную поддержку. Если мы зарегистрируем сервлет самостоятельно, мы можем включить эту асинхронную поддержку. Поэтому замените предыдущую функцию этой функцией:

 override def init(context: ServletContext) {
 
    // reference the project configuation, this is implicatly passed into the 
    // helloScalatraServlet
    implicit val bindingModule = ProjectConfiguration
 
    val servlet = new HelloScalatraServlet
 
    val reg = context.addServlet(servlet.getClass.getName,servlet);
    reg.addMapping("/*");
    reg.setAsyncSupported(true);
  }

 

Теперь мы явно включаем асинхронную поддержку. Снова создайте пакет и используйте git для развертывания веб-приложения в openshift.

sbt package 
git add hello-scalatra_2.9.1-0.1.0-SNAPSHOT.war && git commit -m 'update' && git push

И теперь у вас есть рабочая версия вашего API, работающая на openshift!