Java类隔离应用多Jar包支持
案例需求
现在有一个"统一管理平台",用于统一对接三方平台,屏蔽相同业务三方平台的差异性,减少内部平台对接的成本。正常情况下三方平台提供的 SDK 是通用的(和内部平台无关),但是有一些比较特殊的三方(假如是三方平台 A),他提供的 SDK 是给内部平台定制的。
这时就需要根据访问"统一管理平台"的内部平台类型,动态的选择使用哪个三方平台 A 的 Jar 包,比如内部平台 A 访问三方平台 A,就需要调用为 A 定制的 Jar 包。
这个需求需要解决如下两个问题:如何在同一套环境中同时存在多个同平台不同版本的 Jar 包(这些 Jar 包中的类大部分相同,只有预设的配置参数不同)?如何根据内部平台类型,选择需要调用的 Jar 包?类加载
我们知道如果想要使用一个类,那么这个类必须通过类加载器将其加载到内存中,在未自定义类加载器之前,JVM 是通过 ApplicationClassLoader、ExtensionClassLoader、BootstrapClassLoader 这三个类加载器基于双亲委派机制完成类的加载。这三个类加载器具有各自加载类的范围如下图所示:
类隔离机制
要想解决上面的第一个问题(多个同平台不同版本 Jar 包同时存在),就必须先了解一下类隔离机制。
类隔离机制原理其实很简单,就是让每个三方平台 A 定制的 Jar 使用单独的类加载器来加载,这样每个 Jar 包之间相互隔离不会相互影响。这是因为即使同一个类使用不同的类加载器加载,对于 JVM 也是两个不同的类(虽然类的结构相同),在 JVM 中类的唯一标识是:类加载器 + 类名。
要保证不同 Jar 包内的类隔离,还需要做到一点,就是 Jar 包中的某个类使用某个类加载器加载,那么其引用的类均使用该类加载器加载,这就是类加载传导规则 。
代码实现
使用 IDEA 创建三个 Maven 项目:third-party-A-for-A:三方平台 A 为内部平台 A 定制的 Jarthird-party-A-for-B:三方平台 A 为内部平台 B 定制的 Jarunified-management-platform:统一管理平台,用于通过访问的内部平台类型,动态选择调用三方平台 A 的 Jar
项目:third-party-A-for-A
pom.xml<?xml version="1.0" encoding="UTF-8"?> 4.0.0 com.thirdparty.A third-party-A-for-A 1.0-SNAPSHOT 三方平台 A 为内部平台 A 定制 Jar 包 17 17 cn.hutool hutool-all 5.8.5
定义两个类:TPAAccessService:用于提供给调用方的统一调用入口类SendRequestProvider:Jar 内部使用的类,用于提供向三方平台 A 发送请求的类,另外一个作用是验证"类加载传导规则"
TPAAccessService.java/** * TPA(Third Party A:三方平台 A 简称) * 该类为调用方提供统一的方法调用入口,调用三方 A 只需要使用该类即可 * * @since 2023/1/14 9:45 */ public class TPAAccessService { public static void send() { SendRequestProvider.send(); } }
SendRequestProvider.javaimport cn.hutool.core.lang.Console; /** * 该类提供向三方平台 A 发送请求的方法 * * @since 2023/1/14 9:48 */ class SendRequestProvider { /** * 三方平台 A 为内部平台 A 预设的密钥,用于加解密 */ private static final String SECRET_KEY = "AAAAAAAAAAA"; /** * 发送请求到三方平台 A */ public static void send() { Console.log("[A -> TPA] 密钥:{} ClassLoader:{}", SECRET_KEY, SendRequestProvider.class.getClassLoader()); } }
项目:third-party-A-for-B
与 third-party-A-for-A 基本相同,除了 SendRequestProvider.java 中的密钥不同,如下所示:class SendRequestProvider { /** * 三方平台 A 为内部平台 B 预设的密钥,用于加解密 */ private static final String SECRET_KEY = "BBBBBBBBBBB"; /** * 发送请求到三方平台 A */ public static void send() { Console.log("[B -> TPA] 密钥:{} ClassLoader:{}", SECRET_KEY, SendRequestProvider.class.getClassLoader()); } }
项目:
unified-management-platform
pom.xml<?xml version="1.0" encoding="UTF-8"?> 4.0.0 com.ump unified-management-platform 1.0-SNAPSHOT 统一管理平台 17 17 org.projectlombok lombok 1.18.24 cn.hutool hutool-all 5.8.5
定义两个类:TPAClassLoader:自定义类加载器,用于加载为内部平台定制的相应 Jar 中类的类Main:测试内部平台调用效果
TPAClassLoader.javaimport cn.hutool.core.lang.Console; import lombok.SneakyThrows; import java.io.File; import java.io.FileNotFoundException; import java.net.URL; import java.net.URLClassLoader; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * 为加载三方平台 A 提供的 Jar 自定义的类加载器 * * @since 2023/1/14 10:02 */ public class TPAClassLoader extends URLClassLoader { /** * 用于缓存相应平台的类加载器,防止重复创建和加载类,造成内存泄漏 */ private static final ConcurrentMap CLASS_LOADER_CACHE = new ConcurrentHashMap<>(); private TPAClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } /** * 用于获取相应三方平台 Jar 包中的类,如果已经加载直接返回,未加载通过 TAPClassLoader 加载类,完成后返回 * * @param internalPlatformCode 内部平台编码,例如:内部平台 A 的编码就是 A * @param tapJarPath 为相应内部平台定制的三方平台 Jar 路径 * @param className 待获取类的全限定类名 * @return 类的 Class 对象 */ @SneakyThrows public static Class<?> getClass(String internalPlatformCode, String tapJarPath, String className) { TPAClassLoader classLoader = getInstance(internalPlatformCode, tapJarPath); Console.log("获取内部平台 {} 的类:{}", internalPlatformCode, className); return classLoader.loadClass(className); } /** * 用于获取对应内部平台的类加载器,类加载器相对于内部平台是单例的,保证单例使用单例设计模式 DCL 的方式 * * @param internalPlatformCode 内部平台编码,例如:内部平台 A 的编码就是 A * @param tapJarPath 为相应内部平台定制的三方平台 Jar 路径 * @return 内部平台对应的类加载器 */ private static TPAClassLoader getInstance(String internalPlatformCode, String tapJarPath) throws Exception { final String key = buildKey(internalPlatformCode, tapJarPath); TPAClassLoader classLoader = CLASS_LOADER_CACHE.get(key); if (classLoader != null) { return classLoader; } synchronized (TPAClassLoader.class) { classLoader = CLASS_LOADER_CACHE.get(key); if (classLoader != null) { return classLoader; } File jarFile = new File(tapJarPath); if (!jarFile.exists()) { throw new FileNotFoundException("未找到三方平台 A Jar 包文件:" + tapJarPath); } classLoader = new TPAClassLoader(new URL[]{jarFile.toURI().toURL()}, getSystemClassLoader()); Console.log("为内部平台 {} 创建类加载器:{}", internalPlatformCode, classLoader); CLASS_LOADER_CACHE.put(key, classLoader); return classLoader; } } /** * 用于生成缓存对应内部平台类加载器的 Key * * @param internalPlatformCode 内部平台编码,例如:内部平台 A 的编码就是 A * @param tapJarPath 为相应内部平台定制的三方平台 Jar 路径 * @return 缓存 Key */ private static String buildKey(String internalPlatformCode, String tapJarPath) { return internalPlatformCode.concat("::").concat(tapJarPath); } }
Main.javaimport cn.hutool.core.lang.Console; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.ReflectUtil; import lombok.SneakyThrows; import java.util.HashMap; import java.util.Map; /** * Main * * @author ZhaoHaichun * @since 2023/1/14 10:34 */ public class Main { /** * 该 Map 只是测试使用,用于临时保持三方平台 A 提供的 Jar 包路径,实际开发会通过文件上传到服务器,然后获取上传路径,通过路径加载 */ private static final Map TPA_JAR_PATH_MAP = new HashMap<>(); private static final String TAP_ACCESS_SERVICE_NAME = "com.thirdparty.TPAAccessService"; static { TPA_JAR_PATH_MAP.put("A", "C:UserszhaohDesktopTemptap_jarthird-party-A-for-A-1.0-SNAPSHOT.jar"); TPA_JAR_PATH_MAP.put("B", "C:UserszhaohDesktopTemptap_jarthird-party-A-for-B-1.0-SNAPSHOT.jar"); } @SneakyThrows public static void main(String[] args) { for (int i = 0; i < 5; i++) { // 用于随机生成待访问的内部平台 String internalPlatformCode = String.valueOf((char) RandomUtil.randomInt("A", "B" + 1)); // 通过访问的内部平台查询三方平台 A 为其提供的 Jar 路径 String jarPath = TPA_JAR_PATH_MAP.get(internalPlatformCode); // 通过上述信息,使用相应的类加载器加载或直接获取类 "com.thirdparty.TPAAccessService" Class<?> clazz = TPAClassLoader.getClass(internalPlatformCode, jarPath, TAP_ACCESS_SERVICE_NAME); // 调用其相应的方法 ReflectUtil.invokeStatic(clazz.getMethod("send")); Console.log("================================================================"); } } }测试步骤
编写完成上述代码后,按照下面步骤执行:使用 Maven package 打包项目:third-party-A-for-A、third-party-A-for-B将打包完成的 Jar 拷贝到测试目录,上面实例代码为:"C:UserszhaohDesktopTemptap_jar"目录下修改 Main 类静态代码块中的路径与 Jar 包路径一致执行 Main 类中的 main 方法
输出结果如下:(每次输出可能不同)为内部平台 A 创建类加载器:com.ump.TPAClassLoader@568db2f2 获取内部平台 A 的类:com.thirdparty.TPAAccessService [A -> TPA] 密钥:AAAAAAAAAAA ClassLoader:com.ump.TPAClassLoader@568db2f2 ================================================================ 获取内部平台 A 的类:com.thirdparty.TPAAccessService [A -> TPA] 密钥:AAAAAAAAAAA ClassLoader:com.ump.TPAClassLoader@568db2f2 ================================================================ 为内部平台 B 创建类加载器:com.ump.TPAClassLoader@179d3b25 获取内部平台 B 的类:com.thirdparty.TPAAccessService [B -> TPA] 密钥:BBBBBBBBBBB ClassLoader:com.ump.TPAClassLoader@179d3b25 ================================================================ 获取内部平台 B 的类:com.thirdparty.TPAAccessService [B -> TPA] 密钥:BBBBBBBBBBB ClassLoader:com.ump.TPAClassLoader@179d3b25 ================================================================ 获取内部平台 A 的类:com.thirdparty.TPAAccessService [A -> TPA] 密钥:AAAAAAAAAAA ClassLoader:com.ump.TPAClassLoader@568db2f2 ================================================================
通过上面的输出结果可以看出:内部平台 A 和 B 分别只创建了一次类加载器创建完成类加载器后,后续均通过缓存中获取相应的类加载器在 Jar 包中 TPAAccessService 调用了 SendRequestProvider,而 SendRequestProvider 输出的日志中类加载器同加载 TPAAccessService 的类加载器相同,说明类加载传导规则内部平台 A 调用,输出的密钥是"AAAAAAAAAAA",B 调用输出的密钥是"BBBBBBBBBBB",说明为内部平台提供的 Jar 均加载到内存,而且通过类加载器实现了类的隔离