Understanding Class.forName (二)

The Launcher constructor, after constructing both the ExtClassLoader and AppClassLoader, sets the thread context ClassLoader to be the AppClassLoader instance. This means, unless your code (or other code loaded by your application) changes it, the thread’s context ClassLoader is the AppClassLoader instance. This also means that now the AppClassLoader instance is available to Extension classes:

import java.lang.reflect.*;

public class FixedDynamicLoader

{

public static void main(String[] args)

throws Exception

{

// Instead of

// Class toRun = Class.forName(args[0]);

// use:

Thread t = Thread.currentThread();

ClassLoader cl = t.getContextClassLoader();

Class toRun = cl.loadClass(args[0]);

String[] newArgs = scrubArgs(args);

Method mainMethod = findMain( toRun );

mainMethod.invoke(null, new Object[] { newArgs });

}

private static String[] scrubArgs(String[] args)

{

String[] toReturn = new String[args.length-1];

for (int i=1; i<args.length; i++)

{

toReturn[i-1] = args[i].toLowerCase();

}

return toReturn;

}

private static Method findMain(Class clazz)

throws Exception

{

Method[] methods = clazz.getMethods();

for (int i=0; i<methods.length; i++)

{

if (methods[i].getName().equals("main"))

return methods[i];

}

return null;

}

}

After compiling, placing into a .jar and dropping that .jar into the Extensions directory, we achieve success:

> java FixedDynamicLoader Echo ONE TWO THREE

Echo arg0 = one

Echo arg1 = two

Echo arg2 = three

So the short answer is to use the ClassLoader associated with the current Thread to call loadClass to load the class desired, instead of using Class.forName.

 

Those readers familiar with multi-threaded environments may be wondering about Threads created by user-code—what ClassLoader is set to be the context ClassLoader for user-created Threads? A Thread, when created, inherits the context ClassLoader of the Thread that created it, so all Threads, unless modified, will have AppClassLoader set as their context ClassLoader. Use the 3-arg form of Class.forName. Sun introduced a new form of the forName method that takes 3 arguments, instead of just one. In addition to the name of the Class to load, callers pass a boolean parameter indicating whether to initialize the loaded Class or not, and a ClassLoader parameter to load the code through. This method performs the same steps as the 1-arg Class.forName; in fact, the 1-arg version of Class.forName calls directly into the 3-arg version, passing “true” and the caller’s ClassLoader as the second and third parameters, respectively. Class.forName(classname, init_flag, classloader) will load the code “through” the ClassLoader passed to it; crudely put, it functions somewhat similar to the following:

Class forName(String classnm, boolean init, ClassLoader loader)

throws . . .

{

// . . .

loader.loadClass(classnm);

// . . .

}

This is a gross oversimplification, but the point is clear—instead of using the caller’s ClassLoader to do the load, it uses the ClassLoader instance passed in. This now means, to avoid the problem that started this paper, FixedDynamicLoader could also do:

public class FixedDynamicLoader

{

public static void main(String[] args)

throws Exception

{

//Class toRun = Class.forName(args[0]);

Class toRun =

Class.forName(args[0],

true,

ClassLoader.getSystemClassLoader());

String[] newArgs = scrubArgs(args);

Method mainMethod = findMain( toRun );

mainMethod.invoke(null, new Object[] { newArgs });

}

// . . .

}

