跳至主要內容

ClassLoader

wangdx大约 9 分钟

ClassLoader

ClassLoader 解析

ClassLoader 按照字面上的意思理解本身所描述的就属于类加载器,为什么需要类加载器呢?在整个 Java 程序运行的过程之中,JVM 的进程负贵加载所有要执行的程序类,随后启动相应的主线程,并且依据程序的定义产生若干个子线程。

如果不能够很好的理解类加载器的概念,那么就无法深入的研究 JVM 的体系结构,如果不能够理解 JVM 的体系结构,那么就无法进行代码的调优,如果不能够进行代码的调优,那么你在面试的时候就没有任何的机会,如果没有机会,你又搞个什么开发?

在整个的 Java 之中,所有的程序都是通过“*.class”字节码的文件进行存储的,这样在 JDK 执行类的时候每一个类都会存在有一个相应的类加载器,如果要想获得这些类加载器,直接依据 Class 类即可,在 Class 类中提供有如下一个重要方法:

public ClassLoader getClassLoader()

详情
public class YootkDemo {    // 李兴华编程训练营:yootk.ke.qq.com
    public static void main(String[] args) throws Exception {
        String message = "www.yootk.com" ; // 字符串,为系统类
        System.out.println(message.getClass().getClassLoader());
    }
}

虽然这个时候通过代码的执行发现,String 类的 ClassLoader 是一个 null,主要的原因这个 nul 是由 JVM 来实现的类加载,咱程序是无法得到这个类加载器的。

现在已知的 ClassLoader 一共有如下的几个:

  • Bootstrap:此为 JVM 系统内部所提供的类加载器的处理机制,JVM 原生提供的类加载机制,专门用于加载系统类。
  • PlatformClassLoader:平台类的加载器,在 JDK1.8 及以前的版本里面此为“ExtGlassLoader”.
    • 在 JDK1.8 及以前的所有的 JDK 里面为了方便进行第三方程序组件的扩展,会提供有一个专属的 ext 目录:
      • ext 目录位置:D:Wava\idk1.8.0191\ire\lib\ext,此目录的优先级高于 CLASSPATH;
  • AppClassLoader:应用程序类加载器,用户自己定义类的加载器。

之所以在整个 Java 设计的范畴里面为整个的 JVM 设计有不同层次的类加载器,主要的原因在于要实现“双亲加载”,为了提高类加载器的安全级别,在 java 之中对于所有的系统类使用系统类的加载器,如果是用户自定义的类使用自定义类的加载器,这样的目的是为了防止类重名的时候所造成的困扰,例如:现在某一个恶意的用户定义了一个“java.lang.String”类。

详情
2package com.yootk.demo;
class Book {}
public class YootkDemo {    // 李兴华编程训练营:yootk.ke.qq.com
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Book.class ; // 获得自定义类的Class对象
        System.out.println(clazz.getClassLoader());
        System.out.println(clazz.getClassLoader().getParent());
        System.out.println(clazz.getClassLoader().getParent().getParent()); // Bootstrap
    }
}

3package java.lang;

public class String {   // 自定义的String类
}


4package com.yootk.demo;
class YootkString {}    // 假设它是String
public class YootkDemo {    // 李兴华编程训练营:yootk.ke.qq.com
    public static void main(String[] args) throws Exception {
        Class<?> clazz = YootkString.class ; // 获得自定义类的Class对象
        System.out.println(clazz.getClassLoader());
        System.out.println(clazz.getClassLoader().getParent());
        System.out.println(clazz.getClassLoader().getParent().getParent()); // Bootstrap
    }
}

如果说此时这个类里面存在有许多恶意的程序代码,那么在用户不知情的情况下使用了这个类,并且加载了恶意代码就会导致程序的安全性的问题,那么为了解决这一点,才使用不同的加载器,以上面的程序为例观察现在的类加载器。

此时用户自定义的 String 类和系统中的 String 类所进行加载的时候采用的加载器是有区别的,所以才能够保证程序类的加载 正确,所谓的双亲就是防止恶意程序与系统类重名所造成的恶意加载。

自定义文件类加载器

