最近项目需要使用jar包隔离技术,所以了解了几种方案,本文针对几种不同方案进行了介绍,不同问题有各自合适的方案,正在解决类似问题的同学可以通过本文快速了解jar包隔离的几种技术。
通常自己写代码是,大家都忽略这些问题了,但是当项目越大,需要对接的业务越多时,这些问题就日益严重了, 这时候,就需要对jar包进行统一管理了,可以统一组件jar包版本,也可以容器化隔离方案。今天就给大家介绍一下解决jar包冲突的几种方式。
归纳了解了几种业内的解决方案如下,各有优劣
spring boot方式,统一管理各个组件版本,简洁高效,但遇到必须使用不同版本jar包时,就不行了OSGI技术,用容器对jar包进行暴露和隔离,实际上是通过不同classload加载类来达到目的,真正的jar包隔离,还能做模块化,服务热部署,热更新等,缺点就是太重了。sofa-ark 用FatJar技术去实现OSGI的功能,jar包隔离原理上跟osgi一致,不过基于fat jar技术,通过maven 插件来简化复杂度,比较轻量,也支持服务热部署热更新等功能。shade 也有maven插件,通过更改jar包的字节码来避免jai包冲突,jar包冲突的本质是类的全限定名(包名+类名)冲突了,通过全限定名不能定位到你想用的那个类,maven-shade插件可以更改jar包里的包名,来达到解决冲突的目的。自己定义classload,反射调用冲突方法,代码量太大,不通用,但是会帮助理解上面组件的原理。此外,还有一些其他解决的方式,比如java 9 模块化,提供了一种新的打包方式来解决类冲突、gradle 组件、代码内嵌等。
想要理解jar包隔离,就要了解java的类加载机制,先看下基础知识
加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的,所以是通过这个机制来实现类隔离的,sofa-ark实际上也是定义了不同的classload来加载不同的模块,进而实现类隔离
JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:
它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
下面程序可以获得根类加载器所加载的核心类库,并会看到本机安装的Java环境变量指定的jdk中提供的核心jar包路径:
public class ClassLoaderTest {
public static void main(String[] args) { URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for(URL url : urls){ System.out.println(url.toExternalForm()); } }} 运行结果:
它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。
类加载器加载Class大致要经过如下8个步骤:
检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。 从文件中载入Class,成功后跳至第8步。 抛出ClassNotFountException异常。 返回对应的java.lang.Class对象。
JVM的类加载机制主要有如下3种。
全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
了解了上面这些,来看下几种方案解决类冲突的原理: 当两个class全限定名一样时,jvm中只能有其中一种class,这也是由于jvm类加载机制决定的,所以要同时加载两个class,就要打破双亲委托,通过定义新的classload来加载另一种class,避免冲突,sofa-ark就是通过这种方,通过一些配置就可以实现这种另一种方式就是更改class的全限定名,使两个class可以被同一个classload加载,maven-shade就是通过这种方式。
使用方法及具体demo 可以去看这篇文章 https://juejin.im/post/5b6b982451882569fd2898d7 需要注意几点 ,如果plugin 和 biz 都在编译器的同一个工作空间的话,运行 biz 时需要将 plugin 删除,否则,maven 会自动引入 plugin 项目而不是 jar 包,就达不到jar包隔离的目的了。 SofaArkBootstrap.launch(args);容器启动函数要在main方法第一句
sofa-ark 框架支持单独application 和 sofaboot 两种方式,满足单独使用和web框架下的jar包隔离,还能基于zk 完成服务热部署等高大上的功能,但是配置方式略复杂。很不幸我的应用是跑在spark里的,做不到将容器启动函数放在main的第一句,因为本来就在spark的容器里了,所以此种方案pass。
在这要感谢https://stanzhai.site/blog/post/stanzhai/%E4%BC%98%E9%9B%85%E5%9C%B0%E8%A7%A3%E5%86%B3Spark-Application-jar%E5%8C%85%E5%86%B2%E7%AA%81%E9%97%AE%E9%A2%98这篇文章的作者,让我了解到了shade方式的jar包隔离,这是一种取巧的方式,并没有定义不同的classload,而是通过更改jar包的字节码,避免jar包内package的名字相同,带来的问题就是,有反射方式调用jar包内方法时,反射部分的代码也要修改,虽然这种方式看起来没有sofa-ark高端,但是生产力高啊,不需要复杂的配置,需要更改的代码也不多,所以我采用了这种方式。下面是对kafka jar包shade的一个样例
首先建立一个工具工程引入需要shade jar 包配置 maven-shade插件,通过 pattern shadedPattern属性来配置要更改的包的名字使用shade 后的jar包,import 更改后的包名字,若有反射调用,也反射更改后的包名字 <dependencies> <dependency> <groupId>com.test</groupId> <artifactId>kafka_2.11-0.10.0.1</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>com.test</groupId> <artifactId>kafka-client-0.10.0.1</artifactId> <version>1.0</version> </dependency> //kafka的依赖太多,这是单独提出需要shade的两个jar 并发布到本地库后引入 </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <minimizeJar>true</minimizeJar> <relocations> <relocation> <pattern>org.apache.kafka</pattern> 需要替换的包的名字 <shadedPattern>org.apache.tc_kafka</shadedPattern> 替换后的名字 </relocation> <relocation> <pattern>kafka</pattern> 需要替换的包的名字 <shadedPattern>tc_kafka</shadedPattern> 替换后的名字 支持通配符可以将包名中所有含kafka的替换为tc_kafka </relocation> </relocations> </configuration> </execution> </executions> </plugin> </plugins> </build>使用maven打包发布后可得到shade之后的jar包 原始kafka jar包 shade后的kafka jar包 使用shade 后的jar包消费
import org.apache.tc_kafka.clients.consumer.KafkaConsumer; ...... props.put("key.deserializer", "org.apache.tc_kafka.common.serialization.StringDeserializer"); props.put("value.deserializer", "org.apache.tc_kafka.common.serialization.StringDeserializer"); ...... 其他部分跟正常消费一致此种方式是基础,实现jar包隔离根本的方式,但是一般很少用这种方法,代码量太大,下面是一个反射调用kafka消费的样例,自定义classload引入了jar包进行消费,跟main 函数使用不同的classload,在其他地方使用不同版本的kafka jar包也就不会引起冲突。
package com.tc.kafka.test; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import com.sun.org.apache.bcel.internal.generic.NEW; public class Copy1_consumer_sub { public static void main(String[] args) throws IOException, InterruptedException, IllegalArgumentException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, SecurityException, InvocationTargetException, NoSuchFieldException, InstantiationException { Properties props = new Properties(); props.put("bootstrap.servers", "*****"); props.put("key.deserializer", "org.apache.kafka.common.serialization.ByteArrayDeserializer"); props.put("value.deserializer", "org.apache.kafka.common.serialization.ByteArrayDeserializer"); props.put("group.id", "1023"); props.put("auto.offset.reset", "earliest"); props.put("acks", "1"); props.put("retries", "10"); props.put("batch.size", "10"); props.put("linger.ms", "50"); props.put("buffer.memory", "33554432"); // props.put("max.request.size", "20971520"); props.put("request.timeout.ms", "35000"); props.put("session.timeout.ms", "30000"); props.put("heartbeat.interval.ms", "100"); props.put("fetch.max.wait.ms", "3000"); props.put("max.block.ms", "10000"); props.put("fetch.message.max.byte", "10000"); props.put("fetch.max.bytes", "10000"); props.put("enable.auto.commit", "false"); File file = new File("E:\\eclipse_2019_workspace\\kafka\\lib\\kafka-clients-0.10.0.1.jar"); File file1 = new File("E:\\eclipse_2019_workspace\\kafka\\lib\\kafka_2.11-0.10.0.1.jar"); ClassLoader classLoader = new URLClassLoader(new URL[] { file.toURL(),file1.toURL() }, Copy1_consumer_sub.class.getClassLoader()); String KafkaConsumer_n = "org.apache.kafka.clients.consumer.KafkaConsumer"; String ConsumerRecord_n = "org.apache.kafka.clients.consumer.ConsumerRecord"; String ConsumerRecords_n = "org.apache.kafka.clients.consumer.ConsumerRecords"; String Utils_n="org.apache.kafka.common.utils.Utils"; Class KafkaConsumer_c = Class.forName(KafkaConsumer_n,true,classLoader); Class ConsumerRecord_c = Class.forName(ConsumerRecord_n,true,classLoader); Class ConsumerRecords_c = Class.forName(ConsumerRecords_n,true,classLoader); Class utils_c=Class.forName(Utils_n,true,classLoader); Method getContextOrKafkaClassLoader=utils_c.getMethod("getContextOrKafkaClassLoader", null); Constructor KafkaConsumer_p = KafkaConsumer_c.getConstructor(Properties.class); Thread.currentThread().setContextClassLoader(classLoader); System.out.println(classLoader); System.out.println(getContextOrKafkaClassLoader.invoke(utils_c, null)); System.out.println(Copy1_consumer_sub.class.getClassLoader()); Object consumer = KafkaConsumer_p.newInstance(props); Method subscribe = KafkaConsumer_c.getMethod("subscribe", Collection.class); subscribe.invoke(consumer, Arrays.asList("topic_1029")); Method pool = KafkaConsumer_c.getMethod("poll", long.class); Field value = ConsumerRecord_c.getDeclaredField("value"); value.setAccessible(true); while (true) { Iterable ConsumerRecords = (Iterable) pool.invoke(consumer, 100); for (Object object:ConsumerRecords) { System.out.println("----"); System.out.println(new String((byte[]) value.get(object))); } } } }以上是我了解的jar包隔离方案,大家可以根据自己遇到的问题,选择合适的方案,有更好的方案也可以评论交流
借鉴了以下文章,感谢 https://stanzhai.site/blog/post/stanzhai/%E4%BC%98%E9%9B%85%E5%9C%B0%E8%A7%A3%E5%86%B3Spark-Application-jar%E5%8C%85%E5%86%B2%E7%AA%81%E9%97%AE%E9%A2%98 https://blog.mythsman.com/post/5d29b12c373f140fc98304a1/ 原文链接:https://blog.csdn.net/m0_38075425/article/details/81627349 链接:https://juejin.im/post/5b6b982451882569fd2898d7