Думаю, многие из вас знают о выходе 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<Void> callback);
взаимодействие с сервером будет выглядеть так:
Future<Void> makeServerRequest();
Мне показалась очень привлекательной реализация Future в Scala, и захотелось сделать что-то подобное. Вот что получилось:
public interface Future<T> { public void onComplete(Consumer<Try<T>> consumer); public void handle(Consumer<Throwable> errorHandler, Consumer<T> successHandler); public void forEach(Consumer<T> consumer); public <R> Future<R> map(Function<T, R> function); public <R> Future<R> flatMap(Function<T, Future<R>> function); public T get(); }
public class FutureImpl<T> implements Future<T> { private List<Consumer<Try<T>>> completeFunctions = new ArrayList<>(); private Option<Try<T>> result = Option.getEmpty(); public FutureImpl() { } @Override public void onComplete(Consumer<Try<T>> consumer) { result.forEach(consumer::accept); completeFunctions.add(consumer); } @Override public void handle(Consumer<Throwable> errorHandler, Consumer<T> successHandler) { onComplete((result) -> { if (result.isSuccess()) successHandler.accept(result.get()); else errorHandler.accept(result.getCause()); }); } public void completeWithResult(Try<T> result) { this.result = Option.create(result); for (Consumer<Try<T>> completeFunction : completeFunctions) { completeFunction.accept(result); } } public void completeWithSuccess(T result) { completeWithResult(new Success<T>(result)); } public void completeWithFailure(Throwable ex) { completeWithResult(new Failure<T>(ex)); } @Override public void forEach(Consumer<T> consumer) { onComplete((result) -> { if (result.isSuccess()) { consumer.accept(result.get()); } }); } @Override public <R> Future<R> map(Function<T, R> function) { FutureImpl<R> future = new FutureImpl<R>(); onComplete((result) -> { if (result.isSuccess()) { future.completeWithSuccess(function.apply(result.get())); } }); return future; } @Override public <R> FutureImpl<R> flatMap(Function<T, Future<R>> function) { FutureImpl<R> mapped = new FutureImpl<R>(); onComplete((result) -> { if (result.isSuccess()) { Future<R> 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<List<Country>> callback); @GET @Path("/regions/{countryId}/") public void getRegions(@PathParam("countryId") Integer countryId, MethodCallback<List<Region>> callback); }
Вот несколько примеров использования этого сервиса с применением Future и без него:
- Самый простой пример. Мы хотим взять список стран и отобразить его в нашем View:
Без Future:countryService.getCountries(new MethodCallback<List<Country>>() { @Override public void onFailure(Method method, Throwable exception) { } @Override public void onSuccess(Method method, List<Country> response) { view.displayCountries(response); } });
С Future:
countryService.getCountries().forEach(view::displayCountries);
Метод forEach это своего рода onSuccess callback’a. То есть при успешном выполнении вызовется метод displayCountries у View.
- Пример посложнее. Допустим, нам нужно обработать исключение и отобразить его.
Без Future:countryService.getCountries(new MethodCallback<List<Country>>() { @Override public void onFailure(Method method, Throwable exception) { view.displayError(exception.getMessage()); } @Override public void onSuccess(Method method, List<Country> response) { view.displayCountries(response); } });
С Future:
countryService.getCountries().handle(t -> view.displayError(t.getMessage()), view::displayCountries);
В метод
Future.handle
мы передаем две функции. Одна отвечает за обработку ошибки, вторая за обработку успешного выполнения с результатом. - Нам нужно взять первую страну из списка и отобразить список регионов для нее:
Без Future:countryService.getCountries(new MethodCallback<List<Country>>() { @Override public void onFailure(Method method, Throwable exception) { view.displayError(exception.getMessage()); } @Override public void onSuccess(Method method, List<Country> response) { countryService.getRegions(response.get(0).getId(), new MethodCallback<List<Region>>() { @Override public void onFailure(Method method, Throwable exception) { view.displayError(exception.getMessage()); } @Override public void onSuccess(Method method, List<Region> 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<List<Country>>
вFuture<Integer>
, это вернет нам countryId при успешном выполнении. Затем получаем Future со списком регионов. И, наконец, обрабатываем результат. - Нам нужно получить все регионы всех стран.
Решение такой задачи без использования Future является довольно громоздким. Поэтому приведу сразу второй вариант.Разобьем задачу на несколько этапов:
Future<List<Future<List<Region>>>> regionFutures = countryService.getCountries() .map( countries -> countries.map(country -> countryService.getRegions(country.getId())) );
Здесь мы получаем список Future всех регионов.
В следующей трансформации надо привести
List<Future<R>>
кFuture<List<R>>
. То есть наш Future выполнится тогда, когда все Future внутри списка будут завершены.Future<Future<List<List<Region>>>> regions = regionFutures.map(FutureUtils::toFutureOfList);
И, наконец, приводим
Future<Future<R>>
кFuture<R>
, а так же трансформируем список списков в одномерный список: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-методах.
ссылка на оригинал статьи http://habrahabr.ru/post/218731/