在大多数状况下,系统默认提供的类加载器实现已经能够知足需求。可是在某些状况下,您仍是须要为应用开发出本身的类加载器。好比您的应用经过网络来传输 Java 类的字节代码,为了保证安全性,这些字节代码通过了加密处理。这个时候您就须要本身的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在 Java 虚拟机中运行的类来。下面将经过两个具体的实例来讲明类加载器的开发。java
要建立自定义的类加载器只须要扩展java.lang.ClassLoader类就能够,而后覆盖它的findClass(String name)方法便可。该方法根据参数指定的类的名字,去找对应的class文件。而后返回class对应的对象。下面咱们就根据咱们自定义的类加载器的源码来具体详解一下这个自定义的步骤:数组
自定义的类加载器:安全
public class MyClassLoader extends ClassLoader { private String name; // 类加载器的名字 private String path = "d:\\"; // 加载类的路径 private final String fileType = ".class"; // class文件的扩展名 public MyClassLoader(String name) { super(); // 让系统类加载器成为该类加载器的父加载器 this.name = name; } public MyClassLoader(ClassLoader parent, String name) { super(parent); // 显式指定该类加载器的父加载器 this.name = name; } @Override public String toString() { return this.name; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } /** * @param 类文件的名字 * @return 类文件中类的class对象 * * 在这里咱们并不须要去显示的调用这里的findclass方法,在上篇文章中,咱们经过查看 * loadclass的源码能够发现,她是在loadclass中被调用的,因此这里咱们只需重写这个方法, * 让它根据咱们的想法去查找类文件就ok,他会自动被调用 * * * defineClass()将一个 byte 数组转换为 Class 类的实例。必须分析 Class,而后才能使用它 * 参数: * name - 所须要的类的二进制名称,若是不知道此名称,则该参数为 null * b - 组成类数据的字节。off 与 off+len-1 之间的字节应该具备《Java Virtual Machine Specification 》定义的有效类文件的格式。 * off - 类数据的 b 中的起始偏移量 * len - 类数据的长度 */ @Override public Class<?> findClass(String name) throws ClassNotFoundException { byte[] data = this.loadClassData(name);//得到类文件的字节数组 return this.defineClass(name, data, 0, data.length);// } /** * * @param 类文件的名字 * @return 类文件的 字节数组 * 经过类文件的名字得到类文件的字节数组,其实主要就是用 * 输入输出流实现。 */ private byte[] loadClassData(String name) { InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = null; try { this.name = this.name.replace(".", "\\"); is = new FileInputStream(new File(path + name + fileType)); baos = new ByteArrayOutputStream(); int ch = 0; while (-1 != (ch = is.read())) { baos.write(ch); } data = baos.toByteArray(); } catch (Exception ex) { ex.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (Exception ex) { ex.printStackTrace(); } } return data; }
我想上面的注释中已经足够让你们明白这个自定义类加载器的原理了。在这我在重复的从上到下的再说一遍,加深一下你们的理解。首先在构造方法中,咱们能够经过构造方法给类加载器起一个名字,也能够显示的指定他的父累加器器,若是没有显示的指出父类加载器的话他默认的就是系统类加载器。因为咱们继承了ClassLoader类,因此它自动继承了父类的loadclass方法。咱们之前看过loadclass的源码知道,它调用了findclass方法去查找类文件。因此在这里咱们重写了ClassLoader的findclass方法。在这个方法中首先调用loadClassData方法,经过类文件的名字得到类文件的字节数组,其实主要就是用输入输出流实现。而后调用defineClass()将一个 字节 数组转换为 Class 类的实例。有时候咱们手动生成的二进制码的class文件被加密了。因此在咱们在利用咱们自定义的类加载器的时候还要写一个解密的方法进行解密,这里咱们就不实现了。服务器
咱们实现了自定义类加载器,下一步咱们来看一下咱们怎么来应用咱们这个自定义的类加载器:网络
public static void main(String[] args) throws Exception { //建立一个loader1类加载器,设置他的加载路径为d:\\serverlib\\,设置默认父加载器为系统类加载器 MyClassLoader loader1 = new MyClassLoader("loader1"); loader1.setPath("d:\\myapp\\serverlib\\"); //建立一个loader2类加载器,设置他的加载路径为d:\\clientlib\\,并设置父加载器为loader1 MyClassLoader loader2 = new MyClassLoader(loader1, "loader2"); loader2.setPath("d:\\myapp\\clientlib\\"); //建立一个loader3类加载器,设置他的加载路径为d:\\otherlib\\,并设置父加载器为根类加载器 MyClassLoader loader3 = new MyClassLoader(null, "loader3"); loader3.setPath("d:\\myapp\\otherlib\\"); test(loader2); System.out.println("----------"); test(loader3); } public static void test(ClassLoader loader) throws Exception { Class clazz = loader.loadClass("com.bzu.csh.test.Sample"); Object object = clazz.newInstance(); }
当执行这段代码的时候。首先让loader2去加载Sample类文件,固然咱们在执行这段代码的前提时在各个默认加载器中已经有咱们Sample的class文件。Loader2首先让父加载器是loader1去加载,而后loader1会让系统类加载器去加载,系统类加载器会让扩展类加载器加载,扩展类加载器会让根类加载器加载,因为系统类加载器,扩展类加载器,根类加载器的默认路径中都没有咱们要的sample类,因此loader2的默认路径有sample这个类,也就是说loader2会去加载这个sample类。当执行test(loader3)的时候,因为loader3的默认父加载器是根类加载器,而且根类加载前默认路径没有对应的sample.class文件,因此,直接的loader3类加载器就去加载这个类。app
最后要说明的一点是,自定义类加载不光只能从咱们本地加载到class文件,咱们也能够加载网络,即基本的场景是:Java字节代码(.class)文件存放在服务器上,客户端经过网络的方式获取字节代码并执行。当有版本更新的时候,只须要替换掉服务器上保存的文件便可。经过类加载器能够比较简单的实现这种需求。其实他的实现和本地差很少,基本上就是geclassdata方法改变了一些。下面咱们来具体看一下:ide
private byte[] getClassData(String className) { String path = classNameToPath(className); try { URL url = new URL(path); InputStream ins = url.openStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } return null; }
在经过网络加载了某个版本的类以后,通常有两种作法来使用它。第一种作法是使用 Java 反射 API。另一种作法是使用接口。须要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,由于咱们写的类加载器被加载所用的类加载器和咱们加载的网络类不是同一个类加载器,因此客户端代码的类加载器找不到这些类。使用 Java 反射 API 能够直接调用 Java 类的方法。而使用接口的作法则是把接口的类放在客户端中,从服务器上加载实现此接口的不一样版本的类。在客户端经过相同的接口来使用这些实现类。this
不一样类加载器的命名空间关系:加密
1.同一个命名空间内的类是相互可见的。url
2.子加载器的命名空间包含全部父加载器的命名空间。所以子加载器加载的类能看见父加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。
3.由父加载器加载的类不能看见子加载器加载的类。
4.若是两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。
5.当两个不一样命名空间内的类相互不可见时,能够采用Java的反射机制来访问实例的属性和方法。