龙空技术网

Java 类隔离应用:多 Jar 包支持

JU帮 314

前言:

而今大家对“apache的jar包判断对象是否相等”大体比较关心,你们都需要了解一些“apache的jar包判断对象是否相等”的相关内容。那么小编也在网摘上网罗了一些关于“apache的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"?><project xmlns=";         xmlns:xsi=";         xsi:schemaLocation=" ;>    <modelVersion>4.0.0</modelVersion>    <groupId>com.thirdparty.A</groupId>    <artifactId>third-party-A-for-A</artifactId>    <version>1.0-SNAPSHOT</version>    <description>三方平台 A 为内部平台 A 定制 Jar 包</description>    <properties>        <maven.compiler.source>17</maven.compiler.source>        <maven.compiler.target>17</maven.compiler.target>    </properties>    <dependencies>        <dependency>            <groupId>cn.hutool</groupId>            <artifactId>hutool-all</artifactId>            <version>5.8.5</version>        </dependency>    </dependencies></project>

定义两个类:

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.java

import 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"?><project xmlns=";         xmlns:xsi=";         xsi:schemaLocation=" ;>    <modelVersion>4.0.0</modelVersion>    <groupId>com.ump</groupId>    <artifactId>unified-management-platform</artifactId>    <version>1.0-SNAPSHOT</version>    <description>统一管理平台</description>    <properties>        <maven.compiler.source>17</maven.compiler.source>        <maven.compiler.target>17</maven.compiler.target>    </properties>    <dependencies>        <dependency>            <groupId>org.projectlombok</groupId>            <artifactId>lombok</artifactId>            <version>1.18.24</version>        </dependency>        <dependency>            <groupId>cn.hutool</groupId>            <artifactId>hutool-all</artifactId>            <version>5.8.5</version>        </dependency>    </dependencies></project>

定义两个类:

TPAClassLoader:自定义类加载器,用于加载为内部平台定制的相应 Jar 中类的类Main:测试内部平台调用效果

TPAClassLoader.java

import 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<String, TPAClassLoader> 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.java

import 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<String, String> 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:\\Users\\zhaoh\\Desktop\\Temp\\tap_jar\\third-party-A-for-A-1.0-SNAPSHOT.jar");        TPA_JAR_PATH_MAP.put("B", "C:\\Users\\zhaoh\\Desktop\\Temp\\tap_jar\\third-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:\\Users\\zhaoh\\Desktop\\Temp\\tap_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 均加载到内存,而且通过类加载器实现了类的隔离

标签: #apache的jar包判断对象是否相等