前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >自定义类加载器

自定义类加载器

原创
作者头像
benym
发布2022-07-07 18:50:35
9910
发布2022-07-07 18:50:35
举报
文章被收录于专栏:后端知识体系后端知识体系

为什么需要自定义类加载器

自定义类加载器是从实际场景出发,解决一些应用上的问题,比如:

  1. 热部署、插件化类:常用的比如SpringBoot-devtoolsArthas等工具,其实现原理就用到了类加载机制
  2. 加密:有些核心代码不想公开,但又必须使用,可以通过加密类字节码的方式将编译后的加密代码交给类加载器加载,再采用某种解密算法将真正的类载入JVM,保证核心代码不被反编译泄漏
  3. 类隔离:在项目中可能不同的微服务用的某个类的版本不一样,某些应用依赖于特定版本的SDK功能,自定义类加载器可以解决某个同名的Class想要加载不同的版本的场景,实现同名Class多版本共存,相互隔离从而达到解决版本冲突的目的。如Java模块化规范 OSGi、蚂蚁金服的类隔离框架SOFAArk
  4. 非标准化来源加载代码:编译后的字节码在数据库、云端等情况

双亲委派模型

想要自定义类加载器,一定需要了解双亲委派模型

双亲委派模型加载class的步骤可为如下几步:

  1. 根据全限定类名(name)**判断类是否加载**,如果已经加载则直接返回已加载类。
  2. 如果没有加载,尝试**委托父类加载器**加载此类。同时,**父类加载器也会采用相同的策略**,查看是否自己已经加载该类,如果有就返回,没有就继续委托给父类进行加载,直到BootStrapClassLoader。如果父类加载器为null则代表使用BootStrapClassLoader进行加载。
  3. 父类无法加载,交给子类进行加载。按照从BootStrapClassLoader->ExtClassLoader->AppClassLoader->自定义类加载器的顺序依次尝试加载。

其加载class的核心方法loadClass源码如下:

代码语言:java
复制
    protected Class<?> loadClass(String name, boolean resolve)

        throws ClassNotFoundException

    {

        synchronized (getClassLoadingLock(name)) {

            // First, check if the class has already been loaded

            // 第一步,检查需要加载的这个类是否已经被加载过

            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 loade

                    // 捕获父类加载器无法加载的请求

                }



                if (c == null) {

                    // If still not found, then invoke findClass in orde

                    // to find the class.

                    long t1 = System.nanoTime();

                    // 如果父类加载器不能加载,就尝试采用子类加载器加载

                    c = findClass(name);



                    // this is the defining class loader; record the stats

                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);

                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);

                    PerfCounter.getFindClasses().increment();

                }

            }

            if (resolve) {

                // 链接指定类

                resolveClass(c);

            }

            return c;

        }

    }

下图更好的展示了双亲委派的过程:

image.png
image.png

双亲委派的好处

  1. 安全性。避免了用户自己编写的类与Java的核心类冲突,如自定义了java.lang.String.class类不会被系统加载,因为顶层启动类加载器会先于自定义加载器加载该类,防止核心API被修改
  2. 避免类的重复加载。如果父类已经加载过该类,则直接返回,在JVM中区分不同的类,不仅需要根据全限定名,且需要判断是否是同一个ClassLoader。相同的class文件被不同的ClassLoader加载就是不同的两个类。

自定义步骤

自定义类加载器只需要继承ClassLoader,同时覆盖findClass方法(而不是loadClass方法)即可

::: tip

Subclasses of ClassLoader are encouraged to override findClass(String), rather than this method.

Unless overridden, this method synchronizes on the result of getClassLoadingLock method during the entire class loading process. 官方推荐

:::

准备两个类,一个类作为实体,一个类作为Service

TestUser.java

代码语言:java
复制
public class TestUser {



    private String name;



    public TestUser() {

    }



    public TestUser(String name) {

        this.name = name;

    }



    public String getName() {

        return name;

    }



    public void setName(String name) {

        this.name = name;

    }



    @Override

    public String toString() {

        return "========这是User测试文件1号========";

    }

}

TestService.java

代码语言:java
复制
public class TestService {



    public String testPrint(String name) {

        String result = name + "调用当前方法";

        System.out.println(name + "调用当前方法");

        return result;

    }

}

自定义类加载器CustomClassLoader.java

代码语言:java
复制
package com.test.custom;



import java.io.FileInputStream;





public class CustomClassLoader extends ClassLoader {



    private String classPath;



    public CustomClassLoader() {

    }



    public CustomClassLoader(String classPath) {

        this.classPath = classPath;

    }



