Статьи

Groovy ++ в действии: Gretty / GridGain / REST / Websockets

Эта статья может рассматриваться как учебник по веб-серверу Gretty и еще одна демонстрация того, насколько мощным и выразительным может быть Groovy ++ .

Gretty — очень простая веб-платформа для построения асинхронных веб-серверов и клиентов. Он основан на потрясающей библиотеке Netty и унаследовал от нее очень впечатляющую производительность. Gretty написан на Groovy ++ и использует как выразительность чистого Groovy, так и быстродействие java производительности скомпилированного кода. Если вы Java-парень, который много слышал о Node.js, но не хочет иметь дело с javascript, вы должны попробовать Gretty.

Gretty — очень молодой проект, и у него пока нет собственного сайта. Вы можете найти источники и примеры на https://github.com/alextkachman/gretty

Давайте начнем с самого простого веб-сервера examplr, который почти ничего не делает, кроме обработки некоторого статического контента и перенаправления каждого запроса, который не может быть удовлетворен статическим контентом, на определенную веб-страницу. Вот код Groovy ++

@Typed package example

import org.mbte.gretty.httpserver.GrettyServer

new GrettyServer() [
// server local address
localAddress: new InetSocketAddress(8081),

// directory where to find static resources
static: "./static",

// default handler for requests which don't match any other pattern
default: {
redirect "/test.html"
}
].start ()

Здесь мы используем Groovy ++ оператор «multi-property-set» для установки множества свойств одним выражением.

Специалист по Groovy может заинтересоваться, почему он лучше именованных параметров конструктора. Причина в том, что в реальной жизни экземпляр GrettyServer может поступать из Spring или Guice, и различные службы или подсистемы могут вносить вклад в то, какой запрос сервер может обработать.

Конечно, тот же код может быть написан на Java. Ну, возможно, это будет немного более многословно, но Gretty можно использовать из Java, Scala или любого другого языка JVM. Вот Java-версия кода.

package example;

import org.mbte.gretty.httpserver.GrettyHttpHandler;
import org.mbte.gretty.httpserver.GrettyServer;

import java.net.InetSocketAddress;
import java.util.Map;

public class Step0_Java {
public static void main(String[] args) {
GrettyServer server = new GrettyServer();

// server local address
server.setLocalAddress(new InetSocketAddress(8081));

// directory where to find static resources
server.setStatic("./static");

// default handler for requests which don't match any other pattern
server.setDefault(new GrettyHttpHandler() {
public void handle(Map<String, String> pathArguments) {
redirect("/test.html");
}
});

server.start();
}
}


Как хороший гражданин Groovy сообщества Gretty обеспечивает специальную привязку для чистого Groovy.
Он выглядит почти так же, как и для Groovy ++, за исключением некоторых мелких деталей (в основном из-за особенностей языка, которые доступны в Groovy ++, но не в Groovy Core)

Вот тот же пример с использованием обычного Groovy. Единственное отличие (за исключением того, что у нас больше нет статической компиляции) заключается в том, что вместо оператора «multi-property-set» мы назначаем Map of settings специальному свойству ‘groovy’

package example

import org.mbte.gretty.httpserver.GrettyServer

def server = new GrettyServer()
server.groovy = [
// server local address
localAddress: new InetSocketAddress(8081),

// directory where to find static resources
static: "./static",

// default handler for requests which don't match any other pattern
default: {
redirect "/test.html"
}
]
server.start ()

Теперь давайте сделаем что-то менее тривиальное. Мы хотим достичь двух целей сейчас

  • чтобы иметь возможность проверить наш сервер
  • переместить часть статического контента (некоторые широко используемые файлы javascript) за пределы нашего сервера

Оба довольно просты. Gretty повторно использует прекрасную способность Netty не только работать по IP-соединениям, но и использовать связь в памяти. В тестовом режиме нам просто нужно привязать локальный адрес нашего сервера не к IP-сокету, а к внутреннему.

String [] args = binding.variables.args
def inTestMode = args?.length && args[0] == 'test'

new GrettyServer() [
// server local address
// if in test mode we use in-memory communication, otherwise IP
localAddress: !inTestMode ? new InetSocketAddress(9000) : new LocalAddress("test_server"),

// directory where to find static resources
static: "./static",

// default handler for request which don't match any rule
default: {
response.redirect "/webkvstore.html"
},

public: {
// redirect googlib path to google servers
get("/googlib/:path") {
def googleAjaxPath = 'http://ajax.googleapis.com/ajax/libs'

switch(it.path) {
case 'jquery.js':
redirect "${googleAjaxPath}/jquery/1.6.1/jquery.min.js"
break

case 'prototype.js':
redirect "${googleAjaxPath}/prototype/1.7.0.0/prototype.js"
break

default:
redirect "${googleAjaxPath}/${it.path}"
}
}
},
].start()

