关于这个问题,网上众说纷纭,类似的问题还有:java.lang.System,java.lang.String,java.lang.Math是否自己写一个jdk同名的类。
大概的答案主要分成以下2派:
-
可以,自定义classloader破坏双亲委派机制(由于系统自带的3个类加载器都加载特定目录下的类,如果我们自己的类加载器放在1个特殊的目录,那末系统的加载器就没法加载,也就是终究还是由我们自己的加载器加载。)
点评:这个明显是没有实践过的一派人的发言,实践一下就会发现自定义classloader是行不通的。 -
不可以,即使自定义classloader,重写loadClass或者findClass方法,这样可以加载到自己指定目录下的class文件,但是由于自定义的classloader必须继承 ClassLoader,loadClass 方法会调用父类的 defineClass 方法。
而父类的这个 defineClass 是一个 final 方法,无法被重写。
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
private ProtectionDomain preDefineClass(String name, ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
解密
可以自己写一个java.lang.System,java.lang.String,java.lang.Math,也能够得到编译,然而常规的方式会发现自己写的这个类没有机会被加载到JVM。
JVM中类加载采用的是双亲委派机制,BootStrapClassLoader是顶层父类,ExtClassLoader是BootStrapClassLoader类的子类,AppClassLoader又是ExtClassLoader的子类。
这里以java.lang.String为例,当我是使用到这个类时,Java虚拟机会将java.lang.String类的字节码加载到内存中。
这个时候jdk中自带一个java.lang.String,然而我们自己又写了一个java.lang.String,JVM会加载哪一个呢?
- 因加载某个类时,优先使用父类加载器加载需要使用的类。如果我们自定义了java.lang.String这个类,
- 加载该自定义的String类,该自定义String类使用的加载器是AppClassLoader,根据优先使用父类加载器原理,
- AppClassLoader加载器的父类为ExtClassLoader,所以这时加载String使用的类加载器是ExtClassLoader,但是类加载器ExtClassLoader在jre/lib/ext目录下没有找到String.class类。然后使用ExtClassLoader父类的加载器BootStrapClassLoader,
- 父类加载器BootStrap在JRE/lib目录的rt.jar找到了String.class,将其加载到内存中。这就是类加载器的委托机制。
参考文章
https://www.cnblogs.com/guweiwei/p/6641785.html
https://www.cnblogs.com/alexlo/p/5664543.html
终极结论
上面,只说对了一半(“可以自己写一个java.lang.System,java.lang.String,java.lang.Math,也能够得到编译,然而常规的方式会发现自己写的这个类没有机会被加载到JVM。”)
虽然jdk classloader与自定义classloader搞不定,但是不表示没有其它办法。
既然bootstrapclassloader可以加载java.lang.ArrayList ,那么bootstrapclasscloader扫描的包路径是如何指定的呢
解决方案
Java 命令行提供了如何扩展bootStrap 级别class的简单方法.
- -Xbootclasspath: 完全取代基本核心的Java class 搜索路径. 不常用,否则要重新写所有Java 核心class
- -Xbootclasspath/a: 后缀在核心class搜索路径后面.常用!!
- -Xbootclasspath/p: 前缀在核心class搜索路径前面.不常用,避免引起不必要的冲突.
先来创建一个java.lang.JavaAlioo的java文件,编译并打包成jar javademo-javapackage-1.0.0-SNAPSHOT.jar
% more java/lang/JavaAlioo.java
package java.lang;
public class JavaAlioo {
static {
System.out.println("JavaAlioo加载成功了,哈哈哈哈");
}
public void sayhi() {
System.out.println("HELLO WORLD");
}
public static void main(String[] args) {
new JavaAlioo().sayhi();
}
}
其次编写测试用例JavaTest.java
% more JavaTest.java
package com.alioo;
public class JavaTest {
public static void main(String[] args) throws Exception {
Class a = Class.forName("java.lang.JavaAlioo");
}
}
常规运行会报如下错误:
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:468)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:264)
at com.alioo.JavaTest.main(JavaTest.java:7)
将javademo-javapackage-1.0.0-SNAPSHOT.jar指定到bootstrappath中再运行一把发现就可以了
启动命令中增加如下参数:
-Xbootclasspath/p:/Users/mac/work/gitstudy/javademo/javademo-javapackage/target/javademo-javapackage-1.0.0-SNAPSHOT.jar
说明这里使用-Xbootclasspath/p 与 -Xbootclasspath/a都是可以的
JavaAlioo加载成功了,哈哈哈哈
进一步验证下jdk中的已经有的类java.lang.ArrayList来看看效果
package java.lang;
public class ArrayList {
static {
System.out.println("ArrayList加载成功了,哈哈哈哈");
}
public void sayhi() {
System.out.println("ArrayListArrayListArrayListArrayList");
}
}
启动命令中增加如下参数:
-Xbootclasspath/p:/Users/mac/work/gitstudy/javademo/javademo-javapackage/target/javademo-javapackage-1.0.0-SNAPSHOT.jar
说明这里只能使用-Xbootclasspath/p , 如果使用-Xbootclasspath/a 那么加载的就是jdk自带的java.lang.ArrayList了。
测试结果
ArrayList加载成功了,哈哈哈哈
至此,我们已经成功搞定了自己写的java.lang.JavaAlioo,java.lang.ArrayList的运行效果了,惊不惊喜,意不意外。
再提供另外一种方式也可以搞定,思路也是通过增加bootclasspath,api如下:
instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile));
开源的arthas中用的就是这种方式,有兴趣的可以读读。
参考文章
https://www.cnblogs.com/duanxz/p/3482311.html