首页 > 其他分享 >甲方扔给两个存在包名与类名均相同的Jar包,要在工程中同时使用怎么办?

甲方扔给两个存在包名与类名均相同的Jar包,要在工程中同时使用怎么办?

时间:2024-08-26 09:15:29浏览次数:4  
标签:包名 String third Jar 代码 jar provider 类名 加载

你的项目是否曾遇到过有jar包冲突,而这些冲突的jar包又必须同时存在的情况?一般来说,jar 冲突都是因不同的上层依赖项,自身又依赖了相同 jar 包的不同版本所致,解决办法也都是去除其中一个即可。需要同时保留冲突jar包的情况,实属罕见。

在与第三访系统集成通信时,有一种方式是由被集成方提供Jar包,业务代码调用Jar包里提供的相关Java类或接口,并且很多都同时附带一份集成开发文档。
如果第三方在不同时期提供的jar包,相互存在冲突,而工程中又必须同时使用这两个 jar 包,该怎么办呢?

冲突场景

如下图所示,有两个 jar 包,分别是:third-provider-1.0.0.jar 和 third-provider-1.3.13.jar。两个包的异同点如下:

  • 相同的 package : guzb.cnblogs.classloader.third
  • 相同的类名 : SampleApi.java
  • 相同的方法签名(含返回值):public SampleDevice checkDevice(String deviceNo), 返回值类型 SampleDevice 也是包名与类名完全相同
  • 返回值类型的结构不同:两个Jar 包的 checkDevice 方法都返回 SampleDevice,但各自 SampleDevice 下的字段不完全一致(图中未体现出这一点)
  • 入口类既包含相同的签名方法,也包含不同签名的方法

conflicting-jar-illustration

而这两个 jar 包都必须保留,并且需要在工程中同时使用。因为冲突的那些类中所包含的方法,不完全一样,都要保留和使用。

根据类加载规则,同名的类,只会加载一次,因此,如果 1.0.0 包中的 SampleApi 被加载,则 1.3.13 包中的 SampleApi 不会被加载。可我们要在业务代码时同时使用这两个版本的 SampleApi,要如何完成呢?

解决方案1:微服务隔离

最简单的办法就是将两个版本的Jar包,做成两个独立的微服务,然后再将使用到的接口或方法,包装成 Http 服务,业务代码调用这些服务即可。

以 1.0.0 包中的 SampleApi 为例,整个包装过程要做的事项如下(以 SpringWeb 为例):

  1. 新建一个Web工程
    它依赖 1.0.0 这个jar包。并且最终将部署为一个Web服务,这样业务工程便可以调用它所提供的 http 服务

  2. 编写 Web Controller
    将 SampleApi 中相应的方法,通过 WebController 包装为 Http服务

    根据需要,还可以将 SampleDevice 这个返回类也包装成一个新类。不过这一步可以省略,由调用方法代码自己去编写也可以。因为业务工程与此 Web 服务是通过 http 交换信息的(通常都是 json 串)。

由于当前(2024-06-11)微服务非常流行,也不在此啰嗦该怎么做了。重点阐述一下第二种实现方式。

解决方案2:类加载器隔离

jar 包冲突是因同名的类只会被加载一次,但还有一个重要的细节:何为同名的类?一般而言,当两个类的 package name (包名) 与 class name (类名) 都相同时,即为同名类。不过还有一个隐藏的区分项,就是类加载器。
在程序运行期间,Java 的类是由类加载器载入到运行环境的。对于同一个类加载器来说,包名与类名相同的类只会被加载一个。但不同类加载,可以各自单独加载包名与类名均相同的类。

