类隔离自定义类加载器实现,你学会了吗?

开发 前端
本文分享的方式是从类加载器方向出发,实现最终的类隔离,避免了不同模块间不同类的冲突,其中顺便也简单带过了jvm类加载相关的知识点,也算是一劳多得。

前言

由于微服务的快速迭代、持续集成等特性,越来越多的团队更倾向于它。但是也体现出了一些问题,比如在基础设施建设过程中,需要把通用功能下沉,把现有大而全的基础设施按领域拆分,考虑需要兼容现有生产服务,会产生不同的依赖版本,有时不注意就可以引发问题。比如本文遇到的依赖包版本冲突问题,以及如何利用类隔离技术解决的分析。

类隔离是什么?

类隔离是一种通过类加载器实现加载所需类的实现方式,使得不同版本类间隔离,避免了使用冲突问题,最终的效果就是不同模块的内容被不同的类加载器加载,满足同一环境下同时兼容不同接口实现类。

使用场景

比如业务服务A和业务服务B均需要消息通知等,均依赖消息中间件,但所引用版本不一致,导致最终只有一个版本加载到JVM,在某一个服务调用时会出现 NoSuchMethodError或NoSuchClassError问题,这就很难排查出来,没准会影响项目进度,最终月度的绩效(“鸡腿”)不保。

服务A pom.xml:

<!-- common-message-->
<dependency>
<groupId>com.lgy</groupId>
<artifactId>spring-common-message</artifactId>
<version>1.0.0<version>
</dependency>

服务B pom.xml:

<!-- common-message-->
<dependency>
<groupId>com.lgy</groupId>
<artifactId>spring-common-message</artifactId>
<version>2.0.0<version>
</dependency>

业务调用流程:

// 业务A调用微信服务通知
MessageUtil.sendMessage(content,peopleId,templateId,"wechat");
// 业务B调用微信服务通知
MessageUtil.sendToWechat(content,peopleId,templateId);

JVM最终加载的为 2.0.0 版本的依赖,导致业务A在调用时抛异常java.lang.NoSuchMethodError。

解决方案

大体的解决思路就是,在不改变业务代码的前提下, 业务A调用 1.0.0 版本的消息工具类, 业务B调用2.0.0版本的消息工具类,因此需要JVM能够利用自定义类加载器加载所需的类或关联的类。

实现思路

  • 重写类加载器,实现自定义类加载(java.lang.ClassLoader)
  • 重写类加载函数

重写 findClass(String name)

重写 loadClass(String name)

涉及的知识点

  • JVM加载过程:加载-》链接-》初始化(具体后续介绍)
  • 双亲委派机制:委托父加载器查询;如果父加载器查询不到,则调用自身的findClass加载

重写findClass:

import java.io.*;
import java.util.HashMap;
import java.util.Map;

public class CustomerFindClass extends ClassLoader {
private Map<String, String> classPathMap = new HashMap<>();
public CustomerFindClass() {
// 业务A的自定义类加载器
classPathMap.put("com.lgy.businessA.service.impl.MessageServiceImpl", "E:/dataway-demo/example/target/classes/com/lgy/businessA/service/impl/MessageServiceImpl.class");
classPathMap.put("com.lgy.v1.message.util.MessageUtil", "E:/dataway-demo/example/target/classes/com/lgy/v1/message/util/MessageUtil.class");
}

/**
* findClass方式加载类
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String classPath = classPathMap.get(name);
File file = new File(classPath);
if (!file.exists()) {
throw new ClassNotFoundException();
}
byte[] bytes = getClassData(file);
if (null == bytes || 0 == bytes.length) {
throw new ClassNotFoundException();
}
return defineClass(bytes, 0, bytes.length);
}

private byte[] getClassData(File file) {
try (InputStream ins = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[]{};
}

最终结果与预期的结果不一致:

  • 预期结果:业务A的MessageServiceImpl与MessageUtil由CustomerFindClass加载
  • 实际结果:业务A的MessageServiceImpl由CustomerFindClass加载,而MessageUtil由sun.misc.AppClassLoader加载。
  • 分析:由于JVM类加载的双亲委托机制,业务A调用消息工具类时,类加载器(CustomerFindClass)会委托父类加载器(AppClassLoader)加载类,如果存在,则不再执行自身的findClass方法加载,导致结果不理想。(main 方法类默认情况下都是由 JDK 自带的 AppClassLoader 加载的)。

重写loadClass

private ClassLoader classLoader;

/**
* 重新loadClass方法
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class result = null;
try {
//这里要使用 JDK 的类加载器加载 java.lang 包里面的类
result = classLoader.loadClass(name);
} catch (Exception e) {
// ignore error
}
if (null != result) {
return result;
}
String classPath = classPathMap.get(name);
File file = new File(classPath);
if (!file.exists()) {
throw new ClassNotFoundException();
}
byte[] bytes = getClassData(file);
if (null == bytes || 0 == bytes.length) {
throw new ClassNotFoundException();
}
return defineClass(bytes, 0, bytes.length);
}

满足业务A的MessageServiceImpl与MessageUtil由CustomerFindClass加载

注意:这种方式破坏了双亲委托机制,但由于重写了loadClass方法,所有类均会有CustomerFindClass加载器加载,需要过滤出不需要隔离的类,如java.lang包下的类,需要由ExtClassLoader 来加载。

总结

本文分享的方式是从类加载器方向出发,实现最终的类隔离,避免了不同模块间不同类的冲突,其中顺便也简单带过了jvm类加载相关的知识点,也算是一劳多得。

责任编辑:武晓燕 来源: Java技术指北
相关推荐

2023-01-28 10:40:56

Java虚拟机代码

2022-06-27 08:16:34

JSON格式序列化

2022-02-17 07:10:39

Nest自定义注解

2023-10-06 00:01:08

UML类图图形

2023-10-19 09:14:34

Java开发

2023-11-06 07:25:51

Spring配置应用程序

2022-01-20 07:31:49

架构

2022-06-16 07:50:35

数据结构链表

2023-12-26 00:55:51

资源隔离CPU

2024-01-19 08:25:38

死锁Java通信

2023-07-26 13:11:21

ChatGPT平台工具

2024-02-04 00:00:00

Effect数据组件

2023-01-10 08:43:15

定义DDD架构

2020-12-30 08:01:07

Java隔离加载

2023-05-08 07:41:07

Linux内核ELF文件

2022-07-08 09:27:48

CSSIFC模型

2023-01-30 09:01:54

图表指南图形化

2023-12-12 08:02:10

2023-10-10 11:04:11

Rust难点内存

2024-01-02 12:05:26

Java并发编程
点赞
收藏

51CTO技术栈公众号