[Из песочницы] GWT, Java 8 и Future

Добрый день.Думаю, многие из вас знают о выходе Java 8, и о том, какие нововведения она несет. К сожалению, последняя версия GWT (2.6.0) на данный момент до сих пор не поддерживает лямбды и default-методы в интерфейсах. Поскольку фреймворк GWT довольно востребован, многим приходится часто сталкиваться с разработкой именно на нем, мне не терпелось попробовать писать на GWT с использованием вновь добавленных в язык фич.В этой статье речь пойдет о том, как добавить поддержку Java 8 фич в GWT, а так же о том, для чего, собственно, все это нужно — на примере использования Future. Если вы когда-либо работали с GWT, то представляете все недостатки и неудобства, связанные с callback’ами при обращении к серверу. В то время, как в мире javascript многие уже давно используют Future/Promise, а в некоторых языках эта концепция встроена в стандартную библиотеку, в GWT до сих пор используются callbacks в любых способах взаимодействия между клиентом и сервером, будь то RemoTeServiceServlet, RPC-Dispatch или RequestFactory.Итак, приступим.Собриаем GWT После недолгого поиска был найден экспериментальный форк GWT. В нем заявлена довольно сносная поддержка Java 8 (за исключением JRE Emulation Library). На деле это оказалось не совсем так. Версия jdt-core, которая используется в этом форке, довольно старая и не способна нормально приводить типы. Пришлось поднять версию до 3.9.5, благо править надо было немного (поменялись лишь некоторые сигнатуры методов).Итак, берем исходники gwt отсюда и gwt-tools отсюда. После клонирования необходимо прописать переменную окружения GWT_TOOLS=path/to/gwt-tools. Далее идем в директорию с исходниками GWT и запускаем ant-build. Готово, в директории gwt-sandbox/build/lib появились библиотеки gwt-dev.jar, gwt-user.jar, gwt-codeserver.jar.Правим RestyGWT Для нашего примера будем использовать модифицированную библиотеку RestyGWT.Здесь находится RestyGWT с поддержкой Future.

Теперь вместо

void makeServerRequest (MethodCallback callback); взаимодействие с сервером будет выглядеть так: Future makeServerRequest (); Мне показалась очень привлекательной реализация Future в Scala, и захотелось сделать что-то подобное. Вот что получилось:

Интерфейс public interface Future {

public void onComplete (Consumer> consumer);

public void handle (Consumer errorHandler, Consumer successHandler);

public void forEach (Consumer consumer);

public Future map (Function function);

public Future flatMap (Function> function);

public T get ();

} Имплементация public class FutureImpl implements Future {

private List>> completeFunctions = new ArrayList<>(); private Option> result = Option.getEmpty ();

public FutureImpl () { }

@Override public void onComplete (Consumer> consumer) { result.forEach (consumer: accept); completeFunctions.add (consumer); }

@Override public void handle (Consumer errorHandler, Consumer successHandler) { onComplete ((result) → { if (result.isSuccess ()) successHandler.accept (result.get ()); else errorHandler.accept (result.getCause ()); }); }

public void completeWithResult (Try result) { this.result = Option.create (result); for (Consumer> completeFunction: completeFunctions) { completeFunction.accept (result); } }

public void completeWithSuccess (T result) { completeWithResult (new Success(result)); }

public void completeWithFailure (Throwable ex) { completeWithResult (new Failure(ex)); }

@Override public void forEach (Consumer consumer) { onComplete ((result) → { if (result.isSuccess ()) { consumer.accept (result.get ()); } }); }

@Override public Future map (Function function) { FutureImpl future = new FutureImpl(); onComplete ((result) → { if (result.isSuccess ()) { future.completeWithSuccess (function.apply (result.get ())); } }); return future; }

@Override public FutureImpl flatMap (Function> function) { FutureImpl mapped = new FutureImpl(); onComplete ((result) → { if (result.isSuccess ()) { Future f = function.apply (result.get ()); f.onComplete (mapped: completeWithResult); } }); return mapped; }

@Override public T get () { return result.get ().get (); }

} Использование Для чего мы все это проделали? Попробую объяснить, что называется, «на пальцах».Допустим, у нас есть сервис для получения списка стран и регионов: @Path (»…/service») @Consumes (MediaType.APPLICATION_JSON) public interface CallbackCountryService extends RestService {

@GET @Path (»/countires/») public void getCountries (MethodCallback> callback);

@GET @Path (»/regions/{countryId}/») public void getRegions (@PathParam («countryId») Integer countryId, MethodCallback> callback);

} Вот несколько примеров использования этого сервиса с применением Future и без него:

