热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

纠正网上的错误:自定义一个类叫java.lang.System/String的方法

本文纠正了网上关于自定义一个类叫java.lang.System/String的错误答案,并详细解释了为什么这种方法是错误的。作者指出,虽然双亲委托机制确实可以阻止自定义的System类被加载,但通过自定义一个特殊的类加载器,可以绕过双亲委托机制,达到自定义System类的目的。作者呼吁读者对网上的内容持怀疑态度,并带着问题来阅读文章。

前语:不要为了读文章而读文章,一定要带着问题来读文章,勤思考。

作者:一汪清水  来源:https://dwz.cn/i7Pf6VwZ


最近,学习了下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类放置在特殊目录,然后系统自带的加载器无法加载,这样最终还是由我们自己的加载器加载(因为我们自己的加载器知道其所在的特殊目录)。这种说法好像逻辑上没有问题,那么我们就来实验下了。


# 代码验证


测试类结构及内容如下:

public class 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)
return super.loadClass(name);

byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
}catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}

public class ClassLoaderTest {

public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ClassLoader myLoader = new MyClassLoader();
Object obj = myLoader.loadClass("java.lang.Math").newInstance();
System.out.println(obj);
}

}

public final class Math {

public static void main(String[] args) {
System.out.println("hello world");
}
}

public class MyMath {

public static void main(String[] args) {
System.out.println("hello world");
}
}

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

我们先直接运行下Math类,输出如下:

java.lang.NoSuchMethodError: main
Exception in thread "main" 

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


再直接运行MyMath类,输出如下:

java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:479)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:625)
at java.lang.ClassLoader.defineClass(ClassLoader.java:615)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:141)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:283)
at java.net.URLClassLoader.access$000(URLClassLoader.java:58)
at java.net.URLClassLoader$1.run(URLClassLoader.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
Exception in thread "main" 

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


禁止使用包名:java.lang。


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

private ProtectionDomain preDefineClass(String name,
ProtectionDomain protectionDomain)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);

if ((name != null) && name.startsWith("java.")) {
throw new SecurityException("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (protectiOnDomain== null) {
protectiOnDomain= getDefaultDomain();
}

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

return protectionDomain;
}

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


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

/java/lang/Math.class
sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@a981ca
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:479)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:625)
at java.lang.ClassLoader.defineClass(ClassLoader.java:615)
at java.lang.ClassLoader.defineClass(ClassLoader.java:465)
at com.tq.MyClassLoader.loadClass(MyClassLoader.java:28)
at com.tq.ClassLoaderTest.main(ClassLoaderTest.java:8)
Exception in thread "main" java.lang.ClassNotFoundException
at com.tq.MyClassLoader.loadClass(MyClassLoader.java:31)
at com.tq.ClassLoaderTest.main(ClassLoaderTest.java:8)

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


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


# 总结


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




推荐阅读
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 怎么在PHP项目中实现一个HTTP断点续传功能发布时间:2021-01-1916:26:06来源:亿速云阅读:96作者:Le ... [详细]
  • 解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法
    本文介绍了解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法,包括检查location配置是否正确、pass_proxy是否需要加“/”等。同时,还介绍了修改nginx的error.log日志级别为debug,以便查看详细日志信息。 ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
  • 图像因存在错误而无法显示 ... [详细]
  • 本文介绍了Android中的assets目录和raw目录的共同点和区别,包括获取资源的方法、目录结构的限制以及列出资源的能力。同时,还解释了raw目录中资源文件生成的ID,并说明了这些目录的使用方法。 ... [详细]
  • ***byte(字节)根据长度转成kb(千字节)和mb(兆字节)**parambytes*return*publicstaticStringbytes2kb(longbytes){ ... [详细]
  • 本文介绍了解决java开源项目apache commons email简单使用报错的方法,包括使用正确的JAR包和正确的代码配置,以及相关参数的设置。详细介绍了如何使用apache commons email发送邮件。 ... [详细]
  • 本文介绍了关于Java异常的八大常见问题,包括异常管理的最佳做法、在try块中定义的变量不能用于catch或finally的原因以及为什么Double.parseDouble(null)和Integer.parseInt(null)会抛出不同的异常。同时指出这些问题是由于不同的开发人员开发所导致的,不值得过多思考。 ... [详细]
  • 使用freemaker生成Java代码的步骤及示例代码
    本文介绍了使用freemaker这个jar包生成Java代码的步骤,通过提前编辑好的模板,可以避免写重复代码。首先需要在springboot的pom.xml文件中加入freemaker的依赖包。然后编写模板,定义要生成的Java类的属性和方法。最后编写生成代码的类,通过加载模板文件和数据模型,生成Java代码文件。本文提供了示例代码,并展示了文件目录结构。 ... [详细]
author-avatar
520sweet跃_322
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有