JAR Files
So far we’ve seen how Java source files are compiled into binary class files, and how these are located at runtime when the containing directory is placed on the classpath. This approach works for the single file application developed previously, but most applications consist of many classes, and make use of libraries which themselves define classes and interfaces of their own. Managing and publishing directories of class files would be cumbersome, but fortunately Java defines a standard for packaging related classes into a single file called a Java ARchive, or JAR file.
Lib Hello World
After showing our groundbreaking salutatory application to our chief architect, they are mainly positive, but have some suggestions:
- Instead of hard-coding the message, it should be supplied as input to the program
- Messages could come from anywhere, not just the command line
- Messages could be written anywhere, not just the console
- Messages could be of arbitrary size
- Other teams might want to take advantage of our work, so we should publish it as a library
After some back-and-forth, we settle on defining interfaces for sources and destinations for messages:
src/libhello/MessageSource.java
package enterprisey;
import java.io.Reader;
import java.io.IOException;
public interface MessageSource {
Iterable<Reader> messages() throws IOException;
}
src/libhello/MessageSink.java
package enterprisey;
import java.io.Reader;
import java.io.IOException;
public interface MessageSink {
void writeMessage(Reader message) throws IOException;
}
In addition, we define a source of messages read from the command-line, and a sink which writes messages a PrintStream
:
src/libhello/CommandLinkMessageSource.java
package enterprisey;
import java.io.Reader;
import java.io.StringReader;
import java.io.IOException;
import java.util.Iterator;
import java.util.NoSuchElementException;
public class CommandLineMessageSource implements MessageSource {
private final String[] args;
public CommandLineMessageSource(String[] args) {
this.args = args;
}
public Iterable<Reader> messages() throws IOException {
return new Iterable<Reader>() {
public Iterator<Reader> iterator() {
return new Iter();
}
};
}
private class Iter implements Iterator<Reader> {
private int index = 0;
public boolean hasNext() {
return this.index < args.length;
}
public Reader next() {
if (this.hasNext()) {
String next = args[this.index];
this.index++;
return new StringReader(next);
} else {
throw new NoSuchElementException();
}
}
}
}
src/libhello/PrintStreamMessageSink.java
package enterprisey;
import java.io.PrintStream;
import java.io.IOException;
import java.io.Reader;
import java.nio.CharBuffer;
public class PrintStreamMessageSink implements MessageSink {
private final PrintStream dest;
public PrintStreamMessageSink(PrintStream dest) {
this.dest = dest;
}
public void writeMessage(Reader message) throws IOException {
char[] buffer = new char[1024];
int read;
while ((read = message.read(buffer)) != -1) {
CharBuffer buf = CharBuffer.wrap(buffer, 0, read);
this.dest.print(buf);
}
this.dest.println();
this.dest.flush();
}
}
We compile these classes as usual and output the corresponding class files to the libhello
directory:
javac -d classes/libhello src/libhello/*.java
This outputs the class files under the classes/libhello
directory. We can then build a JAR file from this directory:
jar --create --file libhello.jar -C classes/libhello .
this creates a libhello.jar
file in the current directory. We can list the contents of this file with the --list
command:
jar --lib --file libhello.jar
This shows the archive contains the .class
files as their expected locations on the classpath, along with a META-INF/MANIFEST.MF
file.
This file is called the _manifest
file and is described below.
META-INF/
META-INF/MANIFEST.MF
enterprisey/
enterprisey/MessageSource.class
enterprisey/PrintStreamMessageSink.class
enterprisey/CommandLineMessageSource$1.class
enterprisey/CommandLineMessageSource.class
enterprisey/CommandLineMessageSource$Iter.class
enterprisey/MessageSink.class
Note that JAR files are also zip files, so their contents can be listed with the unzip
command:
unzip -l libhello.jar
Now we have build our library, we can re-write our application to use it:
src/app/Echo.java
package test;
import enterprisey.*;
import java.io.Reader;
import java.io.IOException;
public class Echo {
public static void main(String[] args) {
MessageSource source = new CommandLineMessageSource(args);
MessageSink sink = new PrintStreamMessageSink(System.out);
try {
for(Reader message : source.messages()) {
sink.writeMessage(message);
}
} catch (IOException ex) {
System.err.println("Error processing messages");
}
}
}
As before, we compile it with javac
. Since the application references the classes in libhello.jar
, we have to place it on the classpath
to make the class definitions available.
javac -d classes/app -cp libhello.jar src/app/Echo.java
This writes the test.Echo
class to the classes/app
directory. Now we can run the application. Again we have to make the classes in libhello
available by adding the jar file to the classpath. We also add the build output directory for the app so the main test.Echo
class can be resolved:
java -cp libhello.jar:classes/app test.Echo Hello world '!'
As expected, the application reads each message from the command line and writes it to the console
Hello
world
!
Of course we should also create a JAR for the application:
jar --create --file echo.jar -C classes/app/ .
which we can then execute by placing both the lib and app JARs on the classpath:
java -cp libhello.jar:echo.jar test.Echo Hello world '!'
Manifest files
When listing the contents of the JAR file above, we saw a META-INF/MANIFEST.MF
file included. The META-INF
directory within a JAR contains
files used to configure aspects of the JVM. The MANIFEST.MF
file contain various sections of key-value pairs used to describe the contents of the JAR.
The format of the META-INF
directory and MANIFEST.MF
files are described in detail within the JAR file specification.
The jar
tool can be used to extract the contents of the default manifest file:
jar --extract --file echo.jar META-INF/MANIFEST.MF
this is fairly minimal by default:
Manifest-Version: 1.0
Created-By: 14.0.2 (Oracle Corporation)
There are two additional manifest properties we would like to set when building the application JAR:
Main-Class
: This is the name of the main class the JVM should load on startupClass-Path
: The classpath to configure
We can add these properties to a file to be added to the manifest when building the application JAR:
echo-manifest.mf
Main-Class: test.Echo
Class-Path: libhello.jar
> jar --create --file echo.jar --manifest=echo-manifest.mf -C classes/app .
Adding the main class and classpath to the application manifest means the application can be run with the -jar
option
without needing to specify the classpath and main class manually:
> java -jar echo.jar Hello world '!'
‘Fat’ JARs
Defining the classpath and main class within the application manifest simplifies the command required to invoke the application,
but it still requires ensuring the dependency JARs are in the expected location as defined by the Class-Path
entry in the application
manifest. From a deployment perspective it would be easier if the application and all its dependencies were packaged in a single file.
Since a JAR file represents a single root on the classpath, multiple JAR files can be combined into one by simply extracting them all to a single directory and re-packaging them within a new JAR file. Such JARs are usually referred to as ‘fat’ (or ‘uber’) JARs.
We can first extract our library and application JARs to a temporary directory:
> unzip -d uber libhello.jar
> unzip -o -d uber echo.jar
and build a new JAR with a new manifest file. Unlike the previous application manifest, this does not need to specify the classpath since all classes are contained within the new JAR.
uber-manifest.mf
Main-Class: test.Echo
We can now build the new JAR:
> jar --create --file echo-uber.jar --manifest=uber-manifest.mf -C uber .
and run it with the -jar
option as before:
> java --jar echo-uber.jar Hello world '!'
File collisions
When building the uberjar above, we simply overwrote (using the -o
option for unzip
) any existing files within libhello.jar
when extracting the echo.jar
file. This file for our simple application since there are no collisions, except for the default manifest
files in the two files, which should be the same. However, this simple strategy may not work for more complicated applications. It’s possible
that different JAR files will contain different files at the same path which need special handling to produce the corresponding file in the
output JAR. One example is libraries which configure ServiceLoader
classes which are configured by files in the META-INF/services
directory.