通过之前的分析已经清楚了有许多的 JDK 内部提供的类加载器,但是这些类加载器都是按照固定的套路执行的类加载,例如在 Java 里面提供有一个 CLASSPATH 属性,那么对于类加载的时候需要将类名称直接保存在对应的包结构目录里面才可以正常加载,但是我个人觉得这样的加载实在是太啰嗦了,我希望可以随意加载磁盘上的类文件,那么就需要考虑自定义类加载器的问题了

  • 随意定义一个程序类,并且通过自定义类加载器进行加载。
  • 将此类生成的字节码文件拷贝到任意的磁盘上,路径为“h:\Message.class”(没有包,只是一个纯粹的类)。
  • 定义新的类加载器,这个类加载器内部必须通过 ClassLoader 类中提供的以下方法进行加载:

在此方法中需要接收有如下的几个参数内容:

“string name”:要加载的类名称(包.类名称);

“byte[]b”:要加载类文件的字节信息(字节流读取);

“int off”:字节数组的开始索引;。

“int len”:最终有多少个字节需要参与到此次加载(字节数组长度)。

此时既然已经成功的定义了一个类加载器,下面就使用该加载器进行 Message 类对象的加载。

详情
public class Message {
    public String echo(String msg) {
        return "【ECHO】" + msg ;
    }
}
5package com.yootk.util;

public class Message {
    public String echo(String msg) {
        return "【ECHO】" + msg ;
    }
}


6package com.yootk.util;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

// 如果要想进行加载器的创建则必须继承自“ClassLoader”父类,并且依据方法进行调用
public class FileClassLoader extends ClassLoader { // 创建一个专属的文件加载器
    // 定义要加载的字节码文件所处的绝对路径,必须通过File进行路径的拼凑
    private static final String CLASS_FILE_PATH = "H:" + File.separator + "Message.class";
    // 此时自定义了一个类加载的方法,这个类加载的方法一定不要重名
    public Class<?> loadData(String className) throws Exception {
        byte data [] = this.loadFileClassData() ; // 加载要使用的类文件
        if (data != null) { // 类信息已经成功的进行了加载
            return super.defineClass(className, data, 0, data.length);
        }
        return null ;
    }
    // 自定义一个新的方法,将根据给定的文件路径进行加载,为了简化没有进行严格的异常控制
    private byte[] loadFileClassData() throws Exception {
        // 获取要加载文件的二进制字节输入流对象
        InputStream input = new FileInputStream(new File(CLASS_FILE_PATH)) ;
        // 最终需要将所有的数据保存在内存之中,并且要利用字节数组返回
        ByteArrayOutputStream bos = new ByteArrayOutputStream() ; // 内存输出流
        input.transferTo(bos) ; // 轻松的解决字节数据的读取
        byte data [] = bos.toByteArray() ; // 获取全部的Class文件数据
        input.close();
        bos.close();
        return data ; // 返回二进制数据
    }
}


7package com.yootk.demo;

import com.yootk.util.FileClassLoader;

import java.lang.reflect.Method;

public class YootkDemo {    // 李兴华编程训练营:yootk.ke.qq.com
    public static void main(String[] args) throws Exception {
        FileClassLoader fileClassLoader = new FileClassLoader() ;// 实例化自定义类加载器
        Class<?> clazz = fileClassLoader.loadData("com.yootk.util.Message") ; // 加载类
        Object messageObject = clazz.getDeclaredConstructor().newInstance() ;
        Method method = clazz.getMethod("echo", String.class) ; // 获取echo()方法对象
        System.out.println(method.invoke(messageObject, "沐言优拓:www.yootk.com"));
    }
}


class Test2 {
    public static void main(String[] args) throws Exception {
        FileClassLoader fileClassLoader = new FileClassLoader();// 实例化自定义类加载器
        Class<?> clazz = fileClassLoader.loadData("com.yix.test.Message"); // 加载类
        ClassLoader loader = clazz.getClassLoader();
        while (loader != null) {
            System.out.println(loader);
            loader = loader.getParent();
        }
    }
}

