Saturday, September 12, 2015

JAR Manifest Class-Path is Not for Java Application Launcher Only

I've known almost since I started learning about Java that the Class-Path header field in a Manifest file specifies the relative runtime classpath for executable JARs (JARs with application starting point specified by another manifest header called Main-Class). A colleague recently ran into an issue that surprised me because it proved that a JAR file's Manifest's Class-Path entry also influences the compile-time classpath when the containing JAR is included on the classpath while running javac. This post demonstrates this new-to-me nuance.

The section "Adding Classes to the JAR File's Classpath" of the Deployment Trail of The Java Tutorials states, "You specify classes to include in the Class-Path header field in the manifest file of an applet or application." This same section also states, "By using the Class-Path header in the manifest, you can avoid having to specify a long -classpath flag when invoking Java to run the your application." These two sentences essentially summarize how I've always thought of the Class-Path header in a manifest file: as the classpath for the containing JAR being executed via the Java application launcher (java executable).

It turns out that the Class-Path entry in a JAR's manifest affects the Java compiler (javac) just as it impacts the Java application launcher (java). To demonstrate this, I'm going to use a simple interface (PersonIF), a simple class (Person) that implements that interface, and a simple class Main that uses the class that implements the interface. The code listings are shown next for these.

PersonIF.java
public interface PersonIF
{
   void sayHello();
}
Person.java
import static java.lang.System.out;

public class Person implements PersonIF
{
   public void sayHello()
   {
      out.println("Hello!");
   }
}
Main.java
public class Main
{
   public static void main(final String[] arguments)
   {
      final Person person = new Person();
      person.sayHello();
   }
}

As can be seen from the code listings above, class Main depends upon (uses) class Person and class Person depends upon (implements) PersonIF. I will intentionally place the PersonIF.class file in its own JAR called PersonIF.jar and will store that JAR in a (different) subdirectory. The Person.class file will exist in its own Person.jar JAR file and that JAR file includes a MANIFEST.MF file with a Class-Path header referencing PersonIF.jar in the relative subdirectory.

I will now attempt to compile the Main.class from Main.java with only the current directory on the classpath. I formerly would have expected compilation to fail when javac would be unable to find PersonIF.jar in a separate subdirectory. However, it doesn't fail!

This seemed surprising to me. Why did this compile when I had not explicitly specified PersonIF.class (or a JAR containing it) as the value of classpath provided via the -cp flag? The answer can be seen by running javac with the -verbose flag.

The output of javac -verbose provides the "search path for source files" and the "search path for class files". The "search path for class files" was the significant one in this case because I had moved the PersonIF.java and Person.java source files to a completely unrelated directory not in those specified search paths. It's interesting to see that the search path for class files (as well as the search path for source files) includes archive/PersonIF.jar even though I did not specify this JAR (or even its directory) in the value of -cp. This demonstrates that the Oracle-provided Java compiler considers the classpath content specified in the Class-Path header of the MANIFEST.MF of any JAR on specified on the classpath.

The next screen snapshot demonstrates running the newly compiled Main.class class and having the dependency PersonIF.class picked up from archive/PersonIF.jar without it being specified in the value passed to the Java application launcher's java -cp flag. I expected the runtime behavior to be this way, though admittedly I had never tried it or even thought about doing it with a JAR whose MANIFEST.MF file did not have a Main-Class header (non-executable JAR). The Person.jar manifest file in this example did not specify a Main-Class header and only specified a Class-Path header, but was still able to use this classpath content at runtime when invoked with java.

The final demonstration for this post involves removing the Class-Path header and associated value from the JAR file and trying to compile with javac and the same command-line-specified classpath. In this case, the JAR containing Person.class is called Person2.jar and the following screen snapshot demonstrates that its MANIFEST.MF file does not have a Class-Path header.

The next screen snapshot demonstrates that compilation with javac fails now because, as expected, PersonIF.class is not explicitly specified on the classpath and is no longer made available by reference from the MANIFEST.MF Class-Path header of a JAR that is on the classpath.

We see from the previous screen snapshot that the search paths for source files and for class files no longer include archive/PersonIF.jar. Without that JAR available, javac is unable to find PersonIF.class and reports the error message: "class file for PersonIF not found."

General Observations

  • The Class-Path header in a MANIFEST.MF file has no dependency on the existence of a Main-Class header existing in the same JAR's MANIFEST.MF file.
    • A JAR with a Class-Path manifest header will make those classpath entries available to the Java classloader regardless of whether that JAR is executed with java -jar ... or is simply placed on the classpath of a larger Java application.
    • A JAR with a Class-Path manifest header will make those classpath entries available to the Java compiler (javac) if that JAR is included in the classpath specified for the Java compiler.
  • Because the use of Class-Path in a JAR's manifest file is not limited in scope to JARs whose Main-Class is being executed, class dependencies can be potentially inadvertently satisfied (perhaps even with incorrect versions) by these rather than resolving explicitly specified classpath entries. Caution is advised when constructing JARs with manifests that specify Class-Path or when using third-party JARs with Class-Path specified in their manifest files.
  • The importance of the JAR's manifest file is sometimes understated, but this topic is a reminder of the usefulness of being aware of what's in a particular JAR's manifest file.
  • This topic is a reminder of the insight that can be gleaned from running javac now and then with the -verbose flag to see what it's up to.
  • Whenever you place a JAR on the classpath of the javac compiler or the java application launcher, you are placing more than just the class definitions within that JAR on the classpath; you're also placing any classes and JARs referenced by that JAR's manifest's Class-Path on the classpath of the compiler or application launcher.

Conclusion

There are many places from which a Java classloader may load classes for building and running Java applications. As this post has demonstrated, the Class-Path header of a JAR's MANIFEST.MF file is another touch point for influencing which classes the classloader will load both at runtime and at compile time. The use of Class-Path does not affect only JARs that are "executable" (have a Main-Class header specified in their manifest file and run with java -jar ...), but can influence the loaded classes for compilation and for any Java application execution in which the JAR with the Class-Path header-containing manifest file lies on the classpath.

1 comment:

Philippe said...

And of course it affects EAR classloading as well.