描述
最近在公司开发一个新的需求:同事写好了一个工具类并且通过@Component的方式交给Spring容器进行管理,然后我在代码中按名字从Spring容器中获取这个对象。在获取的时候出现了以下的错误:
com.utils.A cannot not be cast to com.utils.A
代码如下:
A a = (A)applicationContext.getBean("a");
证实问题
百度搜索之后发现全限定名相同的类进行转换出现错误可能是类加载器不相同而导致的。
类型转换时,首先使用checkcast指令检测类型是否一致。检测内容为一下两点:
1. 全限定名是否一致
2. 类加载器是否一致
接下来我使用Class.getClassLoader()验证是否类加载器不一致。
需要提醒的是:放在Spring中对象此时不能再通过按类型取出,因为本身类型已经不一致了,此时可以通过按名字获取。
getClassLoader() == null时,是指类加载器为BootStrap ClassLoader
验证之后证实:从Spring容器中获取的对象是通过WebappClassLoader加载的,而运行的代码路径上的类是通过java.net.URLClassLoader加载的。
解决问题
发现了问题之后,接下来就是分析问题是怎么产生的。
首先需要明白的是为什么这个类会加载两份?是谁来调用类加载器加载了两份?
1. 当发现一个类还没有进行加载时,此时会调用当前类的类加载器区加载这个不存在的类;
2. 判断类是否加载到内存中来的方式是查看当前类的类加载器可见的范围是否存在该类。
由以上的几点信念我们可以推断出为什么类会加载两份:首先在初始化Spring时,使用了WebappClassLoader来加载了一次。其次当Http 请求到达微服务时,因为加载我的jar包的类加载器是java.net.URLClassLoader,但是这个类加载器看不到WebappClassLoader加载的类,所以此时会重新加载一份类,这就说明了为什么类会加载两份。同时也说明了都是谁加载了这个类。
类似于Tomcat的双亲委派模型,我们的项目也是使用类加载器来对不同的类路径进行隔离。
我的项目是这样的一个结构:项目里有两个lib目录,平台级别的jar包放在外层lib目录,业务级的jar包放在里层lib目录。里面的jar包里面的类可以引用外面的jar包里的类,但是外面的jar包里的类不能引用里面jar包里的类。
外层的lib目录的代码使用Java.net.URLClassLoader加载,里层的lib目录下的jar文件使用WebappClassLoader加载。但是WebClassLoader加载类时,首先使用java.net.URLClassLoader加载,如果没有的话,再在自己管理的里层lib目录下的寻找相应的类。