JVM初识之自定义类加载器

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Weixiaohuai/article/details/86509637

一、简介

通常情况下,我们都是直接使用系统类加载器,但是有些时候,由于某种特殊需求,我们也需要自定义类加载器。比如,应用程序是根据网络来传输字节码文件信息, 为了保证在网络传输过程中字节码文件的安全,通常都会进行加密,这样我们在加载类的时候,就需要进行解密,这种需求使用系统提供的类加载器是实现不了的,这就需要我们自己定义加密解密类加载器。自定义类加载器一般都是继承ClassLoader类。

二、自定义类加载器流程

【a】继承java.lang.ClassLoader类

【b】首先检查请求的类型是否已经被这个类加载器加载到命名空间中,如果已经装载则直接返回

【c】委托类加载请求给父类加载器,如果父类加载器能够完成加载工作,则返回父类加载器加载的Class实例

【d】调用本类的findClass()方法,试图获取对应的字节码,如果获取的到,则调用defineClass()导入类型到方法区,如果获取不到,返回异常给loadClass()方法,loadClass()转抛异常,终止加载过程。

  • 注意:被两个类加载器加载的同一个类,JVM并不认为是相同的类。

三、自定义类加载器示例

下面我们自定义一个文件系统类加载器,实现传入类的全限定名,然后根据IO流读取.class字节码信息。

首先在d:java下面新建一个Test.java:

package com.wsh;

public class Test {
    public static void main(String[] args) {
        System.out.println("test");
    }
}

接着使用命令行工具编译Test.java,生成Test.class字节码文件:

这样在d:/java/com/wsh/路径下生成了Test.class字节码文件:

/**
 * @Description: 自定义文件系统类加载器
 * @author: weishihuai
 * @Date: 2019/1/16 16:32
 */