由于现在的 Message 类的对象是通过自定义的 FileClassLoader 实现加载,所以最底层类加载器就为自定义的类加载器。

自定义网络类加载器

经过了之前的分析之后应该就已经清楚类加载器的作用了,但是如果现在的类加载器仅仅是通过本地的磁盘来进行类加载的话,未免觉得可操作性太差了,那么现在最好的一种做法是结合网络服务器的形式来完成。

本次将采用 jave.net 开发包里面所提供的 TCP 协议的实现组件实现网络服务器的开发以及客户端 ClassLoader 的服务器交互操作功能。

范例:开发一个网络服务器

详情
package com.yix.classloader.w01.server;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author wangdx
 */
public class MessageServer {
    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(9999);
        System.out.println("开启网络服务,等待客户端访问");
        Socket client = serverSocket.accept();//进入阻塞
        File classFile = new File("D:" + File.separator + "Message");
        InputStream input = new FileInputStream(classFile);
        OutputStream clientOutput = client.getOutputStream();//获取客户端输出流
        byte[] data = new byte[200];
        int len = 0;
        while ((len = input.read(data)) != -1) {
            clientOutput.write(data, 0, len);
        }
        serverSocket.close();
    }
}

此时的服务器比较简单,就是运行 9999 监听端口,同时只提供有一次连接服务,因为没有加入各种循环的处理机制。服务器开发完成之后随后需要开发一个自定义的网络加载器。

范例:开发网络加载器

详情
package com.yix.classloader.w01.util;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.Socket;

/**
 * @author wangdx
 */
// 如果要想进行加载器的创建则必须继承自“ClassLoader”父类,并且依据方法进行调用
public class NetClassLoader extends ClassLoader {
    // 此时需要通过特定的网络服务器进行类的加载,那么就必须明确的定义出服务器的地址以及连接端口
    private static final String SERVER_HOST = "localhost"; // 直接进行本机网络服务访问
    private static final int SERVER_PORT = 9999; // 服务器的连接端口

    // 此时自定义了一个类加载的方法,这个类加载的方法一定不要重名
    public Class<?> loadData(String className) throws Exception {
        byte[] data = this.loadFileClassData(); // 加载要使用的类文件
        if (data != null) {// 类信息已经成功的进行了加载
            return super.defineClass(className, data, 0, data.length);
        }
        return null;
    }

    private byte[] loadFileClassData() throws Exception {
        // 获取要加载文件的二进制字节输入流对象
        Socket client = new Socket(SERVER_HOST, SERVER_PORT);// 进行网络服务器的连接
        // 获取网络服务器响应给客户端的字节输入流
        InputStream input = client.getInputStream(); // 网络输入流
        // 最终需要将所有的数据保存在内存之中,并且要利用字节数组返回
        ByteArrayOutputStream bos = new ByteArrayOutputStream(); // 内存输出流
        input.transferTo(bos);  // 轻松的解决字节数据的读取
        byte[] data = bos.toByteArray(); // 获取全部的Class文件数据
        client.shutdownInput();  // 等待网络数据接收完毕后关闭连接
        input.close();
        bos.close();
        return data; // 返回二进制数据
    }
}

package com.yix.classloader.w01.test;

import com.yix.classloader.w01.util.NetClassLoader;

import java.lang.reflect.Method;

/**
 * @author wangdx
 */
public class NetTest {
    public static void main(String[] args) throws Exception {
        NetClassLoader fileClassLoader = new NetClassLoader();// 实例化自定义类加载器
        Class<?> clazz = fileClassLoader.loadData("com.yix.test.Message"); // 加载类
        Object messageObject = clazz.getDeclaredConstructor().newInstance();
        Method method = clazz.getMethod("echo", String.class); // 获取echo()方法对象
        System.out.println(method.invoke(messageObject, "沐言优拓:www.yootk.com"));
    }
}

如果你现在需要编写更加灵活的项目,那么就需要通过远程服务器实现一些关键类的维护性的管理操作,这个时候就需要使用到 ClassLoader 类了,这些问题也是在实际的找工作的环节里面最为常见的问题。

上次编辑于: