网站首页 > java教程 正文
案例需求
现在有一个“统一管理平台”,用于统一对接三方平台,屏蔽相同业务三方平台的差异性,减少内部平台对接的成本。正常情况下三方平台提供的 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 定制的 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("================================================================");
}
}
}
测试步骤
编写完成上述代码后,按照下面步骤执行:
- 使用 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 均加载到内存,而且通过类加载器实现了类的隔离
猜你喜欢
- 2025-06-15 Linux中如何通过Shell脚本来控制Spring Boot的Jar包启停服务?
- 2025-06-15 推荐一款超棒的SpringCloud 脚手架项目
- 2025-06-15 IDEA将项目打包成jar包(idea打包普通java项目)
- 2025-06-15 Spring Boot3 项目 jar 包打包成 Docker 镜像全攻略
- 2025-06-15 记录Dockerfile将jar包构建成部署所需的镜像
- 2025-06-15 项目基础部署汇总八---linux下xxl-job安装
- 2025-06-15 Spring Boot Jar 包秒变 Docker 镜像实现多环境部署
- 2025-06-15 终端执行 java -jar example.jar 时报错:“没有主清单属性” 的解决
- 2025-06-15 如何将本地JAR文件添加到Maven项目中
- 2025-06-15 # 怎么让 java -jar example.jar 产生的日志输出到指定文件
你 发表评论:
欢迎- 06-15Linux中如何通过Shell脚本来控制Spring Boot的Jar包启停服务?
- 06-15推荐一款超棒的SpringCloud 脚手架项目
- 06-15IDEA将项目打包成jar包(idea打包普通java项目)
- 06-15Spring Boot3 项目 jar 包打包成 Docker 镜像全攻略
- 06-15记录Dockerfile将jar包构建成部署所需的镜像
- 06-15项目基础部署汇总八---linux下xxl-job安装
- 06-15Spring Boot Jar 包秒变 Docker 镜像实现多环境部署
- 06-15终端执行 java -jar example.jar 时报错:“没有主清单属性” 的解决
- 最近发表
-
- Linux中如何通过Shell脚本来控制Spring Boot的Jar包启停服务?
- 推荐一款超棒的SpringCloud 脚手架项目
- IDEA将项目打包成jar包(idea打包普通java项目)
- Spring Boot3 项目 jar 包打包成 Docker 镜像全攻略
- 记录Dockerfile将jar包构建成部署所需的镜像
- 项目基础部署汇总八---linux下xxl-job安装
- Spring Boot Jar 包秒变 Docker 镜像实现多环境部署
- 终端执行 java -jar example.jar 时报错:“没有主清单属性” 的解决
- 如何将本地JAR文件添加到Maven项目中
- Java 类隔离应用:多 Jar 包支持(java接口隔离原则)
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)