ClassLoader和类加载机制

1、背景

最近在做项目的过程中,由于系统需要提供一个对外接口,使系统使用者可以以脚本的形式提交自己的代码,每个用户可以在系统规范的约束下编写脚本,由系统去执行用户的代码,实现了热部署。什么叫热部署呢?简单来说就是把代码当成U盘或者外设一样即插即用,每个用户可以维护自己的解决方案(也就是一段脚本,一个单独的类),在更新修改解决方案的过程中而不需要重新编译启动整个系统。我们采用的方案就是GroovyClassLoader,我主要讲一讲自己对ClassLoader的理解和使用。

2、类加载与类加载器

类加载:

类加载的过程就是将Class文件中描述的各种信息加载到虚拟机中,供程序后期运行和使用的。
类加载的生命周期主要分为五个步骤:

1、加载:

通过一个类的全限定名来获取描述此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法去的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class 对象,作为方法区的各种数据类型的入口

2、验证

为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害到自身的安全。包括文件格式验证,元数据验证,字节码验证,符号引用验证。

3、准备

为变量分配内存,设置类变量的初始值。

4、解析

将常量池中的符号应用替代为直接引用。

5、初始化

是类加载生命周期的最后一个过程,执行类中定义的java程序代码

该过程如图所示:


0_1319366219Na16.gif
0_1319366219Na16.gif

类加载器:

在前面的类加载过程中,大部分动作都是完全由虚拟机主导和控制的。而类加载器使得用户可以在加载的过程中参与进来,结合前面的内容,类加载器就是将“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部来实现。将主动权交给程序猿。
类加载器和这个类本身确定了其在java虚拟机中的唯一性,每一个类加载器都有一个独立的类命名空间,也就意味着,如果比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就注定不相同。

java的类加载器:


0_1319366276S7Uf.gif
0_1319366276S7Uf.gif

1、Bootstrap Class Loader:负责加载JAVA_HOME/lib目录下或-Xbootclasspath指定目录的jar包;

2、Extention Class Loader:加载JAVA_HOME/lib/ext目录下的或-Djava.ext.dirs指定目录下的jar包。

3、System Class Loader:加载classpath或者-Djava.class.path指定目录下的类或jar包。

ClassLoader各司其职,加载在不同路径下的class文件,值得注意的是,类加载采用的是双亲委托的设计模式,即传入一个类限定名,逐层向上到Bootstrap Class Loader中查找,如果找到即返回,若没有找到,则在Extention Class Loader中找,若还没有找到则在System Class Loader下找,即classpath中,如果还没有找到,则调用findClass(name)方法,执行用户自己的类加载逻辑(可能在其他的地方)

ClassLoader中的几个重要的方法:

