关于java:JVM自定义类加载器在代码扩展性的实践

44次阅读

共计 10612 个字符,预计需要花费 27 分钟才能阅读完成。

一、背景

名单管理系统是手机上各个模块将须要管控的利用配置到文件中,而后下发到手机上进行利用管控的零碎,比方各个利用的耗电量管控;各个模块的管控利用文件思考到平安问题,有本人的不同的加密形式,依照以往的教训,咱们能够利用模板办法 + 工厂模式来依据模块的类型来获取到不同的加密办法。代码类层次结构示意如下:

获取不同加密办法的类结构图

利用工厂模式和模板办法模式,在有新的加密办法时,咱们能够通过增加新的 handler 来满足 ” 对批改敞开,对扩大凋谢 ” 的准则,然而这种形式不可避免的须要批改代码和须要从新发版本和上线。那么有没有更好的形式可能去解决这个问题,这里就是咱们明天要重点讲的主题。

二、类加载的机会

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经验加载(Loading)、验证(Verification)、筹备(Preparation)、解析(Resolution)、初始化(Initialization)、应用(Using)和卸载(Unloading)七个阶段,其中验证、筹备、解析三个局部统称为连贯(Linking)。这七个阶段的产生程序如图 1 所示。

尽管 classloader 的加载过程有简单的 7 步,但事实上除了加载之外的四步,其它都是由 JVM 虚拟机管制的,咱们除了适应它的标准进行开发外,可能干涉的空间并不多。而加载则是咱们管制 classloader 实现非凡目标最重要的伎俩了。也是接下来咱们介绍的重点了。

三、加载

“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段。在加载阶段,Java 虚拟机须要实现以下三件事件:

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

《Java 虚拟机标准》对这三点没有进行特地具体的要求,从而留给虚拟机实现与 Java 利用的灵便度都是相当大的。例如“通过一个类的全限定名来获取定义此类的二进制字节流”这条规定,它并没有指明二 进制字节流必须得从某个 Class 文件中获取,确切地说是基本没有指明要从哪里获取、如何获取。比方咱们能够从 ZIP 压缩包中读取、从网络中获取、运行时计算生成、由其余文件生成、从数据库中读取。也能够能够从加密文件中获取。

从这里咱们能够看出,只须要咱们可能获取到加密类的.class 文件,咱们就能够通过类加载器获取到对应的加密类 class 对象,进而通过反射去调用具体的加密办法。因而类加载器在.class 文件的加载过程有着至关重要的位置。

四、双亲委派模型

目前 Java 虚拟机曾经存在三品种加载器,别离为启动类加载器、扩大类加载器和应用程序类加载器;绝大多数的 Java 程序都会应用这三品种加载器进行加载。

4.1 启动类加载器

这个类由 C ++ 实现,负责加载寄存在 \lib 目录,或者被 -Xbootclasspath 参数所指定的门路中寄存的,而且是 Java 虚拟机可能辨认的(依照文件名辨认,如 rt.jar、tools.jar,名字不合乎的类库即便放在 lib 目录中也不会被加载)类库加载到虚拟机的内存中。启动类加载器无奈被 Java 程序间接援用,用户在编写自定义类加载器时,如果须要把加载申请委派给疏导类加载器去解决,那间接应用 null 代替即可。

4.2 扩大类加载器

这个类加载器是在类 sun.misc.Launcher$ExtClassLoader 中以 Java 代码的模式实现的。它负责加载 \lib\ext 目录中,或者被 java.ext.dirs 零碎变量所指定的门路中所有的类库。依据“扩大类加载器”这个名称,就能够推断出这是一种 Java 零碎类库的扩大机制,JDK 的开发团队容许用户将具备通用性的类库搁置在 ext 目录里以扩大 Java SE 的性能,在 JDK9 之后,这种扩大机制被模块化带来的人造的扩大能力所取代。因为扩大类加载器是由 Java 代码实现的,开发者能够间接在程序中应用扩大类加载器来加载 Class 文件。

4.3 应用程序类加载器

