java类加载机制:到底能不能自己自定义java.lang.String类

概述

这个是一个经典的面试题: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方法
发布了233 篇原创文章 · 获赞 211 · 访问量 90万+

猜你喜欢

转载自blog.csdn.net/fgyibupi/article/details/88574544