探讨ClassLoader引发的 java.lang.LinkageError

本文将通过多个例子来说明 LinkageError 出现的场景。其中有些例子很有趣,甚至会出乎你的意料。

例子1:Web应用

jetty 部署应用的时候,出现了Java LinkageError:

java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.eclipse.jetty.webapp.IterativeDescriptorProcessor.visit(IterativeDescriptorProcessor.java:84)
...
Caused by: 
java.lang.LinkageError: loader constraint violation: when resolving method "org.slf4j.impl.StaticLoggerBinder.getLoggerFactory()Lorg/slf4j/ILoggerFactory;" the class loader (instance of org/eclipse/jetty/webapp/WebAppClassLoader) of the current class, org/slf4j/LoggerFactory, and the class loader (instance of java/net/URLClassLoader) for the method's defining class, org/slf4j/impl/StaticLoggerBinder, have different Class objects for the type org/slf4j/ILoggerFactory used in the signature
	at org.slf4j.LoggerFactory.getILoggerFactory(LoggerFactory.java:418)
	at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:357)
	at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:383)
	at test.TestListener.<clinit>(TestListener.java:10)
...

环境

应用配置 web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5">
    <listener>
        <listener-class>test.TestListener</listener-class>
    </listener>
</web-app>

TestListener的代码:

package test;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class TestListener implements ServletContextListener {
    private static final Logger logger = LoggerFactory.getLogger(TestListener.class);

    public void contextInitialized(ServletContextEvent sce) {
        logger.info("context initialized.");
    }

    public void contextDestroyed(ServletContextEvent sce) {
        logger.info("context destroyed.");
    }
}

maven的配置 pom.xml

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.22</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

jetty的目录结构

.
├── conf
│   ├── jetty-channel.xml
│   └── jetty.xml
├── jetty-runner.jar
├── lib 
│   ├── logback-classic-1.0.13.jar
│   ├── logback-core-1.0.13.jar
│   └── slf4j-api-1.7.22.jar
├── startup (启动脚本)
├── tmp (war包解压的目录)
└── webapps
    ├── jetty-test-1.0.war
    ├── jetty-test-1.0.xml

jetty 的启动脚本:startup

java -jar jetty-runner.jar \
 --lib lib \
 --out server.out \
 --port 18080 \
 --stop-port 18081 \
 --stop-key stop_jetty \
 --config conf/jetty-channel.xml  \
 --config conf/jetty.xml \
 webapps/jetty-test-1.0.xml

解决方法

方案1:修改maven pom.xml,对 slf4j-api dependency 添加 provided的scope

		<dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.22</version>
            <scope>provided</scope>  <!-- 新增部分 -->
        </dependency>

方案2:修改maven pom.xml,加上 slf4j 的实现依赖

		<dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.22</version>
        </dependency>
        <!-- 以下为新增部分 -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.0.13</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.0.13</version>
        </dependency>

问题探讨

先好好解读一下异常信息:

  1. 存在2个ClassLoader:WebAppClassLoaderURLClassLoader
  2. WebAppClassLoader加载的LoggerFactory,在调用自身getILoggerFactory()方法时,需要解析调用StaticLoggerBinder.getLoggerFactory()方法
  3. StaticLoggerBinderURLClassLoader加载
  4. LoggerFactory.getILoggerFactory()方法,返回类型是ILoggerFactory,由WebAppClassLoader加载
  5. StaticLoggerBinder.getLoggerFactory()方法,返回类型为ILoggerFactory,由URLClassLoader加载
  6. JVM认为这2个ILoggerFactory虽然源于同一个类,但是由不同的ClassLoader加载,所以它们拥有不同的签名,不能被视作同一类型,于是报错

