![]() |
![]() |
![]() |
![]() |
|
com.mydomain.mypackage.Main
, you can simply say:
java com.mydomain.mypackage.MainHowever, this assumes that some magic has been applied to your shell environment to set the value of your
CLASSPATH
so that the java runtime can actually find this class.
The CLASSPATH
also has to contain all of the other classes which will be loaded and executed
by your main program.
This is where things start to get much more complicated. Multiple entries on the CLASSPATH
must
be separated by a special character, and this special character is unfortunately operating system
dependent (since it would otherwise interact badly with the common command shells). For example
on UNIX it is ':', on DOS it is ';'. If these choices were reversed, the command shell on each
operating system would become very confused. And there had better not be version any conflicts between
entries on the CLASSPATH
: the order of entries
controls the order of class resolution, incompatible versions created by incorrect orderings
will lead to obscure NoSuchMethodException
exceptions and other runtime problems.
Wouldn't it be nice if you could just wrap up all your classes into one big executable,
and run it? Well the Java2 Runtime Environment has a way of doing this using a JAR launcher.
If you bundle your class, and all the supporting classes in a file called myapp.jar, and
set the MANIFEST.MF
appropriately, you can simply say:
java -jar myapp.jarVery nice. So what's the catch? There are numerous. One catch is that you must first unjar all of the supporting classes and flatten them out into a single directory tree before creating the final myapp.jar. For example: what happens if your application depends on some of the web-services JAR files from the Java Web Services Developer Pack? There are many of them and they all contain code which lives in the same packages (javax.*, com.sun.* etc).
Is this a problem? Well it may be. Consider that after you have expanded all of the
code into a single tree, you have lost all trace of any code signatures that might have been
present. Why? Because code signatures are located in /META-INF/MANIFEST.MF
file, and
each jar file will contain one of these manifest files, with different content. Likewise,
any resources which have the same name (a distinct possibility given that the jar files
themselves may contain flattened out packages) will be in conflict: you can only have one
file called log4j.properties
to control logging, where you might like to enable/disable
logging on a boundary set by the individual jar files.
Wouldn't it be nice if you could just bundle the supporting jar-files into your myapp.jar
file without expanding them? This is the problem addressed, and solved, by One-JAR.
META-INF/MANIFEST.MF
property called Class-Path
to
solve this problem.
But first, lets set up a concrete example of what we're trying to do. Suppose our
main class, plus other classes we have written is in a JAR file called main.jar
Suppose that our application depends on two other JAR files, called a.jar, b.jar
Can we construct a manifest which will allow the JAR launcher to run this? First, we construct
our compound Jar file:
$ jar -tf myapp.jar META-INF/MANIFEST.MF lib/a.jar lib/b.jar main/main.jarThe manifest contains a
Main-Class
setting to launch our application,
and entries for the supporting JAR files a.jar
and b.jar
.
Manifest-Version: 1.0 Class-Path: main/main.jar lib/a.jar lib/b.jar Main-Class: com.main.MainWhat happens if you run this?
$ java -jar myapp.jar A() B()Looks good, exactly what we want right? Wrong. What's happening here is that the
Class-Path
entries are being resolved in the file-system, not inside
the JAR file. If you move this JAR file somewhere else and try to run it, you get the following:
$ java -jar myapp.jar Exception in thread "main" java.lang.NoClassDefFoundError: com/main/MainThis is most unfortunate, and most frustrating, and pretty much unexpected by developers who follow this route.
What about using the 'jar:' protocol in the Class-Path
entry? This
doesn't work either. It appears that the JAR loader only supports file-based URLS. Which is where One-JAR
comes in.
main
lib
If you're really impatient to see One-JAR in action without writing any code or jarring any jars, follow this link.
At this point we have to augment our myapp.jar
file with some bootstrap
code, and a META-INF/MANIFEST.MF
file to let the bootstrap code assume
control.
One-JAR ships with a pre-built bootstrap jar file called one-jar-boot.jar
that contains all the code you need
to do this. You can download the latest pre-built
one-jar-boot.jar here:
$ jar -tf one-jar-boot.jar META-INF/MANIFEST.MF com/simontuffs/onejar/Boot.class com/simontuffs/onejar/Handler$1.class com/simontuffs/onejar/Handler.class com/simontuffs/onejar/JarClassLoader$ByteCode.class com/simontuffs/onejar/JarClassLoader.class boot-manifest.mfThere are five classes which control the bootstrap process, all residing in the
com.simontuffs.onejar
package. We'll be discovering the details of these later.
There is also a pre-built manifest file called boot-manifest.mf
. To complete
the One-JAR deployment process, create a suitable directory and expand this file into it, then
update the myapp.jar
file as follows:
$ mkdir boot $ cd boot $ jar -xvf ../one-jar-boot.jar $ jar -uvfm ../myapp.jar boot-manifest.mf .Thats it! You can now run your application in the simplest possible fashion:
$ java -jar myapp.jar A() B()
Well, One-JAR has some built-in diagnostics that you can enable to see what it is
doing on behalf of your application. These can be enabled using command-line switches
to the JVM, placed before the -jar
option:
-Done-jar.info
: Enable INFO
level diagnostics. These
show high-level operations inside the One-JAR support code.-Done-jar.verbose
: Enable VERBOSE
level diagnostics. These
show in tedious detail the operations inside One-JAR, including class-loading and
resource resolution. $ java -Done-jar.info -jar myapp.jar Boot: Info: using JarClassLoader JarClassLoader: Info: caching lib/a.jar JarClassLoader: Info: caching lib/b.jar JarClassLoader: Info: caching main/main.jar A() B()Now we see that something interesting is indeed happening inside the application: One-JAR is using a custom classloader called
JarClassLoader
to load information
from the myapp.jar
file. We can get more detail:
$ java -Done-jar.verbose -jar myapp.jar Boot: Info: using JarClassLoader JarClassLoader: Info: caching lib/a.jar JarClassLoader: Cached bytes for class com.a.A.class JarClassLoader: Info: caching lib/b.jar JarClassLoader: Cached bytes for class com.b.B.class JarClassLoader: Info: caching main/main.jar JarClassLoader: Cached bytes for class com.main.Main.class JarClassLoader: findClass(com.main.Main) JarClassLoader: found com.main.Main in main/main.jar JarClassLoader: findClass(com.a.A) JarClassLoader: found com.a.A in lib/a.jar A() JarClassLoader: findClass(com.b.B) JarClassLoader: found com.b.B in lib/b.jar B()Notice how the
JarClassLoader
keeps track of where classes came
from: for example the com.a.A
class is contained inside the lib/a.jar
file inside myapp.jar
.
At this stage you should be asking "How does One-JAR decide which is the main class?".
The conventional way would be to have a manifest attribute in the top-level JAR
file myapp.jar
and require this to be edited before the final jar was
assembled. But this is all unnecessary: One-JAR simply looks for a jar file inside the main
sub-directory of the composite JAR file myapp.jar
, and provided that that JAR file has a Main-Class
manifest
attribute, it will be used as the main entry point. This allows an existing main-class JAR file to be bundled inside
a One-JAR archive without further modification!
That's a high-level overview of One-JAR, next we'll get into the details behind the
custom classloader JarClassLoader
and what it does to make all of this
possible.
If you're really in a hurry to see One-JAR in action, there is a pre-built One-JAR example which
can be downloaded here:
Once you save this file to disk you can run it as shown below. Note that this example is fairly complex (it's really a regression test for the One-JAR product and should be broken out into JUnit tests). We'll be discussing precisely what is going on in the next section.
You can gain more insight into the role of the One-JAR
classloader by running with a java -Done-jar.verbose
command. Don't be perturbed by the failure, it is expected
and caused by pathalogical behaviour in one of the test cases.
$ java -jar one-jar-example.jar Main: com.simontuffs.onejar.example.main.Main.main() DetectClassLoader: Warning: com.simontuffs.onejar.example.main.Test$TestLoader is a ClassLoader DetectClassLoader: Warning: loaded from codesource (jar:file:/C:/work/eclipse-2.1.1/workspace-simontuffs/one-jar/dist/one-jar-example.jar!/main/main.jar) DetectClassLoader: Warning: and declared 'loadClass(String)'. It may not be able to load classes without being modified. Test: loaded by com.simontuffs.onejar.DetectClassLoader@1888759 Test: codesource is jar:file:/C:/work/eclipse-2.1.1/workspace-simontuffs/one-jar/dist/one-jar-example.jar!/main/main.jar Util: loaded by com.simontuffs.onejar.DetectClassLoader@1888759 Util.sayHello() Test.useResource(/duplicate.txt) OK ------------------------------------------- This is an example of a duplicate resource file named duplicate.txt, placed into main.jar ------------------------------------------- Util.InnerClass loaded by com.simontuffs.onejar.DetectClassLoader@1888759 Util.StaticInnerClass loaded by com.simontuffs.onejar.DetectClassLoader@1888759 Test.useUtil() OK Test.useResource(/main-manifest.mf) OK dumpResource: /main-manifest.mf ------------------------------------------- Manifest-Version: 1.0 Main-Class: com.simontuffs.onejar.example.main.Main ------------------------------------------- Test.useResource(/duplicate.txt) OK dumpResource: /duplicate.txt ------------------------------------------- This is an example of a duplicate resource file named duplicate.txt, placed into main.jar ------------------------------------------- Test.useResource(main.txt) OK dumpResource: main.txt ------------------------------------------- This is main.txt ------------------------------------------- Test.loadCodeSource(): dumping entries in jar:file:/C:/work/eclipse-2.1.1/workspace-simontuffs/one-jar/dist/one-jar-example.jar!/main/main.jar Test: entry=main-manifest.mf Test: entry=duplicate.txt Test: entry=com/simontuffs/onejar/example/util/Duplicate.class Test: entry=com/simontuffs/onejar/example/main/Main.class Test: entry=com/simontuffs/onejar/example/main/Test$TestLoader.class Test: entry=com/simontuffs/onejar/example/main/Test.class Test: entry=com/simontuffs/onejar/example/main/main.txt Creating new TestLoader() loading com.simontuffs.onejar.example.util.Util TestLoader.loadClass(com.simontuffs.onejar.example.util.Util) Test.classLoader() failed: java.lang.ClassNotFoundException: com.simontuffs.onejar.example.util.Util classURL(): Opening onejar resource using new URL(onejar:/com/simontuffs/onejar/example/main/Main.class) classURL(): Opened: onejar:/com/simontuffs/onejar/example/main/Main.class classURL(): OK. classURL(): opening using getResource(/com/simontuffs/onejar/example/main/Main.class) classURL(): OK. Main: finished in 31 ms 1 failure (TODO: JUnit!)
![]() |
![]() |
![]() |
![]() |