从代理模式再出发!ClassLoader初探

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

前面几篇文章或多或少涉及到了代理模式的方方面面,从静态代理到动态代理的介绍使用,到mybaties对代理模式的实际运用,我们会想到代理模式再深一层是什么?我们只会用jdkproxy,但是在java语言的视角中,代理模式又是怎么运行的? 这个系列文章将从代理模式再出发!探讨跟代理相关的classloader,jvm字节码等概念,最终的目标是实现一个自己的代理框架。这些文章不是面面俱到的大谈jvm,classloader等,只是一个总结,给的是一个研究的方向,自己也是刚刚入jvm的小白,多多总结,多多学习。

一.代理模式的原理

在我们写java代码的时候,ide帮我们将java文件编译成class文件,然后class文件会交给jvm虚拟机去运行,把java文件编译成class文件这一过程中就有许多学问。我们可以在提交给jvm虚拟机运行前修改class文件(通过asm框架等方法),这个灵活的特性可以帮我们实现很多功能,比如我们的代理模式也是因为插手了这一步,修改了class文件(对class文件动了手术),然后虚拟机接收到的文件已经是修改后的class文件,从而实现动态增强类功能,达到代理的目的。当然,在jdkproxy中,不是直接对被代理类动手术,而是创建了一个新类,这个新类实现了被代理类的接口,然后通过InvocationHandler对象实现动态代理功能。这样做既对被代理类达到无损的效果,又实现了功能。

二.ClassLoader初探

前面说到“java文件->class文件->class文件提交虚拟机”,ClassLoader 在这个过程中扮演者很重要的角色,它使得 Java 类可以被动态加载到 Java 虚拟机中并执行,java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个Java类,即java.lang.Class类的一个实例。在java系统中,ClassLoader不是只有一个,你可以自定义ClassLoader,只需要继承ClassLoader即可,为什么java设计允许多个ClassLoader呢?比如之前代理的需求,在你需要对某个class文件特殊处理时,你渴望拥有自己的ClassLoader,在一些代码加密,热部署之类的领域,可以写自己特定需求的ClassLoader就显得极为方便。后面我们会写自己的ClassLoader,先理解完概念。

  • 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自
    java.lang.ClassLoader。
  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java
    虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java
    类。一般来说,Java 应用的类都是由它来完成加载的。可以通过
    ClassLoader.getSystemClassLoader()来获取它。

这里写图片描述

在上图中,可以看出java系统也有好几层ClassLoader,它们似乎有联系,这就引出了“双亲委派模型”

双亲委派模型

每个ClassLoader有自己的父类,这个父类不是继承概念上的父类,而是类似于责任链模式的关系,每一个ClassLoader有自己的上级(ext 和 bootstrap classloader 除外,它是最顶级),双亲委派的工作原理就是 当一个类加载器收到类加载请求,它会先看看自己是否以前加载过该类,如果有,会直接返回上次加载过的,如果没有,则会给自己的上级类加载器加载,一级一级往上,直到父加载器说我找不到这个类,也就是加载不到这个类,这个类就会交给子加载器加载。

使用双亲委派模型有个很好的好处就是 不会不同的类加载器都加载同一个类,不然会造成程序混乱,因为对一个对象是否一样,除了它的类名和包名是否相等,还要考虑是否是同一个类加载器加载的。

刚刚都是通过概念了解classloader,现在通过代码示例加深对classloader的直观印象。

三.自己写一个ClassLoader

在写之前,我们先看一下classloader的入口方法,loadClass方法,如果你没有复写这个方法,默认就是双亲委派模型的形式,看源码:

synchronized (getClassLoadingLock(name)) {
            // 首先看看这个类是不是加载过,这里有个缓存机制
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                    // 如果有父加载器,就先调用父加载器的loadClass方法
                        c = parent.loadClass(name, false);
                    } else {
                    // 如果该类加载器已经没有父加载器,就直接找Bootstrap classloader,Bootstrap ClassLoader是用C++编写的,在Java中看不到它
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    //如果父加载器没法找到该类,那就只能自己来加载了,调用findClass方法查找。
                    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();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

整个过程就是双亲委派的代码实现,很简单,但是这个实现却保证了在java系统里不会出现一个类被同时两个类加载器加载的情况。

然后我们开始写一个简单的classloader:


public class SimpleClassLoader extends ClassLoader {

    //加载class类的入口靠的是这个方法,在双亲委派模型中,如果父加载器的findClass方法找不到类时,最后就会执行本方法。
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
//      bootstrap ext app 三个类加载器都找不到,最后还要我自己来找,那我也找不到,我就随意直接返回Test类吧
        InputStream map = Test.class.getResourceAsStream("Test.class");
        try {
            byte[] bytes = new byte[map.available()];
            map.read(bytes);
            return super.defineClass(Test.class.getName(), bytes, 0, bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    //    如果你的classloader不想破坏双亲委派模型,就不要自己复写
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return super.loadClass(name);
    }
}

测试类 Test:

public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        SimpleClassLoader simpleClassLoader = new SimpleClassLoader();
        Class<?> aClass = simpleClassLoader.loadClass("JDK.Array.Man1");
        System.out.println(aClass);
    }
}

simpleClassLoader 用空的构造器会默认关联 appclassloader ,这是默认 该线程关联的classloader,每个运行中的线程都有一个成员contextClassLoader,用来在运行时动态地载入其它类,使用

Thread.currentThread().getContextClassLoader() ;

可以知道当前是appclassloader作为线程上下文类加载器。

然后我们调用入口方法 loadClass ,里面写一个不存在的类名,这样根据双亲委派的规则,父加载器都不会加载到这个类,最后必须要用我们的findClass方法解决这个问题,再看一下我们复写的findClass方法,我们直接用Test测试类作为加载的类,最后有一个defineClass方法,把字节数组 中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。所以这个方法是加载器的核心方法,且不能继承复写。

最后我们可以发现加载的类是由simpleClassLoader加载
这里写图片描述

如果我们写的类名是真实存在的,换句话说 appclassloader能够加载到,那我们的simpleClassLoader就只是加载类的发起者,而真正的执行者却是appclassloader

simpleClassLoader.loadClass("JDK.classLoader.Test");

这里写图片描述

三.总结

我们在后面 自己实现动态代理的时候 需要用到calssloader,所以这篇文章只是大致的介绍,网上关于calssloader的文章也是数不胜数,感谢网上的一些资料,这里做一个学习的总结。

猜你喜欢

转载自blog.csdn.net/lovejj1994/article/details/78020814