LoggerFactory的源码:

    public static ILoggerFactory getILoggerFactory() {
		...
        switch (INITIALIZATION_STATE) {
        case SUCCESSFUL_INITIALIZATION:
            return StaticLoggerBinder.getSingleton().getLoggerFactory(); // 在这里调用的
		...
    }

jetty源码分析

查看jetty源码,可以发现,jetty启动时候如果带了"–lib xxx"参数,就会使用URLClassLoader来加载对应的jar包

    public void configure(String[] args) throws Exception
    {
        // handle classpath bits first so we can initialize the log mechanism.
        for (int i=0;i<args.length;i++)
        {
            if ("--lib".equals(args[i]))
            {
                try(Resource lib = Resource.newResource(args[++i]))
                {
                    if (!lib.exists() || !lib.isDirectory())
                        usage("No such lib directory "+lib);
                    _classpath.addJars(lib);
                }
            }
         ....
        }

		initClassLoader();
		...
    }


    protected void initClassLoader()
    {
        URL[] paths = _classpath.asArray();

        if (_classLoader==null && paths !=null && paths.length > 0)
        {
            ClassLoader context=Thread.currentThread().getContextClassLoader();

            if (context==null)
                _classLoader=new URLClassLoader(paths);
            else
                _classLoader=new URLClassLoader(paths, context);

            Thread.currentThread().setContextClassLoader(_classLoader);
        }
    }

在例子1中,jetty 启动脚本是带了 "–lib"参数,lib目录含有的jar包:

├── lib 
│   ├── logback-classic-1.0.13.jar
│   ├── logback-core-1.0.13.jar
│   └── slf4j-api-1.7.22.jar

jetty在加载web应用的时候,每个应用都使用一个独立的WebAppClassLoader,它继承了URLClassLoader,应用启动时,会把自己lib目录下的所有jar包和classes目录添加到它的资源列表中(URLClassPath),查看它的 loadClass()方法,可以知道它会优先从自己的资源列表中查找class:

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            ClassNotFoundException ex = null;
            Class<?> parent_class = null;
            Class<?> webapp_class = null;

			// 如果已经加载过,则直接返回
            webapp_class = findLoadedClass(name);
            if (webapp_class != null) {
                if (LOG.isDebugEnabled())
                    LOG.debug("found webapp loaded {}", webapp_class);
                return webapp_class;
            }

			// 看是否优先使用parent来查找
            if (_context.isParentLoaderPriority()) {
           		// 一般都不会走这个分支,因为web应用应该优先使用自己的class和jar
                ... 
            } else {
                // 从这里可以看出,应用会优先使用自己的资源来查找class
                // loadAsResource方法会调用URLClassLoader的findResource来查找class的URL
                webapp_class = loadAsResource(name, true); 

                if (webapp_class != null) {
                    return webapp_class;
                }
				// 往下是使用parent来加载
				...
            }
        }
    }

在例子1中,应用的lib目录下包含了slf4j-api.jar (在pom.xml中指定的依赖)


例子2:查看Jetty的类加载

测试代码:

package test;

public class TestJetty {

    public static void main(String[] ss) throws Exception {
        ClassLoader testLoader = new ClassLoader(Thread.currentThread().getContextClassLoader()) {
            @Override
            protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
                if (name.contains(".slf4j."))
                    System.out.println("==========Jetty loader load class: " + name);
                return super.loadClass(name, resolve);
            }
        };
        Thread.currentThread().setContextClassLoader(testLoader);

        String dir = "/xxx/jetty-test/jetty-app/";
        String[] args = new String[]{
                "--lib", dir + "lib",
                "--port", "8080",
                "--stop-port", "8081",
                "--stop-key", "stop_jetty-test",
                "--config", dir + "conf/jetty-channel.xml",
                "--config", dir + "conf/jetty.xml",
                dir + "webapps/jetty-test-without-log.xml"
        };
        Class<?> runnerClass = testLoader.loadClass("org.eclipse.jetty.runner.Runner");
        runnerClass.getDeclaredMethod("main", String[].class)
                .invoke(null, (Object) args);
    }
}

maven的pom.xml:

    <dependencies>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-runner</artifactId>
            <version>9.4.14.v20181114</version>
        </dependency>
    </dependencies>