这个类加载器由 sun.misc.Launcher$AppClassLoader 来实现。因为应用程序类加载器是 ClassLoader 类中的 getSystemClassLoader()办法的返回值,所以有些场合中也称它为“零碎类加载器”。它负责加载用户类门路(ClassPath)上所有的类库,开发者同样能够间接在代码中应用这个类加载器。如果应用程序中没有自定义过本人的类加载器,个别状况下这个就是程序中默认的类加载器。

因为现有的类加载器加载门路都有非凡的要求,本人所编译的加密类所产生的.class 文件所寄存的门路不在三个现有类加载器的门路外面,因而咱们有必要本人定义类加载器。

五、自定义类加载器

除了根类加载器,所有类加载器都是 ClassLoader 的子类。所以咱们能够通过继承 ClassLoader 来实现本人的类加载器。

ClassLoader 类有两个要害的办法:

  • protected Class loadClass(String name, boolean resolve):name 为类名,resove 如果为 true,在加载时解析该类。
  • protected Class findClass(String name):依据指定类名来查找类。

所以,如果要实现自定义类,能够重写这两个办法来实现。但举荐重写 findClass 办法,而不是重写 loadClass 办法,重写 loadClass 办法可能会毁坏类加载的双亲委派模型,因为 loadClass 办法外部会调用 findClass 办法。

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 loader
                }
 
                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;
        }
    }

loadClass 加载办法流程:

  • 判断此类是否曾经加载;
  • 如果父加载器不为 null,则应用父加载器进行加载;反之,应用根加载器进行加载;
  • 如果后面都没加载胜利,则应用 findClass 办法进行加载。

所以,为了不影响类的加载过程,咱们重写 findClass 办法即可简略不便的实现自定义类加载。

六、代码实现

6.1 实现自定义的类加载器

public class DynamicClassLoader extends ClassLoader {
 
    private static final String CLASS_EXTENSION = "class";
 
    @Override
    public Class<?> findClass(String encryptClassInfo) {EncryptClassInfo info = JSON.parseObject(encryptClassInfo, EncryptClassInfo.class);
        String filePath = info.getAbsoluteFilePath();
        String systemPath = System.getProperty("java.io.tmpdir");
        String normalizeFileName = FilenameUtils.normalize(filePath, true);
        if (StringUtils.isEmpty(normalizeFileName) || !normalizeFileName.startsWith(systemPath)
                ||getApkFileExtension(normalizeFileName) == null
                || !CLASS_EXTENSION.equals(getApkFileExtension(normalizeFileName))) {return null;}
 
        String className = info.getEncryptClassName();
        byte[] classBytes = null;
        File customEncryptFile = new File(filePath);
        try {Path path = Paths.get(customEncryptFile.toURI());
            classBytes = Files.readAllBytes(path);
        } catch (IOException e) {log.info("加密谬误", e);
        }
        if (classBytes != null) {return defineClass(className, classBytes, 0, classBytes.length);
        }
        return null;
    }
 
    private static String getApkFileExtension(String fileName) {int index = fileName.lastIndexOf(".");
        if (index != -1) {return fileName.substring(index + 1);
        }
        return null;
    }
}

这里次要是通过集成 ClassLoader,复写 findClass 办法,从加密类信息中获取到对应的.class 文件信息,最初获取到加密类的对象

6.2 .class 文件中的 encrypt()办法

public String encrypt(String rawString) {
        String keyString = "R.string.0x7f050001";
        byte[] enByte = encryptField(keyString, rawString.getBytes());
        return Base64.encode(enByte);
    }

6.3 具体的调用

public class EncryptStringHandler {private static final Map<String, Class<?>> classMameMap = new HashMap<>();
 
    @Autowired
    private VivofsFileHelper vivofsFileHelper;
 
    @Autowired
    private DynamicClassLoader dynamicClassLoader;
 
    public String encryptString(String fileId, String encryptClassName, String fileContent) {
        try {Class<?> clazz = obtainEncryptClass(fileId, encryptClassName);
            Object obj = clazz.newInstance();
            Method method = clazz.getMethod("encrypt", String.class);
            String encryptStr = (String) method.invoke(obj, fileContent);
            log.info("原字符串为:{}, 加密后的字符串为:{}", fileContent, encryptStr);
            return encryptStr;
        } catch (Exception e) {log.error("自定义加载器加载加密类异样", e);
            return null;
        }
    }
 
