一、背景

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

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

利用工厂模式和模板办法模式,在有新的加密办法时,咱们能够通过增加新的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@Componentpublic 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