Pull to refresh

GWT, Java 8 и Future

Reading time 6 min
Views 21K
Добрый день.
Думаю, многие из вас знают о выходе 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 и без него:
  1. Самый простой пример. Мы хотим взять список стран и отобразить его в нашем 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.

  2. Пример посложнее. Допустим, нам нужно обработать исключение и отобразить его.
    Без 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 мы передаем две функции. Одна отвечает за обработку ошибки, вторая за обработку успешного выполнения с результатом.

  3. Нам нужно взять первую страну из списка и отобразить список регионов для нее:
    Без 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> в Future, это вернет нам countryId при успешном выполнении. Затем получаем Future со списком регионов. И, наконец, обрабатываем результат.


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

    Разобьем задачу на несколько этапов:

    Future<List<Future<List<Region>>>> regionFutures = countryService.getCountries()
        .map(
                countries ->
                        countries.map(country -> countryService.getRegions(country.getId()))
        );
    

    Здесь мы получаем список Future всех регионов.

    В следующей трансформации надо привести
    List<Future> к Future<List>. То есть наш Future выполнится тогда, когда все Future внутри списка будут завершены.

    Future<Future<List<List<Region>>>> regions = regionFutures.map(FutureUtils::toFutureOfList);


    И, наконец, приводим
    Future<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-методах.
Tags:
Hubs:
+16
Comments 7
Comments Comments 7

Articles