自定义java.lang.String能否被加载

1,157 阅读4分钟

欢迎关注公众号【sharedCode】致力于主流中间件的源码分析, 个人网站:www.shared-code.com/

从网上找了一篇针对的java.lang.String类能否被加载的文章,写的很简洁明了,在这里分享给大家

public class MyClassLoader extends ClassLoader{
    public Class findClass(String name){
        byte[] b = null;
        try {
            b = getByte(name);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return defineClass(null, b, 0, b.length);
    }
    private byte[] getByte(String name) throws IOException{
        FileInputStream in = new FileInputStream(name);
        byte[] b = new byte[1024];
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int len = 0;
        while((len = in.read(b)) != -1){
            out.write(b, 0, len);
        }
        out.close();
        b = out.toByteArray();
        return b;
    }
    }

测试类(path1,path2是我存放编译后的class文件的路径):

public class MyClassLoaderTest {
    public static void main(String [] args) throws Exception{
        MyClassLoader myloader = new MyClassLoader();
        String path1 = "F:/Software/java/jdk/lib/String.class";
        String path2 = "E:/JavaTest/String.class";
        Class c = myloader.findClass(path1);
        //Class c = myloader.findClass(path2);
        Object obj = c.newInstance();
        System.out.println(obj.getClass().getName());
        Method m = c.getMethod("say", null);
        m.invoke(obj, null);
    }
}

下面展示了几个自己写的package不同其他内容相同的String类,编译后放在上述path1,path2下各一份。

package java.lang;
    public class String{
        public void say(){
        System.out.println("I'm String class");
        }
    }

执行main方法,抛异常了:

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

若String写成下面这样(注意,只是package有变化):

package java;
    public class String{
        public void say(){
        System.out.println("I'm String class");
        }
    }

抛异常如下:

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

把String的package改成下面这样

package java.haha;
    public class String{
        public void say(){
        System.out.println("I'm String class");
        }
    }

还是抛异常:

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

注意,以上三个String类只是package不同。而从以上所抛异常来看,jvm不允许我们自己用"java"开头的package。再看以下两种package写法:

(不写package)

public class String{
        public void say(){
        System.out.println("I'm String class");
        }
    }

(package不以“java”开头)

package lang.java;
    public class String{
        public void say(){
        System.out.println("I'm String class");
        }
    }

两种写法的执行结果,输出如下:

String
I'm String class

这时候我自己写的String类,被我自己写的类加载器加载成功,并且通过反射机制,执行了其中的say()方法。

总结:

  • 自己写的类的package名称不可以"java"开头,否则不会被类加载器加载。 (所以自己写的java.lang.String,肯定也不会被加载成功了,即使是自己写的类加载器)
  • package不以"java"开头的类,可被成功加载。 然而,此时的String类,类名叫做"String",还是叫做"Ergou",还是叫做"ZhangSan"都是无所谓的,只是非常普通的一个名字而已,因为所属的package不同,该String类和java.lang.String并不冲突。

结论:

至此,自己写的java.lang.String能否被加载已经有了答案。 不能! 能被加载的,是lang.java.String,是haha.String,是zhangsan.String…是所有package不以"java"开头的String。此时,他早已不是那个他…

扩展:

为什么自定义类加载器还是无法完成系统类的加载呢 , 那是因为

public class MyClassLoader extends ClassLoader{
    public Class findClass(String name){
        byte[] b = null;
        try {
            b = getByte(name);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        // 重点在这里
        return defineClass(null, b, 0, b.length);
    }
}

上面的类继承了ClassLoader, 调用了父类的defineClass方法,我们可以看一下ClassLoader方法的实现

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    { 
        // 在进行类加载之前,需要调用preDefineClass进行校验,
        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
        Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
        postDefineClass(c, protectionDomain);
        return c;
    }

在进行类加载之前,需要调用preDefineClass进行校验, preDefineClass方法里面针对类的名称进行的严格的限制

 /* Determine protection domain, and check that:
        - not define java.* class, 不能存在java.*开头的类
        - signer of this class matches signers for the rest of the classes in
          package.  验证签名
          
          
    */
    
    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;
    }

这个方法验证两点:

  1. 不能存在java.*开头的类
  2. 该类的签名者与包中其他类的签名不能匹配

终极结论

  1. 你可以定义为java.lang.String的class,但是系统的加载器是不会加载你的类的,加载的还是JDK里面的String,所以所有的方法都是不可用的

  2. 实现自己的类加载器去尝试加载自己定义的java.lang.String,

  首先必须放在其他路径下,否则双亲委派机制,还是会加载自己系统的类

  如果破坏双亲委派,在defineClass的时候也会抛出异常,不允许定义java.开头的类

所以无论如何也是无法实现加载自己定义的java.lang.String的