Intro
I've been investigating the options for writing UI code in Java recently. In my previous post I investigated Eclipse RAP and found that it can be integrated with Spring Boot in one executable jar. This time I wanted to do the same trick with GWT.
Everyone likes Spring Boot. It makes a lot of things so much cleaner and easier. But historically the frameworks for creating the UI in the web browser had their own methods to do some of the things Spring Boot does. Unfortunately in many cases their methods look old and outdated. So the goal was to use Spring Boot as much as possible and use GWT only for UI.
I have to warn the readers this post is actually a classic example of TL;DR :-)).
GWT Approach
GWT uses a special compiler to generate Javascript code from Java code. The method is to create a module description file .gwt.xml, use it to import some other modules and write the code in Java with GWT widgets. Then their compiler will generate a lot of javascript code that needs to be included on the html page. They have a tutorial at www.gwtproject.org that explains the basics.
They use GWT RPC to call methods on the backend. This approach requires an interface that is shared between the client and the server. The client uses the interface to call the RPC method. The server-side implementation of the method is registered in web.xml as a Servlet with the appropriate URL pattern.
One major issue in my opinion is debugging. GWT in latest versions adopted a radical approach of source maps. This means Java code debugging occurs in the browser with source maps enabled and not in Eclipse (or maybe I couldn't make it work in Eclipse). I tried this in Chrome and it actually works but it looks like a bit of a kludge. GWT does not even generate source maps by default. In order to use them one must start the code server and load a different javascript in the html page from this code server. Most people in this case add an option to the compiler.
I really mean no offence to the GWT team and the supporters of this technology but it looks a little outdated in general. They do not spend too much time developing new features. Even the build plugins are maintained by enthusiasts.
Goals
Here is what I wanted to achieve in my investigation:
- Use GWT only for generating the Javascript code that is to be archived together with everything else into the executable jar.
- Use Spring Boot for REST endpoints and avoid GWT RPC completely
- Use Spring Boot's executable jar to start the application and service the GWT html files with the embedded Tomcat. This also means all the other great Spring Boot features can be used.
Build Tool
In order to achieve goal #1 we need a good build tool. I've created the sample project from the tutorial with the Maven plugin. Here is the complete configuration that worked for me:
<plugin>
<groupId>net.ltgt.gwt.maven</groupId>
<artifactId>gwt-maven-plugin</artifactId>
<version>1.0-rc-6</version>
<executions>
<execution>
<goals>
<goal>import-sources</goal>
<goal>compile</goal>
<goal>import-test-sources</goal>
<goal>test</goal>
</goals>
</execution>
</executions>
<configuration>
<moduleName>org.example.gwt.StockWatcher</moduleName>
<moduleShortName>StockWatcher</moduleShortName>
<failOnError>true</failOnError>
<!-- GWT compiler 2.8 requires 1.8, hence define sourceLevel here if you use
a different source language for java compilation -->
<sourceLevel>1.8</sourceLevel>
<!-- Compiler configuration -->
<compilerArgs>
<!-- Ask GWT to create the Story of Your Compile (SOYC) (gwt:compile) -->
<arg>-compileReport</arg>
<arg>-XcompilerMetrics</arg>
</compilerArgs>
<!-- DevMode configuration -->
<warDir>${project.build.directory}/${project.build.finalName}</warDir>
<classpathScope>compile+runtime</classpathScope>
<!-- URL(s) that should be opened by DevMode (gwt:devmode). -->
<startupUrls>
<startupUrl>StockWatcher.html</startupUrl>
</startupUrls>
</configuration>
</plugin>
With the GWT Eclipse plugin I made it work and even debugging was working in Chrome because the GWT plugin for Eclipse starts the code server automatically and somehow updates the html file to load the javascript from the code server.
The bottom line is: the GWT Maven plugin works :-)). But integrating Spring Boot and GWT will be a complex task. I'll need to run the GWT compilation first, then add the resulting javascript to the executable Jar. Maybe it is possible to do this with Maven but for this task I decided to use Gradle.
Gradle is a rapidly developing build tool. The DSL and API are not stable yet but it offers substantial flexibility. While Maven has a fairly straight line of build phases Gradle can execute tasks in any order. This flexibility is what I need.
After some digging I found one working Gradle plugin for GWT: de.esoco.gwt. It is a fork of the Putnami plugin. The documentation is good enough to make this plugin work. I didn't notice any major issues. The configuration in build.gradle is inside the gwt block:
gwt {
gwtVersion = gwtVersion
module("org.example.gwt.StockWatcher2", "de.richsource.gradle.plugins.gwt.example.Example")
// other configuration options
}
This plugin adds some tasks to the gradle build. The most important of them is gwtCompile. This task actually generates the javascript code and puts it in ${buildDir}/gwt/out. These values (both gwt and out) are hardcoded in the Gradle GWT plugin.
It is important to remember that the code that is compiled into javascript is specified in the GWT module file like this:
<source path='client'/>
<source path='shared'/>
REST and Resty
The next goal is to use Spring Boot's REST endpoints. I found RestyGWT that helped me do just that. They have a simple how-to on the front page.
I added the required dependencies to build.gradle:
implementation("javax.ws.rs:javax.ws.rs-api:2.0.1")
compileOnly group: "org.fusesource.restygwt", name: "restygwt", version: "2.2.0"
implementation group: "com.fasterxml.jackson.jaxrs", name: "jackson-jaxrs-json-provider", version: "2.8.9"
The JAX-RS dependencies are necessary because RestyGWT uses the annotation from JAX-RS to declare the endpoints. As far as I understood Jackson is also necessary to parse the JSON.
I added the dependency in the GWT module as well:
<inherits name="org.fusesource.restygwt.RestyGWT"/>
Here is the service I created with RestyGWT:
public interface TestService extends RestService {
@GET
@Path("test") void test1(@QueryParam("input") String inp,
MethodCallback<TestResult> callback);
}
I call this service in a ClickHandler (I mostly used the code from the original GWT tutorial):
private final TestService testSrv = GWT.create(TestService.class);
btnCallServer.addClickHandler(clkEvent -> {
testSrv.test1("TestString", new MethodCallback<TestResult>() {
@Override
public void onSuccess(Method method, TestResult response) {
testLabel.setText("Srv success " + response.getStr1());
}
@Override
public void onFailure(Method method, Throwable exception) {
testLabel.setText("Srv failure " + exception.getMessage());
}
});
});
This service calls this simple method in a Spring Boot controller:
@GetMapping("/test")
public TestResult test1(@RequestParam(name="input", required=false) String inp) {
return new TestResult(inp + " qqq");
}
Executable Jar
The third goal is to actually bundle all that into one executable fat jar. In this section I can finally make use of Gradle's flexibility.
First I put the html files to /src/main/resources/static.
I created a task to copy the generated javascript into the static folder in ${buildDir} during the build:
task copyGWTCode(dependsOn: ["gwtCompile"], type: Copy) {
from file("${buildDir}/gwt/out")
into file("${buildDir}/resources/main/static")
}
Next I made the bootJar task dependent on this task and copied the jar to the more traditional target directory:
bootJar {
dependsOn copyGWTCode
doLast {
mkdir "${buildDir}/target"
setDestinationDir(file("${buildDir}/target"))
copy()
}
}
Debugging in GWT
One extra chapter on GWT debugging.
I found a fairly simple way to debug GWT UI in Chrome (Chrome can handle it better than Firefox). Here are the steps to make it work. I used the project from the GWT tutorial but renamed it to "stockwatcher2".
I found a fairly simple way to debug GWT UI in Chrome (Chrome can handle it better than Firefox). Here are the steps to make it work. I used the project from the GWT tutorial but renamed it to "stockwatcher2".
1. Add a new html file for debugging to src/main/resources/static. If the original file was for example StockWatcher2.html the new file should be StockWatcher2debug.html. In this new file replace the line
<script type="text/javascript" src="stockwatcher2/stockwatcher2.nocache.js"></script>
with this line (javascript from the code server):
<script src="http://localhost:9876/recompile-requester/stockwatcher2"></script>
2. Execute the task bootJar and run it.
3. Start the code server from the projects folder with "gradle gwtCodeServer".
4. Open http://<host>:<port>/<somepath>/StockWatcher2debug.html in Chrome
5. Now you can find the source maps in Developer Tools -> Sources under 127.0.0.1:9876. The breakpoint can be set and hit in Chrome directly.
The idea with a separate file is to exclude it from production builds but keep it in developer builds. It is easy with Gradle. There is only one issue with this approach and that is the REST endpoints that are called from the debug sources are different from the endpoints that are called from the "normal" sources. Adding one more mapping solves the problem.
Conclusion
I congratulate the heroic persons who have reached this conclusion! You are true programmers and those who gave up are miserable cowards!
But the bottom line is that working with GWT is quite tough. The build tools are quite unwieldy and lack important features. There is practically no integration (for example with Spring Boot). Debugging is unnecessary complicated.
If someone were to choose between GWT and Eclipse RAP I would recommend Eclipse RAP.
No happy ending :-(.
I inherited a GWT/ Java EE project and took a similar approach. I broke it up in to three modules; the GWT frontend (gwt-app), the Java EE backend (war), and a shared library (jar) with common code between the two. The shared library was needed due to use of GWT RPC. This approach also allowed use of RestyGWT and GWT RPC in the same project.
ReplyDeletePackaging the projects into one WAR involved using the Maven War Plugin to overlay the GWT frontend WAR on to the Java EE backend WAR.
Frontend debugging was handled using the gwt:codeserver goal of the plugin net.ltgt.gwt.maven:gwt-maven-plugin. From there debugging was extremely simple using Chrome Dev Tools. The codeserver provides bookmarkable scripts that puts your running GWT application (codeserver) into debug mode.
Yes, there is a steep learning curve with GWT however, after dealing with it on that project I would definitely use it again if need be.
Interesting article! If you checked out GWT and RAP the following java application framework might be of interest for you too: https://eclipsescout.github.io/ - there exists already a demo application together with Spring Boot.
ReplyDelete