需求缘起

最近学习了下java类加载相关的知识。看到网上经常会有一道这样的有一道面试题是,能不能自己写个类叫java.lang.System?

网上提供的答案:通常不可以,但可以采取另类方法达到这个需求。所谓的另类方法指自己写个类加载器来加载java.lang.System达到目的。

首先表明下我的观点。上述答案完全是误导读者,是不正确的答案。我就纳闷了网上怎么把这种完全不正确的搜索结果排在前面,而且几乎搜到的都是这种不正确的答案。可能很多不明真相的朋友就这么被误导了,所以还是希望大家对网上的内容先持怀疑态度为好。下面详细说明为什么

 

网上错误答案的详细解释

"为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。

但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。

概念说明

类加载器可分为两类:一是启动类加载器(Bootstrap ClassLoader),是C++实现的,是JVM的一部分;另一种是其它的类加载器,是Java实现的,独立于JVM,全部都继承自抽象类java.lang.ClassLoader。jdk自带了三种类加载器,分别是启动类加载器(Bootstrap ClassLoader),扩展类加载器(Extension ClassLoader),应用程序类加载器(Application ClassLoader)。后两种加载器是继承自抽象类java.lang.ClassLoader。关于这三种加载器各自的作用这里不做详细说明,有兴趣的可以自己了解下。

类加载器是有层次的

一般是: 自定义类加载器 >> 应用程序类加载器 >> 扩展类加载器 >> 启动类加载器

上面的层次关系被称为双亲委派模型(Parents Delegation Model)。除了最顶层的启动类加载器外,其余的类加载器都有对应的父类加载器。

再简单说下双亲委托机制:如果一个类加载器收到了类加载的请求,它首先不会自己尝试去加载这个类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是加此,因此所有的加载请求最终到达顶层的启动类加载器,只有当父类加载器反馈自己无法完成加载请求时(指它的搜索范围没有找到所需的类),子类加载器才会尝试自己去加载。

再回去看下解释内容,我相信前面的部分大家应该很看懂了,也没什么大问题。最后的如果部分"如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。" 我就不明白所以了,逻辑完全不通。我想它的本意可能是,将自己的java.lang.System类放置在特殊目录,然后系统自带的加载器无法加载,这样最终还是由我们自己的加载器加载(因为我们自己的加载器知道其所在的特殊目录)。这种说法好像逻辑上没有问题,那么我们就来实验下了。

自己验证

java hanlp 自定义单词 自定义java.lang.string_类加载器

主要包括4个类:

MyClassLoader:

package com.xyq.maventest.classloader;

import java.io.InputStream;

 

/**

*

*/

/**

* @ClassName: package-info

* @Description: 自定义类加载器

* @author youqiang.xiong

* @date 2018年1月24日下午3:16:11

*

*/

publicclass MyClassLoader extends ClassLoader{

 

public MyClassLoader() {

super(null);

}

 

@Override

public Class<?> loadClass(String name) throws ClassNotFoundException {

try{

String className = null;

if(name.startsWith("java.lang")){

className = "/" + name.replace('.', '/') + ".class";

}else{

className = name.substring(name.lastIndexOf('.') + 1) + ".class";

}

System.out.println(className);

InputStream is = getClass().getResourceAsStream(className);

System.out.println(is);

if(is == null)

returnsuper.loadClass(name);

 

byte[] b = newbyte[is.available()];

is.read(b);

return defineClass(name, b, 0, b.length);

}catch (Exception e) {

e.printStackTrace();

thrownew ClassNotFoundException();

}

}

}




ClassLoaderTest:

package com.xyq.maventest.classloader;

 

publicclassClassLoaderTest {

 

publicstaticvoid main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {

ClassLoader myLoader = new MyClassLoader();

Object obj = myLoader.loadClass("java.lang.Math").newInstance();

System.out.println(obj);

}

}

Math:

package java.lang;

 

/*****

*

* @ClassName: Math

* @Description: 自定义Math类,复写Java.lang.Math,检查是否会加载

* @author youqiang.xiong

* @date 2018年1月24日下午3:18:41

*

*/

publicfinalclassMath {

 

publicstaticvoid main(String[] args) {

System.out.println("hello world");

}

}

MyMath:

package java.lang;

 

