案例需求 现在有一个统一管理平台,用于统一对接三方平台,屏蔽相同业务三方平台的差异性,减少内部平台对接的成本。正常情况下三方平台提供的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项目:thirdpartyAforA:三方平台A为内部平台A定制的JarthirdpartyAforB:三方平台A为内部平台B定制的Jarunifiedmanagementplatform:统一管理平台,用于通过访问的内部平台类型,动态选择调用三方平台A的Jar 项目:thirdpartyAforA pom。xmllt;?xmlversion1。0encodingUTF8?projectxmlnshttp:maven。apache。orgPOM4。0。0xmlns:xsihttp:www。w3。org2001XMLSchemainstancexsi:schemaLocationhttp:maven。apache。orgPOM4。0。0http:maven。apache。orgxsdmaven4。0。0。xsdmodelVersion4。0。0modelVersiongroupIdcom。thirdparty。AgroupIdthirdpartyAforAartifactIdversion1。0SNAPSHOTversiondescription三方平台A为内部平台A定制Jar包descriptionpropertiesmaven。compiler。source17maven。compiler。sourcemaven。compiler。target17maven。compiler。targetpropertiesdependenciesdependencygroupIdcn。hutoolgroupIdhutoolallartifactIdversion5。8。5versiondependencydependenciesproject 定义两个类:TPAAccessService:用于提供给调用方的统一调用入口类SendRequestProvider:Jar内部使用的类,用于提供向三方平台A发送请求的类,另外一个作用是验证类加载传导规则 TPAAccessService。javaTPA(ThirdPartyA:三方平台A简称)该类为调用方提供统一的方法调用入口,调用三方A只需要使用该类即可since20231149:45publicclassTPAAccessService{publicstaticvoidsend(){SendRequestProvider。send();}} SendRequestProvider。javaimportcn。hutool。core。lang。Console;该类提供向三方平台A发送请求的方法since20231149:48classSendRequestProvider{三方平台A为内部平台A预设的密钥,用于加解密privatestaticfinalStringSECRETKEYAAAAAAAAAAA;发送请求到三方平台Apublicstaticvoidsend(){Console。log(〔ATPA〕密钥:{}ClassLoader:{},SECRETKEY,SendRequestProvider。class。getClassLoader());}} 项目:thirdpartyAforB 与thirdpartyAforA基本相同,除了SendRequestProvider。java中的密钥不同,如下所示:classSendRequestProvider{三方平台A为内部平台B预设的密钥,用于加解密privatestaticfinalStringSECRETKEYBBBBBBBBBBB;发送请求到三方平台Apublicstaticvoidsend(){Console。log(〔BTPA〕密钥:{}ClassLoader:{},SECRETKEY,SendRequestProvider。class。getClassLoader());}} 项目: unifiedmanagementplatform pom。xmllt;?xmlversion1。0encodingUTF8?projectxmlnshttp:maven。apache。orgPOM4。0。0xmlns:xsihttp:www。w3。org2001XMLSchemainstancexsi:schemaLocationhttp:maven。apache。orgPOM4。0。0http:maven。apache。orgxsdmaven4。0。0。xsdmodelVersion4。0。0modelVersiongroupIdcom。umpgroupIdunifiedmanagementplatformartifactIdversion1。0SNAPSHOTversiondescription统一管理平台descriptionpropertiesmaven。compiler。source17maven。compiler。sourcemaven。compiler。target17maven。compiler。targetpropertiesdependenciesdependencygroupIdorg。projectlombokgroupIdlombokartifactIdversion1。18。24versiondependencydependencygroupIdcn。hutoolgroupIdhutoolallartifactIdversion5。8。5versiondependencydependenciesproject 定义两个类:TPAClassLoader:自定义类加载器,用于加载为内部平台定制的相应Jar中类的类Main:测试内部平台调用效果 TPAClassLoader。javaimportcn。hutool。core。lang。Console;importlombok。SneakyThrows;importjava。io。File;importjava。io。FileNotFoundException;importjava。net。URL;importjava。net。URLClassLoader;importjava。util。concurrent。ConcurrentHashMap;importjava。util。concurrent。ConcurrentMap;为加载三方平台A提供的Jar自定义的类加载器since202311410:02publicclassTPAClassLoaderextendsURLClassLoader{用于缓存相应平台的类加载器,防止重复创建和加载类,造成内存泄漏privatestaticfinalConcurrentMapString,TPAClassLoaderCLASSLOADERCACHEnewConcurrentHashMap();privateTPAClassLoader(URL〔〕urls,ClassLoaderparent){super(urls,parent);}用于获取相应三方平台Jar包中的类,如果已经加载直接返回,未加载通过TAPClassLoader加载类,完成后返回paraminternalPlatformCode内部平台编码,例如:内部平台A的编码就是AparamtapJarPath为相应内部平台定制的三方平台Jar路径paramclassName待获取类的全限定类名return类的Class对象SneakyThrowspublicstaticClasslt;?getClass(StringinternalPlatformCode,StringtapJarPath,StringclassName){TPAClassLoaderclassLoadergetInstance(internalPlatformCode,tapJarPath);Console。log(获取内部平台{}的类:{},internalPlatformCode,className);returnclassLoader。loadClass(className);}用于获取对应内部平台的类加载器,类加载器相对于内部平台是单例的,保证单例使用单例设计模式DCL的方式paraminternalPlatformCode内部平台编码,例如:内部平台A的编码就是AparamtapJarPath为相应内部平台定制的三方平台Jar路径return内部平台对应的类加载器privatestaticTPAClassLoadergetInstance(StringinternalPlatformCode,StringtapJarPath)throwsException{finalStringkeybuildKey(internalPlatformCode,tapJarPath);TPAClassLoaderclassLoaderCLASSLOADERCACHE。get(key);if(classLoader!null){returnclassLoader;}synchronized(TPAClassLoader。class){classLoaderCLASSLOADERCACHE。get(key);if(classLoader!null){returnclassLoader;}FilejarFilenewFile(tapJarPath);if(!jarFile。exists()){thrownewFileNotFoundException(未找到三方平台AJar包文件:tapJarPath);}classLoadernewTPAClassLoader(newURL〔〕{jarFile。toURI()。toURL()},getSystemClassLoader());Console。log(为内部平台{}创建类加载器:{},internalPlatformCode,classLoader);CLASSLOADERCACHE。put(key,classLoader);returnclassLoader;}}用于生成缓存对应内部平台类加载器的KeyparaminternalPlatformCode内部平台编码,例如:内部平台A的编码就是AparamtapJarPath为相应内部平台定制的三方平台Jar路径return缓存KeyprivatestaticStringbuildKey(StringinternalPlatformCode,StringtapJarPath){returninternalPlatformCode。concat(::)。concat(tapJarPath);}} Main。javaimportcn。hutool。core。lang。Console;importcn。hutool。core。util。RandomUtil;importcn。hutool。core。util。ReflectUtil;importlombok。SneakyThrows;importjava。util。HashMap;importjava。util。Map;MainauthorZhaoHaichunsince202311410:34publicclassMain{该Map只是测试使用,用于临时保持三方平台A提供的Jar包路径,实际开发会通过文件上传到服务器,然后获取上传路径,通过路径加载privatestaticfinalMapString,StringTPAJARPATHMAPnewHashMap();privatestaticfinalStringTAPACCESSSERVICENAMEcom。thirdparty。TPAAccessService;static{TPAJARPATHMAP。put(A,C:UserszhaohDesktopTemptapjarthirdpartyAforA1。0SNAPSHOT。jar);TPAJARPATHMAP。put(B,C:UserszhaohDesktopTemptapjarthirdpartyAforB1。0SNAPSHOT。jar);}SneakyThrowspublicstaticvoidmain(String〔〕args){for(inti0;i5;i){用于随机生成待访问的内部平台StringinternalPlatformCodeString。valueOf((char)RandomUtil。randomInt(A,B1));通过访问的内部平台查询三方平台A为其提供的Jar路径StringjarPathTPAJARPATHMAP。get(internalPlatformCode);通过上述信息,使用相应的类加载器加载或直接获取类com。thirdparty。TPAAccessServiceClasslt;?clazzTPAClassLoader。getClass(internalPlatformCode,jarPath,TAPACCESSSERVICENAME);调用其相应的方法ReflectUtil。invokeStatic(clazz。getMethod(send));Console。log();}}}测试步骤 编写完成上述代码后,按照下面步骤执行:使用Mavenpackage打包项目:thirdpartyAforA、thirdpartyAforB将打包完成的Jar拷贝到测试目录,上面实例代码为:C:UserszhaohDesktopTemptapjar目录下修改Main类静态代码块中的路径与Jar包路径一致执行Main类中的main方法 输出结果如下:(每次输出可能不同)为内部平台A创建类加载器:com。ump。TPAClassLoader568db2f2获取内部平台A的类:com。thirdparty。TPAAccessService〔ATPA〕密钥:AAAAAAAAAAAClassLoader:com。ump。TPAClassLoader568db2f2获取内部平台A的类:com。thirdparty。TPAAccessService〔ATPA〕密钥:AAAAAAAAAAAClassLoader:com。ump。TPAClassLoader568db2f2为内部平台B创建类加载器:com。ump。TPAClassLoader179d3b25获取内部平台B的类:com。thirdparty。TPAAccessService〔BTPA〕密钥:BBBBBBBBBBBClassLoader:com。ump。TPAClassLoader179d3b25获取内部平台B的类:com。thirdparty。TPAAccessService〔BTPA〕密钥:BBBBBBBBBBBClassLoader:com。ump。TPAClassLoader179d3b25获取内部平台A的类:com。thirdparty。TPAAccessService〔ATPA〕密钥:AAAAAAAAAAAClassLoader:com。ump。TPAClassLoader568db2f2 通过上面的输出结果可以看出:内部平台A和B分别只创建了一次类加载器创建完成类加载器后,后续均通过缓存中获取相应的类加载器在Jar包中TPAAccessService调用了SendRequestProvider,而SendRequestProvider输出的日志中类加载器同加载TPAAccessService的类加载器相同,说明类加载传导规则内部平台A调用,输出的密钥是AAAAAAAAAAA,B调用输出的密钥是BBBBBBBBBBB,说明为内部平台提供的Jar均加载到内存,而且通过类加载器实现了类的隔离