Открытый раздел определяет обработчики, которые будут использоваться для сопоставления запросов. В нашем случае любой запрос http GET на URL, начинающийся с ‘/ googlib /’, будет перенаправлен на серверы Google.

Сам обработчик не является чем-то особенным, кроме экземпляра org.mbte.gretty.httpserver.GrettyHttpHandler, который мы видели уже при импорте Java-версии нашего кода. Будучи так называемым «единственным абстрактным методом», GrettyHttpHandler почти всегда можно заменить закрытием.


Теперь мы готовы написать тесты для нашего сервера.
Мы будем использовать клиентскую часть Gretty и общение в памяти.

if(inTestMode) {
try {
TestRunner.run(new TestSuite(BasicTest))
}
finally {
System.exit(0)
}
}

class BasicTest extends GroovyTestCase implements HttpRequestHelper {
void testRedirectToMainPage () {
doTest("/nosuchurl") { response ->
assert response.status == HttpResponseStatus.MOVED_PERMANENTLY
assert response.getHeaders(HttpHeaders.Names.LOCATION)[0] == "/webkvstore.html"
}
}

void testMainPage () {
doTest("/webkvstore.html") { response ->
assert response.contentText == new File("./static/webkvstore.html").text
}
}

void testRedirectToGoogle () {
doTest("/googlib/prototype.js") { response ->
assert response.status == HttpResponseStatus.MOVED_PERMANENTLY
assert response.getHeaders(HttpHeaders.Names.LOCATION)[0] == "http://ajax.googleapis.com/ajax/libs/prototype/1.7.0.0/prototype.js"

}
doTest("/googlib/jquery.js") { response ->
assert response.status == HttpResponseStatus.MOVED_PERMANENTLY
assert response.getHeaders(HttpHeaders.Names.LOCATION)[0] == "http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"
}
}
}

 Примечание Использование HttpRequestHelper черта

Он смешивает полезные методы тестирования с нашим тестовым классом


Примечание по методологии тестирования

Мы помещаем наши тесты в тот же скрипт Groovy, что и сам сервер. Будучи очень удобным на этапе создания прототипа, он становится не очень хорошим, когда ваше приложение растет, поэтому вы, вероятно, будете использовать Guice или Spring или любой другой подход, который вам нравится, чтобы запустить сервер в режиме модульного тестирования. Дело в том, что общение в памяти все еще в вашем полном распоряжении.

Теперь давайте добавим еще несколько полезных функций на наш сервер. Мы хотим, чтобы клиенты могли хранить некоторые временные данные на сервере как операция, подобная карте. Самым простым решением является подход REST — операции http GET / PUT / DELETE с ключами, закодированными в URL. Для хранения будем использовать красивый распределенный кеш GridGain.

Полную версию исходного кода, включая тест, можно найти в репозитории github. Здесь мы покажем только важные изменения.

Прежде всего, перед запуском нашего сервера нам нужно подключиться к GridGain

// start GridGain and obtain cache instance
def cache = GridFactory.start("spring-cache.xml").cache('replicated')

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

    public: {
// redirect googlib path to google servers
get("/googlib/:path") {
....................................................
}

get("/api/:key") {
if(it.key)
response.json = [result: cache.get(it.key)]
else
response.json = [result: cache.keySet()]
}

delete("/api/:key") {
response.json = [result: cache.remove(it.key)]
}

put("/api/:key") {
response.json = [result: cache.put(it.key, request.contentText)]
}
},

Обратите внимание, насколько просто и удобно мы создаем ответы в формате JSON.

Gretty utilizes Jackson library — probably the fastest JSON library for Java available today

As we said above test cases are available at github repository but there is one thing, which is important to know. Not every browser is capable to send AJAX requests other than GET & POST, so our story with PUT & DELETE operations can be useless. Fortunately there is common workaround recommended at least by both Microsoft and Google in their APIs — X-HTTP-Method-Override header.

Gretty supports this header both on client and server side as shown in following code snippet for test cases

        // request done as 'POST' method with additional header
