专业的JAVA编程教程与资源

网站首页 > java教程 正文

Java 类隔离应用:多 Jar 包支持(java接口隔离原则)

temp10 2025-06-15 19:10:07 java教程 4 ℃ 0 评论

案例需求

现在有一个“统一管理平台”,用于统一对接三方平台,屏蔽相同业务三方平台的差异性,减少内部平台对接的成本。正常情况下三方平台提供的 SDK 是通用的(和内部平台无关),但是有一些比较特殊的三方(假如是三方平台 A),他提供的 SDK 是给内部平台定制的。

这时就需要根据访问“统一管理平台”的内部平台类型,动态的选择使用哪个三方平台 A 的 Jar 包,比如内部平台 A 访问三方平台 A,就需要调用为 A 定制的 Jar 包。

Java 类隔离应用:多 Jar 包支持(java接口隔离原则)

这个需求需要解决如下两个问题:

  1. 如何在同一套环境中同时存在多个同平台不同版本的 Jar 包(这些 Jar 包中的类大部分相同,只有预设的配置参数不同)?
  2. 如何根据内部平台类型,选择需要调用的 Jar 包?

类加载

我们知道如果想要使用一个类,那么这个类必须通过类加载器将其加载到内存中,在未自定义类加载器之前,JVM 是通过 ApplicationClassLoader、ExtensionClassLoader、BootstrapClassLoader 这三个类加载器基于双亲委派机制完成类的加载。这三个类加载器具有各自加载类的范围如下图所示:

类隔离机制

要想解决上面的第一个问题(多个同平台不同版本 Jar 包同时存在),就必须先了解一下类隔离机制。

类隔离机制原理其实很简单,就是让每个三方平台 A 定制的 Jar 使用单独的类加载器来加载,这样每个 Jar 包之间相互隔离不会相互影响。这是因为即使同一个类使用不同的类加载器加载,对于 JVM 也是两个不同的类(虽然类的结构相同),在 JVM 中类的唯一标识是:类加载器 + 类名。

要保证不同 Jar 包内的类隔离,还需要做到一点,就是 Jar 包中的某个类使用某个类加载器加载,那么其引用的类均使用该类加载器加载,这就是类加载传导规则

代码实现

使用 IDEA 创建三个 Maven 项目:

  • third-party-A-for-A:三方平台 A 为内部平台 A 定制的 Jar
  • third-party-A-for-B:三方平台 A 为内部平台 B 定制的 Jar
  • unified-management-platform:统一管理平台,用于通过访问的内部平台类型,动态选择调用三方平台 A 的 Jar

项目:third-party-A-for-A

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <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="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <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("================================================================");
        }
    }
}

测试步骤

编写完成上述代码后,按照下面步骤执行:

  1. 使用 Maven package 打包项目:third-party-A-for-A、third-party-A-for-B
  2. 将打包完成的 Jar 拷贝到测试目录,上面实例代码为:“C:\\Users\\zhaoh\\Desktop\\Temp\\tap_jar”目录下
  3. 修改 Main 类静态代码块中的路径与 Jar 包路径一致
  4. 执行 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 均加载到内存,而且通过类加载器实现了类的隔离

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表