阅读一下文章,你能学习到一种新的,更加低耦合的开发方式。ClassLoader的使用方式进阶版URLClassLoader。在正式开始之前,先简单回顾一下反射和双亲委派机制
反射
在讲反射之前,我们先回顾一下Class的信息,当然这是我个人知识体系上面的回顾,可能存在误区,希望大伙指明错误,谢谢
在我们编写完程序保存后,生成了.java文件,通过javac指令把.java文件通过JIT编译器编译成.class文件。在通过java指令运行.class。jvm虚拟机就会把class的信息保存在方法区当中。这是全有对象的认祖归宗的最重要的信息,在我们new创建对象的执行过程当中,一个从无到有的过程当中,对象首先存在数据就是对象头当中的数据。
java的对象头由以下三部分组成:
- Mark Word
- 指向类的指针
- 数组长度(只有数组对象才有);
知道了这些基础信息,让我们来看看获取Class信息的3种方式,你就知道能大致明白对应的方式
public class ReflectTest {
public static void main(String[] args) throws Exception {
//不会真正加载到jvm当中
Class<CCN> ccnClass = CCN.class;
//会真正加载到jvm当中
Class<?> aClass = Class.forName("com.ccn.reflect.CCN");
//会真正加载到jvm当中
CCN ccn = new CCN();
Class<? extends CCN> aClass1 = ccn.getClass();
}
}
我们能想象出这三种方法分别获取的方式 :Xxx.class。直接通过文件获取class信息,obj.getClass()通过对象获取class信息。重点介绍Class.forName(“全限定类名”)。
Class.forName(“全限定类名”)。
再点进去
我们发现是会用当前执行代码的全限定类名去加载我们的class.forName()对象。为什么呢?其实不然我们知道,main方法是我们程序的入口函数。它是必然保证已经加载到jvm当中,也必然保证拥有的classLoader对象。用它来帮助我们的将类加载到jvm当中。当然不仅main方法,只要调用者,也必然保证他们已经初始化完成,同样也有如上的效果。
如果classLoader找不到该类就会报ClassNotFoundException错误
双亲委派机制
java自带使用三种加载器把.class文件加载到jvm当用运动。如下图。当然开发者们也能自行扩展。
- Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
- ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
- AppClassLoader:主要负责加载应用程序的主函数类
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 首先,检查是否已经被类加载器加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 存在父加载器,交由父加载器(递归调用)
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 直到最上面的Bootstrap类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
执行过程如下图(借用于作者:IT烂笔头)
我们言归正传理解Class.forName();
通过dubug我们惊奇的发现。它是会跑到ClassLoader.loadClass当中的。精彩的事情马上开始,开始前先梳理一下执行。
Class.forName() -> native代码(C++代码) -> ClassLoader.loadClass();
分析ClassLoader.loadClass(“com.ccn.leetcode.CCN”)
当第一次执行ClassLoader.loadClass(“com.ccn.leetcode.CCN”),发现存在parent类加载器(Ext 扩展类加载器),基于双亲委派机制规则,递归调用,尝试使用parent类加载器加载“com.ccn.leetcode.CCN”;
(递归调用)当第二次执行ClassLoader.loadClass(“com.ccn.leetcode.CCN”)。发现parent类加载器为空,尝试使用启动类加载器加载(由此我们得知,扩展类加载器是没有父类加载器的)
接下来可能有点绕,大伙赶紧上车,做好准备。
c = findBootstrapClassOrNull(name);
发现 c == null成立 尚未加载成功。
第一次调用findClass(尝试使用当前类加载器加载对象(当前的类加载器是什么???【1】))然后findClass就会调用URLClassLoader.findClass()尝试加载对象(我们的主角终于出现了!!!)
【1】:Ext 扩展类加载器
关于源码中AccessController.doPrivileged。查看
Ext 扩展类加载器尝试加载但是失败了。所以抛出ClassNotFoundException异常,但是我们要知道,这是第二次调用loadClass的第一次findClass操作。在我们使用扩展类加载器之前,我们是首先也有一个类加载器这个类加载器是什么???【2】
【2】 : APP 应用程序类加载器
当我第一次调用ClassLoad.loadClass。使用的是Applicaton ClassLoader的时候。这个方法栈帧捕获到了来自非空 parent加载器的ClassNotFoundException异常。所以尝试自己执行findCLass。
执行成功!!!
到此整一个加载顺叙,我们就梳理完成了,是不是有种头皮拔凉拔凉的感觉。不要紧张。剃光就好了。接下来让我们讲解这篇文章的主题URLClassLoader
URLClassLoader
开始之前。我们先讲一下什么是全限定类名,什么是ClassPath路径。这个问题之前一直在我脑海中傻傻分不清,原因是我们把他们分开记了,通过下面分析,我希望大家把两者合在一起记住。
在我们使用Class.forName("")时候,一定要使用权限定类名,那什么是全限定类名。从内容上理解,他们就package名 + 类名
com.ccn.reflect + .CCN 就是这个当前类的全限定类名。
那什么是ClassPath路径呢?以前的知识什么说src下路径,打包成target后,又不知道去哪里了,jar包,war包一样。傻傻的分不清。
ClassPath路径
URLClassLoader.findClass通过path,调用URLClassPath找到了我们的CCN.class所在的地方,什么我们在使用电脑找文件,点击文件一步步找下去的想法, URLClassPath + path = CCN.class文件所在的位置。到这里我们应该可以理解 ClassPath 路径了吧。不管打包成什么样子,jvm加载.class的方式(除非你破坏它)是不会改变的,是通过地址找到.class文件io操作把他加载。 path是已知道,全限定类名。那么我们在我们的项目下如果找到文件,也就是jvm如何找到文件。ClassPath就是我们找到文件的路径 *减去 全限定类名转换成文件路径
终于终于开始了
我觉得全限定类名不好,也耦合了,我不希望这样做那应该怎么做?
package com.ccn.leetcode;
public class Person {
public void say() {
System.out.println("Person");
}
}
package com.ccn.leetcode;
public class CCN extends Person {
@Override
public void say() {
System.out.println("CCN");
}
}
public static void main(String[] args) throws Exception {
URL[] urls = new URL[1];
URLStreamHandler streamHandler = null;
File classPath = new File("E:\\IdeaProjects\\SpringMVC\\offer\\src\\com\\ccn\\leetcode");
String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString() ;
System.out.println(repository);
urls[0] = new URL(null, repository, streamHandler);
ClassLoader loader = new URLClassLoader(urls);
Class<?> aClass = loader.loadClass("CCN");
Person o =(Person) aClass.newInstance();
o.say();
}
URLClassLoder类是ClassLoader类的一个直接子类。他有很多个构造函数,在此我们使用最简单的 URLClassLoader(URL[ ] urls);
urls是一个java.net.URL对象数组,**当裁人一个类时每个URL对象指明的类加载器到哪里查找类(本地/网络)**若一个URL以“/”结尾,则表明它指向一个目标(上述代码就是利用这点 +File.separator)否则,URL默认指向一个JAR文件。
由于之前路径代码path
找得是.class文件。所以我们需要自己手动编译一下。(当然我当初后者驱动前者,尝试了很久)
回到运行主函数。如无意外出错了
NoClassDefFoundError是在运行时JVM加载不到类或者找不到类。它跟ClassNotfoundException很像。但ClassNotfoundException表示在编译时JVM加载不到类或者找不到类导致的; Class.forName()全限定类名写错导致的错误。
经过我不懈的尝试(其实是看深入剖析Tomcat的代码)找出问题
因为我们把包名去掉了,idea出错先他他注销,在运行代码。。
重新编译!!见证奇迹的时候到了
到此我们整个学习过程就完全结束了!这功能到底有什么用呢?
总结:首先我们看到了面向接口编程的重要性,这种降低耦合度的方式,是我目前学到最强的了(我觉得有点像SPI),我们完全就可以通过IO编程,对编写更改文本的方式来改变执行的代码。学到这里其实我们知道URLClassLoader 其实不是ClassLoader进阶版,然后还是他的子类,我们平时使用的时候也是在用URLClassLoader原本封装好的方法,所以才可以直接用全限定内名来加载代码。
为什么不能变为原来的子类???
我们把包路径删掉了,它对应的就是唯一一个没有包路径的CCN。那么它对应就是直接放在SRC下的CCN。
我想应该没有人这样编写程序吧,所以最好面向接口编程。突然想到,因为我在load.class用得是"CCN"”所以有跟有package的CCN就不是同一个玩意,所以之前才报NoClassDefFoundError的错误。因为在项目当中包名+类名 == 唯一ID。我没有尝试,但是我认为如果load.class传进去全限定类名的话(又回到原点),代码是不需要改直接编译后就可以运行,也可以直接向下转换成功。我没尝试交给大伙试一下了。所以还是面向接口编程最好。更低耦合