// X-HTTP-Method-Override: DELETE
// we use methodOveride only in order to make sure that it handled correctly
doTest([uri: "/$api/1", methodOverride: HttpMethod.DELETE ]) { response ->
assert Map.fromJson(response.contentText).result == '932'
}

Interesting to note that we developed our server without opening browser even once (modulo of course checking design of web pages)

But there still something non-perfect in our server. It is synchronious! That means that while we do network round trip to GridGain the server thread is blocked waiting for result to arrive. That does not sound like right utilization of resources. Let us fix it.

Again whole source code can be found at github repository We will only provide important changes.

First of all we will use alternative and more flexible approach to define our new API. Instead of putting everything in to public section of our server we will define separate web context dedicated to our API. In fact public section is just shortcut for default web context.

Secondly, we will use GridGain asynchronious operations like getAsync etc. Asynchronious operations return almost immidiately and instead of operation’s result return subclass of java.util.concurrent.Future called GridFuture. The huge benefit of GridFuture is the fact that it allows adding listeners for operation completion (OMG! why standard JDK future does not)

So we call asynchronious operation, register listener and free resources for handling of other requests (by returning from current handler). When listener invoked we are ready to send response back to the client.

Does not this code look elegant?

    public: {
..............................
},

webContexts: [
// alternative approach
// - use of web context instead of default context
// - use of asynchronous operations
"/asyncapi" : [
public: {
get("/:key") {
if(it.key) {
cache.getAsync(it.key) << { f ->
response.json = [result: f.get()]
}
}
else {
cache.getAllAsync() << { f ->
response.json = [result: f.get().keySet()]
}
}
}

delete("/:key") {
cache.removeAsync(it.key) << { f ->
response.json = [result: f.get()]
}
}

put("/:key") {
cache.putAsync(it.key, request.contentText) << { f ->
response.json = [result: f.get()]
}
}
}
]
]

 

Well, truly speaking, there is one small but important detail, which we did not explain yet. Obviously GridGain (designed for Java and Scala) does not provide Groovish leftShift API (<< operator). Even more important question is how does server know (except the fact that we responded nothing yet) that response is not completed and some continuation (our listener) plans to do it later.

To understand both issues we need to see how do we integrate GridGain in to our GrettyServer. Magically it is done by static compilation. In the code snippet below we define GrettyGridInClosure which subclass GridInClosure (base class for listener) from GridGain and mix it in with GrettyAsyncFunction trait from Gretty. GrettyAsyncFunction is capable to remember all necessary information of web request handled when it was created and than to reuse this knowledge when executed. At the same time leftShift method has parameter of type GrettyGridInClosure, which let compiler treat closure we provide as new instance of this class. Voila!

/**
* Utility class to make GridGain callback Gretty asynchronious
*/
abstract class GrettyGridInClosure<T> extends GridInClosure<T> implements GrettyAsyncFunction<T, Object> {
final void apply(T t) {
handlerAction(t)
}
}

/**
* Convenient << method for GridFuture
*/
static <V> void leftShift(GridFuture<V> self, GrettyGridInClosure<GridFuture<V>> listener) {
self.listenAsync listener
}

Very good — we have beautiful and covered with tests web server. The last feature we would like to add is to have similar API accessable via websockets. Yes, Gretty does support websockets.

The full code can be found in github repository which I recommend at least to look in order to see how websockets can be tested over in memory communication. Here we only provide snippet important for support of websockets. I hope the code is self-explaining (probably modulo «labeled closure» syntax used to define method onConnect)

   webContexts: [
// alternative approach
// - use of web context instead of default context
// - use of asynchronous operations
"/asyncapi" : [
public: {
......................................
websocket("/") { String msg ->
onConnect: {
send([msg: "welcome"].toJsonString())
}

def command = Map.fromJson(msg)
switch(command.method) {
case 'put':
cache.putAsync(command.key, command.value) << { f ->
send([result: f.get()].toJsonString())
}
break

case 'get':
if(command.key) {
cache.getAsync(command.key) << { f ->
sendJson([result: f.get()])
}
}
else {
cache.getAllAsync() << { f ->
sendJson([result: f.get().keySet()])
}
}
break

case 'delete':
cache.removeAsync(command.key) << { f ->
sendJson([result: f.get()])
}
break
}
}
}
]
]

Gretty is very young but already powerful project combining together brilliant Netty and capabilities of Groovy++. Maybe one day you will find it useful for your work.

Thank you for reading (hope it was interesting) and till next time.