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

目录
  • 1. 背景&简介
  • 2. 业务监控平台脚本调试流程
    • 2.1 业务监控的脚本开发调试流程图
  • 3. 自定义Classloder | 打破双亲委派
    • 3.1 什么是Classloader
    • 3.2 Classloader动态加载依赖文件
    • 3.3 自定义类加载器
    • 3.4 业务监控使用CustomClassloader
    • 3.5 业务监控动态加载JAR和脚本的实现
  • 4. 问题&原因&方案
  • 5. 总结

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 {
/**
 * @param jarPath jar文件目录地址
 * @return
 */
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();
    }
    /**
     * 重写loadClass方法,按照类包路径规则拉进行加载Class到jvm
     * @param name 类全限定名
     * @return
     * @throws ClassNotFoundException
     */
    @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对象的创建源码:

/**
 *
 * @param serverName 服务名 (@FeignClient主键中的name值)
 *  eg:@FeignClient("demo-interfaces")
 * @param beanName feign对象名称 eg: DemoFeignClient
 * @param targetClass feign的Class对象
 * @param <T> FeignClient主键标记的Object
 * @return
 */
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业务监控隔离技术的资料请关注我们其它相关文章!

(0)

相关推荐

  • java自定义ClassLoader加载指定的class文件操作

    继承ClassLoader并且重写findClass方法就可以自定义一个类加载器,具体什么是类加载器以及类加载器的加载过程与顺序下次再说,下面给出一个小demo 首先定义一个类,比如MyTest,并且将其编译成class文件,然后放到一个指定的文件夹下面,其中文件夹的最后几层就是它的包名,这里我将这个编译好的类放到 : /Users/allen/Desktop/cn/lijie/MyTest.class package cn.lijie; public class MyTest { public

  • JVM中ClassLoader类加载器的深入理解

    JVM的体系结构图 先来看一下JVM的体系结构,如下图: JVM的位置 JVM的位置,如下图: JVM是运行在操作系统之上的,与硬件没有直接的交互,但是可以调用底层的硬件,用JIN(Java本地接口调用底层硬件) JVM结构图中的class files文件 class files文件,是保存在我们电脑本地的字节码文件,.java文件经过编译之后,就会生成一个.class文件,这个文件就是class files所对应的字节码文件,如下图: JVM结构图中的类加载器ClassLoader的解释 类加

  • SpringBoot详细讲解通过自定义classloader加密保护class文件

    目录 背景 maven插件加密 注意事项 自定义classloader 隐藏classloader 被保护class手动加壳 总结 背景 最近针对公司框架进行关键业务代码进行加密处理,防止通过jd-gui等反编译工具能够轻松还原工程代码,相关混淆方案配置使用比较复杂且针对springboot项目问题较多,所以针对class文件加密再通过自定义的classloder进行解密加载,此方案并不是绝对安全,只是加大反编译的困难程度,防君子不防小人,整体加密保护流程图如下图所示 maven插件加密 使用自

  • Java源码解析之ClassLoader

    一.前言 一个完整的Java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常.而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它cl

  • Python深度学习之简单实现猫狗图像分类

    一.前言 本文使用的是 kaggle 猫狗大战的数据集 训练集中有 25000 张图像,测试集中有 12500 张图像.作为简单示例,我们用不了那么多图像,随便抽取一小部分猫狗图像到一个文件夹里即可. 通过使用更大.更复杂的模型,可以获得更高的准确率,预训练模型是一个很好的选择,我们可以直接使用预训练模型来完成分类任务,因为预训练模型通常已经在大型的数据集上进行过训练,通常用于完成大型的图像分类任务. tf.keras.applications中有一些预定义好的经典卷积神经网络结构(Applic

  • Java ClassLoader虚拟类实现代码热替换的示例代码

    目录 总结 ClassLoader 虚拟类方法 实现代码热替换 实现 改进思考 总结 类加载器是负责加载类的对象.类ClassLoader是一个抽象类.给定类的全限定类名,类加载器应尝试查找或生成构成该类定义的数据Class文件.典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的类文件 每个Class对象都包含一个Class.getClassLoader()方法可以获取到定义它的ClassLoader 数组类的Class对象不是由类加载器创建的,而是根据Java运行时的要求自动创建的.

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

    目录 1. 背景&简介 2. 业务监控平台脚本调试流程 2.1 业务监控的脚本开发调试流程图 3. 自定义Classloder | 打破双亲委派 3.1 什么是Classloader 3.2 Classloader动态加载依赖文件 3.3 自定义类加载器 3.4 业务监控使用CustomClassloader 3.5 业务监控动态加载JAR和脚本的实现 4. 问题&原因&方案 5. 总结 1. 背景&简介 业务监控平台是得物自研的一款用于数据和状态验证的平台.能快速便捷发现

  • Mysql中explain作用详解

    一.MYSQL的索引 索引(Index):帮助Mysql高效获取数据的一种数据结构.用于提高查找效率,可以比作字典.可以简单理解为排好序的快速查找的数据结构. 索引的作用:便于查询和排序(所以添加索引会影响where 语句与 order by 排序语句). 在数据之外,数据库还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用数据.这样就可以在这些数据结构上实现高级查找算法.这些数据结构就是索引. 索引本身也很大,不可能全部存储在内存中,所以索引往往以索引文件的形式存储在磁盘上. 我们

  • 关于Java中的 JSP 详解

    目录 1.JSP的特点 2.JSP的优势 3.JSP的缺点 4.JSP的用途 前言: JSP 代表 Java 服务器页面.它是一种在应用服务器端使用的编程工具.JSP 基本上用于支持平台 – 独立和动态的方法来构建 Web 依赖的应用程序.JSP 页面类似于 ASP 页面,因为它们是在服务器上编译的,而不是在用户的 Web 浏览器上进行编译. JSP 是由 Sun Microsystems 公司于 1999 年开发的.JSP 的开发使用语言,其中内置的所有功能都是用 Java 编程语言创建的.

  • tomcat 集群监控与弹性伸缩详解

    目录 如何给 tomcat 配置合适的线程池 如何监控 tomcat 线程池的工作情况 tomcat 线程池扩缩容 tomcat 是如何避免原生线程池的缺陷的 如何给 tomcat 配置合适的线程池 任务分为 CPU 密集型和 IO 密集型 对于 CPU 密集型的应用来说,需要大量 CPU 计算速度很快,线程池如果过多,则保存和切换上下文开销过高反而会影响性能,可以适当将线程数量调小一些 对于 IO 密集型应用来说常见于普通的业务系统,比如会去查询 mysql.redis 等然后在内存中做简单的

  • Android中Service服务详解(二)

    本文详细分析了Android中Service服务.分享给大家供大家参考,具体如下: 在前面文章<Android中Service服务详解(一)>中,我们介绍了服务的启动和停止,是调用Context的startService和stopService方法.还有另外一种启动方式和停止方式,即绑定服务和解绑服务,这种方式使服务与启动服务的活动之间的关系更为紧密,可以在活动中告诉服务去做什么事情. 为了说明这种情况,做如下工作: 1.修改Service服务类MyService package com.ex

  • Java中Volatile关键字详解及代码示例

    一.基本概念 先补充一下概念:Java内存模型中的可见性.原子性和有序性. 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情.为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的.也就是一个线程修改的结果.另一个线程马上就能看到.比如:用volatile修饰的变量,就会具有可见性.volatile修饰的

  • spring在IoC容器中装配Bean详解

    1.Spring配置概述 1.1.概述 Spring容器从xml配置.java注解.spring注解中读取bean配置信息,形成bean定义注册表: 根据bean定义注册表实例化bean: 将bean实例放入bean缓存池: 应用程序使用bean. 1.2.基于xml的配置 (1)xml文件概述 xmlns------默认命名空间 xmlns:xsi-------标准命名空间,用于指定自定义命名空间的schema文件 xmlns:xxx="aaaaa"-------自定义命名空间,xx

  • 在Django中自定义filter并在template中的使用详解

    Django内置的filter有很多,然而我们由于业务逻辑的特殊要求,有时候仍然会不够用,这个时候就需要我们自定义filter来实现相应的内容.接下来让我们从自定义一个get_range(value)来产生列表的filter开始吧. 首先在你的django app的models.py的同级目录建立一个templatetags的文件夹,并在里面新建一个init.py的空文件,这个文件确保了这个文件夹被当做一个python的包.在添加了templatetags模块之后,我们需要重新启动服务器才能使其

  • Spring IoC学习之ApplicationContext中refresh过程详解

    refresh() 该方法是 Spring Bean 加载的核心,它是 ClassPathXmlApplicationContext 的父类 AbstractApplicationContext 的一个方法 , 顾名思义,用于刷新整个Spring 上下文信息,定义了整个 Spring 上下文加载的流程. public void refresh() throws BeansException, IllegalStateException { synchronized(this.startupShu

  • 设计模式系列之组合模式及其在JDK和MyBatis源码中的运用详解

    组合模式及其在JDK源码中的运用 前言组合和聚合什么是组合模式示例透明组合模式透明组合模式的缺陷安全组合模式 组合模式角色组合模式在JDK源码中的体现组合模式应用场景享元模式优缺点总结 前言 本文主要会讲述组合模式的用法,并会结合在JDK和MyBatis源码中的运用来进一步理解组合模式. 在编码原则中,有一条是:多用组合,少用继承.当然这里的组合和我们今天要讲的组合模式并不等价,这里的组合其实就是一种聚合,那么聚合和组合有什么区别呢? 组合和聚合 人在一起叫团伙,心在一起叫团队.用这句话来诠释组

随机推荐