Java class loaders for archive files

In the previous article we saw how to load simple classes in Java. But loading classes one by one, and even exporting class files at the same way, is less practice that working with archives as JARs.

This article will present the way of working with JARs. In the first part we'll talk about theoretical concepts of JARs. The next part will be about some coding features. It'll present which classes can be used to work with JAR files. Before programming our own class loader for jars, we'll present one potential problem that can result of bad JARs loading. Only the last, the fourth, part will show how to implement custom class loader for JAR files.

Class loaders for archives (JAR, WAR)

Before dealing with loading classes from archives, we need to define JAR archives. JAR letters mean Java ARchive. As its name indicates, this is a kind of compressed file which can contain multiple Java classes and other files. Using JARs we simplify the program logic - all classes are placed in the same file, downloaded once and compressed. But how to load the classes stored in JARs ?

Firstly, the classes are represented inside JAR under below format:

# fragment of spring-security-acl-3.2.0.RELEASE.jar (Spring Security project)
org/springframework/security/acls/AclPermissionCacheOptimizer.class
org/springframework/security/acls/AclEntryVoter.class

To simplify, all JAR's classes are listed as in book index. As you can deduce, to load a class from JAR file you need to do exactly the same thing as for loading .class file (you can see an example in article about class loaders in Java) - read containing bytes and construct java.lang.Class instance from it. The only difference consists on the way of finding the class inside archive, but we'll see it in the next part.conflictual reads of the same classes placed on different class loaders.

And what about WAR (Web application ARchive) files ? WARs are like JARs but theirs main purpose are web applications. They contain not only libraries, classes and supplementary files as properties, but also JavaScript or HTML files that are necessary to deploy a web application. Because both are archive files, their content aren't different, so they still can be read at the same way. Take a look on a fragment of WAR archive:

WEB-INF/classes
WEB-INF/META-INF/persistence.xml
role/admin.jsp

Reading JARs and WARs with Java

Java provides the objects that simplify the work with JARs. They're placed inside java.util.jar package. The first class is JarFile. It can be constructed with File instance or String representing path to read archive. The most interesting methods concern content analyze:
- entries: returns an enumeration containing all entries (files) of read archive.
- getJarEntry: its String parameter means the name of the entry to retrieve.
- getManifest: it's used to get archive's manifest file. Manifest file is a kind of descriptor for JAR content. It defines applied signatures, versions or authors. Only one manifest file can exist in JAR and it's located on META-INF/MANIFEST.MF.

Another important class of JAR package is JarEntry. It can be found thanks to getEntry or entries methods. It represents an entry of read archive, so for example the name of class or HTML file. JarEntry can be used to get the information about physical representation of the entry (file), as size, creation date, compression size, method used to compress the file, comment or type (file/directory).

JAR hell

Often when you start to work with JARs, class loading problems occur, for example when you incidentally put two JARs of one project with different versions. This issue is called JAR hell and can be produced under 1 of 3 following conditions:

  1. Two libraries require different version of the same third library Imagine that you have two libraries: lib.A.jar and lib.B.jar. Some of theirs methods need to call a method of third library, lib.C.jar. But the lib.C.jar evolved and changed the signatures of this method, without maintaining backward compatibility. Let's suppose that called method is readXml which in the version used by lib.A.jar (0.1) has this signature: readXml(String xmlContent) and in the version used by lib.B.jar (0.2) this method looks like readXml(XmlContent content). Both methods belong to the StaticXmlReader class.

    Now, if you put both versions of lib.C.jar in the classpath, only one will be loaded. In consequence, either lib.A.jar or lib.B.jar won't work correctly.
  2. Human error - two libraries with the same class available in classpath This problem looks familiar to the previous one, but it's simpler. Imagine that at the begin of development, your application uses lib.D.jar with version 0.1. But meantime, the developers of lib.D.jar discovered and fixed some bugs. They made a lib.D.jar with version 0.2. You add this new dependency on your project manually by downloading JAR and putting it into classpath [not a good practice...]. You make some compatibility fixes in your application but you forget to delete lib.D.jar with version 0.1. Now, depending on class loader, old version of JAR can still be loaded and produce errors in your application. The problems can be represented, for example, by method that changed the signature (loaded class has old signature while your program uses the new one, from 0.2 version).
  3. Issues related to class loaders family dependencies As we mentioned at the article about class loaders in Java, class loader usually has parent loader. This dependency can introduce some problems with JARs loading. Normally, the class loaders delegates the loading to the parent class first. But some implementations can make the opposite: load from child and after from parent. An example of error which can occur on bad implemented delegation is the object construction in multi-threading environment. Imagine that you have following simplified class:
    class RaceFail {
      private ClassLoader classLoader;
        
      public void setClassLoader(ClassLoader classLaoder) {
        this.classLoader = classLoader;
      }
    
      public void constructObject() {
        ClassLoader usedLoader = this.classLoader;
        if (usedLoader == null) {
                usedLoader = Thread.currentThread().getContextClassLoader();
        }
        Class<?> neededClass = Class.forName("mysite.com.neededClass", true, usedLoader);
        // construct object with neededClass constructor
        Constructor<?> constructor = neededClass.getConstructor();
        constructor.newInstance(3);
      }
            
    }	
    

    Now, imagine some clients using this class, both launched in separate threads:

    Client c1;
    Client c2;
    // construction here...
    c1.setClassLoader(projectClassLoader);
    c2.constructObject();
    

    If c2 is executed before c1 (so before setting class loader), the demanded object can't be created correctly. It may be caused by the fact that used class loader (for example, Bootstrap) contains old version of constructor that doesn't support int as parameter. Similar problem was described in the article about class loading problems with Java loggers.

Custom JAR class loader