应用的war包跟例子1相同。

另外修改一下WebAppClassLoader.loadClass()方法,加入一些打印信息,运行结果如下:

...
WebApp load class: test.TestListener
--> load by self
WebApp load class: org.slf4j.LoggerFactory
--> load by self
WebApp load class: org.slf4j.ILoggerFactory
--> load by self
...
WebApp load class: org.slf4j.impl.StaticLoggerBinder
--> delegate to parent...
==========Jetty load class: org.slf4j.impl.StaticLoggerBinder
==========Jetty load class: org.slf4j.spi.LoggerFactoryBinder
==========Jetty load class: org.slf4j.ILoggerFactory
==========Jetty load class: org.slf4j.Logger
==========Jetty load class: org.slf4j.spi.LocationAwareLogger
java.lang.LinkageError: loader constraint violation: when resolving method "org.slf4j.impl.StaticLoggerBinder.getLoggerFactory()Lorg/slf4j/ILoggerFactory;" the class loader (instance of org/eclipse/jetty/webapp/WebAppClassLoader) of the current class, org/slf4j/LoggerFactory, and the class loader (instance of java/net/URLClassLoader) for the method's defining class, org/slf4j/impl/StaticLoggerBinder, have different Class objects for the type org/slf4j/ILoggerFactory used in the signature
	at org.slf4j.LoggerFactory.getILoggerFactory(LoggerFactory.java:418)
	at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:357)
	at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:383)
	at test.TestListener.<clinit>(TestListener.java:10)
	...

接下来的几个例子,将会探讨 LinkageError 出现的场合

将会用到的公共代码:

AbstractClassLoader.java

package test;

public class AbstractClassLoader extends URLClassLoader {
    private String name;
    private String msg1;
    private String msg2;

    public AbstractClassLoader(String name, String msg1, String msg2, URL[] urls, ClassLoader parent) {
        super(urls, parent);
        this.name = name;
        this.msg1 = msg1;
        this.msg2 = msg2;
    }

    @Override
    public String toString() {
        return name;
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        try {
            Class<?> clazz = findLoadedClass(name);
            if (clazz == null)
                clazz = findClass(name);
            System.out.println(msg1 + ": " + name);
            return clazz;
        } catch (Exception e) {
            System.out.println(msg2 + ": " + name);
            return getParent().loadClass(name);
        }
    }
}

class ParentClassLoader extends AbstractClassLoader {
    public ParentClassLoader(String msg1, String msg2, URL[] urls, ClassLoader parent) {
        super("ParentLoader", msg1, msg2, urls, parent);
    }
}

class ChildClassLoader extends AbstractClassLoader {
    public ChildClassLoader(String msg1, String msg2, URL[] urls, ClassLoader parent) {
        super("ChildLoader", msg1, msg2, urls, parent);
    }
}

Intf.java

package test;

public class Impl implements Intf {
}

Impl.java

package test;

public class Impl implements Intf {
}

Caller.java

package test;

public class Caller {
    public Intf test() {
        return new Impl();
    }
}

例子3:以为错,却不出错

先把类copy到不同的目录
test-all-class目录:

test-all-class
└── test
    ├── Caller.class
    ├── Impl.class
    └── Intf.class

test-class3目录:

test-class3
└── test
    ├── Caller.class
    └── Intf.class

下面是测试代码:

package test;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;

