The most important feature of Spring Boot’s new 1.3.0 release is the ability to create colored banners. Another interesting feature is the creation of fully executable JARs. It’s possible to run these jars directly, without java -jar .... This made me curious. How is this implemented?

It turns out that zip files (and therefore jar files) save the information about their contents at the end of the file. This means that it’s valid to add some unrelated data at the beginning of a zip file. At least, it’s tolerated by most tools. This allows for some really funky tricks.

So all that’s needed for a fully executable jar is to prepend it with a shell script that executes itself with java -jar $0. Spring Boot provides this feature only for Unix systems. They’ll have good reasons for it, but why not provide it also for Windows? And, wouldn’t it be cool to have the same file for both Windows and Unix?

After some googling and playing, I came up with this solution:

:; java -jar $0 #
:; exit #

@echo off
java -jar %~f0
exit /b

In Windows batch files, lines beginning with : are interpreted as label and therefore not executed. Unix interprets : as true and ; as a command separator. The file has windows style newlines \r\n. So, every line in the unix part ends with a # to comment out the \r. This way, we have a nicely packaged, self contained executable file.

But we still need a java runtime installed on the machine. Couldn’t we get rid of this requirement, too? Couldn’t we bring our own java runtime together with the executable? This way, we’d have a truly natively executable. Yes, that’s possible. The idea is simply to pack a JRE into the jar file, unzip it the first time and then use it to run java.

The corresponding Unix shell script was written pretty quickly:

if [ ! -d ~/.my-java ] #
then #
    mkdir ~/.my-java #
    unzip $0 -d ~/.my-java > /dev/null 2>&1  #
    unzip ~/.my-java/ -d ~/.my-java/jre > /dev/null 2>&1 #
fi #
~/.my-java/jre/bin/java -jar $0 #
exit #

Nothing special. Just unzip from the jar into ~/.my-java, then unzip itself and use it to run java. The trick with :; did not work with if, so I had to find another solution:

@echo off
goto :cmd_part

# unix script
exit #

# windows script

For Windows, it’s again the trick with : being interpreted as label. Then goto is used to jump into the windows part. Unix interprets the first four lines as here document, meaning that everything between ::cmdliteral is piped into : without processing and thus ignored.

The windows script to unzip the JRE was a little bit trickier. First, there is no unzip command in windows. But VBScript and PowerShell provide such a function. But both are picky about the prepended zip file and refuse to unzip it. So I had to write a PowerShell script that first cleans up the zip file before unzipping it.

if not exist %userprofile%\.my-java (
    mkdir %userprofile%\.my-java
    powershell.exe -nologo -noprofile -command "& { $stream = [System.IO.File]::OpenRead('%~f0'); $barr = New-Object byte[] 10000; $ostream = [System.IO.File]::OpenWrite('%userprofile%\.my-java\'); $stream.Seek(%len, [System.IO.SeekOrigin]::Begin) >$null; while( $bytesRead = $stream.Read($barr,0,10000)){ $ostream.Write($barr,0,$bytesRead); } $ostream.close(); }"
    powershell.exe -nologo -noprofile -command "& { Add-Type -A 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::ExtractToDirectory('%userprofile%\.my-java\', '%userprofile%\.my-java'); }"
    powershell.exe -nologo -noprofile -command "& { Add-Type -A 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::ExtractToDirectory('%userprofile%\.my-java\', '%userprofile%\.my-java\jre'); }"
%userprofile%\.my-java\jre\bin\java -jar %~f0
exit /b

Additionally, during build time, %len is replaced by the actual length of the script. First, I adjusted it manually each time the script changed, but this gets tedious really quickly. The script works, but my opinion about windows didn’t improve…

So it is possible to write truly self contained, natively executable files that run on both Windows and Mac OS (Linux would be possible too). The complete project is available on github. I don’t know how much importance this has, but it was an interesting task for a weekend.