一.问题描述
今天在与老师讨论JAVA中类的继承与抽象类的实现时,涉及到了如下一个情况:
如果子类想要在父类的基础上利用父类的reps(变量)去实现一些功能,但是父类在保证safe from rep exposure(表示泄露)时需要对自己的reps加修饰符的限定。那么相对被private修饰过的父类reps子类无权访问,protected修饰过的reps就可以直接让子类访问操作,并且同时使得父类包外的其余类无法直接访问,一定程度上保证了父类的安全。
但是考虑到protected不仅使子类可以访问父类reps,与父类在同一个包下的类都能访问其reps,我觉得protected修饰符的范围有些宽泛,可能存在特殊的危险情况。
那么到底protected能不能代替private成为一个保证invariant和ADT完整的修饰符呢?
我在老师的引导下,做出以下实验:
二.实验探索
首先我为了验证protected修饰符修饰的变量是可以被同包下的其他类直接访问修改的,写了个简单的测试。
(定义两个不同文件夹下相同命名的包,然后从一个包中调用另一个包的类生成实例,并直接访问并修改其protected变量,结果符合预期)。
从上例可以看出这种(伪造同包绕过包限制)的现象确实很自然的出现了。
接下来,老师举出了Java.util包。
同样的实验方式带入:
然后却得出了这样的结果:
这个报错我们可以很容易可以看出来,是一个SecurityException异常。
为什么会出现这样的异常呢?
看到上图(橙色行)我们可以看出这里有一个对一级包名的限制,如果是以java开头的包会直接抛出错误。
也就是说在实际开发过程中,JAVA是禁止开发者采取java作为一级包名。
但是心存侥幸,既然java包被禁止了,那么javax包是否可以实验成功呢?
继续实验:构造同名包试图访问javax中类的protected变量
结果:
Exception in thread "main" java.lang.IllegalAccessError: tried to access field javax.swing.tree.AbstractLayoutCache.rootVisible from class javax.swing.tree.Ligengat javax.swing.tree.Ligeng.change(Ligeng.java:6)
at javax.swing.tree.Ligeng.main(Ligeng.java:11)
可以看到抛了IllegalAccessError的错误,这又是为什么呢?
这里并不能回溯去寻找出错点。并且反思,实验中采取的这种尝试混入java核心库然后利用protected的行为应该是很容易做到的,然而java是一个有严格安全机制的强类型语言,除了序列化处第三方包容易出漏洞外,其余是很安全的。故思索java必有针对这种情况的安全机制。
三.Classloader细节与双亲指派制度
java确实存在这样的安全机制。
java在生成中间字节码,以及将字节码放入jvm的过程中,涉及到调用classloader(类加载器)的步骤。这个步骤比较复杂涉及jvm并且接近java实现底层,没有基础的深究这部分不容易理解且意义不大,故我将给出相关详细链接(见底),愿意研究的大佬可以去看看,此处只讲述与实验有关的部分。
ClassLoader是类加载器,它的作用是将字节码对应到类,从而根据字节码创建jvm中的类实例。所以说每一个我们java源代码生成的实例类,最终都是需要变成字节码被ClassLoader加载进jvm的。
但是针对不同被加载对象,加载过程的先后次序,java共自带三种加载器:启动加载器(也称引导加载器),扩展加载器,系统加载器(也称应用加载器)。
一.启动加载器Bootstrap class loader
启动类加载器是c++写成的非java源代码编译成的,它是jvm的一部分。在实际中,同学如果去做相关实验发现获取某个类加载器为null则意味着其对应的是启动类加载器。启动加载器加载的对象是java核心类,具体来讲就是环境变量“sun.boot.class.path”下的jar包:(C:\.......是因为我是默认目录装的java 1.8)
C:\Program Files\Java\jdk1.8.0_191\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_191\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_191\jre\lib\sunrsasign.jar;
C:\Program Files\Java\jdk1.8.0_191\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_191\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_191\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_191\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_191\jre\classes(这个不是jar包是class文件,我的jre下实际上没有,有兴趣的同学自己查吧)
所以我们在做以上实验时,调用的java包中的类和javax包中的核心类都是由这个加载器加载的。
二.扩展加载器Extensions class loader
扩展加载器加载器加载的就是“java.ext.dirs”环境变量下的包
实际上的地址就是:
C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext;
C:\WINDOWS\Sun\Java\lib\ext
这里面放满了各种扩展包,但是上述实验采用的javax包中的类并不是在这里加载的。具体如何判断一个类是被什么加载器加载的,只需要获取类的class再调用getClassLoader()方法即可,如果不是null则会返回一个ClassLoader实例回来。
System.out.println(Ligeng.class.getClassLoader());//sun.misc.Launcher$AppClassLoader@18b4aac2
三.系统加载器 system class loader (AppClassLoader是实际类名)
这种加载器是默认加载器,不是上述的环境变量目录下的类,默认就会被该加载器加载,前提是在CLASSPATH里(CLASSPATH是java用于寻找开发者自己开发的类的位置的环境变量,IDE一般在项目中会自动为项目生成特有的CLASSPATH,故不用担心)。
所以,我们自己实现的类即使冠以java核心包的包名,如果没有塞进对应目录下的jar文件,很遗憾还是得被system class loader给加载,而不是被Bootstrap class loader加载。
四.双亲指派机制
ClassLoader的指派模式即双亲指派机制。对于这个机制我们只要理解,如果一个类加载器要加载目标类,那么它首先会去调用自己的父加载器去尝试加载这个类,如果父类失败,自己才去尝试加载。
这里父加载类不是父类,父加载类关系是
Bootstrap ClassLoader <-- Extention ClassLoader <-- AppClassLoader
所以很显然的是,AppClassLoader是无法加载java核心库的,因为一旦它尝试加载,它的父加载类也必被调用于加载核心库,而父加载类会成功加载,最终把生成的实例类返回给子加载类。同样的,父类加载类加载范围和子加载类范围不同,所以父加载类压根就加载不了开发者的类,只有子加载类自己去加载。
五.java访问机制
对java来说,同一个包下的类按道理讲是满足包私有限制的,所以protected变量是可以被同包其他类直接访问的,这在编译阶段没有问题。
但是在实际运行时jvm中只有被classloader加载形成的类,所以这时的每个类和classloader都有很紧密的联系,package-private限制上升到了一种新的等级。即只有被同一个加载器加载的同包类,jvm才承认是package-private的,其中的protected修饰变量和方法才能被正常访问,否则将会触发非法访问异常(错误)。这也就解释了为什么我们对javax的实验通过了静态编译检查,最后却会报错。
五.总结
讲了这么多这么远,终于可以总结了。
在我们的整个实验分析过程中,从一开始对java.util的包的尝试,我们就已经可以看出classloader的一点端详了(触发的异常是classloader的defineclass方法,关于defineclass详见链接)。在后续javax的实验中,我们更是直接看到这种机制对我们实现的影响。
通过classloader机制,java保证了自己核心库的安全,不同的加载器将“包”实际范围缩小了,使得核心库中的只对自己库中其余类开放的一些变量方法,无法被众多java开发者直接访问与修改,保证了抽象结构的安全。
但是除去了java核心库,我们自己开发的库往往也有安全需求与合作需求,如果采取protected修饰符,很可能就没有classloader安全机制来帮助我们防止rep exposure ,就如第一个实验,被直接修改一样。所以protected的利用安全性仍存在争议。
总结的总结,像classloader机制一样,java中有很多我们平时开发没有注意到的实现细节,有时候它们会直接影响到我们的开发与实践,而影响形式看起来诡异神奇,而这往往是因为我们不了解java的实现底层。像本次实验中的报错等,这些需要我们多多去见识,积累。
附:
IBM的ClassLoader教程
逻辑讲解附带源码的ClassLoader博客(大佬)
深入分析ClassLoader(大神)