When we implemented a class loader above, we had a fully operational class loader that paralleled the first class loaders that were used by Java's appletviewer or by a Java-enabled browser. However, there are other extensions to the class loader that are often useful.
We started with a complete class loader suitable for use in appletviewer-type programs where the classes are to be loaded from the network. This is good as far as it goes, but let's delve a little more into the security issues that surround that class loader.
In the world of Java-enabled browsers, an applet can retrieve classes from only one site--the CODEBASE specified in the applet's HTML tag. There are other reasons why an applet can only make a network connection to its CODEBASE (which we'll discuss in Chapter 4, "The Security Manager Class"), but one of the reasons is contained in the discussion we outlined above: because classes loaded by the same class loader are considered to be in the same package, and an applet that loaded classes from multiple sites could run the risk of classes from different sites interfering with each other.
In an ideal world, however, a Java program may want to load classes from several locations on the network. Consider the deployment outlined in Figure 3-2 for XYZ Corporation: XYZ Corporation employs a network support group to manage its departmental servers, and within each department, there are programmers who are responsible for deploying the department's applications on those servers.
When the corporate network support group develops some useful JavaBeansTM components, everyone in the corporation is encouraged to use them in their departmentally developed applications. This gives the applications a certain consistency between departments as well as promoting reuse of the efforts of the network support group. But as it stands now, the support group must distribute the Java Bean class files to each department so that these beans can be used by programs that are hosted on each departmental server.
Of course, there are technologies outside of Java that can manage distribution, but this is just a variation of the same application distribution problem that Java was originally hailed for solving. Unfortunately, the single-host-based class loader employed by standard Java-enabled browsers doesn't address this situation.
One improvement that we might make is to allow our class loader to load classes from multiple hosts on the network. There's some overhead involved here: when a program running on a machine on the HR network needs to load a class, does it check for the class on the HR server first or on the support group server first? Either way, there will be a number of lookups that check the wrong server first, which is somewhat inefficient. Judicious use of package names could help: if the support group beans were all placed in a single package, the class loader could be smart enough to contact the support group server only when asked to load classes from that package.
Remember that this intelligence about package names solves a logistical problem as well. Say that the support group writes a Java bean called Check that provides a nice graphical representation of a checkbox; this graphical representation is part of the look-and-feel on which XYZ Corporation wants to standardize. Now the HR group wants to create a payroll application, so they create a Check class representing the financial instrument that is used to pay their employees. Now when an HR applet wants to instantiate a Check object, what is it referring to--a GUI class or a financial instrument?
Solving this problem in the intranet world is straightforward--it's easy for the support and HR groups to coordinate their namespace so that the class loader won't see these collisions (e.g., by having the support group use names in a particular package, which again could make the class loader more efficient). In the case of the freewheeling Internet, this type of coordination is not possible: there can be no guarantee that two unrelated sites won't use classes that are in the same package. So the multiple-site class loader is only appropriate for intranet use.
There are various ways in which the multiple-site class loader could be implemented--for this example, we'll assume that any classes that are in the com.XYZ.support package should be loaded from the network support group's server (which we'll hardcode into the class loader, though we would normally configure this to be a property). Any other classes should come from the server that initialized the class loader. So our new class loader looks like this:
public class MultiLoader extends JavaRunnerLoader { private static final String server = "support.xyz.com/"; public MultiLoader(String url, ClassLoader parent) { super(url, parent); } protected Class findClass(String name) { URL codeURL; SecurityManager sm = System.getSecurityManager(); if (sm != null) { int i = name.lastIndexOf('.'); if (i >= 0) sm.checkPackageDefinition(name.substring(0, i)); } try { String codeName = name.replace('.', '/') + ".class"; if (name.startsWith("com.xyz.support")) codeURL = new URL("http://" + server + codeName); else codeURL = new URL(urlBase, codeName); if (printLoadMessages) System.out.println("Loading " + name); InputStream is = codeURL.openConnection().getInputStream(); byte buf[] = getClassBytes(is); return defineClass(name, buf, 0, buf.length, null); } catch (Exception e) { return null; } } }
If you're thinking clearly about the security ramifications of this code, then you've already spotted a potential error: just because we're asked to load a class named com.xyz.support.Car doesn't necessarily mean that we should contact our internal server to do so--we should only contact that internal server if the other classes that we are loading are also from our internal network. That is, if we use this class loader in a browser that is loading an applet from www.EvilSite.org that requests the class com.xyz.support.Car, we should attempt to load that class from EvilSite and not from our support group's server; we should only load com.xyz.support classes from support.xyz.com when the other classes in the program come from another machine in the xyz.com domain.
We could have put the logic to deal with that possibility into the class loader itself; however, it's equally possible to put that logic elsewhere into our application. The JavaRunner program, for example, must instantiate a new class loader for each program it loads, and it's simpler to instantiate a MultiLoader class loader when the program is being loaded from a machine within the xyz.com domain, and to instantiate a regular JavaRunnerLoader when the program is being loaded from a machine outside the xyz.com domain.
Note the different approach taken here and in the URLClassLoader class: in this case, we contact a second machine only when we have classes in a particular package that we expect to find on that machine. If we had constructed a URLClassLoader as follows:
URL urls[] = new URL[2]; urls[0] = new URL("http://hr.xyz.com/"); urls[1] = new URL("http://support.xyz.com/"); URLClassLoader ucl = new URLClassLoader(urls);
then we would have functionally achieved something similar. However, with the URL class loader, when we search for a class named com.xyz.support.Check, we'll always contact the HR server first, which is slightly less efficient. On the other hand, the technique used by the URL class loader is clearly more flexible than the approach we've outlined above. In addition, the present implementation of the URLClassLoader will not work with multiple HTTP-based URLs, so for the present, you must write your own class loader to handle that case.
There is one important feature present in many class loaders that we haven't yet mentioned, and that is the ability to load a single file that contains many classes. JAR files have a significant advantage over individual class files: loading several classes in a single file can be orders of magnitude faster than loading those same classes through individual HTTP connections. The reason for this comes from a property of the HTTP protocol: it takes a relatively long time to set up an HTTP connection. In fact, the time it takes to transfer the data in a Java class file over a network is usually much shorter than the time required to set up the HTTP connection. Hence, JAR files are often preferred because they can greatly speed up the time it takes to download an applet.
In browsers based on 1.0.2, support for JAR files is browser-dependent; those browsers that support them refer to the JAR file as an archive. In browsers based on 1.1, support for JAR files is present within the JDK itself using classes in the java.util.zip package, because a JAR file is really just a zip file with some additional information. In Java 1.2, there is an additional set of classes in the java.util.jar package that can help to process these files as well (including the additional information in the JAR file).
Of course, there's a flip side to using JAR files. If you use a large word-processing program in Java, you'll probably want to avoid loading a lot of the classes when you download the program: there's no need to spend the time downloading all the class files that implement the spellchecker until it is actually time to check the document's spelling. With JAR files, you don't have that luxury; you must load all the classes in a single shot. Even in those browsers in which you can specify multiple JAR files, the class loader has no way of knowing which particular JAR file contains which particular classes, so it still has to load all of them at once.[5]
[5]A Java application could be more clever about this: it could know to load the archive containing the classes to perform the spellcheck when it was time to run the spellchecker. But an applet cannot do that, because an applet has no mechanism that it can use to tell the browser to load a new archive.
Nevertheless, JAR files are very popular, and they certainly have their place for programs where all (or at least most) of the classes are likely to be used every time the program is run. So we'll look into the additions that must be made to our class loader in order for it to support loading a JAR file. This may seem to be taking us somewhat far afield of our discussion about application security, but there is another reason JAR files are important: they provide the necessary support for digitally signed classes. We typically speak of a signed class as an entity unto itself; in fact, a signed class can only be delivered as part of a JAR file. Hence, a class loader that can process JAR files is very important.
So, to complete our understanding of the class loader and to prepare us for those future examples, we'll show how to add JAR support to our custom class loader. In order to support a JAR file, we'll create a new class. Although the logic is similar to our JavaRunnerLoader class, we get no benefit from extending that class, so we'll show the full implementation here. Changes to the JavaRunnerLoader class are shown in bold.
public class JarLoader extends SecureClassLoader { private URL urlBase; public boolean printLoadMessages = true; Hashtable classArrays; public JarLoader(String base, ClassLoader parent) { super(parent); try { if (!(base.endsWith("/"))) base = base + "/"; urlBase = new URL(base); classArrays = new Hashtable(); } catch (Exception e) { throw new IllegalArgumentException(base); } } private byte[] getClassBytes(InputStream is) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); BufferedInputStream bis = new BufferedInputStream(is); boolean eof = false; while (!eof) { try { int i = bis.read(); if (i == -1) eof = true; else baos.write(i); } catch (IOException e) { return null; } } return baos.toByteArray(); } protected Class findClass(String name) { String urlName = name.replace('.', '/'); byte buf[]; Class cl; SecurityManager sm = System.getSecurityManager(); if (sm != null) { int i = name.lastIndexOf('.'); if (i >= 0) sm.checkPackageDefinition(name.substring(0, i)); } buf = (byte[]) classArrays.get(urlName); if (buf != null) { cl = defineClass(name, buf, 0, buf.length, null); return cl; } try { URL url = new URL(urlBase, urlName + ".class"); if (printLoadMessages) System.out.println("Loading " + url); InputStream is = url.openConnection().getInputStream(); buf = getClassBytes(is); cl = defineClass(name, buf, 0, buf.length, null); return cl; } catch (Exception e) { System.out.println("Can't load " + name + ": " + e); return null; } } public void readJarFile(String name) { URL jarUrl = null; JarInputStream jis; JarEntry je; try { jarUrl = new URL(urlBase, name); } catch (MalformedURLException mue) { System.out.println("Unknown jar file " + name); return; } if (printLoadMessages) System.out.println("Loading jar file " + jarUrl); try { jis = new JarInputStream( jarUrl.openConnection().getInputStream()); } catch (IOException ioe) { System.out.println("Can't open jar file " + jarUrl); return; } try { while ((je = jis.getNextJarEntry()) != null) { String jarName = je.getName(); if (jarName.endsWith(".class")) loadClassBytes(jis, jarName); // else ignore it; it could be an image or audio file jis.closeEntry(); } } catch (IOException ioe) { System.out.println("Badly formatted jar file"); } } private void loadClassBytes(JarInputStream jis, String jarName) { if (printLoadMessages) System.out.println("\t" + jarName); BufferedInputStream jarBuf = new BufferedInputStream(jis); ByteArrayOutputStream jarOut = new ByteArrayOutputStream(); int b; try { while ((b = jarBuf.read()) != -1) jarOut.write(b); classArrays.put(jarName.substring(0, jarName.length() - 6), jarOut.toByteArray()); } catch (IOException ioe) { System.out.println("Error reading entry " + jarName); } } public void checkPackageAccess(String name) { SecurityManager sm = System.getSecurityManager(); if (sm != null) sm.checkPackageAccess(name); } }
The bulk of the change in this example is the addition of two new methods (the readJarFile() and loadClassBytes() methods). These two new methods are used to process the JAR file.
The classes in the java.util.jar package handle all the details about the JAR file for us, and we're left with a simple implementation: we use the getNextJarEntry() method to obtain each file in the archive and process each one sequentially. For maximum efficiency, we don't actually need to create the class from the bytes until necessary: the loadClassBytes() method just creates an array of bytes for each class in the JAR file.
This necessitates a slight change to the logic in our findClass() method: now when we need to provide a class that is not a system class, we check first to see if that class is in the classArrays hashtable. If it is, we obtain the bytes for the class from that hashtable (where they were stored in the readJarFile() method) rather than opening a URL to obtain the bytes for the class over the network.
If you need to produce a similar class loader under 1.1, you can use the java.util.zip package instead of the java.util.jar package. In this example, the two are functionally equivalent, and you may simply substitute Zip every time you see Jar (and zip for jar) with one exception: replace the get-NextJarEntry() method with the getNextEntry() method. Later, when we deal with signed JAR files, that substitution will not work: the difference between the two packages is that the jar package understands the signature format and manifest of the JAR file.
This implementation is similar to the procedure followed by the URLClassLoader class; in that case, the JAR files occur as elements in the array of URLs passed to the class.
Copyright © 2001 O'Reilly & Associates. All rights reserved.