写在前面:希望大家可以踊跃发言,大家阅读后有什么感受或者心得体会以及建议都可以在下方留言板小程序内留言的,根据你的留言,郭老师会及时调整讲解内容做一些优化和改进,得不到大家的反馈,不知道讲解是否有问题,是否符合大家的口味以及能否帮助到大家
首先要明白一个问题:为什么要写自己的Class Loader?不管那么多不是一样好好在用吗?一般情况确实是这样,不过有些时候为了一些特殊需求,我们会用到自己定制的Class Loader。比如1998年,Sun内部为完成JDK1.2忙得热火朝天,我也在里面打酱油,我们有一个小组提供一个工具,给Java生成的字节码加密,原因是字节码太规整,用一些工具很容易反编译,反编译之后的结果很容易供人看懂(比许多程序员手工编的程序还容易懂),导致知识产权保护不力。于是就想着把.class文件加密,但是一个加密之后的.class文件肯定又不能被正确加载。怎么办呢?就要自己做一个Class Loader,拿到.class文件,先进行一步解密,解密之后就成了正常的字节码,就可以用普通的application class loader的方式去继续加载了。
还有一些别的场景,也需要我们自定义Class Loader,如动态生成的class文件,如多版本class运行。
我们先来看,从哪里下手做这个工作。
从大的过程,对象的创建分成两大步骤,一个是类级别的工作,一个是对象的实例化。显然,我们要在类级别工作着一个步骤动脑筋。而在类级别的工作中,分成加载Loading,链接Linking,初始化Initialization三步。根据上面讲解的一些知识,我们应该在Loading过程中搞一点名堂。
回到ClassLoader的定义,我们来看提供了哪些方法。
public Class<?> loadClass(String name, boolean resolve) throwsClassNotFoundException
这个loadClass()方法根据类的全名加载类。既然是这样,那就应该从这里下手了。我们看看源码:
protectedClass<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized(getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name,false);
} else {
c =findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown ifclass not found
// from the non-null parent classloader
}
if (c == null) {
// If still not found, then invokefindClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
我们可以看到这个过程,先看这个类是不是已经加载了,如果没有就让上层的class loader去加载,最后由自己加载findClass(name)。
事情就追到findClass(name)方法了。看它的定义:
protected Class<?> findClass(String name) throwsClassNotFoundException
在ClassLoader里面,它只是一个空的方法,没有做实现。那么我们就可以实现它来进行我们自己的工作了。
好,我们在findClass()里面做什么呢?自然先要从外部如文件系统或者网络获取.class字节流,然后呢?我们自己把它弄成类模型吗?理论上是,但是实际上我们不需要。接着找ClassLoader为我们提供了什么,我们可以看到有一个defineClass()方法,定义如下:
protected final Class<?>defineClass( String name, byte[] b, intoff, int len) throws ClassFormatError
这个方法就是用于将字节流转成类的。
并且这个方法是final的,我们用它并且只能用它。
有了这个方法,我们自己的工作就简单了,只需要对字节流进行处理后交给defineClass()就好了。
我们可以动手编程序了。
先做一个类,MyClass.java,代码如下:
package com.demo;
public class MyClass {
public MyClass() {
}
public void show() {
System.out.println("show test!");
}
}
很简单,不解释了。
接着我们写自己的classloader,为了简单起见,我们的这个class loader不进行任何加密处理,只是简单地读取.class文件,生成类,模仿标准的AppClassLoader的行为。MyClassLoader.java代码如下:
package com.demo;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class MyClassloader extends ClassLoader {
@Override
protectedClass<?> findClass(String name) throws ClassNotFoundException {
StringclassFileName = name.replace(".","/") + ".class";
byte[] cLassBytes =null;
Path path = null;
try {
path =Paths.get(getResource(classFileName).toURI());
cLassBytes =Files.readAllBytes(path);
} catch (IOException |URISyntaxException e) {
e.printStackTrace();
}
Class<?> clazz = defineClass(name,cLassBytes, 0, cLassBytes.length);
return clazz;
}
}
正如上面根据原理分析的,我们自己写的class loader override了findClass(),这里面我们做了几步:
一是根据class名字拼出文件名,StringclassFileName = name.replace(".","/") + ".class";;
二是根据名字定位到文件路径,path = Paths.get(getResource(classFileName).toURI());;
三是把文件读到字节数组中,cLassBytes= Files.readAllBytes(path);;
四是调用defineClass()生成类,Class<?>clazz = defineClass(name, cLassBytes, 0, cLassBytes.length);。
有了这个自定义的classloader之后,我们写一个测试类,MyClassTest.java,代码如下:
package com.demo;
import java.lang.reflect.Method;
public class MyClassTest {
public static voidmain(String[] args) throws ClassNotFoundException {
MyClassloader loader =new MyClassloader();
Class<?> aClass= loader.findClass("com.demo.MyClass");
try {
Object obj =aClass.newInstance();
Method method =aClass.getMethod("show");
method.invoke(obj);
} catch (Exception e){
e.printStackTrace();
}
}
}
在程序中,我们新建一个自定义的类加载器MyClassloader loader = new MyClassloader();
然后调用findClass()去加载类Class<?>aClass = loader.findClass("com.demo.MyClass");
之后根据类定义创建新的对象实例并调用方法:
Object obj =aClass.newInstance();
Method method =aClass.getMethod("show");
method.invoke(obj);
试着运行一下,程序正确出了结果。
到了这一步,恭喜你!你已经知道如何做Class Loader了,We Made It!
再回到加密的问题,这一讲不讲加密本身,但是我们要根据上面的代码知道如何处理加密。看我们自己的findClass()的实现,里面有两句话:
cLassBytes = Files.readAllBytes(path);
Class<?> clazz = defineClass(name, cLassBytes, 0, cLassBytes.length);
在defineClass之前,我们要准备出一个正常的字节数组,因此对于一个加密的.class文件,我们只需要在之前处理进行解密即可:
cLassBytes = decrypt(Files.readAllBytes(path));
Class<?> clazz = defineClass(name, cLassBytes, 0,cLassBytes.length);
假定有一个decrypt()方法,我们就可以了做到了。
以前在Sun公司的时候,一个同事曾经说过class loader就像是一个汤勺,用它来抓汤圆吃。这个比喻真有点像。不过这个比喻不够,因为class loader不光是加载类,还规定了命名空间,不同的class loader加载的类是不能互相访问的(正因为这样才会有同一个类的多版本支持的技术)。我们可以把class loader圈起来的这片空间理解为class园地。因此,class loader更加像是一个碗,把汤圆盛到里头了。
这个话题超越了进阶阶段,先按下不表。不过到了这里,我们应该能感受到Class Loader的强大了,从心底里对Java的发明人一次次投以敬佩的目光,他们二十几年前竟然有如此深刻的洞察。对我们Java程序员来讲,James Gosling就是我们的上帝。在未来的讲座里,我还会跟大家一次次展示出造物者的匠心独运。
学习的过程,是一步步深入的。很多人工作多年,并没有深入,有些时候也不是因为不努力,而是因为他们在的编程任务总是在无休止地对付客户需求的变更,没有坐下来好好整理。而从应用的层面,学习技术就成了追逐新的热点,不断地换最新推出的环境框架工具。而写应用程序,大体上用不到深入的内容,非专业的人突击学几个月之后也能一起做编程了,有的时候比本专业的人做得还要好。那么学习本专业的价值在哪里?这是许多年轻人曾经的疑惑。或许与国家的发展阶段有关,前些年都是拿别人现成的平台框架工具,或开源或盗版,自己针对客户需要快速搭建应用系统,赚快钱,现在慢慢认识到基础的重要性了,真正愿意深入理解技术的人和公司也多起来了。这对于希望掌握和使用更深技术的程序员是一件好事情。
从事技术工作是一个良心活儿,学习技术则是一个慢工夫,急不来,就得要老老实实一个课题一个课题解决掉。只要方向对,扎扎实实,总能有成。
先贤语录:不积跬步,无以至千里。