• Java自定义ClassLoader加载外部类


    前言废话

      前几天在GitHub上下载了一个开源项目想要运行起来,启动这个项目会关联到数据库的一些表。因此想要运行起来还需要把数据库表建好。但是这个项目涉及到几十张表,并且作者没有给建表的SQL语句。(后续:吐血中,作者在项目给了sql语句。。。。。)

      想用这个项目那就只好自己建表了,但是几十张表自己建一时半会儿也弄不好。有这时间还不如重新找一个新项目于是果断放弃了这个项目,结果找了一圈发现这种类型的开源Java项目还真是少。于是,没办法还是回到这个项目。但是想到要手动创建几十张表就头皮发麻,那能不能自动生成SQL呢?根据Java对象生成sql语句,大概思路还是简单的。

      要实现这个想法关键就是要获取到Java的class对象,利用反射获取到类名,字段信息根据这些信息就可以生成建表SQL了;但是这段代码只能放到这个项目执行;想要把这段代码独立出来就不行,比如我将这段代码放到一个新建项目上,想利用Class.forName(“”)获取开源项目里的类就获取不到。因为项目原生的classLoader加载不到外部的类;想要加载到外部指定的class类,那只能自定义类加载器ClassLoader了;

      之前对类加载过程有一点点了解,知道一些名词比如双亲委派模型,类的加载有哪些阶段。但仅凭这些完全不知道该怎么样去自定义ClassLoader?于是百度搜了代码修修改改之后能跑,并且代码很短,比想象中简单。最简单的自定义ClassLoader只需要我们做一件事就是指定要加载哪些类,其余的事情都交给父类去加载。因为自定义一个简单的ClassLoader比较简单,所以有了了解这方面知识的动力。下面会简单介绍类加载过程,双亲委派模型。(内容出自《深入理解Java虚拟机(第四版)-周志明》第7章:虚拟机类加载机制)

    类生命周期


       类加载全过程包括:【加载】,【验证】,【准备】,【解析】,【初始化】;

    加载(Loading)

       在【加载】阶段,虚拟机需要完成三件事:

    • 通过类的全限定类名获取到二进制的字节流;
    • 将字节流所代表的静态存储结构,转换成方法区的运行时结构;
    • 在内存中生成一个Class对象;

       简单来说,在【加载】阶段就是通过全限定类名将class的二进制文件加载到内存,并生成Class对象;

    验证(Verification)

       这一阶段的主要目的是为了确保加载的二进制流中的信息是符合当前虚拟机的要求,并且不会对自身造成危害。比如纯Java代码,不能访问数组边界以外的数据,不能将它转换为一个未实现的类,如果这样做了编译器将拒绝编译。

      Class文件不一定由Java源码编译,可以使用任何途径产生,包括使用十六进制编译器直接写出来产生class文件。在字节码层面,上诉Java源码无法做到的都是可以实现的,至少语义上是可以表达出来的。虚拟机如果不检查输入的字节流很可能会因为载入了有害的字节流而导致系统崩溃,因此验证是虚拟机对自身保护的一项重要工作。验证主要分为4个阶段:文件格式验证,元数据验证,字节码验证,符号引用验证。

    【文件格式验证】

    • 是否以魔数0xCAFEBABE开头;
    • 主次版本号是否在当前虚拟机处理范围内;(这个报错就遇到过,如果较低版本的虚拟机处理高版本的class文件就会报错);
    • 常量池中是否有不被支持的类型

    还有很多项,就不一一列举了。这个阶段验证保证了格式上符合Java类型信息的要求。

    【元数据验证】

    • 这个类是否有父类(除了Object之外,其他类都有父类);
    • 这个类的父类是否继承不允许被继承的类(被final修饰的类);
    • 如果这个类不是抽象类,是否实现了父类或接口中要求实现的方法。
    • 。。。。。。

    这个阶段对元数据信息进行语义校验,保证了不在村不符合Java语义的元数据信息

    【字节码验证】

    • 保证操作数栈的数据类型与指令代码序列能配合工作;【人话:不会出现类似这样的情况:在操作栈放了一个int类型数据,使用时却按照long类型加载入本地变量中】
    • 保证方法体中的类型转化是有效的;
    • 。。。。。。。。

    如果一个方法没有通过字节码验证,那肯定有问题;但通过了校验也不一定是安全的。比如一个无限递归,就能使栈内存溢出;这个阶段主要是检查方法体,保证方法在运行时不会做出危害虚拟机的安全事件。

    【符号引用验证】

    • 符号引用中通过字符串描述的全限定性类名是否能找到类;
    • 符号引用中的类,字段,方法的访问性:private,protected,public,default是否可能被当前类访问;
    • 。。。。。

    符号引用验证的目的是为了确保后面的【解析】动作能够正常执行,如果无法通过引用验证那么将会抛出异常,比如:NoSuchFieldError,NoSuchMethodError,IllegalAccessError…。对虚拟机的类加载机制来说,验证阶段是非常重要的阶段,但不一定是必须的。如果运行的代码都已经被反复使用和验证过,那么在部署项目的时候就可以考虑使用 -Xverify:none 参数来关闭大部分类的验证措施,以缩短虚拟机类加载的时间。

    准备(Preparation)

       【准备】阶段是正式为类变量分配内存并设置类变量初始值的阶段,使用的内存都在方法区(jdk1.8是元数据区)。首先,这个阶段进行内存分配的仅包括类变量(被static修饰的变量)而不包括实例变量。实例变量会在对象实例化随着分配到堆内存。其次,初始值通常情况下,是数据类型的零值。比如:public static int val = 111;初始值是0,而不是111;如果被final修饰,public static final int val = 111;,那么val值就是111;

    解析(Resolution)

      【解析】阶段是虚拟机将常量池内的符号引用替换成直接引用的过程;符号引用以一组符号来描述锁引用的目标,符号可以是任意形式的字面量,只要使用时能准确定位引用目标即可。所谓符号引用,只是一个符号而已,只是告知jvm,此类需要哪些调用方法,引用或者继承哪些类等等信息。但是JVM在使用这些资源的时候,只有这些符号是不行的,必须详细知道这些资源的地址,才能正确地调用相关资源。直接引用,就是这样一类指针,它直接指向目标。解析过程,就是完成将符号引用转化为直接引用的过程,方便后续资源的调用。【引用自博客园

    初始化(Initalization)

      初始化是类加载的最后一个过程,是执行构造方法的阶段。在这个阶段会在堆内存分配一块内存实例化对象;

    类加载器

       通过全限定类名获取类的二进制字节流,实现这个动作的就是类加载器;类加载器有3类:启动类加载器(Bootstrap ClassLoader),拓展类加载器(Extension ClassLoader 【sun.misc.Launcher$ExtClassLoader】),应用程序类加载器(Application ClassLoader 【sun.misc.Launcher$AppClassLoader】) 。

    双亲委派模型

      加载机制:先检查是否已经被加载–> 没有被加载则调用父类的加载器。若父类为空则调用启动类的加载器。如果父类加载失败,则由自己的加载器加载。

    // ClassLoader类的loadClass
    
        protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            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 {//父类加载器为空,调用启动类的加载器
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        // ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                    }
    
    		//c == null ,说明父类没有加载成功,应该是抛异常了;调用自己的加载器;
                    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();
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    自定义ClassLoader的2种方式

      前面讲了类加载的过程有哪些阶段?这些阶段都干了什么,可以看出类加载过程非常复杂。自定义ClassLoader有2种方式,在自定义ClassLoader的时候可以由自己决定是否破坏双亲委派模型;

    • 继承URLClassLoader
        上面提到的拓展类加载器ExtClassLoader,应用程序类加载器AppClassLoader都是继承的URLClassLoader;我们自定义ClassLoader也可以继承URLClassLoader,然后调用URLClassLoader的loadClass(String name)方法来加载类就可以了。在调用loadClass方法时指定class文件的位置,URLClassLoader的loadClass方法就会完成类加载的全过程,这种方式不会破坏双亲委派模型。因为URLClassLoader没有loadClass方法,这个方法是继承了ClassLoader的loadClass方法;因此加载类的模型还是双亲委派模型。
    public class MyClassLoader  extends URLClassLoader {
        public MyClassLoader(URL[] urls){
            super(urls,getSystemClassLoader());
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    测试类:

        @Test
        public void test() throws Exception {
            //指定加载的class文件路径;
            /**
             * 比如一个class文件的位置:
             * E:/ASD/SDF/GRY/WE/RT/TYU/com/org/entry/Account.class
             * Account类的类路径:com.org.entry.Account
             * 那么root = E:/ASD/SDF/GRY/WE/RT/TYU
             */
            String root  = "/E:/Project/IDEA/2022/javaSEtest/target/classes";
           //创建一个类加载器
            MyClassLoader classLoader = new MyClassLoader(new URL[]{new File("file:"+root).toURI().toURL()});
           //根据类名加载类
            Class bean = classLoader.loadClass("ioc.bean.BeanDefinition");//类全限定名;
    
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

      自定义类加载器代码:GitHub

    • 继承ClassLoader

      继承ClassLoader,重写findClass方法;利用ClassLoader的defineClass方法将二进制的字节码解析成Class对象;这种方式在生成Class对象时还是会先调用ClassLoader的loadClass方法(可以在loadClass中debug调试),因此不会破坏双亲委派机制。(像要破坏双亲委派机制,就重写loadClass方法,去掉找父类加载器的逻辑,直接用下面findClass方法生成Class对象。)

    
    
    public class MyClassLoader  extends  ClassLoader{
    
    
       private String classpath;
       public  MyClassLoader(String classpath){
                this.classpath = classpath;
        }
    
        @Override
        protected Class<?> findClass(String name)  {
            String path = name.replaceAll("\\.","/");
            try{
                byte[] classBytes = getClassBytes(classpath+"/"+path+".class");
                return defineClass(name,classBytes,0,classBytes.length);
            }catch (Throwable e){
                throw new RuntimeException("load class error:"+name);
            }
        }
    
    
        private byte[] getClassBytes(String path){
            try(InputStream fis = new FileInputStream(path); ByteArrayOutputStream classBytes = new ByteArrayOutputStream()){
                byte[] buffer = new byte[1024];
                int len = 0;
                while ((len = fis.read(buffer)) != -1) {
                    classBytes.write(buffer, 0, len);
                }
                return classBytes.toByteArray();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    测试

        @Test
        public void test() throws Exception{
           
            String root = "E:/Project/IDEA/spring-mini-0819/ioc-aop/target/classes";
            MyClassLoader  classLoader = new MyClassLoader (root);
            //要加载的类名
            Class<?> beanClass = classLoader.findClass("ioc.bean.BeanDefinition");
            Object o = beanClass.newInstance();
            System.out.println(o.getClass().getClassLoader());
        }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  • 相关阅读:
    为什么电力公司很少用轨道式的电表?
    QuantLib学习笔记——看看几何布朗运动有没有股票走势的感觉
    Linux CentOS 8(用户管理)
    〖全域运营实战白宝书 - 运营角色认知篇③〗- 运营的底层逻辑是什么?
    探索 LinkedList 原理
    Unsupervised Medical Image Translation with Adversarial Diffusion Models
    [Qt][C++]static与extern关键字
    《设计模式:可复用面向对象软件的基础》——行为模式(3)(笔记)
    中忻嘉业:无货源抖音小店核心玩法
    第二章:数字类型(下)
  • 原文地址:https://blog.csdn.net/m0_37550986/article/details/127624209