我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Classloader隔离技术在业务监控中的应用详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Classloader隔离技术在业务监控中的应用详解

1. 背景&简介

业务监控平台是得物自研的一款用于数据和状态验证的平台。能快速便捷发现线上业务脏数据和错误逻辑,有效防止资产损失和保证系统稳定性。

数据流向:

上图的过滤和校验步骤的实际工作就是执行一个用户自定义的Groovy核对脚本。业务监控内部通过一个执行脚本的模块来实现。

本篇以脚本执行模块的一个技术问题为切入点,给大家分享利用ClassLoader隔离技术实现脚本执行隔离的经验。

2. 业务监控平台脚本调试流程

业务监控核心执行逻辑是数据校验核对。不同域会有不同的数据校验核对规则。最初版本用户编写一个脚本进行调试的步骤如下:

1.编写数据校验脚本(在业务监控平台规则下),脚本demo:

@Service
public class DubboDemoScript implements DemoScript {
    @Resource
    private DemoService demoService;
    @Override
    public boolean filter(JSONObject jsonObject) {
        // 这里省略数据过滤逻辑 由业务使用方实现
        return true;
    }
    @Override
    public String check(JSONObject jsonObject) {
        Long id = jsonObject.getLong("id");
        // 数据校验,由业务使用方实现
        Response responseResult = demoService.queryById(id);
        log.info("[DubboClassloaderTestDemo]返回结果={}", JsonUtils.serialize(responseResult));
        return JsonUtils.serialize(responseResult);
    }
}

其中DemoScript是业务监控平台定义的一个模板interface,  不同脚本实现此接口并重写 filter和check两个方法。filter方法是用来进行数据过滤的,check方法是进行数据核对校验的-用户主要编写这两个方法中的逻辑。

2.在业务监控平台脚本调试页面进行调试脚本,当脚本中有第三方团队Maven依赖时候,业务监控平台需要在pom.xml中添加Maven依赖并进行发布,之后通知用户再此进行调试。

3.点击脚本调试,查看脚本调试结果。

4.保存并上线脚本。

2.1 业务监控的脚本开发调试流程图

用户想要调试一个脚本需要告知平台开发,平台开发手动将Maven依赖添加到project中并去发布平台进行发布。中间不仅特别耗时,效率低,而且还要频繁发布,严重影响了业务监控平台的用户使用体验且增加平台开发的维护成本。

为此,业务监控平台在新版本中使用了Classloader隔离技术来动态加载脚本中依赖的业务方服务。业务监控不需要再进行特殊处理(添加Maven依赖再进行发布),用户在管控后台直接上传脚本以来的JAR文件就可以完成调试,大大降低了使用和维护成本,提高用户体验。

3. 自定义Classloder | 打破双亲委派

3.1 什么是Classloader

ClassLoader是一个抽象类,我们用它的实例对象来装载类 ,它负责将Java字节码装载到JVM中 , 并使其成为JVM一部分。JVM的类动态加载技术能够在运行时刻动态地加载或者替换系统的某些功能模块,而不影响系统其他功能模块的正常运行。一般是通过类名读入一个class文件来装载这个类。

类装载就是寻找一个类或是一个接口的字节码文件并通过解析该字节码来构造代表这个类或是这个接口的class对象的过程 。在Java中,类装载器把一个类装入Java虚拟机中,要经过三个步骤来完成:装载、链接和初始化。

3.2 Classloader动态加载依赖文件

利用Classloader实现类URLClassloader来实现依赖文件的动态加载。示例代码:

public class CustomClassLoader extends URLClassLoader {

private CustomClassLoader createCustomClassloader(String jarPath) throws MalformedURLException {
    File file = new File(jarPath);
    URL url = file.toURI().toURL();
    List<URL> urlList = Lists.newArrayList(url);
    URL[] urls = new URL[urlList.size()];
    urls = urlList.toArray(urls);
    return new CustomJarClassLoader(urls, classLoader.getParent());
}
public CustomClassLoader(URL[] urls, ClassLoader parent) {
    super(urls, parent);
}

在新增依赖文件的时候,使用Classloader的addURL方法动态添加来进行实现。

如果所有脚本使用同一个类加载器,来进行加载,就会出现问题,原因:同一个类(全限定名一样)只会被类加载器加载一次(双亲委派)。但是不同脚本存在两个全限定名一样的情况,但是方法或者属性不相同,因此加载一次就会导致其中一个脚本核对逻辑出错。

在理解了上面的情况下,我们就需要打破Java双亲委派机制,这里要知道一个知识点:一个类的全限定名以及加载该类的加载器两者共同形成了这个类在JVM中的唯一标识,因此就需要自定义类加载器,让脚本和Classloader一一对应且各不相同。话不多说,直接上干货:

3.3 自定义类加载器

public class CustomClassLoader extends URLClassLoader {
    public JarFile jarFile;
    public ClassLoader parent;
    public CustomClassLoader(URL[] urls, JarFile jarFile, ClassLoader parent) {
        super(urls, parent);
        this.jarFile = jarFile;
        this.parent = parent;
    }
    public CustomClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }
    private static String classNameToJarEntry(String name) {
        String classPath = name.replaceAll("\\.", "\\/");
        return new StringBuilder(classPath).append(".class").toString();
    }
    
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 这里定义类加载规则,和findClass方法一起组合打破双亲
        if (name.startsWith("com.xx") || name.startsWith("com.yyy")) {
           return this.findClass(name);
        }
        return super.loadClass(name, resolve);
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = null;
        try {
            String jarEntryName = classNameToJarEntry(name);
            if (jarFile == null) {
                return clazz;
            }
            JarEntry jarEntry = jarFile.getJarEntry(jarEntryName);
            if (jarEntry != null) {
                InputStream inputStream = jarFile.getInputStream(jarEntry);
                byte[] bytes = IOUtils.toByteArray(inputStream);
                clazz = defineClass(name, bytes, 0, bytes.length);
            }
        } catch (IOException e) {
            log.info("Custom classloader load calss {} failed", name)
        }
        return clazz;
    }
}

