Java 类加载

 

类加载器

Java 预定义了三种类加载器:

  1. 启动类(Bootstrap)加载器: 是一个由C++代码实现的类加载器, 它将负责JRE/lib的大部分核心类库的加载. 因为是由C++编写的非字节码类加载器, 所以无法进行调用.
  2. 标准扩展(Extension)类加载器: sun.misc.Launcher$ExtClassLoader实现(单例), 负责JRE/lib/ext或者由系统变量java.ext.dir指定位置中的类库加载到内存中. 可以进行调用.
  3. 系统(System)类加载器: sun.misc.Launcher$AppClassLoader实现, 负责系统类路径(CLASSPATH)中指定的类库加载到内存中. 可以进行调用.

还有一种线程上下文类加载器. 可以通过java.util.Thread中的getContextClassLoader()setContextClassLoader(ClassLoader cl)进行获取和设置. 如果不进行设置, 默认使用系统类加载器.

双亲委派模型

类加载器在收到加载类请求时, 会先将加载任务委托给他的父类加载器, 并且依次进行递归, 也就是能用”最顶层”的类加载器进行加载的类, 绝不会让子类加载器进行加载.

双亲委派模型

这里当然你可以书写自己的类加载器进行破坏双亲委派模型.

双亲委派模型的好处

  • 防止重复进行加载.类A加载一个com.Test类的字节码, 类B也要加载com.Test, 双亲委派模型会保证他们两个使用的是同一个Test类, 并且类加载器相同.(注意, 两个类是否类型相同, 不仅类全名相同, 也要保证是同一个类加载器进行加载)
  • 安全. 防止恶意加载核心类.

自定义类加载器

自定义的类加载器

package com.classLoader;


import java.io.*;

public class MyClassLoader extends ClassLoader
{
    private String classpath;

    public MyClassLoader(String classpath)
    {
        this.classpath = classpath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
        try
        {
            byte[] classDate = getClassBinaryData(name);
            if (classDate == null)
            {
            }
            else
            {
                return defineClass(name, classDate, 0, classDate.length);
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
		//如果找不到, 就由委托父类加载器
        return super.findClass(name);
    }

	//获取指定的字节码
    private byte[] getClassBinaryData(String className) throws IOException
    {
        InputStream in = null;
        ByteArrayOutputStream out = null;
        String path = classpath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
        try
        {
            in = new FileInputStream(path);
            out = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int len = 0;
            while ((len = in.read(buffer)) != -1)
            {
                out.write(buffer, 0, len);
            }
            return out.toByteArray();
        }
        catch (FileNotFoundException e)
        {
            e.printStackTrace();
        }
        finally
        {
            in.close();
            out.close();
        }
        return null;
    }
}

要加载的类

package com.classLoader.test;


public class MyString {
    private String str;

    public MyString() {
    }

    public String getStr() {
        return str;
    }

    private void setStr(String str) {
        this.str = str;
    }
}
package com.classLoader;

import com.classLoader.test.MyString;


import java.lang.reflect.Method;


public class Main {
    public static void main(String[] args) throws Exception{
        MyClassLoader classLoader = new MyClassLoader("E:\\JavaGrowthRoad\\Base\\target\\classes");
        Class stringClass = classLoader.loadClass("com.classLoader.test.MyString");

        MyString str = (MyString)stringClass.newInstance();
        Method setStr = stringClass.getDeclaredMethod("setStr", String.class);
        setStr.setAccessible(true);
        setStr.invoke(str, "hello");
        System.out.println(str.getStr());
        System.out.println(str.toString());
    }
}

执行结果

自定义类加载器最主要是重写findClass(String name)方法, 来控制类加载的逻辑, 当然破坏双亲委派模型也从这里进行.

同时还有一个重点, 指定用一个类加载器去加载一个类的时候, 会使用该类加载器去加载这个类的父类.

破坏双亲委派模型也是由限度的, 如果你的类名是java开头, 那么即使你自己编写类加载器去加载这个类, 这个类也是始终无法使用的, 因为defineClass(String name, byte[]b, int off, int len)是final方法, 无法进行重写, 与此同时他会调用一个preDefineCLass(String name, ProtectionDomain pd)去检查类名, 不合格的类名, 和以java开头的类名都将会被它弹出异常.

    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;
    }