- Урок I: Настройте scala и scalatra для использования в Eclipse и создайте свое первое приложение.
- Урок II: Запустите встроенный скалатр, создайте API REST, использующий JSON, и протестируйте с помощью specs2
- Урок III: добавьте устойчивость с помощью scalaquery и добавьте слой безопасности на основе hmac
Примеры в этом уроке предполагают, что вы завершили предыдущие три урока. Мы не будем показывать все детали, но сосредоточимся на добавлении новых функций в существующее приложение (из части III). Чтобы быть точным в этом примере, мы покажем вам следующие шаги:
- Сначала мы введем subcut для приложения для внедрения зависимостей
- Далее мы сделаем наши запросы асинхронными, используя фьючерсы Akka.
- Наконец, мы включим CORS, упакуем приложение и развернем его в openshift.
И у нас будет API, который мы можем вызвать в облаке openshift.
Давайте начнем с подреза Добавление добавления зависимости в приложение
В Java есть много структур внедрения зависимостей. Большинство людей слышали о Spring и Guice, и внедрение зависимостей даже имеет свои собственные JSR и спецификации. В Scala, однако, это не так. Было много разговоров о том, нужно ли scala-приложению инфраструктуру DI, поскольку эти концепции также могут быть применены с использованием стандартных языковых конструкций Scala. Когда вы начнете исследовать внедрение зависимостей для Scala, вы быстро столкнетесь с шаблоном тортов ( здесь приведено очень подробное объяснение). Я не буду вдаваться в подробности, почему вы должны или не должны использовать шаблон тортов, но лично для меня это было похоже на то, что он ввел слишком много бессмысленного кода и я хотел чего-то более простого. Для этой статьи я собираюсь использовать subcut . Subcut — это действительно небольшой и простой в использовании фреймворк, который делает использование DI в scala очень простым и ненавязчивым.
Ничто не работает как примеры. Итак, что вам нужно сделать, чтобы subcut управлял вашими зависимостями. Во-первых, нам, конечно, нужно красиво отделить нашу реализацию от нашего интерфейса / черты. В части III мы создаем набор репозиториев, которые мы использовали непосредственно из маршрутов скалатры, создавая их как переменные класса:
1
2
3
|
// repo stores our items val itemRepo = new ItemRepository; val bidRepo = new BidRepository; |
Проблема в том, что это связывает наши маршруты непосредственно с реализацией, чего мы не хотим. Итак, сначала давайте расширим репозитории, определив черту для этих репозиториев.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
trait BidRepo { def get(bid: <a href= "https://www.google.com/search?hl=en&q=allinurl%3Along+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >Long</a>, user: <a href= "https://www.google.com/search?hl=en&q=allinurl%3Astring+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >String</a>) : <a href= "https://www.google.com/search?hl=en&q=allinurl%3Aoption+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >Option</a>[Bid] def create(bid: Bid): Bid def delete(user:<a href= "https://www.google.com/search?hl=en&q=allinurl%3Astring+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >String</a>, bid: <a href= "https://www.google.com/search?hl=en&q=allinurl%3Along+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >Long</a>) : <a href= "https://www.google.com/search?hl=en&q=allinurl%3Aoption+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >Option</a>[Bid] } trait ItemRepo { def get(id: <a href= "https://www.google.com/search?hl=en&q=allinurl%3Anumber+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >Number</a>) : <a href= "https://www.google.com/search?hl=en&q=allinurl%3Aoption+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >Option</a>[Item] def delete(id: <a href= "https://www.google.com/search?hl=en&q=allinurl%3Anumber+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >Number</a>) : <a href= "https://www.google.com/search?hl=en&q=allinurl%3Aoption+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >Option</a>[Item] } trait KeyRepo { def validateKey(key: <a href= "https://www.google.com/search?hl=en&q=allinurl%3Astring+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >String</a>, app:<a href= "https://www.google.com/search?hl=en&q=allinurl%3Astring+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >String</a>, server: <a href= "https://www.google.com/search?hl=en&q=allinurl%3Astring+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >String</a>): <a href= "https://www.google.com/search?hl=en&q=allinurl%3Aboolean+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >Boolean</a> } |
Ничего необычного. Мы используем эту черту из наших реализаций, как показано ниже, и все готово.
1
2
3
|
class BidRepository extends RepositoryBase with BidRepo { ... } |
Теперь, когда мы определили наши черты, мы можем начать использовать subcut для управления нашими зависимостями. Для этого нам понадобится пара вещей:
- Какая реализация связана с какой чертой
- В какие классы нужно вводить ресурсы
- Загрузите «корневой» объект с нашей конфигурацией
Прежде чем мы начнем. Сначала нам нужно обновить наш build.sbt зависимостью subcut и добавить правильный репозиторий.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
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/' , |
Это не только добавляет зависимости subcut, но также и akka, которые мы увидим далее в этой статье.
Привязать реализации к черте
Привязки в subcut определяются в модуле привязки. Таким образом, расширяя модуль, вы создаете конфигурацию для своего приложения. Например, вы можете определить конфигурацию для тестирования, одну для обеспечения качества и другую для производства.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
// 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.
1
2
3
4
|
class HelloScalatraServlet(implicit val bindingModule: BindingModule) extends ScalatraServlet with Authentication with RESTRoutes { .... } |
Это должно быть добавлено ко всем классам, которые хотят, чтобы ресурсы там вводились с помощью subcut. С этим неявным значением subcut имеет доступ к конфигурации и может использовать ее для внедрения зависимостей. Мы определили наши маршруты в признаке RESTRoutes, поэтому давайте посмотрим, как мы настроим эту особенность для работы с subcut:
01
02
03
04
05
06
07
08
09
10
11
12
|
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, где добавляем следующее:
01
02
03
04
05
06
07
08
09
10
11
12
|
... 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 будет обрабатывать зависимости. Если все идет хорошо и все зависимости найдены, приложение запустится успешно. Если одна из зависимостей не может быть найдена, будет выдано сообщение об ошибке:
1
2
3
|
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.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
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 => <a href= "https://www.google.com/search?hl=en&q=allinurl%3Anotfound+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >NotFound</a>( '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 => <a href= "https://www.google.com/search?hl=en&q=allinurl%3Anotfound+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >NotFound</a>( 'Item with id ' + id + ' not found' ); } } } ... } |
Не слишком, чтобы увидеть здесь. Мы просто добавили черту AkkaSupport и обернули тело нашего метода функцией Future. Это запустит блок кода асинхронно. Скалатра будет ждать, пока этот блок не будет сделан, и вернет результат. Здесь нужно отметить одну вещь: у вас нет доступа к переменным контекста запроса, предоставленным scalatra. Поэтому, если вы хотите установить тип содержимого ответа, вам нужно сделать это вне будущего. То же самое касается, например, доступа к параметрам или телу запроса.
Все, что вам нужно сделать сейчас, это установить Akka ActorSystem. Самый простой способ сделать это — просто использовать систему актеров по умолчанию. См. Документацию Akka для дополнительных параметров.
1
2
3
4
5
6
7
|
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 и все готово. Когда вы запустите приложение, вы увидите нечто подобное:
1
2
3
4
5
6
7
|
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» без каких-либо картриджей.
Я не хотел настраивать postgresql, поэтому я создал фиктивную реализацию репо:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
class DummyBidRepository extends BidRepo{ val dummy = new Bid(<a href= "https://www.google.com/search?hl=en&q=allinurl%3Aoption+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >Option</a>(10l), 10 , 10 , 20 , 'FL' ,10l,12345l, <a href= "https://www.google.com/search?hl=en&q=allinurl%3Alist+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >List</a>()); def get(bid: <a href= "https://www.google.com/search?hl=en&q=allinurl%3Along+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >Long</a>, user: <a href= "https://www.google.com/search?hl=en&q=allinurl%3Astring+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >String</a>) : <a href= "https://www.google.com/search?hl=en&q=allinurl%3Aoption+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >Option</a>[Bid] = { <a href= "https://www.google.com/search?hl=en&q=allinurl%3Aoption+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >Option</a>(dummy); } def create(bid: Bid): Bid = { dummy; } def delete(user:<a href= "https://www.google.com/search?hl=en&q=allinurl%3Astring+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >String</a>, bid: <a href= "https://www.google.com/search?hl=en&q=allinurl%3Along+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >Long</a>) : <a href= "https://www.google.com/search?hl=en&q=allinurl%3Aoption+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >Option</a>[Bid] = { <a href= "https://www.google.com/search?hl=en&q=allinurl%3Aoption+java.sun.com&btnI=I%27m%20Feeling%20Lucky" >Option</a>(dummy); } } |
И использовал subcut для внедрения этого, вместо репозитория, который требует базы данных:
1
|
bind [BidRepo] toSingle new DummyBidRepository |
С этим небольшим изменением мы можем использовать sbt для создания файла войны.
1
2
3
4
5
6
7
|
jos @Joss -MacBook-Pro.local:~/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
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
jos @Joss -MacBook-Pro.local:~/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 |
Вы, вероятно, увидите нечто подобное, и теперь все готово. Или, по крайней мере, почти сделано. Причина, что происходит, когда вы получаете доступ к ресурсу:
Хм .. что-то пошло не так. Это сообщение, которое нам интересно:
1
|
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 или используете аннотации в сервлете. В нашем случае, однако, наш сервлет запускается из слушателя:
1
2
3
4
5
6
7
8
9
|
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, который регистрирует сервлет. Однако это не включает асинхронную поддержку. Если мы зарегистрируем сервлет самостоятельно, мы можем включить эту асинхронную поддержку. Поэтому замените предыдущую функцию этой функцией:
01
02
03
04
05
06
07
08
09
10
11
12
|
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.
1
2
|
sbt package git add hello-scalatra_2. 9.1 - 0.1 . 0 -SNAPSHOT.war && git commit -m 'update' && git push |
И теперь у вас есть рабочая версия вашего API, работающая на openshift!
Приятного кодирования и не забудьте поделиться!
Ссылка: Учебное пособие: Начало работы со scala и scalatra — часть IV от нашего партнера JCG Йоса Дирксена из блога Smart Java .