说明:上述自定义类加载器的loadClass和findClass方法一起达到破坏双亲委派机制的关键。其中super.loadClass(name, resolve)方法是不符合自定义类加载器规则的情况下,让其父加载器(这里的父加载器就是LanuchUrlClassloader)进行类加载,自定义类加载器只关注自己要加载的类,并按照脚本维度进行缓存对应的Classloader。

3.4 业务监控使用CustomClassloader

脚本或者调试脚本过程中和Classloader之间的创建关系:

一个脚本对应多个依赖的JAR文件(JAR文件在脚本调试页面上传到HDFS),一个脚本对应一个classloader(并进行本地缓存)(完全相同的两个类在不同的classloader中加载后两个Class对象是不相等的)。

3.5 业务监控动态加载JAR和脚本的实现

在上述的操作中,相信大家对JAR怎么实现脚本加载的,和脚本中@Resource注解标记的属性DemoService类如何创建Bean和注入到Spring容器比较关注。贴张流程图来讲解:

流程图中生成FeignClient对象的创建源码:


public static <T> T build(String serverName, String beanName, Class<T> targetClass) {
    return buildClient(serverName, beanName, targetClass);
}
private static <T> T buildClient(String serverName, String beanName, Class<T> targetClass) {
    T t = (T) BEAN_CACHE.get(serverName + "-" + beanName);
    if (Objects.isNull(t)) {
        FeignClientBuilder.Builder<T> builder = new FeignClientBuilder(applicationContext).forType(targetClass, serverName);
        t = builder.build();
        BEAN_CACHE.put(serverName + "-" + beanName, t);
    }
    return t;
}

流程图中生成注册Dubbo consumer的源码:

public void registerDubboBean(Class clazz, String beanName) {
        // 当前应用配置
    ApplicationConfig application = new ApplicationConfig();
    application.setName("demo-service");
    // 连接注册中心配置
    RegistryConfig registry = new RegistryConfig();
    registry.setAddress(registryAddress);
    // ReferenceConfig为重对象,内部封装了与注册中心的连接,以及与服务提供方的连接
    ReferenceConfig reference = new ReferenceConfig&lt;&gt;(); // 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏
    reference.setApplication(application);
    reference.setRegistry(registry); // 多个注册中心可以用setRegistries()
    reference.setInterface(clazz);
    reference.setVersion("1.0");
    // 注意:此代理对象内部封装了所有通讯细节,这里用dubbo2.4版本以后提供的缓存类ReferenceConfigCache
    ReferenceConfigCache cache = ReferenceConfigCache.getCache();
    Object dubboBean = cache.get(reference);    
    dubboBeanMap.put(beanName, dubboBean);
    // 注册bean
    SpringContextUtils.registerBean(beanName, dubboBean);
    // 注入bean
    SpringContextUtils.autowireBean(dubboBean);
}

以上就是Classloader隔离技术在业务监控平台的实际运用,当然在开发中也遇到一些问题,下面列举2个例子。

4. 问题&原因&方案

问题一: 多个团队的Check脚本运行在一起,单个应用的Metaspace空间占用会不会过大?

答:随着业务的发展,JAR文件的不断增多,确实会出现元数据区占用过大的情况,这也是做Classloader隔离的原因。在做了这一步之后,为后面进行脚本拆分做了铺垫,比如按照应用、团队等维度单独部署应用来运行其对应check脚本。这样脚本和业务监控逻辑上也进行了拆分,也会降低主应用的发布频率带来的噪音。

问题二:Classloader隔离实现上有没有遇到什么难题?

答:中间遇到了一些问题,就是同一个全限定名的类,出现了CastException异常,此类问题是最容易出现的,也最容易想到的。

原因:同一个类被2个不同的Classloader对象加载了2次。解决也很简单,使用同一个类加载器。

5. 总结

该篇文章讲解了自定义Classloader的实现和如何做到隔离,如何动态加载JAR文件,如何手动注册入Dubbo和Feign服务。类加载器动态加载脚本技术,在业务监控平台运用再适合不过了。当然一些业务场景也是可以参考此项技术,来解决一些技术问题。

以上就是Classloader隔离技术在业务监控中的应用详解的详细内容,更多关于Classloader业务监控隔离技术的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Classloader隔离技术在业务监控中的应用详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

队列技术在PHP与MySQL中的流量控制和队列监控的应用

随着互联网的快速发展,许多网站和应用程序面临着高并发访问的问题。为了应对这个问题,队列技术应运而生。队列是一种基于先进先出原则的数据结构,常用于异步处理和流量控制。PHP作为一种流行的服务器端语言,与MySQL数据库相结合,广泛应用于网站开
2023-10-21

队列技术在PHP与MySQL中的消息监控和告警的应用

随着互联网的迅速发展,网站和应用的访问量越来越大,用户对网站性能和响应速度的要求也越来越高。而多数的网站和应用都需要与数据库进行交互,这使得数据库的性能和稳定性显得尤为重要。如果数据库出现问题或者性能下降,对整个系统的影响非常大。因此,实时
2023-10-21

队列技术在PHP与MySQL中的延迟任务处理和流量控制的应用

引言:在Web开发中,处理大量并发请求和延迟任务是一项具有挑战的任务。为了保证系统的稳定性和性能,我们需要合理安排请求的处理顺序和执行时间。队列技术是一种常用的解决方案,它能够很好地管理任务的执行顺序,并且可以进行流量控制。本文将详细介绍队
2023-10-21

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录