Пишем современный REST web-сервис на Scala за 15 минут

в 18:25, , рубрики: ant, express, gradle, grizzly, groovy, ivy, java, JAX-RS, Jersey, maven, sbt, scala, scalatra, sinatra, метки: , , , , , , , , , , , , ,

Мой сайт написан на Node.js, и иногда мне требуется сделать что-то, для чего Node.js не предназначен: например, произвести какие-нибудь математические вычисления.

В этом примере мы будем вычислять «хеш» пароля.

Доверим эту работу «бекенду», написанному на (подходящем для вычислений) функциональном языке программирования. Например, на Scala. Функционально это будет так: Node.js отправляет GET-запрос на хеширование на «бекенд», «бекенд» думает-думает и в ответ отсылает вычисленный хеш в формате Json. Обычный HTTP запрос-ответ, ничего сложного.

Есть множество подходов к решению поставленной задачи в плане выбора набора «фреймворков».

Я немного писал на Ruby (не на «Рельсах»), с удовольствием пользуясь фреймворком Sinatra. И на Node.js пишу, пользуясь клоном Sinatra по имени Express. Выяснилось, что и для Scala тоже написан соответствующий клон — Scalatra. Однако для своей работы она требует некоей непонятной штуки под названием Sbt (Scala Build Tool). Эта штука — типа клон Явовского Maven’а, только гораздо страшнее… Я так и не смог понять, что она делает…

«Хочешь сделать что-то хорошо — сделай это сам» (народная мудрость)

В этой статье мы заложим свой путь написания REST’ового web-сервиса на Scala, без лишних заморочек, как и обещано, за 15 минут.

В качестве REST’ового фреймворка мы воспользуемся Jersey. Это воплощение API JAX-RS. Этот API пришёл из мира Явы Enterprise Edition, но, несмотря на это, не является какой-нибудь монструозной Годзиллой, а представляет собой вполне себе адекватный небольшой фреймворк, в той своей части, которая нам потребуется.

Jersey — это API, и его нужно запустить на каком-нибудь Явовом веб-сервере. Jetty, Tomcat, Grizzly — можно выбирать любой. Я выбрал Grizzly, просто потому что он попался мне на глаза. А так, я не знаю, чем он лучше того же Jetty. Может быть, побыстрее будет.

Как всё это дело запускать

Если по дзен-буддистски, то достаточно двух файлов: build.sh вида “javac -d classes *.java; scalac -classpath […] -d classes *.scala” и run.sh вида “scala -classpath […] -Dпорт=8090 Main”.

Однако лучше использовать «сборщика», чтобы не возиться с масками имён файлов, и с длинным classpath в одну строку: правильный сборщик (не Maven, не Sbt) даст вам гораздо больше свободы (и читаемости), чем shell. Он сам за вас скачает все используемые библиотеки, и сам добавит каждый Jar’ник поимённо в classpath (и вам не придётся делать это руками).

На должность адекватного сборщика мы возьмём молодой (вот-вот выйдет версия 1.0) и развивающийся проект Gradle, который мне больше напоминает старого доброго Ant’а, чем Maven’а, в том, что не запирает разработчика в жёсткие рамки, а, наоборот, даёт полную свободу творчества, да ещё и на адекватном языке Groovy (клон Ruby в мире Явы).

Установка

Ставим JDK. Проверяем: java -version

Качаем Scala, и прописываем bin в PATH. Проверяем: scala –version

Качаем Gradle, и прописываем bin в PATH. Проверяем: gradle –v

Проект

Древо проекта

image

Сам проект расположился на github'е. Здесь я приведу код двух основных файлов.

Инструкции по сборке для Gradle — build.gradle

// используем язык Scala
apply plugin: 'scala'

ext.scala_version = '2.9.1'
ext.jersey_version = '1.12'
ext.description = 'Accessing various calculation tasks'
ext.classes_directory = new File('classes')

// вспомогательная функция удаления папки — аналог «rm -rf»
def delete_folder(File folder)
{
	if (folder.isDirectory())
	{
		String[] children = folder.list()
		int i = 0
		while (i < children.length)
		{
			boolean success = delete_folder(new File(folder, children[i]))
			if (!success)
			{
				return false
			}
			i++
		}
	}
	
	// The directory is now empty so delete it
	return folder.delete()
}

// эта задача очищает папку «classes» — можете запустить вручную, если будет нужно
task clean_output_folders <<
{
	delete_folder(classes_directory)
	classes_directory.mkdirs()
}

// основная задача — запускает наш веб-сервис
// сначала запускает «compileJava» и «compileScalaScala», а потом уже выполняется сама
task go(dependsOn: ['compileJava', 'compileScalaScala'], type: JavaExec) {
	main = 'Main'
	classpath = sourceSets.main.runtimeClasspath
	standardInput = System.in
	//args 'arguments`'
	systemProperty 'port', '8090'
}

// наборы исходных кодов
sourceSets
{
	// главный - Ява
	main
	{
		java
		{
			// исходники искать в папке «sources»
			srcDir 'sources'
		}
	}
	// ещё один - Scala
	scala
	{
		scala
		{
			// исходники искать в папке «sources»
			srcDir 'sources'
		}
	}
}

// скомпилированные Ява-классы класть в папку «classes»
sourceSets.main.output.classesDir = 'classes'
		
// скомпилированные Scala-классы класть в папку «classes»
sourceSets.scala.output.classesDir = 'classes'