public class FileSystemClassLoader extends ClassLoader {
    /**
     * 根目录路径
     */
    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //1. 查找该类加载器是否已经装载这个类,如果已经装载,则直接返回该Class对象
        Class<?> loadedClass = findLoadedClass(name);
        if (null == loadedClass) {
            //如果未装载,依据双亲委托机制,寻找父类加载器进行加载
            ClassLoader parent = this.getParent();
            try {
                loadedClass = parent.loadClass(name);
            } catch (Exception e) {
//                e.printStackTrace();
            }

            //如果父类加载器加载成功,则返回父类加载器加载的Class对象
            if (null != loadedClass) {
                return loadedClass;
            } else {
                //文件流读取返回字节数组
                byte[] classData = getClassData(name);
                //如果自己都加载失败的话直接抛出ClassNotFoundException异常
                if (null == classData) {
                    throw new ClassNotFoundException();
                } else {
                    //使用defineClass()加载类
                    loadedClass = defineClass(name, classData, 0, classData.length);
                }
            }
        } else {
            return loadedClass;
        }
        return loadedClass;
    }

    /**
     * 根据路径名称获取.class字节数组信息
     *
     * @param name 路径
     * @return
     */
    private byte[] getClassData(String name) {  //com.wsh.Test  d:/java/com/wsh/Test.class
        StringBuilder path = new StringBuilder(rootDir).append(File.separator).append(name.replace(".", File.separator)).append(".class");
        ByteArrayOutputStream byteArrayOutputStream = null;
        InputStream inputStream = null;
        try {
            inputStream = new FileInputStream(path.toString());
            byteArrayOutputStream = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                byteArrayOutputStream.write(buffer, 0, len);
            }
            return byteArrayOutputStream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != byteArrayOutputStream) {
                try {
                    byteArrayOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (null != inputStream) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}

测试类:

public class TestFileSystemClassLoader {
    public static void main(String[] args) {
        FileSystemClassLoader fileSystemClassLoader = new FileSystemClassLoader("d:/java");
        try {
            //加载d:/java/com/wsh/Test.class字节码信息
            Class<?> loaderClass = fileSystemClassLoader.loadClass("com.wsh.Test");
            //class com.wsh.Test
            System.out.println(loaderClass);
            //com.wsh.jvm.classloader.FileSystemClassLoader@677327b6
            System.out.println(loaderClass.getClassLoader());

            Object object = loaderClass.newInstance();
            //com.wsh.jvm.classloader.FileSystemClassLoader@677327b6
            System.out.println(object.getClass().getClassLoader());

            Class<?> loaderClass2 = fileSystemClassLoader.loadClass("com.wsh.jvm.classloader.Test");
            //由于双亲委托机制,在classpath下的类默认都会由AppClassLoader类加载器加载
            //sun.misc.Launcher$AppClassLoader@18b4aac2
            System.out.println(loaderClass2.getClassLoader());

            Class<?> loaderClass3 = fileSystemClassLoader.loadClass("java.lang.String");
            //因为Java核心类库是由引导类加载器BootStrapClassLoader进行加载,所以返回为null
            //null
            System.out.println(loaderClass3.getClassLoader());
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}
  • 注意点:因为类Test 本身可以被 AppClassLoader 类加载,因此我们不能把 Test.class 放在类路径下。否则,由于双亲委托机制的存在,会导致该类由AppClassLoader 加载,而不会通过我们自定义类加载器来加载。

四、自定义类加载器示例二

通过上面的示例,对自定义类加载器的流程已有初步的了解,接下来,我们再通过一个示例【自定义加密解密类加载器】加深对自定义类加载器的理解。

因为.class字节码文件是二进制文件,所以简单起见,实现对字节码取反的方式进行加密,然后使用自定义解密类加载器加载该类。

关于取反,我们可以通过异或的方式进行  xxxx ^ 0xff ,通过下图了解取反怎么取:

【第一步】编写加密方法:

    /**
     * 加密方法
     *
     * @param src  源文件路径
     * @param dest 目标文件路径
     */
    public static void encryptClass(File src, File dest) {
        InputStream fileInputStream = null;
        OutputStream fileOutputStream = null;

        try {
            fileInputStream = new FileInputStream(src);
            fileOutputStream = new FileOutputStream(dest);

            //接收长度
            int len;
            while ((len = fileInputStream.read()) != -1) {
                //通过异或操作对读取的输入流进行取反
                fileOutputStream.write(len ^ 0xff);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != fileOutputStream) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (null != fileInputStream) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

【第二步】对.class二进制字节码文件进行取反加密:

public class Test{ 
 public static void main(String[] args) {
        encryptClass(new File("d:/java/com/wsh/Test.class"), new File("d:/java/temp/com/wsh/Test.class"));
    }
}

执行完之后,在temp/com/wsh目录下就可以看到加密之后的字节码文件,下面我们就需要自定义解密类加载器去读取这个字节码文件:

【第三步】自定义解密类加载器

/**
 * @Description: 解密类加载器
 * @Author: weishihuai
 * @Date: 2019/1/16 21:36
 */
public class DecipherClassLoader extends ClassLoader {
    /**
     * 根目录路径
     */
    private String rootDir;

    public DecipherClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //1. 查找该类加载器是否已经装载这个类,如果已经装载,则直接返回该Class对象
        Class<?> loadedClass = findLoadedClass(name);
        if (null == loadedClass) {
            //如果未装载,依据双亲委托机制,寻找父类加载器进行加载
            ClassLoader parent = this.getParent();

            //这里加try-catch的原因是怕父类加载器加载失败之后,报错就不会执行下面的代码.
            try {
                loadedClass = parent.loadClass(name);
            } catch (Exception e) {
//                e.printStackTrace();
            }

            //如果父类加载器加载成功,则返回父类加载器加载的Class对象
            if (null != loadedClass) {
                return loadedClass;
            } else {
                //文件流读取返回字节数组
                byte[] classData = getClassData(name);
                //如果自己都加载失败的话直接抛出ClassNotFoundException异常
                if (null == classData) {
                    throw new ClassNotFoundException();
                } else {
                    //使用defineClass()加载类
                    loadedClass = defineClass(name, classData, 0, classData.length);
                }
            }
        } else {
            return loadedClass;
        }
        return loadedClass;
    }

    /**
     * 根据路径名称获取.class字节数组信息
     *
     * @param name 路径
     * @return
     */
    private byte[] getClassData(String name) {
        StringBuilder path = new StringBuilder(rootDir).append(File.separator).append(name.replace(".", File.separator)).append(".class");
        ByteArrayOutputStream byteArrayOutputStream = null;
        InputStream inputStream = null;
        byte[] data = null;
        try {
            inputStream = new FileInputStream(path.toString());
            byteArrayOutputStream = new ByteArrayOutputStream();
            int len;
            while ((len = inputStream.read()) != -1) {
                //对加密后的二进制文件再次取反就是解密
                byteArrayOutputStream.write(len ^ 0xff);
            }
            data = byteArrayOutputStream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != byteArrayOutputStream) {
                try {
                    byteArrayOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (null != inputStream) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return data;
    }
}

【第四步】测试解密类加载器

如果我们使用之前的FileSystemClassLoader来加载这个加密后的在字节码文件的话,会直接报错:

public class TestDecrptClassLoader {
    public static void main(String[] args) {
        FileSystemClassLoader fileSystemClassLoader = new FileSystemClassLoader("d:/java/temp");
        try {
            Class<?> clazz = fileSystemClassLoader.loadClass("com.wsh.Test");
            System.out.println(clazz);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

下面我们使用DecipherClassLoader来加载该类:

public class TestDecrptClassLoader {
    public static void main(String[] args) {
       DecipherClassLoader decipherClassLoader = new DecipherClassLoader("d:/java/temp");
        try {
            Class<?> clazz2 = decipherClassLoader.loadClass("com.wsh.Test");
            System.out.println(clazz2);
            System.out.println(clazz2.getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

由上图可见,已经成功通过自定义的解密类加载器加载了Test类。

五、总结

通过两个自定义类加载器,对自定义类加载器的流程有了进一步的认识,通常情况下,我们使用系统默认的类加载器即能满足大部分的需求,对于一些特殊的需求,那么我们可以通过自定义类加载器来满足特定的需求。本文是笔者对自定义类加载器的一些见解和认识,仅供大家学习参考,不对之处,希望大家多多指点。

猜你喜欢

转载自blog.csdn.net/Weixiaohuai/article/details/86509637