-
Language:
English
-
Language:
English
5.2.2.3. LinkageErrors - Making Sure You Are Who You Say You Are
Loading constraints validate type expectations in the context of class loader scopes to ensure that a class
X
is consistently the same class when multiple class loaders are involved. This is important because Java allows for user defined class loaders. Linkage errors are essentially an extension of the class cast exception that is enforced by the VM when classes are loaded and used.
To understand what loading constraints are and how they ensure type-safety we will first introduce the nomenclature of the Liang and Bracha paper along with an example from this paper. There are two type of class loaders, initiating and defining. An initiating class loader is one that a
ClassLoader.loadClass
method has been invoked on to initiate the loading of the named class. A defining class loader is the loader that calls one of the ClassLoader.defineClass
methods to convert the class byte code into a Class
instance. The most complete expression of a class is given by <C,Ld>Li
, where C
is the fully qualified class name, Ld
is the defining class loader, and Li
is the initiating class loader. In a context where the initiating class loader is not important the type may be represented by <C,Ld>
, while when the defining class loader is not important, the type may be represented by CLi
. In the latter case, there is still a defining class loader, it's just not important what the identity of the defining class loader is. Also, a type is completely defined by <C,Ld>
. The only time the initiating loader is relevant is when a loading constraint is being validated. Now consider the classes shown in Example 5.5, “Classes demonstrating the need for loading constraints”.
Example 5.5. Classes demonstrating the need for loading constraints
class <C,L1> { void f() { <Spoofed, L1>L1x = <Delegated, L2>L2 x.secret_value = 1; // Should not be allowed } }
class <Delegated,L2> { static <Spoofed, L2>L3 g() {...} } }
class <Spoofed, L1> { public int secret_value; }
class <Spoofed, L2> { private int secret_value; }
The class
C
is defined by L1
and so L1
is used to initiate loading of the classes Spoofed
and Delegated
referenced in the C.f()
method. The Spoofed
class is defined by L1
, but Delegated
is defined by L2
because L1
delegates to L2
. Since Delegated
is defined by L2
, L2
will be used to initiate loading of Spoofed
in the context of the Delegated.g()
method. In this example both L1
and L2
define different versions of Spoofed
as indicated by the two versions shown at the end of Example 5.5, “Classes demonstrating the need for loading constraints”. Since C.f()
believes x
is an instance of <Spoofed,L1>
it is able to access the private field secret_value
of <Spoofed,L2>
returned by Delegated.g()
due to the 1.1 and earlier Java VM's failure to take into account that a class type is determined by both the fully qualified name of the class and the defining class loader.
Java addresses this problem by generating loader constraints to validate type consistency when the types being used are coming from different defining class loaders. For the Example 5.5, “Classes demonstrating the need for loading constraints” example, the VM generates a constraint
SpoofedL1=SpoofedL2
when the first line of method C.f()
is verified to indicate that the type Spoofed
must be the same regardless of whether the load of Spoofed
is initiated by L1
or L2
. It does not matter if L1
or L2
, or even some other class loader defines Spoofed
. All that matters is that there is only one Spoofed
class defined regardless of whether L1
or L2
was used to initiate the loading. If L1
or L2
have already defined separate versions of Spoofed
when this check is made a LinkageError
will be generated immediately. Otherwise, the constraint will be recorded and when Delegated.g()
is executed, any attempt to load a duplicate version of Spoofed
will result in a LinkageError
.
Now let's take a look at how a
LinkageError
can occur with a concrete example. Example 5.6, “A concrete example of a LinkageError” gives the example main class along with the custom class loader used.
Example 5.6. A concrete example of a LinkageError
package org.jboss.book.jmx.ex0; import java.io.File; import java.net.URL; import org.apache.log4j.Logger; import org.jboss.util.ChapterExRepository; import org.jboss.util.Debug; /** * An example of a LinkageError due to classes being defined by more * than one class loader in a non-standard class loading environment. * * @author Scott.Stark@jboss.orgn * @version $Revision: 1.1 $ */ public class ExLE { public static void main(String[] args) throws Exception { ChapterExRepository.init(ExLE.class); String chapDir = System.getProperty("j2eechapter.dir"); Logger ucl0Log = Logger.getLogger("UCL0"); File jar0 = new File(chapDir+"/j0.jar"); ucl0Log.info("jar0 path: "+jar0.toString()); URL[] cp0 = {jar0.toURL()}; Ex0URLClassLoader ucl0 = new Ex0URLClassLoader(cp0); Thread.currentThread().setContextClassLoader(ucl0); Class ctxClass1 = ucl0.loadClass("org.jboss.book.jmx.ex0.ExCtx"); Class obj2Class1 = ucl0.loadClass("org.jboss.book.jmx.ex0.ExObj2"); StringBuffer buffer = new StringBuffer("ExCtx Info"); Debug.displayClassInfo(ctxClass1, buffer, false); ucl0Log.info(buffer.toString()); buffer.setLength(0); buffer.append("ExObj2 Info, UCL0"); Debug.displayClassInfo(obj2Class1, buffer, false); ucl0Log.info(buffer.toString()); File jar1 = new File(chapDir+"/j1.jar"); Logger ucl1Log = Logger.getLogger("UCL1"); ucl1Log.info("jar1 path: "+jar1.toString()); URL[] cp1 = {jar1.toURL()}; Ex0URLClassLoader ucl1 = new Ex0URLClassLoader(cp1); Class obj2Class2 = ucl1.loadClass("org.jboss.book.jmx.ex0.ExObj2"); buffer.setLength(0); buffer.append("ExObj2 Info, UCL1"); Debug.displayClassInfo(obj2Class2, buffer, false); ucl1Log.info(buffer.toString()); ucl0.setDelegate(ucl1); try { ucl0Log.info("Try ExCtx.newInstance()"); Object ctx0 = ctxClass1.newInstance(); ucl0Log.info("ExCtx.ctor succeeded, ctx0: "+ctx0); } catch(Throwable e) { ucl0Log.error("ExCtx.ctor failed", e); } } }
package org.jboss.book.jmx.ex0; import java.net.URLClassLoader; import java.net.URL; import org.apache.log4j.Logger; /** * A custom class loader that overrides the standard parent delegation * model * * @author Scott.Stark@jboss.org * @version $Revision: 1.1 $ */ public class Ex0URLClassLoader extends URLClassLoader { private static Logger log = Logger.getLogger(Ex0URLClassLoader.class); private Ex0URLClassLoader delegate; public Ex0URLClassLoader(URL[] urls) { super(urls); } void setDelegate(Ex0URLClassLoader delegate) { this.delegate = delegate; } protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { Class clazz = null; if (delegate != null) { log.debug(Integer.toHexString(hashCode()) + "; Asking delegate to loadClass: " + name); clazz = delegate.loadClass(name, resolve); log.debug(Integer.toHexString(hashCode()) + "; Delegate returned: "+clazz); } else { log.debug(Integer.toHexString(hashCode()) + "; Asking super to loadClass: "+name); clazz = super.loadClass(name, resolve); log.debug(Integer.toHexString(hashCode()) + "; Super returned: "+clazz); } return clazz; } protected Class findClass(String name) throws ClassNotFoundException { Class clazz = null; log.debug(Integer.toHexString(hashCode()) + "; Asking super to findClass: "+name); clazz = super.findClass(name); log.debug(Integer.toHexString(hashCode()) + "; Super returned: "+clazz); return clazz; } }
The key component in this example is the
URLClassLoader
subclass Ex0URLClassLoader
. This class loader implementation overrides the default parent delegation model to allow the ucl0
and ucl1
instances to both load the ExObj2
class and then setup a delegation relationship from ucl0
to ucl1
. At lines 30 and 31. the ucl0
Ex0URLClassLoader
is used to load the ExCtx
and ExObj2
classes. At line 45 of ExLE.main
the ucl1
Ex0URLClassLoader
is used to load the ExObj2
class again. At this point both the ucl0
and ucl1
class loaders have defined the ExObj2
class. A delegation relationship from ucl0
to ucl1
is then setup at line 51 via the ucl0.setDelegate(ucl1)
method call. Finally, at line 54 of ExLE.main
an instance of ExCtx
is created using the class loaded via ucl0
. The ExCtx
class is the same as presented in Example 5.4, “The ExIAEd class used to demonstrate IllegalAccessException due to duplicate class loaders”, and the constructor was:
public ExCtx() throws IOException { value = new ExObj(); Logger log = Logger.getLogger(ExCtx.class); StringBuffer buffer = new StringBuffer("ctor.ExObj"); Debug.displayClassInfo(value.getClass(), buffer, false); log.info(buffer.toString()); ExObj2 obj2 = value.ivar; buffer.setLength(0); buffer = new StringBuffer("ctor.ExObj.ivar"); Debug.displayClassInfo(obj2.getClass(), buffer, false); log.info(buffer.toString()); }
Now, since the
ExCtx
class was defined by the ucl0
class loader, and at the time the ExCtx
constructor is executed, ucl0
delegates to ucl1
, line 24 of the ExCtx
constructor involves the following expression which has been rewritten in terms of the complete type expressions:
<ExObj2,ucl0>ucl0 obj2 = <ExObj,ucl1>ucl0 value * ivar
This generates a loading constraint of
ExObj2ucl0 = ExObj2ucl1
since the ExObj2
type must be consistent across the ucl0
and ucl1
class loader instances. Because we have loaded ExObj2
using both ucl0
and ucl1
prior to setting up the delegation relationship, the constraint will be violated and should generate a LinkageError
when run. Run the example using the following command:
[examples]$ ant -Dchap=jmx -Dex=0e run-example Buildfile: build.xml ... [java] java.lang.LinkageError: loader constraints violated when linking org/jboss/book/jmx/ex0/ExObj2 class [java] at org.jboss.book.jmx.ex0.ExCtx.<init>(ExCtx.java:24) [java] at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) [java] at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessor Impl.java:39) [java] at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructor AccessorImpl.java:27) [java] at java.lang.reflect.Constructor.newInstance(Constructor.java:494) [java] at java.lang.Class.newInstance0(Class.java:350) [java] at java.lang.Class.newInstance(Class.java:303) [java] at org.jboss.book.jmx.ex0.ExLE.main(ExLE.java:53)
As expected, a LinkageError is thrown while validating the loader constraints required by line 24 of the
ExCtx
constructor.
5.2.2.3.1. Debugging Class Loading Issues
Debugging class loading issues comes down to finding out where a class was loaded from. A useful tool for this is the code snippet shown in Example 5.7, “Obtaining debugging information for a Class” taken from the org.jboss.util.Debug class of the book examples.
Example 5.7. Obtaining debugging information for a Class
Class clazz =...; StringBuffer results = new StringBuffer(); ClassLoader cl = clazz.getClassLoader(); results.append("\n" + clazz.getName() + "(" + Integer.toHexString(clazz.hashCode()) + ").ClassLoader=" + cl); ClassLoader parent = cl; while (parent != null) { results.append("\n.."+parent); URL[] urls = getClassLoaderURLs(parent); int length = urls != null ? urls.length : 0; for(int u = 0; u < length; u ++) { results.append("\n...."+urls[u]); } if (showParentClassLoaders == false) { break; } if (parent != null) { parent = parent.getParent(); } } CodeSource clazzCS = clazz.getProtectionDomain().getCodeSource(); if (clazzCS != null) { results.append("\n++++CodeSource: "+clazzCS); } else { results.append("\n++++Null CodeSource"); }
Firstly, every Class object knows its defining
ClassLoader
and this is available via the getClassLoader()
method. This defines the scope in which the Class
type is known as we have just seen in the previous sections on class cast exceptions, illegal access exceptions and linkage errors. From the ClassLoader
you can view the hierarchy of class loaders that make up the parent delegation chain. If the class loader is a URLClassLoader
you can also see the URLs used for class and resource loading.
The defining
ClassLoader
of a Class
cannot tell you from what location that Class
was loaded. To determine this you must obtain the java.security.ProtectionDomain
and then the java.security.CodeSource
. It is the CodeSource
that has the URL p location from which the class originated. Note that not every Class
has a CoPdeSource
. If a class is loaded by the bootstrap class loader then its CodeSource
will be null. This will be the case for all classes in the java.*
and javax.*
packages, for example.
Beyond that it may be useful to view the details of classes being loaded into the JBoss server. You can enable verbose logging of the JBoss class loading layer using a Log4j configuration fragment like that shown in Example 5.8, “An example log4j.xml configuration fragment for enabling verbose class loading logging”.
Example 5.8. An example log4j.xml configuration fragment for enabling verbose class loading logging
<appender name="UCL" class="org.apache.log4j.FileAppender"> <param name="File" value="${jboss.server.home.dir}/log/ucl.log"/> <param name="Append" value="false"/> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="[%r,%c{1},%t] %m%n"/> </layout> </appender> <category name="org.jboss.mx.loading" additivity="false"> <priority value="TRACE" class="org.jboss.logging.XLevel"/> <appender-ref ref="UCL"/> </category>
This places the output from the classes in the
org.jboss.mx.loading
package into the ucl.log
file of the server configurations log directory. Although it may not be meaningful if you have not looked at the class loading code, it is vital information needed for submitting bug reports or questions regarding class loading problems.