Самый простой пример. Мы хотим взять список стран и отобразить его в нашем View: Без Future: countryService.getCountries (new MethodCallback>() {

@Override public void onFailure (Method method, Throwable exception) {

}

@Override public void onSuccess (Method method, List response) { view.displayCountries (response); } }); С Future:

countryService.getCountries ().forEach (view: displayCountries); Метод forEach это своего рода onSuccess callback’a. То есть при успешном выполнении вызовется метод displayCountries у View. Пример посложнее. Допустим, нам нужно обработать исключение и отобразить его.Без Future: countryService.getCountries (new MethodCallback>() {

@Override public void onFailure (Method method, Throwable exception) { view.displayError (exception.getMessage ()); }

@Override public void onSuccess (Method method, List response) { view.displayCountries (response); } }); С Future:

countryService.getCountries ().handle (t → view.displayError (t.getMessage ()), view: displayCountries); В метод Future.handle мы передаем две функции. Одна отвечает за обработку ошибки, вторая за обработку успешного выполнения с результатом. Нам нужно взять первую страну из списка и отобразить список регионов для нее: Без Future: countryService.getCountries (new MethodCallback>() { @Override public void onFailure (Method method, Throwable exception) { view.displayError (exception.getMessage ()); }

@Override public void onSuccess (Method method, List response) { countryService.getRegions (response.get (0).getId (), new MethodCallback>() {

@Override public void onFailure (Method method, Throwable exception) { view.displayError (exception.getMessage ()); }

@Override public void onSuccess (Method method, List response) { view.displayRegions (response); } }); } }); С использованием Future: countryService.getCountries () .map (countries → countries.get (0).getId ()) .flatMap (countryService: getRegions) .handle (err → view.displayError (err.getMessage ()), view: displayRegions); Сначала мы конвертируем Future> в Future, это вернет нам countryId при успешном выполнении. Затем получаем Future со списком регионов. И, наконец, обрабатываем результат. Нам нужно получить все регионы всех стран.Решение такой задачи без использования Future является довольно громоздким. Поэтому приведу сразу второй вариант.Разобьем задачу на несколько этапов:

Future>>> regionFutures = countryService.getCountries () .map ( countries → countries.map (country → countryService.getRegions (country.getId ())) ); Здесь мы получаем список Future всех регионов.В следующей трансформации надо привести List> к Future>. То есть наш Future выполнится тогда, когда все Future внутри списка будут завершены.

Future>>> regions = regionFutures.map (FutureUtils: toFutureOfList); И, наконец, приводим Future> к Future, а так же трансформируем список списков в одномерный список:

FutureUtils .flatten (regions) .map (ListUtils: flatten) .handle (err → view.displayError (err.getMessage ()), view: displayRegions ()); Недостаток последнего примера в том, что его довольно трудно понять человеку, который не принимал участия в написании этого кода. Однако решение получилось довольно компактным и имеет право на существование.

P.S. Для тех, кто хочет попробовать Java 8 в GWT, подготовлен демонстрационный проект с примерами из статьи. Проект мавенезирован, запускать можно через mvn jetty: run-exploded.Следует понимать, что все предоставленные библиотеки пока лучше не использовать в реальных проектах. Поддержка Future в RestyGWT довольно сырая, еще не оттестирована, и пока не умеет работать, например, с JSONP запросами. Поддержка же default интерфейсов и lambda работает довольно уверенно, хотя компиляция не всегда проходит при использовании лямбд в static-методах.

© Habrahabr.ru