public class LinkageErrorTest {
    public static void main(String[] args) throws Exception {
        String dir = "/xxx/";
        ClassLoader parentLoader = new ParentClassLoader(
                "load by parent",
                "load by system",
                new URL[]{
                        new File(dir + "test-all-class").toURI().toURL()
                }, Thread.currentThread().getContextClassLoader());

        ClassLoader aLoader = new ChildClassLoader(
                "load by self",
                "delegate to parent",
                new URL[]{
                        new File(dir + "test-class3").toURI().toURL(),
                }, parentLoader);

        System.out.println("---- load Caller class -----");
        Class<?> clazz = aLoader.loadClass("test.Caller");
        System.out.println("\n---- new Caller instance -----");
        Object o = clazz.newInstance();
        System.out.println("\n---- get Caller.test() method -----");
        Method method = clazz.getMethod("test");
        System.out.println("\n---- invoke Caller.test() method -----");
        Object retVal = method.invoke(o);

        System.out.println("\n---- self load Intf class -----");
        Class<?> intfClazzFromSelf = aLoader.loadClass("test.Intf");
        System.out.println("check method return type equals self->Intf: " + method.getReturnType().equals(intfClazzFromSelf));

        System.out.println("\n---- parent load Intf class -----");
        Class<?> intfClazzFromParent = parentLoader.loadClass("test.Intf");
        System.out.println("check method return type equals parent->Intf: " + method.getReturnType().equals(intfClazzFromParent));

        try {
            intfClazzFromParent.cast(retVal);
            System.out.println("\ncast return value by parent->Intf is ok.");
        } catch (Exception e) {
            System.out.println("\ncast return value by parent->Intf failed: " + e.getMessage());
        }

        try {
            method.getReturnType().cast(retVal);
            System.out.println("\ncast return value by method return type is ok.");
        } catch (Exception e) {
            System.out.println("\ncast return value by method return type failed: " + e.getMessage());
        }

        System.out.println("\nself->Intf signature: " + intfClazzFromSelf.getClassLoader() + "->" + intfClazzFromSelf.getName());
        System.out.println("parent->Intf signature: " + intfClazzFromParent.getClassLoader() + "->" + intfClazzFromParent.getName());
    }
}

输出的结果:

---- load Caller class -----
delegate to parent: java.lang.Object
load by system: java.lang.Object
load by self: test.Caller

---- new Caller instance -----
load by self: test.Intf

---- get Caller.test() method -----

---- invoke Caller.test() method -----
delegate to parent: test.Impl
load by system: java.lang.Object
load by parent: test.Intf
load by parent: test.Impl

---- self load Intf class -----
load by self: test.Intf
method return type equals self->Intf: true

---- parent load Intf class -----
load by parent: test.Intf
check method return type equals parent->Intf: false

cast return value by parent->Intf is ok.

cast return value by method return type failed: Cannot cast test.Impl to test.Intf

self->Intf signature: ChildLoader->test.Intf
parent->Intf signature: ParentLoader->test.Intf

是不是很神奇?竟然没有出现 LinkageError。

解释说明:

  1. SelfLoader加载了 CallerIntf,当解析调用 Caller.test()方法时,由于没有找到 Impl,所以代理给 ParentLoader来加载
  2. ParentLoader加载 Impl时,也把 Intf加载了
  3. Intf被2个ClassLoader所加载,但是却没有出现 LinkageError
  4. 经过对比,发现 Caller.test()方法的返回类型 Intf 确实是由 SelfLoader加载的
  5. 经过校验,发现test()方法的返回值,类型是ParentLoaderIntf,而不是SelfLoaderIntf
  6. 最后打印了一下两个Intf的签名

那么,问题来了:

  1. 为啥没有出现LinkageError?
  2. 既然签名不同,为啥没有出现 ClassCastException?

例子4:基于例子3的变种

新增一个类 Impl2,注意它跟 Intf 没有半毛钱关系

package test;

public class Impl2 {
}

添加到test-class5目录:

test-class5
└── test
    ├── Caller.class
    ├── Impl2.class
    └── Intf.class

test-all-class目录的内容保持不变:

test-all-class/
└── test
    ├── Caller.class
    ├── Impl.class
    └── Intf.class

接下来,我们修改 Caller的字节码,使得它的 test()方法返回 Impl2的实例:

    public Intf test() {
        return new Impl();
    }
    // 注意:修改后,返回类型不变,但是返回值改变,这是铁定compile不过的,
    // 所以我们需要通过修改字节码的方式来完成
    public Intf test() {
        return new Impl2();
    }