1、loadClass(String name, boolean resolve):加载类的方法,在jdk1.2以前需要重写该方法实现用户自己的逻辑,1.2以后为了向下兼容,仍然可以重写该方法,但是建议用户将自己的加载逻辑实现在findName(name)中。这样系统先向上寻找能否加载到该类,如果加在不到,将调用用户自定义的findName函数加载对象.

   /**
     * @param name    类名字
     * @param resolve  是否解析,如果只是想知道该class是否存在可以设置该参数为false
     * @return 返回一个class泛型
     * @throws ClassNotFoundException
     */
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        /**
         * getClassLoadingLock(name)
         * 为类的加载操作返回一个锁对象。为了向后兼容,这个方法这样实现:如果当前的classloader对象注册了并行能力,
         * 方法返回一个与指定的名字className相关联的特定对象,否则,直接返回当前的ClassLoader对象。
         */
        synchronized (getClassLoadingLock(name)) {
            // 首先查看class是否已经被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //如果父加载器不为空,则委托给父加载器去加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        /**
                         *  如果父加载器为空,说明父加载器已经是Bootstrap ClassLoader了,则直接使用根加载器加载,也就是使用虚拟机加
                         *  载器加载
                         */
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                //如果以上的加载器在自己的路径上面都没有加载到,则调用findClass(name)调用用户自定义的加载器
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            //根据resolve参数决定是否解析该类
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

2、ClassLoader getParent() :可以返回委托的父类加载器。在你自定义加载器找不到相应类的时候,可以调用此方法,不过在ClassLoader的默认实现中,ClassLoader先判断父类加载器是否可以加载,然后再调用用户自定义的findClass方法。

3、 resolveClass():若resolve参数为true的时候,我们需要调用该函数,resolve我们的classLoader。

4、ClassLoader getSystemClassLoader():提供了一个直接访问系统classloader的方法。

3、废话少说上代码!

下面我将以一个例子来阐述如何使用ClassLoader,自定义的ClassLoader将加载被加密的类,而且这个类存储的路径不在ClassPath中,也不可以被Bootstrap Class Loader和Extention Class Loader加载,在实际应用中,可以是网络中传递过来的加密字节流,抑或着是实现脚本的热部署操作。

package com.siyu;


import java.io.*;

public class ClassLoaderTest extends ClassLoader {
    //自定义加载器加载该路径下面的文件
    private String directory;

    public ClassLoaderTest(String directory) {
        this.directory = directory;
    }

    /**
     * 重写findClass,用户可以做以下的事情
     * 1.可以加载boot、ext、system加载器所加载不了的路径下的文件
     * 2.可以解密加密后的class文件
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //解密密钥
        byte key = (byte) 1;
        //加密文件的路径
        String fileName = directory + name + ".class";
        File file = new File(fileName);
        byte[] decryptedByte = readFromFile(file);
        //解密为原始的class文件
        for (int i = 0; i < decryptedByte.length; i++) {
            decryptedByte[i] = (byte) (decryptedByte[i] ^ key);
        }
        //defineClass实现了链接阶段的验证等
        return defineClass(null, decryptedByte, 0, decryptedByte.length);
    }


    private byte[] readFromFile(File fileName) {
        try {
            byte[] bytes = null;
            FileInputStream fin = new FileInputStream(fileName);

            int i;
            if ((i = fin.read()) != -1) {
                //初始化数组大小和文件大小一样
                bytes = new byte[fin.available()];
                fin.read(bytes);
            }
            return bytes;
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return null;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    private byte[] encrypt(byte[] bytes) {
        byte key = (byte) 1;
        //依次加密的代码
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = (byte) (bytes[i] ^ key); //利用异或加密
        }
        return bytes;
    }

    public void encryptFile(String fileName, String directory) {
        try {
            String name = fileName.substring(fileName.lastIndexOf("\\") + 1, fileName.length() - 6);
            //加密文件的路径
            String destFileName = directory + "encryted" + name + ".class";
            //如果加密文件不存在则创建加密文件
            File f = new File(destFileName);
            if (f == null) {
                f.createNewFile();
            }
            //加密
            byte[] encryptedByte = encrypt(readFromFile(new File(fileName)));
            FileOutputStream fos = new FileOutputStream(destFileName);
            //把加密后的字节写入到加密文件中
            fos.write(encryptedByte);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
        //设置加密路径
        ClassLoaderTest classLoaderTest=new ClassLoaderTest("C:\\EncryptedClass\\");
        //将test.class加密后存储到EncryptedClass目录下
        classLoaderTest.encryptFile("C:\\Users\\jasonchu.zsy\\IdeaProjects\\BoKeTest\\out\\production\\BoKeTest\\com\\siyu\\test.class"
                ,"C:\\EncryptedClass\\");
        try {
            Class<?> t=classLoaderTest.loadClass("encrytedtest");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}



在main函数中先将一个编译好的class文件加密后存储在非classpath路径下,然后用自定义classLoader进行加载,加密为了简单起见,使用的是异或加密,利用的原理是二进制的数经过两次异或操作后得到的值是相同的。路径也使用的绝对路径,大家可以根据需要自行进行修改,有什么问题可以继续交流,谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容