1. 程式人生 > >曹工說面試:當應用依賴jar包的A版本,中介軟體jar包依賴B版本,兩個版本不相容,這還怎麼玩?

曹工說面試:當應用依賴jar包的A版本,中介軟體jar包依賴B版本,兩個版本不相容,這還怎麼玩?

# 背景 大一點的公司,可能有一些組,專門做中介軟體的;假設,某中介軟體小組,給你提供了一個jar包,你需要整合到你的應用裡。假設,它依賴了一個日期類,版本是v1;我們應用也依賴了同名的一個日期類,版本是v2. 兩個版本的日期類,方法邏輯的實現,有一些差異。 舉個例子,中介軟體提供的jar包中,依賴如下工具包: ```xml com.example common-v1 0.0.1-SNAPSHOT ``` 該版本中,包含了com.example.date.util.CommonDateUtil這個類。 ```java package com.example.date.util; import lombok.extern.slf4j.Slf4j; @Slf4j public class CommonDateUtil { public static String format(String date) { // 1 String s = date + "- v1"; log.info("v1 result:{}", s); return s; } } ``` 應用中,依賴如下jar包: ```java com.example
common-v2 0.0.1-SNAPSHOT
``` 該jar包中,包含同名的class,但裡面的方法實現不一樣: ```java @Slf4j public class CommonDateUtil { public static String format(String date) { String s = date + "- v2"; log.info("v2 result:{}", s); return s; } } ``` ok,那假設我們是一個spring boot應用,當中間件小組的哥們找到你,讓你整合,你可能就愉快地弄進去了;但是,有個問題時,你的jar包、和中介軟體小哥的jar包,都是放在fatjar的lib目錄的(我這裡解壓了,方便檢視): ![](https://img2020.cnblogs.com/blog/519126/202007/519126-20200704175428182-1431868800.png) 請問,最終載入的CommonDateUtil類,到底是common-v1的,還是commonv2中的呢?因為spring boot載入BOOT-INF/lib時,肯定都是同一個類載入器,同一個類載入器,對於一個包名和類名都相同的類,只會載入一次;那麼,載入了v1,就不可能再載入V2;反之亦然。 那這就出問題了。我們應用要用V2;中介軟體要用V1,水火不容啊,這可怎麼辦? # 分析 首先,我們要重寫spring boot的啟動類,這是毋庸置疑的,啟動類是哪個呢? ![](https://img2020.cnblogs.com/blog/519126/202007/519126-20200704180004136-1888872542.png) 為什麼要重寫這個?因為,我們問題分析裡說了,當打成fat jar執行時,其結構如下: ```java [root@mini2 temp]# tree . ├── BOOT-INF │   ├── classes │   │   ├── application.properties │   │   ├── application.yml │   │   └── com │   │   └── example │   │   └── demo │   │   ├── CustomMiddleWareClassloader.class │   │   └── OrderServiceApplication.class │   └── lib │   ├── classmate-1.4.0.jar │   ├── common-v1-0.0.1-SNAPSHOT.jar │   ├── common-v2-0.0.1-SNAPSHOT.jar │   ├── hibernate-validator-6.0.17.Final.jar │   ├── jackson-annotations-2.9.0.jar │   ├── jackson-core-2.9.9.jar │   ├── jackson-databind-2.9.9.jar │   ├── jackson-datatype-jdk8-2.9.9.jar │   ├── jackson-datatype-jsr310-2.9.9.jar │   ├── jackson-module-parameter-names-2.9.9.jar │   ├── javax.annotation-api-1.3.2.jar │   ├── jboss-logging-3.3.2.Final.jar │   ├── jul-to-slf4j-1.7.26.jar │   ├── log4j-api-2.11.2.jar │   ├── log4j-to-slf4j-2.11.2.jar │   ├── logback-classic-1.2.3.jar │   ├── logback-core-1.2.3.jar │   ├── lombok-1.18.10.jar │   ├── middle-ware-0.0.1-SNAPSHOT.jar │   ├── middle-ware-api-0.0.1-SNAPSHOT.jar │   ├── slf4j-api-1.7.26.jar │   ├── snakeyaml-1.23.jar │   ├── spring-aop-5.1.9.RELEASE.jar │   ├── spring-beans-5.1.9.RELEASE.jar │   ├── spring-boot-2.1.7.RELEASE.jar │   ├── spring-boot-autoconfigure-2.1.7.RELEASE.jar │   ├── spring-boot-loader-2.1.7.RELEASE.jar │   ├── spring-boot-starter-2.1.7.RELEASE.jar │   ├── spring-boot-starter-json-2.1.7.RELEASE.jar │   ├── spring-boot-starter-logging-2.1.7.RELEASE.jar │   ├── spring-boot-starter-tomcat-2.1.7.RELEASE.jar │   ├── spring-boot-starter-web-2.1.7.RELEASE.jar │   ├── spring-context-5.1.9.RELEASE.jar │   ├── spring-core-5.1.9.RELEASE.jar │   ├── spring-expression-5.1.9.RELEASE.jar │   ├── spring-jcl-5.1.9.RELEASE.jar │   ├── spring-web-5.1.9.RELEASE.jar │   ├── spring-webmvc-5.1.9.RELEASE.jar │   ├── tomcat-embed-core-9.0.22.jar │   ├── tomcat-embed-el-9.0.22.jar │   ├── tomcat-embed-websocket-9.0.22.jar │   └── validation-api-2.0.1.Final.jar ├── META-INF │   ├── MANIFEST.MF │   └── maven │   └── com.example │   └── web-application │   ├── pom.properties │   └── pom.xml └── org └── springframework └── boot └── loader ├── archive │   ├── Archive.class │   ├── Archive$Entry.class │   ├── Archive$EntryFilter.class │   ├── ExplodedArchive$1.class │   ├── ExplodedArchive.class │   ├── ExplodedArchive$FileEntry.class │   ├── ExplodedArchive$FileEntryIterator.class │   ├── ExplodedArchive$FileEntryIterator$EntryComparator.class │   ├── JarFileArchive.class │   ├── JarFileArchive$EntryIterator.class │   └── JarFileArchive$JarFileEntry.class ├── data │   ├── RandomAccessData.class │   ├── RandomAccessDataFile$1.class │   ├── RandomAccessDataFile.class │   ├── RandomAccessDataFile$DataInputStream.class │   └── RandomAccessDataFile$FileAccess.class ├── ExecutableArchiveLauncher.class ├── jar │   ├── AsciiBytes.class │   ├── Bytes.class │   ├── CentralDirectoryEndRecord.class │   ├── CentralDirectoryFileHeader.class │   ├── CentralDirectoryParser.class │   ├── CentralDirectoryVisitor.class │   ├── FileHeader.class │   ├── Handler.class │   ├── JarEntry.class │   ├── JarEntryFilter.class │   ├── JarFile$1.class │   ├── JarFile$2.class │   ├── JarFile.class │   ├── JarFileEntries$1.class │   ├── JarFileEntries.class │   ├── JarFileEntries$EntryIterator.class │   ├── JarFile$JarFileType.class │   ├── JarURLConnection$1.class │   ├── JarURLConnection.class │   ├── JarURLConnection$JarEntryName.class │   ├── StringSequence.class │   └── ZipInflaterInputStream.class ├── JarLauncher.class ├── LaunchedURLClassLoader.class ├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class ├── Launcher.class ├── MainMethodRunner.class ├── PropertiesLauncher$1.class ├── PropertiesLauncher$ArchiveEntryFilter.class ├── PropertiesLauncher.class ├── PropertiesLauncher$PrefixMatchingArchiveFilter.class ├── util │   └── SystemPropertyUtils.class └── WarLauncher.class ``` BOOT-INF/lib下,是由同一個類載入器去載入的,而我們的V1和V2的jar包,全部混在這個目錄下。 我們要想同時載入V1和V2的jar包,必須用兩個類載入器來做隔離。 即,應用類載入器,不能載入V1;而中介軟體類載入器,只管載入V2,其他一概不能載入。 ## spring boot 的啟動類 前面我們提到,啟動類是org.springframework.boot.loader.JarLauncher,這個是在BOOT-INF/MANIFEST中指定了的。 這個類在哪裡呢,一般在如下這個依賴中: ```xml org.springframework.boot
spring-boot-loader
``` 該依賴,會在打包階段,由maven外掛,打到我們的fat jar中: ```xml org.springframework.boot spring-boot-maven-plugin ``` 這個jar包,打到哪裡去了呢?實際是解壓後,放到fat jar的如下路徑了,可以再去上面看看那個fat jar結構: ![](https://img2020.cnblogs.com/blog/519126/202007/519126-20200704181219009-649276928.png) 上面那個啟動類,就是在這個裡面。 ## 啟動類簡單解析 先來看看uml: ![](https://img2020.cnblogs.com/blog/519126/202007/519126-20200704181514254-261436609.png) 先看看JarLauncher: ```java public class JarLauncher extends ExecutableArchiveLauncher { static final String BOOT_INF_CLASSES = "BOOT-INF/classes/"; static final String BOOT_INF_LIB = "BOOT-INF/lib/"; public JarLauncher() { } protected JarLauncher(Archive archive) { super(archive); } @Override protected boolean isNestedArchive(Archive.Entry entry) { if (entry.isDirectory()) { return entry.getName().equals(BOOT_INF_CLASSES); } return entry.getName().startsWith(BOOT_INF_LIB); } public static void main(String[] args) throws Exception { // 1 new JarLauncher().launch(args); } } ``` 這裡1處,new了自身,然後呼叫launch。 ```java public abstract class Launcher { /** * Launch the application. This method is the initial entry point that should be * called by a subclass {@code public static void main(String[] args)} method. * @param args the incoming arguments * @throws Exception if the application fails to launch */ protected void launch(String[] args) throws Exception { JarFile.registerUrlProtocolHandler(); // 1 ClassLoader classLoader = createClassLoader(getClassPathArchives()); // 2 launch(args, getMainClass(), classLoader); } ``` 1處這裡,就是建立類載入器了,期間,先呼叫了getClassPathArchives。 我們看看: ```java org.springframework.boot.loader.Launcher#getClassPathArchives protected abstract List getClassPathArchives() throws Exception; ``` 這是個抽象方法,此處使用了模板方法設計模式,在如下類中實現了: ```java org.springframework.boot.loader.ExecutableArchiveLauncher#getClassPathArchives @Override protected List getClassPathArchives() throws Exception { // 1 List archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive)); postProcessClassPathArchives(archives); return archives; } ``` 此處的1處,不用深究,就是獲取類載入器的classpath集合。我們這裡打個斷點,直接看下: ![](https://img2020.cnblogs.com/blog/519126/202007/519126-20200704182115582-877251991.png) 這裡面細節就先不看了,主要就是拿到BOOT-INF/lib下的每個jar包。 然後我們繼續之前的: ```java protected void launch(String[] args) throws Exception { JarFile.registerUrlProtocolHandler(); // 1 ClassLoader classLoader = createClassLoader(getClassPathArchives()); // 2 launch(args, getMainClass(), classLoader); } ``` 現在getClassPathArchives已經ok了,接著就呼叫createClassLoader來建立類載入器了。 ```java protected ClassLoader createClassLoader(List archives) throws Exception { List urls = new ArrayList<>(archives.size()); for (Archive archive : archives) { urls.add(archive.getUrl()); } // 1 return createClassLoader(urls.toArray(new URL[0])); } ``` 1處,繼續呼叫內層函式,傳入了url陣列。 ```java protected ClassLoader createClassLoader(URL[] urls) throws Exception { return new LaunchedURLClassLoader(urls, getClass().getClassLoader()); } ``` 這裡new了一個LaunchedURLClassLoader,引數就是url陣列。我們看看這個類: ```java public class LaunchedURLClassLoader extends URLClassLoader { static { ClassLoader.registerAsParallelCapable(); } /** * Create a new {@link LaunchedURLClassLoader} instance. * @param urls the URLs from which to load classes and resources * @param parent the parent class loader for delegation */ public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } ``` 這個類,繼承了URLClassLoader,所以,大家如果對類載入器有一定了解,就知道,URLClassLoader就是接收一堆的url,然後loadClass的時候,遵從雙親委派,雙親載入不了,就交給它,它就去url數組裡,去載入class。 ## 思路分析 我的打算是,修改fat jar中的啟動類,為我們自定義的啟動類。 ```properties Manifest-Version: 1.0 Implementation-Title: web-application Implementation-Version: 0.0.1-SNAPSHOT Start-Class: com.example.demo.OrderServiceApplication Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Build-Jdk-Spec: 1.8 Spring-Boot-Version: 2.1.7.RELEASE Created-By: Maven Archiver 3.4.0 // 1 Main-Class: com.example.demo.CustomJarLauncher ``` 1處,指定我們自定義的class,這個class,我們會在打好fat jar後,手動拷貝進去。 ![](https://img2020.cnblogs.com/blog/519126/202007/519126-20200704183332111-1190461581.png) 然後,我們自定義啟動類裡面要幹啥呢? ```java public class CustomJarLauncher extends JarLauncher { public static void main(String[] args) throws Exception { new CustomJarLauncher().launch(args); } @Override protected void launch(String[] args) throws Exception { JarFile.registerUrlProtocolHandler(); // 1 List classPathArchives = getClassPathArchives(); /** * 2 */ List allURLs = classPathArchives.stream().map(entries -> { try { return entries.getUrl(); } catch (MalformedURLException e) { return null; } }).filter(Objects::nonNull).collect(Collectors.toList()); // 3 List middleWareClassPathArchives = new ArrayList<>(); for (URL url : allURLs) { String urlPath = url.getPath(); if (urlPath == null) { continue; } boolean isMiddleWareJar = urlPath.contains("common-v1") || urlPath.contains("middle-ware"); if (isMiddleWareJar) { if (urlPath.contains("middle-ware-api")) { continue; } middleWareClassPathArchives.add(url); } } /** * 4 從全部的應用lib目錄,移除中介軟體需要的jar包,但是,中介軟體的api不能移除 */ allURLs.removeAll(middleWareClassPathArchives); // 5 CustomLaunchedURLClassLoader loader = new CustomLaunchedURLClassLoader(allURLs.toArray(new URL[0]), getClass().getClassLoader()); loader.setMiddleWareClassPathArchives(middleWareClassPathArchives); launch(args, getMainClass(), loader); } } ``` * 1處,獲取fat jar的lib目錄下的全部包 * 2處,將1處得到的集合,轉為url集合 * 3處,篩選出中介軟體的包,複製到單獨的集合中,我這邊有2個,直接寫死了(畢竟是demo) * 4處,將原集合中,移除中介軟體的jar包 * 5處,建立一個自定義的classloader,主要是方便我們存放中介軟體相關jar包集合 5處自定義的classloader,這裡可以看下: ```java public class CustomLaunchedURLClassLoader extends LaunchedURLClassLoader { // 中介軟體jar包 List middleWareClassPathArchives; /** * Create a new {@link LaunchedURLClassLoader} instance. * * @param urls the URLs from which to load classes and resources * @param parent the parent class loader for delegation */ public CustomLaunchedURLClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } public List getMiddleWareClassPathArchives() { return middleWareClassPathArchives; } public void setMiddleWareClassPathArchives(List middleWareClassPathArchives) { this.middleWareClassPathArchives = middleWareClassPathArchives; } } ``` 最終,在我們的業務程式碼,要怎麼去寫呢? 我們現在自定義了一個類載入器,那麼,後續業務程式碼都會由這個類載入器去載入。 我們再想想標題說的問題,我們是需要:載入中介軟體程式碼時,不能用這個類載入器去載入,因為這個類載入器中,已經排除了中介軟體相關jar包,是載入不到的。 此時,我們需要自定義一個classloader,去如下類中的middleWareClassPathArchives這個地方載入: ```java public class CustomLaunchedURLClassLoader extends LaunchedURLClassLoader { // 中介軟體jar包 List middleWareClassPathArchives; ... } ``` 只有當它載入不到之後,才丟給應用類載入器去載入。 程式碼如下: ```java @SpringBootApplication @RestController @Slf4j public class OrderServiceApplication { /** * 中介軟體使用的classloader */ static ClassLoader delegatingClassloader; public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { // 1 ClassLoader loader = Thread.currentThread().getContextClassLoader(); Method method = loader.getClass().getMethod("getMiddleWareClassPathArchives"); List middleWareUrls = (List) method.invoke(loader); // 2 delegatingClassloader = new CustomMiddleWareClassloader(middleWareUrls.toArray(new URL[0]), loader); // 3 SpringApplication.run(OrderServiceApplication.class, args); } ``` * 1,這裡,我們要通過當前執行緒,拿到我們的類載入器,此時拿到的,肯定就是我們的自定義類載入器;然後通過反射方法,拿到中介軟體url集合,其實這裡自己去拼這個url也可以,我們這裡為了省事,所以就這麼寫; * 2處,建立一個類載入器,主要就是給中介軟體程式碼使用,進行類載入器隔離。 注意,這裡,我們把當前應用的類載入器,傳給了這個中介軟體類載入器。 ```java @Data @Slf4j public class CustomMiddleWareClassloader extends URLClassLoader { ClassLoader classLoader; public CustomMiddleWareClassloader(URL[] urls, ClassLoader parent) { super(urls); classLoader = parent; } @Override public Class loadClass(String name) throws ClassNotFoundException { /** * 先自己來載入中介軟體相關jar包,這裡呼叫findClass,就會去中介軟體那幾個jar包載入class */ try { Class clazz = findClass(name); if (clazz != null) { return clazz; } throw new ClassNotFoundException(name); } catch (Exception e) { /** * 在中介軟體自己的jar包裡找不到,就交給自己的parent,此處即應用類載入器 */ return classLoader.loadClass(name); } } } ``` # 程式碼結構 ##中介軟體整體模組概覽 在繼續之前,有必要說下程式碼結構。 ![](https://img2020.cnblogs.com/blog/519126/202007/519126-20200704185639983-1439623996.png) 中介軟體總共三個jar包: common-v1,middle-ware,middle-ware-api。 其中,middle-ware的pom如下: ```java ``` ## 中介軟體api模組 該模組無任何依賴,就是個介面 ```java public interface IGisUtilInterface { String getFormattedDate(String date); } ``` 該模組是很有必要的,該api模組必須由應用的類載入器載入,沒錯,是應用類載入器。 類似於servlet-api吧。 大家可以暫時這麼記著,至於原因,那就有點長了。 可以參考: [不吹不黑,關於 Java 類載入器的這一點,市面上沒有任何一本圖書講到](https://www.cnblogs.com/grey-wolf/p/11084379.html) ## 中介軟體實現模組 實現模組的pom: ```xml com.example
middle-ware 0.0.1-SNAPSHOT middle-ware com.example middle-ware-api 0.0.1-SNAPSHOT com.example common-v1 0.0.1-SNAPSHOT ``` 裡面只有一個類,就是實現api模組的介面。 ```java @Slf4j public class GisUtilImpl implements IGisUtilInterface{ @Override public String getFormattedDate(String date) { String v1 = CommonDateUtil.format(date); log.info("invoke common v1,get result:{}", v1); return v1; } } ``` ## spring boot 的自定義loader模組 這部分和業務關係不大,主要就是自定義我們前面的那個fat jar啟動類。 ```xml com.example custom-jar-launch 0.0.1-SNAPSHOT custom-jar-launch Demo project for Spring Boot 1.8 Greenwich.SR3 org.projectlombok lombok 1.18.10 org.springframework.boot spring-boot-loader ``` 這裡有個特別的依賴: ```xml org.springframework.boot spring-boot-loader ``` 該模組,主要包含: com.example.demo.CustomJarLauncher com.example.demo.CustomLaunchedURLClassLoader ## 應用程式 ```java com.example web-application 0.0.1-SNAPSHOT web-application Demo project for Spring Boot com.example common-v2 0.0.1-SNAPSHOT com.example middle-ware-api 0.0.1-SNAPSHOT com.example middle-ware 0.0.1-SNAPSHOT ``` 模擬jar包衝突場景,此時,我們已經同時依賴了v1和v2了。 其測試程式碼如下: ```java public class OrderServiceApplication { /** * 中介軟體使用的classloader */ static ClassLoader delegatingClassloader; @RequestMapping("/") public void test() throws ClassNotFoundException, IllegalAccessException { // 1 Class middleWareImplClass = delegatingClassloader.loadClass("com.example.demo.GisUtilImpl"); // 2 IGisUtilInterface iGisUtilInterface = (IGisUtilInterface) middleWareImplClass.newInstance(); // 3 String middleWareResult = iGisUtilInterface.getFormattedDate("version:"); log.info("middle ware result:{}",middleWareResult); // 4 String result = CommonDateUtil.format("version:"); log.info("application result:{}",result); } ``` * 1處,類似於servlet,也是把servlet實現類寫死在web.xml的,我們這裡也一樣,把中介軟體的實現類寫死了。 可能有更好的方式,暫時先這樣。 * 2處,將實現類(中介軟體類載入器載入),轉換為介面類(應用類載入器載入)。之所以要定義介面,這裡很關鍵。 可以再仔細看看: [不吹不黑,關於 Java 類載入器的這一點,市面上沒有任何一本圖書講到](https://www.cnblogs.com/grey-wolf/p/11084379.html) * 3處,呼叫中介軟體程式碼 * 4處,呼叫應用程式碼 ## 效果展示 ```java 2020-05-22 06:37:13.481 INFO 6676 --- [nio-8082-exec-1] com.example.demo.GisUtilImpl : invoke common v1,get result:version:- v1 2020-05-22 06:37:13.481 INFO 6676 --- [nio-8082-exec-1] c.example.demo.OrderServiceApplication : middle ware result:version:- v1 2020-05-22 06:37:13.482 INFO 6676 --- [nio-8082-exec-1] com.example.date.util.CommonDateUtil : v2 result:version:- v2 2020-05-22 06:37:13.482 INFO 6676 --- [nio-8082-exec-1] c.example.demo.OrderServiceApplication : application result:version:- v2 ``` 可以發現,中介軟體那裡,是v1;而呼叫應用的方法,則是v2。 說明我們成功了。 我這裡用arthas分析了下這個類: ![](https://img2020.cnblogs.com/blog/519126/202007/519126-20200704191637082-1437869440.png) 這個類,還在下面出現: ![](https://img2020.cnblogs.com/blog/519126/202007/519126-20200704191723971-966612789.png) 這個是中介軟體載入的。 所以,大家平安無事地繼續生活在了一起。 # 原始碼 https://gitee.com/ckl111/all-simple-demo-in-work-1/tree/master/jar-conflict 該原始碼怎麼使用? 先正常打包應用為fat jar,然後將custom-jar-launch中的class,拷進fat jar,然後修改META-INF/MANIFEST檔案的啟動類。 然後呼叫介面: ```shell curl localhost:8082 ``` # 總結 希望對大家有所啟發,謝謝。