This code has the advantage of being perhaps a bit more explicit about what the intent of the code is, as opposed to the slightly more obscure use of the Thread’s context ClassLoader. Other (technical) differences between the two are more profound, however. Class.forName vs ClassLoader.loadClass. There are some subtle differences between these two APIs. The method call CL.loadClass(C), where CL is our ClassLoader and C is the name of the class to load, queries the ClassLoader directly for the class by name. This in turn relies on ClassLoader delegation to ask the bootstrap ClassLoader to determine if the class has already been loaded. Conversely, the method call Class.forName(C, false, CL) uses the VM’s internal class resolution mechanism to do the actual loading. Among other things, this allows Class.forName to support the loading of arrays of Classes as a type; for example, calling CL.loadClass(“[C;”) will result in failure, where doing the same with Class.forName(“[C;”, false, CL) will succeed.

Consequences

As with any discussion, there are consequences to each of the solutions set forth within this white paper. Versioning: The 3-arg Class.forName, Thread.getContextClassLoader, and Thread.setContextClassLoader are Java2/JDK1.2 APIs. This means any Java API or class library that wants to remain compatible or usable with JDK 1.1 will not be able to be loaded. Remember, the VM, during its resolution step of classloading (see [4]), will verify that all methods referenced inside of a class actually exist, and will throw an Exception if this is not the case. How, then can code avoid this ClassLoader trap in Java2 code, while remaining compatible with 1.1 code? One solution is to eliminate your JDK 1.1 compatibility requirement. It sounds a bit draconian, but Java2/JDK 1.2 has been out for approximately a year at the time of this writing, Java2/JDK 1.3 is in the final beta stages. More to the point, almost no new development is being done in JDK 1.1 (except for applets, since 1.1 is as far as most browsers have gone in JDK support). However, for many APIs and/or design teams, this is an unacceptable solution. Too many JDK 1.1-based systems exist to simply write off JDK 1.1 environments entirely, and developing a system that fails to work properly in a JDK 1.2 / Java2 environment is simply unthinkable. Fortunately, a convenient middle ground is possible.

Because Java uses a lazy dynamic-loading system (see [2], [4], or [5] for details), classes aren’t loaded into the VM until the last possible moment. This means it’s possible to use a Strategy-pattern approach towards classloading, based on the version of the VM your code is executing within:

package com.javageeks.lang.classloader;

interface VMClassLoader

{

public Class loadClass(String cls)

throws ClassNotFoundException;

}

public class ClassLoaderHelper

Ted Neward

14

{

private static VMClassLoader vmClassLoader;

static

{

String vmVersion = System.getProperty("java.version");

if (vmVersion.startsWith("1.2"))

{

//System.out.println("Loading 1.2 VMClassLoader");

vmClassLoader = new VMClassLoader()

{

public Class loadClass(String cls)

throws ClassNotFoundException

{

Thread t = Thread.currentThread();

ClassLoader cl = t.getContextClassLoader();

return cl.loadClass(cls);

}

};

}

else if (vmVersion.startsWith("1.1") ||

vmVersion.startsWith("1.0"))

{

//System.out.println("Loading 1.1/1.0 VMClassLoader");

vmClassLoader = new VMClassLoader()

{

public Class loadClass(String cls)

throws ClassNotFoundException

{

return Class.forName(cls);

}

};

}

else

{

// ???

}

}

public static Class loadClass(String cls)

throws ClassNotFoundException

{

return vmClassLoader.loadClass(cls);

}

/**

* Test driver.

*/

public static void main(String[] args)

throws Exception

{

//Class toRun = Class.forName(args[0]);

Class toRun =

ClassLoaderHelper.loadClass(args[0]);

String[] newArgs = scrubArgs(args);

java.lang.reflect.Method mainMethod =

findMain( toRun );

mainMethod.invoke(null, new Object[] { newArgs });

Ted Neward

15

}

private static String[] scrubArgs(String[] args)

{

String[] toReturn = new String[args.length-1];

for (int i=1; i<args.length; i++)

{

toReturn[i-1] = args[i].toLowerCase();

}

return toReturn;

}

private static java.lang.reflect.Method findMain(Class clazz)

throws Exception

{

java.lang.reflect.Method[] methods = clazz.getMethods();

for (int i=0; i<methods.length; i++)

{

if (methods[i].getName().equals("main"))

return methods[i];

}

return null;

}

}

The key here is in the static block of the ClassLoaderHelper class. When loaded in a 1.2 VM, an anonymous instance of VMClassLoader is created, which uses the Thread contextClassLoader methods to obtain the ClassLoader and call loadClass. When loaded into a 1.1 (or 1.0, although this is, as of this writing, untested) VM, the anonymous VMClassLoader instance falls back on Class.forName, since 1.1 VMs don’t have the problem described in this paper.

Running this code within a 1.2 VM yields the following:

> java -version

java version "1.2"

Classic VM (build JDK-1.2-V, native threads)

> java ClassLoaderHelper Echo ONE TWO THREE

Loading 1.2 VMClassLoader

Echo arg0 = one

Echo arg1 = two

Echo arg2 = three

Running this code within a 1.1 VM yields the following:

> java -version

java version "1.1.7"

> java ClassLoaderHelper Echo ONE TWO THREE

Loading 1.1/1.0 VMClassLoader

Echo arg0 = one

Echo arg1 = two

Echo arg2 = three

By using ClassLoaderHelper.loadClass instead of Class.forName, code can continue to support both JDK 1.1 and JDK 1.2 (and beyond) VMs without having to maintain two (or more) separate codebases. (ClassLoaderHelper could also be modified to use the 3-arg version of Class.forName instead of the Thread’s context ClassLoader. However, it would require a ClassLoader instance to be passed in, to avoid making the assumption that it should use the system ClassLoader.)  Using Thread.getContextClassLoader relies on the Thread’s context

ClassLoader to be appropriately set. What if the Thread’s current context ClassLoader isn’t the one expected, but is instead set by an arbitrary third-party package (like RMI or JNDI) In fact, there’s not much you can do to prevent this. Because Thread.setContextClassLoader makes a Security check, however, you can take some small comfort in the fact that only those systems that have “setContextClassLoader” permission will be allowed to modify a Thread’s context ClassLoader. Practically speaking, this means in an enterprise system, you can modify your policy file such that only your codebase is permitted to modify the Thread’s context ClassLoader. This doesn’t prevent Sun-sponsored APIs (like RMI, which is “part” of the runtime library, or JNDI, which is part of the runtime library starting in JDK 1.3) from being able to modify it, but at least you can prevent rogue third-parties from doing so.

 Knowing your loading ClassLoader. In order to use the 3-arg form of Class.forName, callers must have the ClassLoader instance they want to load through available to them in order to pass it; under certain circumstances, simply calling ClassLoader.getSystemClassLoader() here will be wildly inappropriate. For example, a Servlet will usually be loaded under its own ClassLoader, and if the Servlet calls Class.forName with the system ClassLoader as the third argument, code loaded under the Servlet’s ClassLoader won’t be found. This situation can arise more commonly than one might expect—many Servlets are bundled together with supporting code directly in the “servlets” directory, placing the supporting code under the Servlet ClassLoader’s domain. Under those circumstances, using the Thread’s context ClassLoader can be the only appropriate parameter; you can either pass it as the third parameter to the 3-arg form of Class.forName, or else call its loadClass method directly. But, as pointed out in the aforementioned point, using it relies on the context ClassLoader being set correctly by the code that loaded your class. It may seem that callers will always know the ClassLoader they should call through. However, keep in mind that the ExtClassLoader/AppClassLoader pair is not the only situation in which ClassLoaders further “up the tree” are going to look to load code further “down” the ClassLoader tree. For example, sophisticated security mechanisms within a Java application might make use of an instance of URLClassLoader to load code on a per-user-role basis; certain parts of the application, however, will be constant, and therefore installed as an Extension. Those constant classes cannot simply assume that the ClassLoader that loaded them will be the correct ClassLoader to pass into Class.forName (it won’t—we’re now right back to the original problem), nor will simply passing ClassLoader.getSystemClassLoader() be correct, either. Instead, they will have to trust that somebody will set the current Thread’s context ClassLoader to be that custom URLClassLoader instance, and use that to dynamically load the code.

Summary

Java2, with the release of JDK 1.2, subtly changed the nature of classloading. When Sun added the “Extens ion” capability to the language/environment, they split the responsibility for loading Extensions code and “Application” (that is, CLASSPATH-based) code into two separate ClassLoaders. Under “normal” circumstances, this change will be invisible to most Java developers, but those working under dynamic-loading systems need to be aware of this change and what it means for them. Many Java developers may believe that the circumstances described in this paper won’t apply to them. “I’m not making use of any of this”, they mutter to themselves. “Why do I care?” It’s a more relevant concern than readers might wish. Several key Java technologies, most notably RMI and JNDI, make use of the Thread context ClassLoaders for precisely this reason—the core classes for both technologies are loaded high up in the CllassLoader tree, but need to be able to load code stored along the CLASSPATH. Worse yet, as more enterprise systems are built under EJB servers, which may well be installed as Extensions, this problem could become more and more common. As Java2 Security becomes more and more well-understood and implemented in Java projects, this issue will again rear its ugly head, since developers will slowly begin to adopt codebase permissions on a per-ClassLoader basis. Multiple ClassLoaders means the potential for code further up the chain looking for code further down the chain, and we’re right back to where we started with all this.

Fortunately, as JDK 1.1 systems slowly phase out, it should become more and more comfortable to use Thread.currentThread().getContextClassLoader().loadClass() or the 3-arg version of Class.forName() directly; until that time; however, the ClassLoaderHelper class should provide a measure of portability across JVM versions for doing dynamic classloading from within an Extension.

Bibliography/Additional Reading

[1] JDK 1.2 documentation bundle. See http://www.javasoft.com for downloading.

[2] << Liang/Bracha OOPSLA paper >>

[3] Java Language Specification.

[4] Java Virtual Machine Specification, 2nd Edition.

[5] Server-Side Java, by Ted Neward. Manning Publishing, Feb 2000.

[6] Personal communication with Peter Jones of Sun Microsystems; used with permission.

Copyright

This paper, and accompanying source code, is copyright © 2000 by Ted Neward. All rights reserved. Usage for any other purpose

than personal or non-commercial education is expressly prohibited without written consent. Code is copywritten under the Lesser

GNU Public License (LGPL). For questions or concerns, contact author.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值