前言
本篇文章介绍一些什么是沙箱机制和自定义加载器有什么好处和怎么操作
沙箱安全机制
沙箱安全机制主要有以下特征
- 保证程序安全
- 保护Java原生的JDK代码
Java安全模型的核心就是Java沙箱(sandbox)
什么是沙箱?沙箱是一个限制程序运行的环境
沙箱机制就是将]ava代码限定在虚拟机(JVN)特定的运行范围中,并且严格限制代码对本地系统资源访问
通过这样的措施来保证对代码的有限隔离,防止对本地系统造成破坏。
沙箱主要限制系统资源访问,那系统资源包括什么?CPU、内存、文件系统、网络
不同级别的沙箱对这些资源访问的限制也可以不一样
所有的Java程序运行都可以指定沙箱,可以定制安全策略
JDK不同时期的安全策略
JDK1.0中如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现
因此在后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限。如下图所示JDK1.1安全模型
在Java1.2版本中,再次改进了安全机制,增加了代码签名
不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示JDK1.2安全模型
在JDK1.6中添加了当前最新的安全机制实现,则引入了域(Domain)的概念
虚拟机会把所有代码加载到不同的系统域和应用域
。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问
虚拟机中不同的受保护域(Protected Domain),对应不一样的权限〈Permission)
存在于不同域中的类文件就具有了当前域的全部权限,如下图所示,最新的安全模型(jdk1.6)
类的自定义加载器
之前的文章我们提到过最常见的类加载器结构主要是如下情况:
那么我们为什么要自定义加载器呢?我们可以从几种不同的场景来说明
隔离加载类
在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。
比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包
比如: Tomcat这类web应用服务器,内部自定义了好几种类加载器,用于隔离同一个web应用服务器上的不同应用程序
修改类加载的方式
类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载
扩展加载源
比如从数据库、网络、甚至是电视机机顶盒进行加载
防止源码泄漏
Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码
常见场景
实现类似进程内隔离,类加载器实际上用作不同的命名空间,以提供类似容器、模块化的效果
例如,两个模块依赖于某个类库的不同版本,如果分别被不同的容器加载,就可以互不干扰。这个方面的集大成者是Java EE和oSGI、JPMS等框架。
应用需要从不同的数据源获取类定义信息,例如网络数据源,而不是本地文件系统。或者是需要自己操纵字节码,动态修改或者生成类型
但是需要注意的是在一般情况下,使用不同的类加载器去加载不同的功能模块,会提高应用程序的安全性
。但是,如果涉及Java类型转换,则加载器反而容易产生不美好的事情。在做Java类型转换时,只有两个类型都是由同一个加载器所加载,才能进行类型转换,否则转换时会发生异常
那么我们自定义加载器的话,先回顾一下ClassLoader与现有加载器的关系图
自定义加载器实现方式
定制自己的类加载器。Java提供了抽象类java.lang.ClassLoader,对于自定义的类加载器都应该继承ClassLoader类
,取决于是间接继承还是直接继承
在自定义classLoader的子类时候,我们常见的会有两种做法:
- 方式一:重写loadclass()方法
- 方式二:重写findClass()方法
针对于这两种方式本质上差不多,毕竟loadclass()也会调用findclass()
,但是从逻辑上讲我们最好不要直接修改loadclass(()的内部逻辑
我们在提到第一次破坏双亲委派机制的时候说到过为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性
,只能在JDK1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法
,而不是在loadClass()中编写代码
loadClass()这个方法是实现双亲委派模型逻辑的地方,擅自修改这个方法会导致模型被破坏
,容易造成问题因此我们最好是在双亲委派模型辉架内进行小范围的改动,不破坏原有的稳定结构
同时,也避免了自己重写loadClass()方法的过程中必须写双亲委托的重复代码
,从代码的复用性来看,不直接修改这个方法始终是比较好的选择
当编写好自定义类加载器后,便可以在程序中调用loadClass()方法来实现类加载操作
说明
- 我们自定义的加载器其父类加载器是系统类加载器
- JVN中的所有类加载都会使用java.lang.ClassLoader.loadCass(string)接口,核心类库也不例外
自定义加载器代码实践
此时我们先定义MyClassLoader直接继承于ClassLoader
public class MyclassLoader extends ClassLoader {
//字节码文件路径
private String byteCodePath;
//赋值字节码文件路径
public MyclassLoader(String byteCodePath) {
this.byteCodePath = byteCodePath;
}
//指定父类加载器,与字节码文件
public MyclassLoader(ClassLoader parent, String bytecodePath) {
super(parent);
this.byteCodePath = byteCodePath;
}
}
接下来我们就需要重写findClass,找到我们的文件并把它加载进来
class MyclassLoader extends ClassLoader {
//....省略其他关键性代码....
@Override
protected Class<?> findclass(String className) throws ClassNotFoundException {
BufferedInputStream bis = null;
ByteArrayOutputStream baos = null;
try{
//获取字节码文件的完整路径
String fileName = byteCodePath + className + ".class";
//获取一个输入流
bis = new BufferedInputStream(new FileInputStream(fileName));
//获取一个输出流
baos = new ByteArrayoutputStream();
//具体读入数据并写出的过程
int len;
byte[] data = new byte[1024];
while ((len = bis.read(data)) != -1) {
baos.write(data,0,len);
}
//获取内存中的直接数据
byte[] bytecodes = baos.toByteArray();
//调用defineclass(),将字节数组数据转换为Class的实例
Class classz = defineClass(null,bytecodes,0,bytecodes.length);
return classz;
}catch(IOException e) {
e.printStackTrace();
}finally {
try{
if (baos != null) {
baos.close();
}
}catch (IOException e) {
e.printStackTrace();
}
try{
if (bis != null) {
bis.close();
}
}catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
接下来我们创建Test测试来,来调用我们自定义的加载器看看怎么样
public class MyClassLoaderTest{
public static void main(String[] args){
MyclassLoader loader = new MyclassLoader("d:/");
try{
class clazz = loader.loadclass("Demo1");
System.out.println("加载此类的加载器为:" + clazz.getClassLoader().getclass().getName();
System.out.println("加载此类的加载器的父类加载器为:" + clazz.getClassLoader().getParent().getclass().getName();
}catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
//运行结果如下:
加载此类的类的加载器为: com.atguigu.java2.MyclassLoader
加载此类的加载器的父类加载器为:sun.misc.Launcher$AppclassLoader
可以看到,即使我们没有传递父类加载器,自定义的加载器父类默认为应用APP加载器