    private Class<?> obtainEncryptClass(String fileId, String encryptClassName) {Class<?> clazz = classMameMap.get(encryptClassName);
        if (clazz != null) {return clazz;}
 
        String absoluteFilePath = null;
        try {String domain = VivoConfigManager.getString("vivofs.host");
            String fullPath = domain + "/" + fileId;
            File classFile = vivofsFileHelper.downloadFileByUrl(fullPath);
            absoluteFilePath = classFile.getAbsolutePath();
            EncryptClassInfo encryptClassInfo = new EncryptClassInfo(encryptClassName, absoluteFilePath);
            String info = JSON.toJSONString(encryptClassInfo);
            clazz = dynamicClassLoader.findClass(info);
            // 设置缓存
            Assert.notNull(clazz, "自定义类加载器加载加密类异样");
            classMameMap.put(encryptClassName, clazz);
            return clazz;
        } finally {if (absoluteFilePath != null) {FileUtils.deleteQuietly(new File(absoluteFilePath));
            }
        }
    }
}

通过上述代码的实现,咱们能够通过在治理平台增加编译好的.class 文件,最初通过自定义的类加载器和反射调用办法,来实现具体方法的调用,防止了咱们须要批改代码和从新发版来适应一直新增加密办法的问题。

七、问题

下面的代码在本地测试时,没有呈现任何异样,然而部署到测试服务器当前呈现了 JSON 解析异样,看上去貌似是 json 字符串的格局不对。

json 解析逻辑次要存在于 DynamicClassLoader#findClass 办法入口处的将字符串转换为对象逻辑,为什么这里会报错,咱们在入口处打印了入参。

发现这里除了咱们须要的正确的入参 (第一个入参信息打印) 外,还多了一个 Base64 的全路径名 cn.hutool.core.codec.Base64。呈现这种状况,阐明因为咱们重写了 ClassLoader 的 findClass 办法,而 Base64 加载的时候会调用原始的 ClassLoader 类的 loadClass 办法去加载,并且外面调用了 findClass 办法,因为 findClass 曾经被重写,所以就会报下面的 json 解析谬误。

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 loader
                }
 
                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;
        }
    }

然而这里冀望的是除了用于加密的.class 文件用自定义类加载器进行以外,不心愿其余的类用自定义类加载器加载,通过对 ClassLoader#loadClass 办法剖析,那么咱们就心愿是否通过其父类加载器加载到 Base64 这个三方类。因为启动类加载器 Bootstrap Class Loader 必定不能加载到 Base64,所以咱们须要显示的设置父类加载器,然而这个父类加载器到底设置为哪一个类加载器,那么就须要咱们理解 Tomcat 类加载器构造。

为什么 Tomcat 须要在 JVM 根底之上做一套类加载构造,次要是为了解决如下问题:

  • 部署在同一个服务器上的两个 web 应用程序所应用的 Java 类库能够实现互相隔离;
  • 部署在同一个服务器上的两个 web 应用程序所应用的 Java 类库能够实现共享;
  • 服务器须要尽可能保障本身平安,服务器所应用的类库应该与应用程序的类库互相独立;
  • 反对 JSP 利用的 Web 服务器,大对数须要反对 HotSwap 性能。

为此,tomcat 扩大出了 Common 类加载器 (CommonClassLoader)、Catalina 类加载器(CatalinaClassLoader)、Shared 类加载器(SharedClassLoader) 和 WebApp 类加载器 (WebAppClassLoader),他们别离加载 /commons/_、/server/_、/shared/ 和 /WebApp/WEB-INF/中的 Java 类库的逻辑。