以前面图片中的冲突场景为例:假定业务代码中有两个类加载器,分别是 loaderA 和 loaderB,若 loaderA 加载了 1.0.0 包中的 guzb.cnblogs.classloader.third.SampleApi,则无法加载 1.3.13 中的 guzb.cnblogs.classloader.third.SampleApi,因为该类加载器已经加载过这个类了。但无论 loaderA 加载了什么,都不影响 loaderB 再去加载一次。也就是说,此时 loaderB 既可以加载 1.0.0 中 SampleApi,也可以加载 1.3.13 包中的 SampleApi,但不能同时加载。不难发现,我们可以通过 loaderA 加载 1.0.0 包的 SampleApi, 和 loaderB 加载 1.3.13 包的中 SampleApi 这种组合方案,实现同一工程中,同时加载这两个存在冲突的 jar 包中的所有类。

类加载器就像一个是沙盒,将两套代码予以隔离,它的这个特性正好用来解决本文的冲突场景。实事上,它在 Java 容器中应用得最多(如tomcat隔离不同的Web项目)。为了保证一些公共基础类(如jdk里rt.jar中的类)不要重复加载,类加载器还引入了双亲委托式的加载机制。关于类加载器本身的基础知识不是本文本的重点,读者请参阅其它相关网文。本文接下来将重点介绍如何应用类加载器,实现一个工程级的方案来更友好地解决前面提到的冲突场景。

类加载器编码不简单

如上图所示,或许不少读者觉得这篇文章到此可以结束了。因为类加载器隔离两个同包同名的类,原理上非常清晰,它一定是可行的。后续讲解不就是显得既多余又啰嗦了么。

OK,尽管通过不同类加载器确实可以解决类名冲突的问题,但同时却又引来了另外一个问题:编写代码时,无法像普通编程那样书写。这是什么意思呢,为了说清楚这个东西,我们先通过类加载器的方式来获取一个类,体验一下其编码的不便。

类加载器编程体验

这里单独创建一个 maven 工程来简单来体验类加载器编程与普通编程在代码书写上的差异, 工程源码:classloader-experience,其结构如下:

┌─ classloader-experience
│   ├─ book-sample                       # 一个业务模块样例,该 module 下的代码将由单独的类加载器加载  
│   │   └─ vip.guzb.clrdemo
│   │       ├─ BookApi                   # book 样例模块的使用入口类,独立类加载器也直接加载它
│   │       ├─ Book                      # 书籍类
│   │       └─ Press                     # 出版社类
│   │
│   └─ main                              # 主程序模块,book-sample 下的类将在 main 模块中加载
│       └─ vip.guzb.clrmain
│           ├─ MyClassLoader             # 一个简单的自定义类加载器,用于从指定目录加载 Class
│           └─ ClassLoaderExperienceMain # 整个类加载器体验程序的主类(入口类)
└─ pom.xml

main 模块是主程序,而 book-sample 下的 class 将由 main 模块使用独立类加载器加载,因此,book-sample 模块下的 class 不能位于 main 模块启动时的 classpath 下,否则,根据 ClassLoader 的双亲委派模型,book-sample 的类加载器将会与 main 模块的类加载器是同一个,而不是我们单独编写的 MyClassLoader,也就达不到目的了。这里将将它们都写在同一个 maven 工程中,是为了方便在博客中展示所有代码。

下面是 book-sample 模块的代码

package vip.guzb.clrdemo;

public class BookApi{
    public String description() {
        return "Hi,你好,很高兴见到你。本内容是来自 BookApi 的 description 方法";
    }

    public Collection<Book> getBooksOfAuthor(String authorName) {
        List<Book> books = new ArrayList<Book>();
        books.add(new Book("TeaHouse", authorName, 135.0, new Press("四川人民出版社", "四川省成都市的一个犄角旮旯处")));
        books.add(new Book("The Life of Mine", authorName, 211.0, new Press("长江文艺出版社", "大陆一个神秘的地方")));
        return books;
    }
}

public class Press {
    private String name;
    private String address;

    public Press(String name, String address) {
        this.name = name;
        this.address = address;
    }

    // omit the getter and setter methods
}

public class Book {
    private String name;
    private String author;
    private Double price;
    private Press press;

    public Book(String name, String author, Double price, Press press) {
        this.name = name;
        this.author = author;
        this.price = price;
        this.press = press;
    }