maven的pom.xml

        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>6.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm-util</artifactId>
            <version>6.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm-analysis</artifactId>
            <version>6.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm-commons</artifactId>
            <version>6.2.1</version>
        </dependency>

先使用asm打印一下生成 Caller字节码的代码:

package pratice.asm;

import org.objectweb.asm.util.ASMifier;
import test.Caller;

public class TestASMifier {
    public static void main(String[] args) throws Exception {
        ASMifier.main(new String[]{Caller.class.getName()});
    }
}

输出结果:

package asm.test;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ConstantDynamic;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.TypePath;
public class CallerDump implements Opcodes {

public static byte[] dump () throws Exception {

ClassWriter classWriter = new ClassWriter(0);
FieldVisitor fieldVisitor;
MethodVisitor methodVisitor;
AnnotationVisitor annotationVisitor0;

classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "test/Caller", null, "java/lang/Object", null);

classWriter.visitSource("Caller.java", null);

{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(3, label0);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
methodVisitor.visitInsn(RETURN);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLocalVariable("this", "Ltest/Caller;", null, label0, label1, 0);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test", "()Ltest/Intf;", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(5, label0);
methodVisitor.visitTypeInsn(NEW, "test/Impl");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "test/Impl", "<init>", "()V", false);
methodVisitor.visitInsn(ARETURN);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLocalVariable("this", "Ltest/Caller;", null, label0, label1, 0);
methodVisitor.visitMaxs(2, 1);
methodVisitor.visitEnd();
}
classWriter.visitEnd();

return classWriter.toByteArray();
}
}

其实你压根不用关心这些代码,照搬就可以。
但是这里我们需要修改一下,把 “test/Impl” 修改为 "test/Impl2"即可,修改后的代码如下:

package test;

import org.objectweb.asm.*;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;

public class TestCreateImpl implements Opcodes {
    public static void main(String[] args) throws Exception {
        byte[] bs = dump();
        String path = "/xxx/test-class5/test/Caller.class";
        try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(path))) {
            out.write(bs, 0, bs.length);
        }
    }

    private static byte[] dump() throws Exception {
        ClassWriter classWriter = new ClassWriter(0);
        FieldVisitor fieldVisitor;
        MethodVisitor methodVisitor;
        AnnotationVisitor annotationVisitor0;

        classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "test/Caller", null, "java/lang/Object", null);
        classWriter.visitSource("Caller.java", null);

        {
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(3, label0);
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            methodVisitor.visitInsn(RETURN);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLocalVariable("this", "Ltest/Caller;", null, label0, label1, 0);
            methodVisitor.visitMaxs(1, 1);
            methodVisitor.visitEnd();
        }

        {
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test", "()Ltest/Intf;", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(5, label0);
            methodVisitor.visitTypeInsn(NEW, "test/Impl2");
            methodVisitor.visitInsn(DUP);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "test/Impl2", "<init>", "()V", false);
            methodVisitor.visitInsn(ARETURN);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLocalVariable("this", "Ltest/Caller;", null, label0, label1, 0);
            methodVisitor.visitMaxs(2, 1);
            methodVisitor.visitEnd();
        }
        classWriter.visitEnd();

        return classWriter.toByteArray();
    }
}

运行后,test-class5目录下的 Caller.class 将会被替换。

测试代码跟例子3的一样,只是把 SelfLoader的目录从test-class3改成test-class5。
运行结果:

---- load Caller class -----
delegate to parent: java.lang.Object
load by system: java.lang.Object
load by self: test.Caller

---- new Caller instance -----
load by self: test.Intf

---- get Caller.test() method -----

---- invoke Caller.test() method -----
load by self: test.Impl2

---- self load Intf class -----
load by self: test.Intf
check method return type equals self->Intf: true

---- parent load Intf class -----
load by system: java.lang.Object
load by parent: test.Intf
check method return type equals parent->Intf: false