JAR class loader will be illustrated with simple JUnit case. We'll try to find a class belonging to Spring framework, org.springframework.util.PropertyPlaceholderHelper. At the second time, it's a non-existent class (org.springframework.util.NotReallyPropertyPlaceHolder) that will be loaded. The first case should load class successfully while the second should fail by throwing ClassNotFoundException.

public class ClassloaderArchiveTest {

  @Test
  public void testJarLoader() {
    FixedLocatedArchiveLoader loader = new FixedLocatedArchiveLoader(new URL[] {
        new URL("file:/home/bartosz/tmp/jars/spring-security-acl-3.2.0.RELEASE.jar"),
        new URL("file:/home/bartosz/tmp/jars/slf4j-api-1.7.5.jar"),
        new URL("file:/home/bartosz/tmp/jars/spring-core-4.0.0.RELEASE.jar"),
        new URL("file:/home/bartosz/tmp/jars/tests.war") 
    });
    loader.addAppliedPackages("org.springframework.util");
    String searchedClass = "org.springframework.util.PropertyPlaceholderHelper";
    Class<?> propClass = Class.forName(searchedClass, true, loader);
    assertTrue("Loaded class should be '"+searchedClass+"' but was '"+propClass.getCanonicalName()+"'", 
      propClass.getCanonicalName().equals(searchedClass));
    // This test should fail
    boolean wasCnfe = false;
    String falseClassName = "org.springframework.util.NotReallyPropertyPlaceHolder";
    try {
      Class.forName(falseClassName, true, loader);
      fail("The class '"+falseClassName+"' shouldn't exist but it was");
    } catch (ClassNotFoundException cnfe) {
      wasCnfe = true;
    } 
    assertTrue(falseClassName+" shouldn't be found and ClassNotFoundException should occur", 
      wasCnfe);
  }
}



class FixedLocatedArchiveLoader extends URLClassLoader  {

  private List<String> appliedPackages;

  public FixedLocatedArchiveLoader(URL[] urls) {
    super(urls);
  }
  
  private List<String> getAppliedPackages() {
    if (appliedPackages == null) {
      this.appliedPackages = new ArrayList<String>();
    }
    return this.appliedPackages;
  }
  
  public void addAppliedPackages(String packageName) {
    getAppliedPackages().add(packageName);
  }
  
  private boolean applyFixedLoad(String className) {
    for (String packageName : getAppliedPackages()) {
      if (className.contains(packageName)) return true;
    }
    return false;
  }

  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
    if (applyFixedLoad(name)) {
      // construct path with .class file
      String classFile = name.replaceAll("\\.", "/")+".class";
      // Read all entries of defined JARs and try to found given class
      URL[] urls = getURLs();
      int i = 0;
      Class<?> searchedClass = null;
      while (searchedClass == null && i < urls.length) {
        URL url = urls[i];
        i++;
        JarFile jarFile = null;
        try {
           /**
            * JarFile is constructed directly from File instance. After 
            * we get ZipEntry which is, in fact, the element composing the JAR. 
            * If entry is found, we construct class instance from read bytes. 
            * The bytes are read in readClassBytesFromFile method.
            */
          File jar = new File(url.getFile());
          jarFile = new JarFile(jar);
          ZipEntry entry = jarFile.getEntry(classFile);
          if (entry != null) {
            byte[] entryBytes = readClassBytesFromFile(jar, classFile);
            searchedClass = defineClass(name, entryBytes, 0, entryBytes.length);
          }
        } finally {
          if (jarFile != null) {
            try {
              jarFile.close();
            } catch (IOException e) {
              e.printStackTrace();
            }
          }
        }
      }
      return searchedClass;
    }
    return super.findClass(name);
  }
        
   /**
    * Reads bytes from given JAR file. The reading is based on special URL implementation 
    * dedicated to JARs access. It starts by jar: protocol and continues with the name of the file.
    * The final !/ separates this two parts from the name of searched class. For example, 
    * this URL jar:file://home/bartosz/tmp/jars/spring-core-4.0.0.RELEASE.jar!/org/springframework/util/PropertyPlaceholderHelper.class
    * - jar: marks the URL as JAR URL
    * - file://home/bartosz/tmp/jars/spring-core-4.0.0.RELEASE.jar: address of the JAR file containing searched class
    * - !/org/springframework/util/PropertyPlaceholderHelper.class: the name of searched class
    * 
    * @param jar File containing searched class.
    * @param className Searched class.
    * @return Array of bytes with searched class (if error, the array is empty).
    */
  private byte[] readClassBytesFromFile(File jar, String className) {
    InputStream stream = null;
    try {
      URL urlTmp = new URL("jar:"+getURI(jar)+"!/"+className);
      JarURLConnection jarUrl = (JarURLConnection)urlTmp.openConnection();
      stream = jarUrl.getInputStream();
      byte[] bytes = new byte[stream.available()];
      stream.read(bytes);
      return bytes;
    }
    return new byte[]{};
  }

  @Override
  protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
    if (applyFixedLoad(name)) {
      Class<?> searchedClass = findClass(name);
      if (resolve) {
        resolveClass(searchedClass);
      }
      return searchedClass;
    }
    return super.loadClass(name, resolve);
  }
 
        
  /**
   * Taken from Tomcat7 class loader: org.apache.catalina.loader.WebappClassLoader
   */
  private URL getURI(File file) throws MalformedURLException, IOException {
    File realFile = file;
    realFile = realFile.getCanonicalFile();
    return realFile.toURI().toURL();
  }
        
}

This article is the continuation of articles about class loading. This time we discovered how to load classes included inside the archive files (JAR and WAR). As we could see, it can be simply done with JarFile and JarURLConnection classes that allow to read archives and get searched class bytes. With these bytes we can create Class instance exactly as for simple loading of single .class files.


If you liked it, you should read:

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!