    private byte[] loadByte(String name) throws Exception {

        name = name.replaceAll("\\.", "/");

        String a = classPath + "/" + name + ".class";

        FileInputStream fileInputStream = new FileInputStream(classPath + "/" + name + ".class");

        int len = fileInputStream.available();

        byte[] data = new byte[len];

        fileInputStream.read(data);

        fileInputStream.close();

        return data;

    }



    @Override

    protected Class<?> findClass(String name) throws ClassNotFoundException {

        try {

            byte[] data = loadByte(name);

            //defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。

            return defineClass(name, data, 0, data.length);

        } catch (Exception e) {

            e.printStackTrace();

        }

        return super.findClass(name);

    }

}

自定义的函数很简单,将全限定名的class文件加载为字节码数组,然后传入defineClass方法进行加载,defineClass的作用是将一个字节数组转化为Class对象

POJO测试类LoadCustomPojoTest

需要注意的是**要加载的Class必须在全限定名一样的目录**下进行javac编译,以为package必须一致,比如com.test.custom.pojo.TestUserTestUser必须在com.test.custom.pojo目录下编译生成字节码

代码语言:java
复制
package com.test.custom;





public class LoadCustomPoJoTest {



    public static void main(String[] args) throws Exception {

        // 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父类加载器设置为应用程序类加载器AppClassLoade

        CustomClassLoader classLoader = new CustomClassLoader(

                "E:/ideaProject/custom-classloader/src");

        // 从磁盘中创建一个目录,将要加载的类的class放入目录

        // Class.forName效果和classLoader.loadClass一致

        Class<?> clazz = Class.forName("com.test.custom.pojo.TestUser", true, classLoader);

        Object obj = clazz.newInstance();

        System.out.println(obj.toString());

        System.out.println(clazz.getClassLoader());



        System.out.println();

        CustomClassLoader classLoader2 = new CustomClassLoader(

                "E:/ideaProject/custom-classloader/src");

        Class clazz1 = Class.forName("com.test.custom.pojo.repeat.TestUser", true, classLoader2);

        Object obj1 = clazz1.newInstance();

        System.out.println(obj1.toString());

        System.out.println(clazz1.getClassLoader());

    }

}

我们测试两个同名的Class对象共存的情况,两个TestUser只在toString方法打印的**数据**和**包名**不一样

必须采用不同的类加载器加载同名对象,否则同一个类加载器会以第一次加载的对象为准

代码语言:java
复制
com.test.custom.pojo.TestUse



package com.test.custom.pojo;



public class TestUser {

    // ......

    public String toString() {

        return "========这是User测试文件1号========";

    }

}
代码语言:java
复制
package com.test.custom.pojo.repeat;



public class TestUser {

    // ......

    public String toString() {

        return "========这是User测试文件2号========";

    }

}
测试POJO结果
代码语言:java
复制
========这是User测试文件1号========

com.test.custom.CustomClassLoader@2133c8f8



========这是User测试文件2号========

com.test.custom.CustomClassLoader@30c7da1e

可以看到,两个类加载器是不一样的,且两个类的方法都已经打印了,说明此时同名类不同版本共存。

Service测试类LoadCustomMethodTest

构建一个带参数的Service方法,加载到方法之后,利用反射进行调用

代码语言:java
复制
package com.test.custom;



import java.lang.reflect.Method;





public class LoadCustomMethodTest {



    public static void main(String[] args) throws Exception {

        // 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父类加载器设置为应用程序类加载器AppClassLoade

        CustomClassLoader classLoader = new CustomClassLoader(

                "E:/ideaProject/custom-classloader/src");

        // 从磁盘中创建一个目录,将要加载的类的class放入目录

        Class clazz = classLoader.loadClass("com.test.custom.service.TestService");

        Object obj = clazz.newInstance();

        Method method = clazz.getDeclaredMethod("testPrint", String.class);

        method.invoke(obj, "李四");

        System.out.println(clazz.getClassLoader());

    }

}
测试Service结果
代码语言:java
复制
李四调用当前方法

com.test.custom.CustomClassLoader@49e4cb85

参考资料

https://www.cnblogs.com/twoheads/p/10143038.html

https://blog.csdn.net/liubenlong007/article/details/103593443

https://www.cnblogs.com/lichmama/p/12858517.html

https://www.cnblogs.com/huangjianping/p/14968403.html

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么需要自定义类加载器
  • 双亲委派模型
  • 双亲委派的好处
  • 自定义步骤
    • POJO测试类LoadCustomPojoTest
      • 测试POJO结果
        • Service测试类LoadCustomMethodTest
          • 测试Service结果
          • 参考资料
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档