cast return value by parent->Intf failed: Cannot cast test.Impl2 to test.Intf

cast return value by method return type failed: Cannot cast test.Impl2 to test.Intf

self->Intf signature: ChildLoader->test.Intf
parent->Intf signature: ParentLoader->test.Intf

输出跟例子3差不多,只是因为我们替换了test()方法的返回值,所以使得2个类型转换都失败,但是代码本身没有报错,这也就说明了,在用反射调用方法时,返回值类型都被认为是Object,所以没有再去校验是否跟方法的返回类型相匹配。


例子5:模拟LinkageError

在例子3的公共类基础上,加入:
Delegate.java

package test;

public class Delegate {
    public Intf get() {
        return new Impl();
    }

    public static Intf getImpl() {
        return new Impl();
    }
}

test-all-class目录的内容:

test-all-class/
└── test
    ├── Caller.class
    ├── Delegate.class
    ├── Impl.class
    └── Intf.class

test-class6目录的内容:

test-class6
└── test
    ├── Intf.class
    ├── TargetCaller1.class
    ├── TargetCaller2.class  
    ├── TargetCaller3.class  
    ├── TargetCaller4.class  
    └── TargetCaller5.class

测试代码:

package test;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;

public class TargetTest {
    public static void main(String[] args) throws Exception {
        String dir = "/xxx/";
        ClassLoader parentLoader = new ParentClassLoader(
                "load by parent",
                "load by system",
                new URL[]{
                        new File(dir + "test-all-class").toURI().toURL()
                }, Thread.currentThread().getContextClassLoader());

        ClassLoader aLoader = new ChildClassLoader(
                "load by self",
                "delegate to parent",
                new URL[]{
                        new File(dir + "test-class6").toURI().toURL(),
                }, parentLoader);
        Class<?> clazz = aLoader.loadClass("test.TargetCaller1");
        Object o = clazz.newInstance();
        Method method = clazz.getMethod("test");
        System.out.println("---------");
        Object retVal = method.invoke(o);
        System.out.println(retVal);
    }
}

TargetCaller1.java

package test;

public class TargetCaller1 {
    private static Intf v = Delegate.getImpl();

    public Intf test() {
        return v;
    }
}

输出结果:

delegate to parent: java.lang.Object
load by system: java.lang.Object
load by self: test.TargetCaller1
delegate to parent: test.Delegate
load by system: java.lang.Object
load by parent: test.Delegate
load by parent: test.Intf
load by parent: test.Impl
Exception in thread "main" java.lang.LinkageError: loader constraint violation: loader (instance of test/ChildClassLoader) previously initiated loading for a different type with name "test/Intf"
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at test.AbstractClassLoader.loadClass(AbstractClassLoader.java:28)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java.lang.Class.getDeclaredMethods0(Native Method)
	at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
	at java.lang.Class.privateGetMethodRecursive(Class.java:3048)
	at java.lang.Class.getMethod0(Class.java:3018)
	at java.lang.Class.getMethod(Class.java:1784)
	at test.TargetTest.main(TargetTest.java:25)

解释说明:

  1. 调用了 TargetCaller1的newInstance()方法,使得 cinit 的代码,也就是static的代码被执行,然后由ParentLoader加载了接口Intf(如果不调用newInstance()方法,是不会报错的)
  2. 当反射获取 TargetCaller1的方法时,ChildLoader尝试去加载Intf,但是JVM发现Intf之前已经被另外的ClassLoader所加载,即存在了不同的签名。

换成TargetCaller2进行测试
TargetCaller2.java

package test;

public class TargetCaller2 {

    public Intf test() {
        return Delegate.getImpl();
    }
}

输出结果:

delegate to parent: java.lang.Object
load by system: java.lang.Object
load by self: test.TargetCaller2
load by self: test.Intf
---------
delegate to parent: test.Delegate
load by system: java.lang.Object
load by parent: test.Delegate
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at test.TargetTest.main(TargetTest.java:26)
Caused by: java.lang.LinkageError: loader constraint violation: loader (instance of test/ParentClassLoader) previously initiated loading for a different type with name "test/Intf"
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at test.AbstractClassLoader.loadClass(AbstractClassLoader.java:28)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at test.TargetCaller2.test(TargetCaller2.java:6)
	... 5 more