publicclassMyMath {

 

publicstaticvoid main(String[] args) {

System.out.println("hello world");

}

}

上面的测试代码没用自定义java.lang.System类,因为测试代码用到了JDK自带的System类进行输出打印,会冲突,所以改用为自定义的java.lang.Math类。如果自定义的Math类能加载,那么自定义的System类同样能加载。

我们先直接运行下Math类,

输出如下:

错误: 在类 java.lang.Math 中找不到 main 方法, 请将 main 方法定义为:

public static void main(String[] args)

否则 JavaFX 应用程序类必须扩展javafx.application.Application

提示Math类没有main方法。首先大家要明白一个概念,当类首次主动使用时,必须进行类的加载,这部分工作是由类加载器来完成的。根据双亲委托原则,Math类首先由启动类加载器去尝试加载,很显然,它找到rt.jar中的java.lang.Math类并加载进内存(并不会加载我们自定义的Math类),然后执行main方法时,发现不存在该方法,所以报方法不存在错误。也就是说,默认情况下JVM不会加载我们自定义的Math类。

再直接运行MyMath类

Jvm弹出异常提示框

java hanlp 自定义单词 自定义java.lang.string_java_02

此时打开jvisualvm 可以监控到此程序的cpu,内存等运行情况

java hanlp 自定义单词 自定义java.lang.string_类加载器_03

点击两次按钮弹出后,会出现如下异常信息:

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang

    at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)

    at java.lang.ClassLoader.defineClass(ClassLoader.java:761)

    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)

    at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)

    at java.net.URLClassLoader.access$100(URLClassLoader.java:73)

    at java.net.URLClassLoader$1.run(URLClassLoader.java:368)

    at java.net.URLClassLoader$1.run(URLClassLoader.java:362)

    at java.security.AccessController.doPrivileged(Native Method)

    at java.net.URLClassLoader.findClass(URLClassLoader.java:361)

    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)

    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)

    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)

    at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)

注意红色部分的内容。由堆栈异常信息可知道,当应用程序类加载器类(AppClassLoader)尝试加载MyMath类时,ClassLoader.java的662行抛出了SecurityException

禁止使用包名:java.lang。

直接查看抽象类java.lang.ClassLoader的preDefineClass方法代码,摘抄如下:

 



private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)

{

if (!checkName(name))

thrownew NoClassDefFoundError("IllegalName: " + name);

 

 

if ((name != null) && name.startsWith("java.")) {

thrownew SecurityException

("Prohibited package name: " +

name.substring(0, name.lastIndexOf('.')));

}

if (pd == null) {

pd = defaultDomain;

}

 

if (name != null) checkCerts(name, pd.getCodeSource());

 

returnpd;

}

可以看到如果加载的类全名称以"java."开头时,将会抛出SecurityException,这也是为什么直接执行MyMath类会出现SecurityException。

照这样,我们自定义的类加载器必须继承自ClassLoader,其loadClass()方法里调用了父类的defineClass()方法,并最终调到preDefineClass()方法,因此我们自定义的类加载器也是不能加载以"java."开头的java类的。

继续运行下ClassLoaderTest

/java/lang/Math.class

sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@4e25154f

java.lang.SecurityException: Prohibited package name: java.lang

    at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)

    at java.lang.ClassLoader.defineClass(ClassLoader.java:761)

    at java.lang.ClassLoader.defineClass(ClassLoader.java:642)

    at com.xyq.maventest.classloader.MyClassLoader.loadClass(MyClassLoader.java:37)

    at com.xyq.maventest.classloader.ClassLoaderTest.main(ClassLoaderTest.java:7)

Exception in thread "main" java.lang.ClassNotFoundException

    at com.xyq.maventest.classloader.MyClassLoader.loadClass(MyClassLoader.java:40)

    at com.xyq.maventest.classloader.ClassLoaderTest.main(ClassLoaderTest.java:7)



备注:红色部分清楚表明,也是在preDefineClass方法中抛出的SecurityException。

通过代码实例及源码分析可以看到,对于自定义的类加载器,强行用defineClass()方法去加载一个以"java."开头的类也是会抛出异常的。

 

总结

不能自己写以"java."开头的类,其要么不能加载进内存,要么即使你用自定义的类加载器去强行加载,也会收到一个SecurityException。