文章目录
概述
这个是一个经典的面试题:java类加载机制:到底能不能自己自定义java.lang.String类
主要考察java的类加载机制。
网络上的错误(不准确)答案
一般来说不可以,即使定义了,也不会加载。依然会读取src包下的S的string类。
但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。
具体也不能说不正确,只不过这个只能在老版本的java可以实现,如java8及以前的版本。
新版本如java11,编译都不会通过。
jdk11
直接编译不通过。
jdk8
可否直接使用自定义的java.lang.String?
自定义java.lang.String类,这个string类拷贝自src包下的string,只是修改他的 equals方法用于后续测试:
public boolean equals(Object anObject) {
System.out.println("dsdsss");
return true;
}
添加断点,调用:
会发现,断点依然是访问的src包下的string类。并没有加载我们的自定义的类。
自定义加载器呢?
网上很多错误的说法是说将
java.lang.String
放到其他位置,这样jdk自带的三类类加载都无法加载了,我们自定义的加载器就可以加载了,这是不对的,下面验证一下。
findClass()用于写类加载逻辑、loadClass()方法的逻辑里如果父类加载器加载失败则会调用自己的findClass()方法完成加载,保证了双亲委派规则。
- 如果不想打破双亲委派模型,那么只需要重写
findClass
方法即可 - 如果想打破双亲委派模型,那么就重写整个
loadClass
方法
重写findclass方法
接下来我们使用自定义加载器来试一下,我这里使用的是jdk11.
首先准备class文件,String内容无所谓,我们只需要验证能否加载到自定义的java.lang.String即可:
测试代码:
@Test
public void test2() throws MalformedURLException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
URLClassLoader diskLoader = new URLClassLoader(new URL[]{new URL("file:/D:/liubenlong/b/")});//最后面的斜杠需要添加
Class clz = diskLoader.loadClass("java.lang.String");
Constructor constructor = clz.getConstructor(String.class);
Object obj = constructor.newInstance("tom");
/**
* 类Hello引用了类Dog,类加载器会主动加载被引用的类。
* 注意一般是我们使用 URLClassLoader 实现自定义的类加载器。如果使用classLoader,则需要重写findClass方法来实现类字节码的加载
*/
Method method = clz.getMethod("sayHello", null);
//通过反射调用Test类的say方法
method.invoke(obj, null);
}
执行结果很明显会报错,无法加载到我们自定义的类:
findclass没有打破双亲委派,所以肯定不行。
来debug看一下,可以发现,实际加载到的java.lang.String类是属于module java.base
的,他的classLoader是null(备注:null表示实际使用到的类加载器是根加载器)。
重写loadClass方法
这里使用loadclass打破双亲委派试试。
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
FileInputStream in = new FileInputStream("D:/liubenlong/b/java/lang/String.class");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len = -1;
while ((len = in.read(buf)) != -1) {
baos.write(buf, 0, len);
}
in.close();
byte[] classBytes = baos.toByteArray();
return defineClass(name, classBytes, 0, classBytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
//测试代码
@Test
public void test3() {
try {
MyClassLoader11 diskLoader = new MyClassLoader11();
//加载class文件
Class clz = diskLoader.loadClass("java.lang.String");
Constructor constructor = clz.getConstructor(String.class);
Object obj = constructor.newInstance("tom");
Method method = clz.getMethod("sayHello", null);
//通过反射调用Test类的say方法
method.invoke(obj, null);
} catch (Exception e) {
e.printStackTrace();
}
}
从运行结果中可以看到,不允许我们自定义java.开头的包。
defineClass
不论自定义类加载器怎么写,都会调用defineClass方法,我们看一下这个方法的实现:
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(this, 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.")
&& this != getBuiltinPlatformClassLoader()) {
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.
开头,则直接报错:Prohibited package name
。
结论
- 不可以加载自定义的java.开头的任何类。
- 因为JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写loadClass搜索类的算法。
- 如果不想打破双亲委派模型,那么只需要重写
findClass
方法即可 - 如果想打破双亲委派模型,那么就重写整个
loadClass
方法