⬆️ ⬇️

GWT, Java 8 and Future

Good day.

I think many of you know about the release of Java 8, and what innovations it brings. Unfortunately, the latest version of GWT (2.6.0) at the moment still does not support lambda and default-methods in interfaces. Since the GWT framework is quite in demand, many often have to deal with the development on it, I was eager to try writing on GWT using newly added features in the language.



This article will discuss how to add support for Java 8 features in GWT, as well as about what, in fact, all this is needed - for example, using Future. If you have ever worked with GWT, then imagine all the disadvantages and inconveniences associated with callbacks when accessing the server. While in the javascript world many have been using Future / Promise for a long time, and in some languages ​​this concept is built into the standard library, GWT still uses callbacks in any methods of interaction between the client and the server, be it RemoTeServiceServlet, RPC-Dispatch or RequestFactory.

So let's get started.



Collect GWT



After a brief search, an experimental fork of the GWT was found. It claims pretty tolerable support for Java 8 (with the exception of the JRE Emulation Library). In fact, this was not quite true. The jdt-core version that is used in this fork is quite old and is not able to properly cast types. It was necessary to raise the version to 3.9.5, it was necessary to edit a little bit (only some signatures of the methods were changed).



Done, the gwt-dev.jar, gwt-user.jar, gwt-codeserver.jar libraries appeared in the gwt-sandbox / build / lib directory.



Rule RestyGWT



')

For our example, we will use the modified RestyGWT library.

Here is a Future RestyGWT.



Now instead of

 void makeServerRequest(MethodCallback<Void> callback); 


interaction with the server will look like this:

 Future<Void> makeServerRequest(); 




I found the implementation of Future in Scala to be very attractive, and I wanted to do something similar. Here's what happened:

Interface
 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(); } 




Implementation
 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(); } } 






Using



What have we done all this for? I will try to explain what is called "on the fingers."

Suppose we have a service for getting a list of countries and regions:



 @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); } 




Here are some examples of using this service with and without Future:

  1. The simplest example. We want to take a list of countries and display it in our View:

    Without 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); } }); 




    With Future:



     countryService.getCountries().forEach(view::displayCountries); 


    The forEach method is a kind of onSuccess callback. That is, if successful, the displayCountries method on the View will be called.



  2. The example is more complicated. Suppose we need to handle an exception and display it.

    Without 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); } }); 




    With Future:



     countryService.getCountries().handle(t -> view.displayError(t.getMessage()), view::displayCountries); 


    We pass two functions to the Future.handle method. One is responsible for handling the error, the second is responsible for handling successful execution with the result.



  3. We need to take the first country from the list and display a list of regions for it:

    Without 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); } }); } }); 


    Using Future:



     countryService.getCountries() .map(countries -> countries.get(0).getId()) .flatMap(countryService::getRegions) .handle(err -> view.displayError(err.getMessage()), view::displayRegions); 


    First we convert Future<List> Future, countryId . Future . , , .





    .

    Future . .



    :



    Future<List> Future, countryId . Future . , , .





    .

    Future . .



    :



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


    Here we get a list of Future of all regions.



    In the next transformation you need to bring
     List<Future>  Future<List>.    Future  ,   Future    . 
        



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




    And finally, we give
     Future<Future>  Future,         : 
        



    FutureUtils .flatten(regions) .map(ListUtils::flatten) .handle(err -> view.displayError(err.getMessage()), view::displayRegions());




    The disadvantage of the last example is that it is quite difficult to understand it to a person who did not participate in the writing of this code. However, the decision was quite compact and has the right to exist.







    PS For those who want to try Java 8 in GWT, a demo project has been prepared with examples from the article. The project is mavenesirovan, you can run through mvn jetty: run-exploded.



    It should be understood that it is better not to use all the libraries provided in real projects. Future support in RestyGWT is quite raw, not yet tested, and so far it does not know how to work, for example, with JSONP requests. Support for default interfaces and lambda works fairly confidently, although compilation does not always work when using lambdas in static methods.

Source: https://habr.com/ru/post/218731/



All Articles