本文将通过多个例子来说明 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>
问题探讨
先好好解读一下异常信息:
- 存在2个ClassLoader:WebAppClassLoader 和 URLClassLoader
- 由WebAppClassLoader加载的LoggerFactory,在调用自身getILoggerFactory()方法时,需要解析调用StaticLoggerBinder.getLoggerFactory()方法
- StaticLoggerBinder由URLClassLoader加载
- LoggerFactory.getILoggerFactory()方法,返回类型是ILoggerFactory,由WebAppClassLoader加载
- StaticLoggerBinder.getLoggerFactory()方法,返回类型为ILoggerFactory,由URLClassLoader加载
- 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。
解释说明:
- SelfLoader加载了 Caller,Intf,当解析调用 Caller.test()方法时,由于没有找到 Impl,所以代理给 ParentLoader来加载
- ParentLoader加载 Impl时,也把 Intf加载了
- Intf被2个ClassLoader所加载,但是却没有出现 LinkageError
- 经过对比,发现 Caller.test()方法的返回类型 Intf 确实是由 SelfLoader加载的
- 经过校验,发现test()方法的返回值,类型是ParentLoader的Intf,而不是SelfLoader的Intf
- 最后打印了一下两个Intf的签名
那么,问题来了:
- 为啥没有出现LinkageError?
- 既然签名不同,为啥没有出现 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)
解释说明:
- 调用了 TargetCaller1的newInstance()方法,使得 cinit 的代码,也就是static的代码被执行,然后由ParentLoader加载了接口Intf(如果不调用newInstance()方法,是不会报错的)
- 当反射获取 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
解释说明:
- 这次出错的地方在 26行,也就是 method.invoke(o),原因是方法运行的时候,才实际去进行class的加载
- 上一次是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:
- 系统存在多个ClassLoader,某些类都被它们加载过
- 它们之间存在代理关系
- 它们在执行方法调用的时候,出现了交集
解决方法:
- 根据错误信息,找出冲突的类所在的URL(jar包,目录,远程资源)
- 看哪些ClassLoader的资源列表引入了这些URL
- 尽量简化为只在其中一个ClassLoader进行加载,即优先通过代理加载
- 或者让各个ClassLoader都拥有完整的资源列表,即不进行代理加载