本文章读者们.默认你们已经对Java的类加载机制有一定了解,一些基本的概念我不会再一一解释,本文将彻底的阐述清楚这个我疑惑了很久的问题.
1.其实网上很多有类似的文章都在讲这个问题,但是我看了他们写的都感觉写的比较乱,而且看到最后也不知道他们说的对不对,决定自己试一下.
首先看下网上的其他人的说法,我总结了一下网上比较流行的说法,并按照理解深度依次描述一下这些观点.
首先是理解最浅显的一种说法: Java类加载机制遵循双亲委派机制,当你自定义了一个java.lang.System类时候并尝试加载时,会将加载请求最终委派给bootstrap 类加载器,而bootstrap类加载器会最终成功的加载jdk中自带的java.lang.System类并返回结果.也就是说这种说法认为是不可以加载自定义System的.
然后是第二种说法: Java类加载机制虽然有双亲委派机制,但是可以通过重写ClassLoader 的loadClass方法打破双亲委派机制,然后在尝试加载自定义的System类的时候,直接使用自己写的方法去加载这个类(跳过双亲委派),也就是说这种观点的人认为可以加载自定义System.
然后是第三种说法: 虽然可以打破双亲委派机制,但是最终还是要调用ClassLoader中defineClass方法,这个方法才是最终将class文件的byte[]数组转换成内存中的class对象,而这个方法会在执行是检查安全,发现包名是java.开头会抛出异常从而阻止你加载自定义的System类,也就是说这种观点认为不可以加载自定义System.
首先,如果你看到以上三种说法的时候,认为第一种说法是对的,那我建议你不要继续往下看了,先去了解一下java的双亲委派机制后再过来看我这篇文章,否则你会看的云里雾里,完全看不懂我在说什么.先看一下ClassLoader的loadClass方法,以下是我简化后的代码,
也就是说只要我们自定义的类加载器重写loadClass和findClass方法,不使用父加载器和根加载器,就可以轻松的反驳掉第一种说法.
这么看来好像第二种说法是对的喽,就是只要打破了双亲委派就可以了,但是答案是否定的,虽然你跳过了双亲委派,但是findClass方法才是你真正能加载类的方法,只有真正的让我们自己写的findClass方法跑起来,可惜findClass方法不会让我们这么顺利重写自己的实现的,其实也就是上说的第三种说法中的观点.咱们先来看下findClass方法具体是做了什么吧,看一下代码
实际上findClass方法什么都没做直接抛异常,就是为了留给自定义classLoader来实现的,包括上面的javadoc也明确说了,这个方法需要被子类重写,然后会被loadClass方法调用.对于这一点ClassLoader的javadoc也提到这一点并且给了一个例子:
从例子中可以看到官方推荐的就是重写findClass方法,在findClass方法中将class文件转换成byte[],之后调用defineClass来完成最终的class文件转内存中的class对象.接下来就我们就来看看你这个defineClass吧,看图
javadoc中明确说明了.如果你的类名以java.开头,不好意思我要抛异常了.这也就是刚刚上面的第三种说法,你虽然打破了双亲委派机制,但是还是逃不过defineClass中的安全检查.仔细看一下这个方法的实现,核心的代码及是Class<?> c = defineClass1(this, name, b, off, len, protectionDomain, source);就是这句,其他的都是一些前置后置处理,我们再看一下这个defineClass1方法具体的实现.
哦噢,这个是一个native方法,作用域是private,综合这些信息总结一下就是,真正转化class文件到内存中的class对象,是调用的这个native方法,这个方法只能本类内调用.看到这里,你是不是觉得第三种说法是对的,毕竟这个defineClass1是没办法外部调用的,而且调用这个native方法的defineClass还是一个finnal修饰的,也是不能重写的.
真的没办法了吗? NO,NO,NO,用反射啊,管你是private还是public,用一个setAccessible(true)统统搞定.怎么样,是不是又看到一丝希望了.下面直接上代码验证.
仔细看一下下面的调用报错的堆栈,可以看出来在调用ClassLoader中的defineClass1方法中抛出了一个SecurityException,异常的message也说明了是一个禁止的包名:java.lang.
看到这里其实基本已经有答案了,ClassLoader最终将class文件转换成内存中的class对象是一个native方法,并在这个native方法中会抛出java.lang.SecurityException: Prohibited package name: java.lang这个异常,因此自定义的System是没办法加载的.
以上所有说明都是基于java1.8.0_151-b12版本.
反转:如果你觉得这个问题已经结束了,那我这篇文章跟网上那些流传的说法123基本就是一个级别的,一点突出的点都没有.所以在这里我明确的告诉你,不是这样的,请继续往下看.如果你说你没有兴趣继续往下看了,那我先把结论告诉你,结论就是:虽然很难.但是事实上是可以加载一个自定义的java.lang.System类的.
之前我一直在用java8这个版本来试验的,但是java11这个LTS已经出来了,所以是不是应该尝试一下呢.接下来就试一下吧,我就直接上结果了:
第一步就是失败, 在java8里起码自己写的java.lang.System还能编译,到java11里直接编译的试就报错了,说java.lang这个package已经存在java.base这个模块里了(不理解的可以看一下java9里引入的模块系统).
那我们就曲线救国吧,java8能编译不能加载,那我们就用java8编译好之后用java11来加载试试看,这里编译过程略过,直接用java11加载的编译后的class文件.
运行代码后发现,直接报错类,原因很快就找到类,通过反射调用的defineClass1方法在java11中变成了静态方法,看下图
所以代码需要修改一下,下面直接上修改后的代码和运行结果
运行结果:
总结:如果你是一直看到这里的,我想你应该有了一个明确的答案了,但是我还是简单的总结一下吧.
1.java8中可以正常编译包名为java.lang开头的类,但是编译后还是没法跳过defineClass1中的安全检查,所以是没法加载自定义System类的.
2.java11中在编译的过程中会检查包名是否与当前已存在的包名冲突,所以想编译java.lang.包开头的类是不行的.
3.但是种种限制实际上并不是不可打破的,只需要是用java8来编译的,使用java11中的platfromClassLoader来加载就可以成功的把自定义的java.lang.System加载到内存当中并实例化对象.
课后思考:上面代码中的main方法实际上已经成功的加载了自定义的java.lang.System,然后代码中还使用Java自带的System类的out.println方法,那么问题来了:
1.那我们在main方法这个System为什么不是自定义System类的,而是jdk自带的?
2.代码是使用的是反射来实例化自定义的System类的对象,能不能直接通过new 的方式来实例化一个自定义的System对象呢?