解释说明:

  1. 这次出错的地方在 26行,也就是 method.invoke(o),原因是方法运行的时候,才实际去进行class的加载
  2. 上一次是ChildLoader出现LinkageError,这次轮到ParentLoader报错

换成TargetCaller3进行测试
TargetCaller3.java

package test;

public class TargetCaller3 {

    public Intf test() {
        return new Delegate().get();
    }
}

输出结果:

delegate to parent: java.lang.Object
load by system: java.lang.Object
load by self: test.TargetCaller3
load by self: test.Intf
---------
delegate to parent: test.Delegate
load by system: java.lang.Object
load by parent: test.Delegate
load by parent: test.Intf
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at test.TargetTest.main(TargetTest.java:26)
Caused by: java.lang.LinkageError: loader constraint violation: when resolving method "test.Delegate.getImpl()Ltest/Intf;" the class loader (instance of test/ChildClassLoader) of the current class, test/TargetCaller3, and the class loader (instance of test/ParentClassLoader) for the method's defining class, test/Delegate, have different Class objects for the type test/Intf used in the signature
	at test.TargetCaller3.test(TargetCaller3.java:6)
	... 5 more

原因可参考例子1和2

换成TargetCaller4进行测试
TargetCaller4.java

package test;

public class TargetCaller4 {

    public Intf test() {
        Intf v = new Impl();
        System.out.println("v classLoader: " + v.getClass().getClassLoader());
        return (Intf) v;
    }
}

输出结果:

delegate to parent: java.lang.Object
load by system: java.lang.Object
load by self: test.TargetCaller4
load by self: test.Intf
---------
delegate to parent: test.Impl
load by system: java.lang.Object
load by parent: test.Intf
load by parent: test.Impl
...
v classLoader: ParentLoader
test.Impl@5674cd4d

变量v的ClassLoader是ParentLoader,它的定义类型没法知道是哪个ClassLoader加载的,但返回时的类型很明显是由ChildLoader来加载的,转换却没出错。

稍微修改一下,换成TargetCaller5测试
TargetCaller5.java:

package test;

public class TargetCaller5 {

    public Intf test() {
        Object v = new Impl();
        System.out.println("v classLoader: " + v.getClass().getClassLoader());
        return (Intf) v;
    }
}

输出结果:

delegate to parent: java.lang.Object
load by system: java.lang.Object
load by self: test.TargetCaller5
load by self: test.Intf
---------
delegate to parent: test.Impl
load by system: java.lang.Object
load by parent: test.Intf
load by parent: test.Impl
...
v classLoader: ParentLoader
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at test.TargetTest.main(TargetTest.java:26)
Caused by: java.lang.ClassCastException: test.Impl cannot be cast to test.Intf
	at test.TargetCaller5.test(TargetCaller5.java:8)
	... 5 more

这次把变量v的类型定义为Object后,返回时的转换就出错了,因为转换是在两个不同的ClassLoader加载的Intf之间发生的。
结合TargetCaller4的结果来看,就会发现很奇怪,TargetCaller4貌似直接把返回时的类型转换给跳过了。


结论

一旦出现LinkageError:

  1. 系统存在多个ClassLoader,某些类都被它们加载过
  2. 它们之间存在代理关系
  3. 它们在执行方法调用的时候,出现了交集

解决方法:

  1. 根据错误信息,找出冲突的类所在的URL(jar包,目录,远程资源)
  2. 看哪些ClassLoader的资源列表引入了这些URL
  3. 尽量简化为只在其中一个ClassLoader进行加载,即优先通过代理加载
  4. 或者让各个ClassLoader都拥有完整的资源列表,即不进行代理加载
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值