自定义类的加载器
咱们书接上回继续说说自定义类类加载器
自定义类加载器有什么用?
- 通过类加载器可以实现非常精妙的插件机制。例如:著名的OSGI组件框架,再如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现。
- 同时,自定义类加载器能够实现应用隔离。例如 Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多,想不修改C/C++程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。
同时注意所有用户自定义类的加载器通常需要继承于抽象类java.lang.ClassLoader
说明:
每个Class对象都会包含一个定义它的ClassLoader的引用
获取ClassLoader的途径:
- 获取当前类的 ClassLoader clazz.getClassloader()
- 获取当前线程的上下文的 ClassLoader Thread.curentThread().getContextClassLoader()
- 获得系统的ClassLoader ClassLoader.getSystemClassLoader()
说明:
站在程序的角度看,引导类加载器与另外两种类加载器(系统类加载器和扩展类加载器)并不是同一个层次意义上的加载器,引导类加载器是使用C++语言编写而成的,而另外两种类加载器则是使用Java语言编写而成的。由于引导类加载器压根儿就不是一个Java类,因此在Java程序中只能打印出空值
数组类的Class对象,不是由类加载器去创建的,而是在Java运行期JVM根据需要自动创建的。对于数组类的类加载器来说,是通过Class.getClassLoader()返回的,与数组当中元素类型的类加载器是一样的;如果数组当中的元素类型是基本数据类型,数组类是没有类加载器的。
为什么要自定义类的加载器?
隔离加载类
在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。再比如:Tomcat这类Web应用服务器,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序。 (类的仲裁–>类冲突)
修改类加载的方式
类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载
扩展加载源
比如从数据库、网络、甚至是电视机机顶盒进行加载
防止源码泄漏
Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码
应用场景有哪些???
实现类似进程内隔离,类加载器实际上用作不同的命名空间,以提供类似容器、模块化的效果。例如,两个模块依赖于某个类库的不同版本,如果分别被不同的容器加载,就可以互不干扰。这个方面的集大成者是Java EE和OSGI、JPMS等框架。
应用需要从不同的数据源获取类定义信息,例如网络数据源,而不是本地文件系统。或者是需要自己操纵字节码,动态修改或者生成类型。
注意:
在一般情况下,使用不同的类加载器去加载不同的功能模块,会提高应用程序的安全性。但是,如果涉及Java类型转换,则加载器反而容易产生不美好的事情。在做Java类型转换时,只有两个类型都是由同一个加载器所加载,才能进行类型转换,否则转换时会发生异常。
自定义类加载器的两种实现方式:
- 重载 loadClass(): 这个方法是双亲委派机制模型逻辑的地方,擅自修改这个方法会导致模型破坏,容易造成问题。因此我们最好是在双亲委派模型框架内进行小范围改动,不破坏原有结构。同时,也避免了自己重写 loadClass() 方法的过程中必须写双亲委派的重复代码,从代码的复用性来看,不直接修改这个方法始终是比较好的选择
- 重载 findClass() 方法
简单的自定义类加载器的实现
public class UserDefineClassLoader extends ClassLoader {
private String rootPath;
public UserDefineClassLoader(String rootPath) {
this.rootPath = rootPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//转换成以文件路径表示的文件
String filePath = classToFilePath(name);
//指定路径的class文件对应的二进制流数据
byte[] data = getBytesFromPath(filePath);
//自定义classloader内部需要调用defineClass()
return defineClass(name,data,0,data.length);
}
private byte[] getBytesFromPath(String filePath) {
FileInputStream in = null;
ByteArrayOutputStream baos = null;
try {
in = new FileInputStream(filePath);
baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
if (baos != null)
baos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
if (in != null)
in.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private String classToFilePath(String name) {
return rootPath + "\\" + name.replace(".", "\\") + ".class";
}
public static void main(String[] args) throws ClassNotFoundException {
UserDefineClassLoader loader =
new UserDefineClassLoader("所要加载的类字节码文件所在路径");
loader.findClass("类的全限定名");
}
}
既然上面收到了双亲委派机制那我们就来看一下到底什么是双亲委派机制?
定义:
一个类加载器在接收到类加载请求时,他不会首先自己去加载这个类,而是把这个任务去委托给父类进行加载,以此递归,如果父类加载器可以完成加载任务则返回,只有在父类无法完成加载任务时才会由自己去加载
双亲委派机制的优劣:
优势:
- 避免类的重复加载,保证类的全局唯一性
- 保护程序安全,防止核心API被随意更改
劣势:
- 双亲委派是单向委托的模型,虽然结构清晰,但是上层类加载器无法访问下层类加载器
结论:由于Java虚拟机规范并没有明确要求类加载器的加载机制一定要使用双亲委派模型,只是建议采用这种方式而已
今天的文章就水到这里啦!!!!!
下期再见