Self-Contained, Rich Web Application JARs
Learn more about self-contained, rich web application JAR files.
Join the DZone community and get the full member experience.
Join For FreeSelf-contained JAR files are taking Java web projects by storm, providing great simplification in complex and bloated enterprise containers world. Thanks to the libraries like Dropwizard and Spring Boot, you can build a complete application environment executed from a single file in just a few lines of code.
The question is: how difficult could a project be? Obviously, it can include the whole backend logic, but what about web interfaces? Can they be the part of a single JAR file? Sure they do; Java offers a lot of decent templating and rendering frameworks to build complicated document structures. The problem is that, nowadays, web applications are, in fact, a lot more than just a simple documents network. Users expect dynamic, stateful, single-page applications with all the bells and whistles offered by the modern JavaScript libraries (as a matter of fact, developers also want to work with the latest JavaScript frameworks).
This is the point when things get tough, especially for Java devs. It's inevitable; you will be forced to get familiar with npm, node.js, brunch.io, Grunt, yeoman, RequireJS… (to mention only project building/organizing libraries). Obviously, there is a lot of tutorials around the web, you can pick three or four of the suggested frameworks to create a scaffold for your application from scratch (usually with the single command). However, such an approach leads to two key problems. First is the magic. The whole process will be done backstage, and you will receive ready-to-deploy pieces of code, which are somehow somewhere generated. Usually, this is just fine, but sometimes, you will learn the hard way what the Pareto principle is and trying to replace an element that was not planned to be replaced. However, the second problem is more serious; the goal of this article is to introduce a way of building self-contained JAR files, which should have nothing in common with the npm or node.js.
Before we go any further, let’s try to write down the requirements that we put before a truly standalone JAR file.
- It should result in a self-contained, executable JAR file.
- It should enclose backend and frontend logic, build with modern web frameworks based on REST API communication.
- Building process should be narrowed to a single environment, preferably Maven (or any other Mavenish project building library).
- Project should be easy in developing (single codebase).
- It should avoid any non-Java-related repositories.
- Project should adopt ‘one command build’ approach.
The first step is easy. All that needs to be done is to pick one of several Java scaffolds libraries which offers a complete environment to build web application: mentioned Spring Boot, Dropwizard, or sparkjava, just to name a few. For the purposes of this article, I selected the Dropwizard project. In my opinion, it provides a great balance between the maturity, completeness, and flexibility which lets you make unbiased architectural decisions. Also, the extensive support of additional libraries simplifies realization of the outlined goals.
Actually, Dropwizard itself addresses several of the assumptions already. It is Maven friendly, REST service driven with an embedded Jetty container. What about static files? No problem, they can be easily hosted from project resources. All you need is a dropwizard-assets module, configured in initialize phase with the resources path and welcome, index file.
bootstrap.addBundle(new AssetsBundle("/assets", "/", "index.html"));
A lot has already been done, but the assumptions are much more sophisticated. Eventually, we want to provide a full-deployable, web-rich project with React.js, JSX, etc.
This is the first place where things get tough. This is what npm is for, right? How we could avoid JavaScrit-related packaging manager in favor of a Maven environment? Fortunately, there is a savior on the horizon — the WebJars project. Sounds revolutionary, but all client-side dependencies (including latest JavaScript frameworks) can be found on Maven central repositories (if anything is not there, no problem, you can request missing dependencies on your own). That solves all front-related dependency problems and lets to forget about the threats of a separate repository (connectivity, availability, etc.). All that is required is proper dependencies coords and good ol’ Maven central.
Unfortunately, the dropwizard-assets plugin itself will not be able to host the WebJars static resources. It has to be supplemented with an additional module: dropwizard-webjars-bundle. The good news is that its installation is just as simple; it requires proper dependency entry and single installation in bootstrap Dropwizard phase.
bootstrap.addBundle(new WebJarBundle("org.webjars.npm"))
To get it all up and running, the welcome file (index.html) should navigate the browser to proper URLs for JavaScript libraries. Determining the correct path in bundled JARs can cause some problems, but once done, all work will be done by the Dropwizard WebJars plugin (it is worth to mention that the library, thanks to aggressive caching policy, is very efficient in its operations).
<script type="text/javascript"
src="/webjars/jquery/jquery.min.js"></script>
For the sake of the completeness of the discussed case, I decided to complicate the code example a bit, basing its implementation on Reactjs with JSX templating. To ensure proper handling of such architecture across the web browsers, it requires the external project to transpile the ‘source’ files to browser-friendly code. It can be accomplished by several libraries, so I decided to use the most common one, BabelJS. Surprisingly, at this point, it does not complicate things much. All that is required is proper Maven WebJars dependency (still no npm!) and a few instructions in the index file, which tells the browser how to handle transpilation on the fly.
<script type="text/javascript"
src="/webjars/babel-standalone/babel.js"></script>
<script type="text/babel" src="src/app.js"></script>
At this point, the project can be considered as utter. There is a complete backend and frontend service, all of which can be executed from the IDE, and it is surprisingly easy to develop, as all parts are in one place. Additionally, modern IDEs support hot changes, so there is no need for restarting the whole container after static file compilation. Changes should be visible after page refresh.
However, the current state of the project does not allow for its installation or deployment. There is no building process defined and JavaScript sources are not prepared for production.
According to the assumptions, its final form should provide one command build, limited to the Maven environment only. That means that the pom file needs to be expanded with building instructions details. At this point, it needs to decide which Maven phase expands to wrap up all instructions. ‘Package’ seems to be a good option as, eventually, we are interested in producing JAR file without any further deployment actions.
The first step is to obtain all external JavaScript libraries and store them in a separate place for further work. This needs to be done by intercepting default Maven processing at default phases and can be achieved by the maven-dependency-plugin. Unfortunately, this step requires to repeat all dependencies from the previous <dependencies> section. It is worth noting that not all libraries need be included here, only those which are required in the final form of the front code or in processing phases.
...
<artifactItem>
<groupId>org.webjars.npm</groupId>
<artifactId>babel-standalone</artifactId>
<version>${babel.version}</version>
<type>jar</type>
<includes>**\/*.js</includes>
<outputDirectory>
${project.build.directory}/classes/assets/jslib/structured
</outputDirectory>
</artifactItem>
...
Once dependencies are shelved, all necessary files needs to be extracted from default directories and stored in separate files. It is important to enclose here all type of files which are required by the end project. That includes not only *.js extensions but also stylesheets, images, fonts, and others (for example, if the Bootstrap framework is included). When done, all source directories can be deleted. Simple file operations can be done using generic maven-antrun-plugin, which effectively lets you proceed with all basic operations (copy/move/delete etc.).
...
<configuration>
<target>
<copy todir="${project.build.directory}/classes/assets/jslib"
flatten="true">
<fileset dir="${project.build.directory}/classes/assets/jslib/structured">
<include name="**/*.js"/>
<include name="**/*.css"/>
</fileset>
</copy>
</target>
</configuration>
...
At this point, Maven can take care of project source files. It needs to run transpilation using Babel. This can be done by the babel-maven-plugin, which requires no access to npm or any other external source. Instead, it relies on provisioned, standalone Babel bundle (again, available in WebJars).
...
<configuration>
<verbose>true</verbose>
<babelSrc>${project.basedir}/target/classes/assets/jslib/babel.min.js</babelSrc>
<sourceDir>${project.basedir}/target/classes/assets/</sourceDir>
<targetDir>${project.basedir}/target/classes/assets/</targetDir>
<jsSourceIncludes>
<jsSourceInclude>src/*.js</jsSourceInclude>
</jsSourceIncludes>
<prefix>trans-</prefix>
<presets>react,es2015</presets>
</configuration>
...
Once it is done and the source files are in the browser-friendly state, they can be minified and merged. According to the recommended web applications architecture, the result code should be delivered in single, compressed file. This step can be proceeded by the minify-maven-plugin. It provides two bundled compilation engines and supports also CSS files. Output files should be placed in reachable destination for a web application (effectively, for user browser). Operation can be done for both vendor and application sources.
...
<configuration>
<webappSourceDir>${project.basedir}/target/classes/</webappSourceDir>
<jsSourceDir>/assets/</jsSourceDir>
<jsSourceFiles>
<jsSourceFile>jslib/jquery.min.js</jsSourceFile>
<jsSourceFile>jslib/react.development.js</jsSourceFile>
<jsSourceFile>jslib/react-dom.development.js</jsSourceFile>
</jsSourceFiles>
<jsFinalFile>app-vendor.js</jsFinalFile>
<webappTargetDir>${project.basedir}/target/classes/</webappTargetDir>
<skipMinify>false</skipMinify>
<nosuffix>true</nosuffix>
<jsEngine>CLOSURE</jsEngine>
</configuration>
...
Application users will never request for these files on their own, the browser will do that for them. That is why the final step is to modify the welcome file (index.html), by replacing the raw paths with newly generated. Extensive Maven plugins library has an answer for this as well — the maven-replacer-plugin. Note that it needs to have some placeholders in HTMLs sources, which marks the places for modifications. Eventually, a few additional comments does not complicate structure and provide strict structure for references placement.
<!--app-->
<script type="text/babel" src="src/app.js"></script>
<script type="text/babel" src="src/welcome.js"></script>
<!--/app-->
It is done. Now, the project can be executed with proper profile and run from single JAR file. The proposed steps are only the basic scope of the project building logic. The process can be easily extended with additional elements like source code checking (jslint/jshint), dynamic code generation, test executions, etc.
Unusual solutions are associated with some complications and it is not different in this case. Firstly, adding any new frontend dependency like jQuery will require to update the same information in several places: Maven dependencies, index.html references, and build details. Missing one of these elements may cause hard-to-debug scenarios. Speaking of debugging, it might be complicated if it comes to Maven processing. If anything goes wrong, the case comes down to juggling between Maven debug logs and current status of generated files in the target directory.
In my opinion, the above issues are not problematic and do not negate the advantages of having a complete building process built using a mature and reliable tool such as Maven.
The complete application can be found on GitHub.
Published at DZone with permission of Milosz Polak. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments