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

来源:脚本之家  责任编辑:小易  
目录
1. 背景&简介2. 业务监控平台脚本调试流程2.1 业务监控的脚本开发调试流程图3. 自定义Classloder | 打破双亲委派3.1 什么是Classloader3.2 Classloader动态加载依赖文件3.3 自定义类加载器3.4 业务监控使用CustomClassloader3.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业务监控隔离技术的资料请关注真格学网其它相关文章!

您可能感兴趣的文章:Java?ClassLoader虚拟类实现代码热替换的示例代码SpringBoot详细讲解通过自定义classloader加密保护class文件Java源码解析之ClassLoaderJVM中ClassLoader类加载器的深入理解Java基础之ClassLoader详解java自定义ClassLoader加载指定的class文件操作

  • 本文相关:
  • javaweb开发之【tomcat 环境配置】myeclipse+idea配置教程
  • java 将文件转为字节数组知识总结及实例详解
  • springmvc+shiro自定义过滤器的实现代码
  • java并发编程之executors类详解
  • 深入了解java file分隔符和path分隔符的使用
  • java使用beanutils.copyproperties踩坑经历
  • java计算程序代码执行时间的方法小结
  • javaee springmybatis是什么? 它和hibernate的区别及如何配置mybatis
  • spring数据访问模板化方法
  • 详解spring自动扫描包
  • 什么是隔离技术?
  • 隔离技术规范试题(85题)
  • 交换机端口隔离技术指的是什么?
  • 隔离技术中各种隔离技术的适用范围?
  • ppe隔离技术?
  • VLAN隔离技术的作用?
  • 锂电池隔离膜技术含量高吗?
  • 消毒,隔离,无菌技术的概念?
  • 网络隔离属于什么防护技术
  • 隔离网闸(GAP)技术的特点是什么?
  • 光电隔离电路的主要作用是什么?衡量光电耦合器件的主要技术...
  • 妆前乳和隔离霜有多大区别?
  • 负荷开关和隔离开关分别是什么,区别呢?
  • 网站首页网页制作脚本下载服务器操作系统网站运营平面设计媒体动画电脑基础硬件教程网络安全c#教程vbvb.netc 语言java编程delphijavaandroidiosswiftscala易语言汇编语言r语言其它相关首页javajava?classloader虚拟类实现代码热替换的示例代码springboot详细讲解通过自定义classloader加密保护class文件java源码解析之classloaderjvm中classloader类加载器的深入理解java基础之classloader详解java自定义classloader加载指定的class文件操作javaweb开发之【tomcat 环境配置】myeclipse+idea配置教程java 将文件转为字节数组知识总结及实例详解springmvc+shiro自定义过滤器的实现代码java并发编程之executors类详解深入了解java file分隔符和path分隔符的使用java使用beanutils.copyproperties踩坑经历java计算程序代码执行时间的方法小结javaee springmybatis是什么? 它和hibernate的区别及如何配置mybatisspring数据访问模板化方法intellij idea 2020最新intellij?idea?2020.2.3永久破解激活java使double保留两位小数的多方法 java保留两位idea2021.2永久激活码最新超详细(激活到2099)idea 2020版本最新破解教程可激活至2089java8 十大新特性详解2020.2 intellij ideajava.net.socketexception: connidea?2020.3最新永久激活码(免费激活到?209java写入文件的几种方法分享java8新特性教程之time包使用总结java并发编程之性能、扩展性和响应详解spring boot jpa整合querydsl来简化复杂操作java多线程深入理解spring boot集群管理工具kafkaadminclient使用方法解java中的异常处理(try,catch,finally,throw,thrspringmvc 跨重定向请求传递数据的方法实现解决springboot 测试类无法自动注入@autowired的问题springboot如何使用aop做访问请求日志详解springboot项目带tomcat和不带tomcat的两种打包方式
    免责声明 - 关于我们 - 联系我们 - 广告联系 - 友情链接 - 帮助中心 - 频道导航
    Copyright © 2017 www.zgxue.com All Rights Reserved