通过剖析,咱们晓得 WebAppClassLoader 类加载器能够加载到 /WEB-INF/目录下的依赖包,而咱们所依赖的类 cn.hutool.core.codec.Base64 所在的包 hutool-all-4.6.10-sources.jar 就存在于 /WEB-INF/目录上面,并且咱们自定义类加载器所在的包 vivo-namelist-platform-service-1.0.6.jar 也在 /WEB-INF/* 下,所以自定义类加载器 DynamicClassLoader 也是 WebAppClassLoader 加载的。

咱们能够写一个测试类测试一下:

@Slf4j
@Component
public class Test implements ApplicationListener<ContextRefreshedEvent> {
 
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {log.info("classLoader DynamicClassLoader:" + DynamicClassLoader.class.getClassLoader().toString());
    }
}

测试后果:

所以咱们能够设置自定义类加载器 DynamicClassLoader 的父加载器为加载其自身的类加载器:

public DynamicClassLoader() {super(DynamicClassLoader.class.getClassLoader());
}

咱们再次执行文件的加解密操作时,曾经没有发现报错,并且通过增加日志,咱们能够看到加载类 cn.hutool.core.codec.Base64 对应的类加载器的确为加载 DynamicClassLoader 对应的类加载器 WebAppClassLoader。

public String encrypt(String rawString) {log.info("classLoader Base64:{}", Base64.class.getClassLoader().toString());
        String keyString = "R.string.0x7f050001";
        byte[] enByte = encryptField(keyString, rawString.getBytes());
        return Base64.encode(enByte);
    }

当初再来思考一下,为什么在 IDEA 运行环境下不须要设置自定义类加载器的父类加载器就能够加载到 cn.hutool.core.codec.Base64。

在 IDEA 运行环境下增加如下打印信息:

public String encrypt(String rawString) {System.out.println("类加载器详情...");
        System.out.println("classLoader EncryptStrategyHandler:" + EncryptStrategyHandlerH.class.getClassLoader().toString());
        System.out.println("classLoader EncryptStrategyHandler:" + EncryptStrategyHandlerH.class.getClassLoader().getParent().toString());
        String classPath = System.getProperty("java.class.path");
        System.out.println("classPath:" + classPath);
        System.out.println("classLoader Base64:" + Base64.class.getClassLoader().toString());
        String keyString = "R.string.0x7f050001";
        byte[] enByte = encryptField(keyString, rawString.getBytes());
        return Base64.encode(enByte);
    }

发现加载.class 文件的类加载器为自定义类加载器 DynamicClassLoader,并且.class 加载器的父类加载器为利用类加载器 AppClassLoader,加载 cn.hutool.core.codec.Base64 的类加载器也是 AppClassLoader。

具体的加载流程如下:

1)先由自定义类加载器委托给 AppClassLoader;

2)AppClassLoader 委托给父类加载器 ExtClassLoader;

3)ExtClassLoader 再委托给 BootStrapClassLoader,然而 BootClassLoader 无奈加载到,于是 ExtClassLoader 本人进行加载,也无奈加载到;

4)再由 AppClassLoader 进行加载;

AppClassLoader 会调用其父类 UrlClassLoader 的 findClass 办法进行加载;

5)最终从用户类门路 java.class.path 中加载到 cn.hutool.core.codec.Base64。

由此,咱们发现在 IDEA 环境上面,自定义的加密类.class 文件中依赖的三方 cn.hutool.core.codec.Base64 是能够通过 AppClassLoader 进行加载的。

而在 linux 环境上面,通过近程调试,发现初始时加载 cn.hutool.core.codec.Base64 的类加载器为 DynamicClassLoader。而后委托给父类加载器 AppClassLoader 进行加载,依据双亲委派原理,后续会交由 AppClassLoader 本人进行解决。然而在用户门路下依然没有找到类 cn.hutool.core.codec.Base64,最终交由 DynamicClassLoader 进行加载,最终呈现了最开始的 JSON 解析谬误。

八、总结

因为类加载阶段没有严格限度如何获取一个类的二进制字节流,因而给咱们提供一个通过自定义类加载器来动静加载.class 文件实现代码可扩展性的可能。通过灵便自定义 classloader,也能够在其余畛域施展重要作用, 例如实现代码加密来防止外围代码透露、解决不同服务依赖同一个包的不同版本所引起的抵触问题以及实现程序热部署来防止调试时频繁重启利用。

九、参考资料

1、《深刻了解 Java 虚拟机:JVM 高级个性与最佳实际(第 3 版)》

2、史上最强 –Java 类加载器的原理及利用

作者:vivo 互联网服务器团队 -Wang Fei

正文完
 0