// используемые библиотеки
dependencies
{
	// служебные библиотеки для обработки Scala из Gradle
	scalaTools group: 'org.scala-lang', name: 'scala-compiler', version: scala_version
	scalaTools group: 'org.scala-lang', name: 'scala-library', version: scala_version
	
	// сама Scala, нужна для компиляции кода на Scala
	compile group: 'org.scala-lang', name: 'scala-library', version: scala_version
	scalaCompile group: 'org.scala-lang', name: 'scala-library', version: scala_version
	
	// прочие библиотеки, используемые в программе
	
	scalaCompile group: 'asm', name: 'asm', version: '3.3.1'
	scalaCompile group: 'javax.ws.rs', name: 'jsr311-api', version: '1.1.1'
	scalaCompile group: 'com.sun.jersey', name: 'jersey-bundle', version: jersey_version
	scalaCompile group: 'com.sun.jersey', name: 'jersey-client', version: jersey_version
	scalaCompile group: 'com.sun.jersey', name: 'jersey-core', version: jersey_version
	scalaCompile group: 'com.sun.jersey', name: 'jersey-server', version: jersey_version
	scalaCompile group: 'com.sun.jersey', name: 'jersey-grizzly2', version: jersey_version
	scalaCompile group: 'com.sun.jersey', name: 'jersey-bundle', version: jersey_version

	runtime group: 'asm', name: 'asm', version: '3.3.1'
	runtime group: 'javax.ws.rs', name: 'jsr311-api', version: '1.1.1'
	runtime group: 'com.sun.jersey', name: 'jersey-bundle', version: jersey_version
	runtime group: 'com.sun.jersey', name: 'jersey-client', version: jersey_version
	runtime group: 'com.sun.jersey', name: 'jersey-core', version: jersey_version
	runtime group: 'com.sun.jersey', name: 'jersey-server', version: jersey_version
	runtime group: 'com.sun.jersey', name: 'jersey-bundle', version: jersey_version
	runtime group: 'com.sun.jersey', name: 'jersey-grizzly2', version: jersey_version
	
	// до кучи можно просто класть jar-ники в папку «libraries», 
	// и они тоже подхватятся в качестве библиотек
	
	scalaCompile fileTree(dir: 'libraries', include: '*.jar')
	scalaCompile files('classes')
	
	runtime fileTree(dir: 'libraries', include: '*.jar')
	runtime files('classes')
}

// компилирует код на Scala
compileScala
{
	// какой-то «демон» для Scala — убыстряет компиляцию
	scalaCompileOptions.useCompileDaemon = true
	// куда
	destinationDir = file('classes')
	// откуда
	source = file('sources')
}

// откуда качать используемые библиотеки
repositories
{
    mavenCentral()
}

И наш REST web-сервис, дающий возможность захешировать пароль на Whirlpool или SHA-512Hasher.scala

package resources

import javax.ws.rs._
import javax.ws.rs.core._

import hash.Whirlpool
import hash.SHA

import com.twitter.json.Json

@Path("/захѣшировать")
class Hasher
{	
	@GET 
	@Produces(Array("text/plain"))
	def приветствие() : String =
	{
		"Доступные алгоритмы: Whirlpool, SHA"
	}

	@GET 
	@Path("Whirlpool/{что}")
	@Produces(Array(MediaType.APPLICATION_JSON))
	def whirlpool(@DefaultValue("") @PathParam("что") что : String) : String =
	{
		if (что == "")
			throw new IllegalArgumentException("Что захешировать?")
	
		Json.build(Map("hash" -> Whirlpool.hash(что))).toString
	}

	@GET 
	@Path("SHA/{что}")
	@Produces(Array(MediaType.APPLICATION_JSON))
	def sha(@DefaultValue("") @PathParam("что") что : String) : String =
	{
		if (что == "")
			throw new IllegalArgumentException("Что захешировать?")
	
		Json.build(Map("hash" -> SHA.hash(что))).toString
	}
}

Запускаем

Качаем архив, распаковываем, заходим в папку и выполняем команду gradle go. При успешном выполнении вы узреете в консоли запуск веб-сервера:

Starting grizzly...
May 11, 2012 9:13:59 PM com.sun.jersey.api.core.PackagesResourceConfig init
INFO: Scanning for root resource and provider classes in the packages:
  resources
May 11, 2012 9:13:59 PM com.sun.jersey.api.core.ScanningResourceConfig logClasses
INFO: Root resource classes found:
  class resources.Hasher
May 11, 2012 9:13:59 PM com.sun.jersey.api.core.ScanningResourceConfig init
INFO: No provider classes found.
May 11, 2012 9:13:59 PM com.sun.jersey.server.impl.application.WebApplicationImpl _initiate
INFO: Initiating Jersey application, version 'Jersey: 1.12 02/15/2012 05:30 PM'
May 11, 2012 9:14:00 PM org.glassfish.grizzly.http.server.NetworkListener start
INFO: Started listener bound to [localhost:8090]
May 11, 2012 9:14:00 PM org.glassfish.grizzly.http.server.HttpServer start
INFO: [HttpServer] Started.

Проверяем

* Я заметил такой глюк своего браузера (Chrome): если зайти по этим адресам до запуска веб-сервера, и жать-жать-жать на «Обновить», то после того, как веб-сервер запуститися, Chrome будет по-прежнему выдавать текст «404», хотя если после этого зайти на тот же Url из другого браузера — всё работает.

Приветствие
Хешируем наш пароль по алгоритму SHA-512
Хешируем наш пароль по алгоритму Whirlpool

Если вам нечем заняться (или интересно), то можете почитать ещё

Как писать сборку на Gradle
Простой REST-сервис на Jersey
О Gradle по-русски
Что такое Scala и чем она удобна

Автор: kuchumovn

* - обязательные к заполнению поля