    // omit the getter and setter methods
    ......
}

主程序将会使用单独的类加载器加载 BookApi,并创建一个该类的实例,调用其 descritpion() 和 getBooksOfAuthor(String name) 方法,然后进一步操作方法的返回值。前者简单地返回一个 java.lang.String 对象, 后者则返回一个集合,集合元素类型为 vip.guzb.clrdemo.Book,Book 类还有一个 vip.guzb.clrdemo.Press 类型的成员字段,因此整个结构是比较复杂的。

OK,现在回到主模块 main 中,该模块做了两件事:

  1. 提供一个自定义的类加载器,用于从磁盘指定目录中加载 Class
  2. 在主程中加载 book-sample 中的类,并调用 BookApi 中的方法,进一步访问方法返回值中的属性
自定义类载器(点击查看代码)
package vip.guzb.clrmain;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public class MyClassloader extends ClassLoader {
    // 要读取的编译后的 Class 在磁盘上的根目录
    private String classRootDir;

    public MyClassloader(String classRootDir) {
        this.classRootDir = classRootDir;
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        // 读取Class的二进制数据
        byte[] classBytes = getClassBytes (className);
        return super.defineClass(className, classBytes, 0, classBytes.length);
    }

    private byte[] getClassBytes(String className) {
        // 解析出class文件在磁盘上的绝对路径
        String classFilePath = resolveClassFilePath(className);

        // 将Class文件读取为二进制数组
        ByteArrayOutputStream bytesReader;
        try (InputStream is = new FileInputStream(classFilePath)) {
            bytesReader = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int readSize = 0;
            while ((readSize = is.read(buffer)) != -1) {
                bytesReader.write(buffer, 0, readSize);
            }
            return bytesReader.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new byte[0];
    }

    private String resolveClassFilePath(String className) {
        return this.classRootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
    }

}

如何编写自定义类加载不是本文的重点,这里简单说明一下,编写自定义类加载器的3个主要步骤:

  1. 自定义类加载器继承java.lang.ClassLoader, 或其子类

  2. 读取所加载类的字节码 <重点>

    根据类的包名和类名找到编译后的 class 字节码所在的地方,可能是在磁盘上,也可能就位于网络上,还可能位于内存的其它位置。把这些字节码读取到一个 byte[] 数组中

  3. 调用父类的 defineClass 方法,完成类的加载

接下来就是整个 main 模块的重点了:加载前面提及的 vip.guzb.clrdmo 包下的 BookApi(加载它的过程,附带会把 Book 和 Press 也加载了),并调用 BookApi 类的 description 和 getBooksOfAuthor 方法,如下所示:

package vip.guzb.clrmain;

import java.lang.reflect.Method;
import java.util.Collection;

/**
 * 类加载器体验主类
 *
 * @author 顾志兵
 * @mail [email protected]
 * @since 2024-05-18
 */
public class ClassLoaderExperienceMain {

    public static void main(String[] args) throws Exception {
        // 1. 实例化一个自定义的类加载器
        //    book-sample 模块上的类所在根目录,请根据自己电脑的实际情况更改
        MyClassloader myClassloader = new MyClassloader("D:\\tmp\\DemoClass");

        // 2. 加载 BookApi 这个Class
        Class bookApiClass = myClassloader.loadClass("vip.guzb.clrdemo.BookApi");

        // 3. 创建 BookApiClass 的实例,
        //    这里不直接写成  DemoA demoA = new DemoA(); 因为 DemoA 在类路径下不存在。
        //    即使存在,根据本文本一开始的场景,也因为同时要加载同名的类,而不允许存在
        Object bookApiObj = bookApiClass.newInstance();

        // 4. 调用 BookApi 的 description() 方法
        //    该方法很简单,返回类型为标准库中的 java.lang.String, 因此代码书写也相对容易
        Method testAMethod = bookApiClass.getMethod("description");
        String resultOfDescription = (String)testAMethod.invoke(bookApiObj);
        System.out.printf("description()方法的调用结果: %s\n\n", resultOfDescription);

        // 5. 调用 BookApi 的 getBooksOfAuthor 方法
        //    该方法的返回值是一个集合,而集合中的对象在 Classpath 中不存在,
        //    获取集合元素的属性和方法的代码将会显示很冗长
        Method getBooksOfAuthorMethod = bookApiClass.getMethod("getBooksOfAuthor", String.class);
        Collection<?> books = (Collection<?>) getBooksOfAuthorMethod.invoke(bookApiObj, "老舍");
        System.out.println("老舍的作品列表: ");
        for (Object book : books) {
            // books 集合中的对象类型为 vip.guzb.clrdemo.Book,
            // 但由于是使用单独的类加载器加载的,不能像平常编码那样直接在源码中书写,依然要通过反射来获取
            Method bookNameMethod = book.getClass().getMethod("getName");
            Method bookPriceMethod = book.getClass().getMethod("getPrice");
            String bookName = (String)bookNameMethod.invoke(book);
            Double price = (Double) bookPriceMethod.invoke(book);

            // 同理, vip.guzb.clrdemo.Press 对象的访问也需要通过反射
            Method pressMethod = book.getClass().getMethod("getPress");
            Object pressObj = pressMethod.invoke(book);
            Method pressNameMethod = pressObj.getClass().getMethod("getName");
            Method pressAddressMethod = pressObj.getClass().getMethod("getAddress");
            String pressName = (String)pressNameMethod.invoke(pressObj);
            String pressAddress = (String)pressAddressMethod.invoke(pressObj);
            System.out.printf(" · 书名: 《%s》, 价格: %.2f, 出版社: %s, 地址: %s\n", bookName, price, pressName, pressAddress);
        }
    }
}

测试代码的第1步就创建了一个 MyClassLoader,该类加载器将会从 D:\tmp\DemoClass 中加载 BookApi。因此,需要将 book-sample 模块下编译后的所有 Class 按 package 的层次复制到 D:\tmp\DemoClass 目录中。

输出结果为:

description()方法的调用结果: Hi,你好,很高兴见到你。本内容是来自 BookApi 的 description 方法

老舍的作品列表: 
 · 书名: 《TeaHouse》, 价格: 135.00, 出版社: 四川人民出版社, 地址: 四川省成都市的一个犄角旮旯处
 · 书名: 《The Life of Mine》, 价格: 211.00, 出版社: 长江文艺出版社, 地址: 大陆一个神秘的地方

从上面的代码可以看出,要访问通过独立类加载器加载的Class和实例,在代码书写上存在以下不便:

  • 不能直接 new 或调用静态代码的方式创建类的实例(需要通过反射,如第3步)
  • 不能通过 obj.xxx() 的方式调用实例的方法(需要通过反射,如第4步)
  • 不能直接获取对象的方法或属性(需要通过反射,如第5步)

总之, 一切都需要以反射的方式来编码,这太糟糕了,不仅代码很冗长(如第5步),而且非常不易阅读。

无法忍受反射式编程代码

中间层接口方案

方案原理

看来起完美的类加载器隔离方案,却为业务代码的书写带来了麻烦,难道就没有好的解决办法了吗?办法还真有,只是需要提前付出一些额外工作,但这是值得的。这个方案为:定义一套中间层API,为两个Jar包中冲突的方法分别定义不同的上层接口,业务代码直接引入和使用这个中间层API中的方法即可。该方案要能执行起来,还需要为两个 jar 包分别单独编写中间层API的实现代码。总结起来,需要完成以下几步:

  1. 定义中间层接口

  2. 分别为两个有冲突的JAR包编写中间层接口的实现代码

  3. 在业务代码中直接使用中间层接口类来编写代码

    这听上去像是废话,这里解释一下,所谓直接使用中间层接口编写代码,隐去了以下细节:

    • 中间层接口类位于业务代码的 classpath 中,它与业务代码使用相同的类加载器,因此业务代码中使用到中间层接口的地方,不需要通过反射来调用,而是最自然最朴实的书写形式。

    • 两个有冲突 Jar 包的中间层实现代码,需要通过独立的类加载器加载,与 类加载器编程体验 章节中的处理方式一致。

    • 同理,最底层的两个原始 jar 包,也需要通过独立的类加载器加载

至此为止,大概你也没明白这是个啥方案

标签:包名,String,third,Jar,代码,jar,provider,类名,加载
From: https://www.cnblogs.com/guzb/p/18379639/load-contains-same-name-classes-jars-by-classloade

相关文章

  • SpringBoot文档之Jar文件格式的阅读笔记
    TheExecutableJarFormat使用spring-boot-maven-plugin构建项目时,生成的目标jar文件的格式的说明。NestedJARs以JarLauncher为例:META-INF/MANIFEST.MF,定义jar的元数据。org.springframework.boot.loader.launch.JarLauncher.class,jar的启动类。BOOT-INF/classes/,放......
  • 源码打包成jar包后如何执行testng的用例
    在将源代码打包为jar文件后,你可以按照以下步骤来执行TestNG的测试用例:确保在jar包中包含了所有的测试类和相关的依赖库。在jar包所在的目录下创建一个TestNG的XML配置文件,可以命名为testng.xml。在配置文件中指定要执行的测试类或方法。你可以使用<classes>和<methods>标......
  • maven本地jar包打包时无法打进jar的解决方式
    <dependency><groupId>cfca-logback</groupId><artifactId>cfca-logback</artifactId><version>4.2.1.0</version><scope>system</scope><systemPath>${project.basedir}/libs/logback......
  • Maven如何手动添加jar包
    关于maven依赖死活都下载不了时,可以使用手动添加jar包解决方案:举例:先搜索 mysql-connector-java对应需要的jar包并下载好,然后本地将jar包下载放在E:\jar文件下在pom.xml中添加文件<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifact......
  • Powershell 调用cmd 运行exe、bat、jar文件
    1.配置路径$nginxPath="C:\path\to\nginx"$redisPath="C:\path\to\redis"$ruoyiAdminJarPath="C:\path\to\ruoyi-admin"2.exe文件,cd到exe所在文件夹,然后执行Write-Output"启动Nginx..."Start-Process-FilePath"cmd.......
  • 使用maven-shade-plugin打包shade jar
    jar分类jar:用于给javaproject依赖的jar包,无法单独执行excutablejar:比普通jar多了一个main类的指定,在jar包里,META-INF/MANIFEST.MF文件里,有一行是指定mainclass的配置Manifest-Version:1.0Created-By:MavenJARPlugin3.3.0Build-Jdk-Spec:21Main-Class:org.ex......
  • Gradle编译项目Druid找不到tools.jar和jconsole.jar
     原因:jdk11之后不支持druid的两个依赖方法一:<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.5</version>方法二:<!--<exclusions><exclusion><gro......
  • springboot项目打包jar 并打包为exe启动
    springboot项目打包jar并打包为exe启动(在无jdk环境下运行)环境SpringBoot+Windows+IDEA实现1.springboot打包为可执行jar(这里使用maveninstall)maven工具栏选择项目->Plugins->install注:如果存在前端页面需同时打包(webapp下);需在pom.xml中进行配置<build>......
  • 元素偏移(offset,scroll,client)介绍,动态设置类名
    文章目录一offset,scroll,client简单介绍二、scroll系列1scrollWidth2scrollHeight3scrollTop4scrollLeft三、offset系列1.offsetHeight2.offsetWidth3.offsetTop4.offsetLeft四client系列1clientTop2clientLeft3clientWidth4clientHeight五案例1动态设置......
  • jar包
    7.6jar包jar包(JavaArchive)是Java中一种常见的归档文件格式。它实际上就是一个压缩文件,通常以.jar作为文件扩展名。jar包可以包含Java类、资源文件、库、元数据等内容,以便在Java应用程序中进行打包、分发和部署。jar包的主要作用包括:打包Java类和相关资源文件:将Java类文件、......