JVM SandBox实现原理详解

本文介绍了JVMSandbox,一种基于Instrumentation的AOP框架,它在运行时实现类隔离和无侵入式增强,适用于线上故障定位、流控等场景。文章详细阐述了其原理、加载机制、类隔离机制和字节码增强过程,并提供了相关应用实例和源码分析链接。

1、什么是JVM SandBox

JVM SandBox(沙箱)实现了一种非侵入式运行期的AOP解决方案。JVM SandBox属于基于Instrumentation的动态编织类的AOP框架,可以在不重启应用的情况下,在运行时完成目标方法的增强和替换,同时沙箱以及沙箱的模块可以随时加载和卸载

主要特性如下

  • 无侵入:目标应用无需重启也无需感知沙箱的存在
  • 类隔离:沙箱以及沙箱的模块不会和目标应用的类相互干扰
  • 可插拔:沙箱以及沙箱的模块可以随时加载和卸载,不会在目标应用留下痕迹
  • 多租户:目标应用可以同时挂载不同租户下的沙箱并独立控制
  • 高兼容:支持JDK[6,11]

常见应用场景如下

  • 线上故障定位
  • 线上系统流控
  • 线上故障模拟
  • 方法请求录制和结果回放
  • 动态日志打印
  • 安全信息监测和脱敏

2、JVM SandBox实现原理

1)、挂载

JVM SandBox支持通过premain()方法在JVM启动的时候加载;也支持agentmain()方法通过Attach API的方式在JVM启动之后被加载

sandbox-agent模块

public class AgentLauncher {
  
    /**
     * 启动加载
     *
     * @param featureString 启动参数
     *                      [namespace,prop]
     * @param inst          inst
     */
    public static void premain(String featureString, Instrumentation inst) {
        LAUNCH_MODE = LAUNCH_MODE_AGENT;
        install(toFeatureMap(featureString), inst);
    }

    /**
     * 动态加载
     *
     * @param featureString 启动参数
     *                      [namespace,token,ip,port,prop]
     * @param inst          inst
     */
    public static void agentmain(String featureString, Instrumentation inst) {
        LAUNCH_MODE = LAUNCH_MODE_ATTACH;
        final Map<String, String> featureMap = toFeatureMap(featureString);
        writeAttachResult(
                getNamespace(featureMap),
                getToken(featureMap),
                install(featureMap, inst)
        );
    }

JVM Sandbox主要包含SandBox Core、Jetty Server和自定义处理模块三部分

在这里插入图片描述

客户端通过Attach API将沙箱挂载到目标JVM进程上,启动之后沙箱会一直维护着Instrumentation对象引用,通过Instrumentation来修改字节码和重定义类。另外,SandBox启动之后同时会启动一个内部的Jetty服务器,这个服务器用于外部和SandBox进行通信,对模块的加载、卸载、激活、冻结等命令等命令操作都会通过Http请求的方式进行

JVM SandBox包括如下模块

  • sandbox-info:沙箱信息模块,查看当前Sandbox的版本等信息
  • sandbox-module-mgr:沙箱模块管理模块,负责管理管理模块的生命周期(加载、冻结、激活等)
  • sandbox-control:负责卸载sandbox
  • 自定义处理模块扩展

JVM SandBox模块的生命周期

在这里插入图片描述

只有当模块处于激活状态,才会真正调用用户的AOP增强逻辑

2)、类隔离机制

在这里插入图片描述

BootstrapClassLoader加载Spy类(真正织入代码的类)

JVM Sandbox中有两个自定义的ClassLoader:SandBoxClassLoader加载沙箱模块功能,ModuleJarClassLoader加载用户定义模块功能

它们通过重写java.lang.ClassLoaderloadClass(String name, boolean resolve)方法,打破了双亲委派约定,达到与目标类隔离的目的,不会引起应用的类污染、冲突

3)、ClassLoader源码解析

SandBoxClassLoader源码如下:

class SandboxClassLoader extends URLClassLoader {
  
    @Override
    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
      	//先走一次已加载类的缓存,如果没有命中,则继续往下加载
        final Class<?> loadedClass = findLoadedClass(name);
        if (loadedClass != null) {
            return loadedClass;
        }

        try {
          	//调用URLClassLoader的findClass方法,从自定义的路径中寻找类
            Class<?> aClass = findClass(name);
            if (resolve) {
                resolveClass(aClass);
            }
            return aClass;
        } catch (Exception e) {
          	//没有找到类,在委托AppClassLoader去加载
            return super.loadClass(name, resolve);
        }
    }  

ModuleClassLoader继承了RoutingURLClassLoader,RoutingURLClassLoader中有一个静态内部Routing类,这里传进来的classLoader是SandboxClassLoader,意思是这些指定正则的路径由SandboxClassLoader加载

Routing类的作用如下

Sandbox是允许有多个module的jar包的,每个module分别new一个ModuleClassLoader去加载,jar包里面要用到@Resource注解注入model还有Sandbox的核心类,如果@Resource注解被ModuleClassLoader加载,那一个JVM实例中就会有多个Resource实例,Sandbox内部的核心类也一样。因此这些类只能由SandboxClassLoader加载

    /**
     * 类加载路由匹配器
     */
    public static class Routing {

        private final Collection<String/*REGEX*/> regexExpresses = new ArrayList<String>();
        private final ClassLoader classLoader;

        /**
         * 构造类加载路由匹配器
         *
         * @param classLoader       目标ClassLoader
         * @param regexExpressArray 匹配规则表达式数组
         */
        Routing(final ClassLoader classLoader, final String... regexExpressArray) {
            if (ArrayUtils.isNotEmpty(regexExpressArray)) {
                regexExpresses.addAll(Arrays.asList(regexExpressArray));
            }
            this.classLoader = classLoader;
        }

        /**
         * 当前参与匹配的Java类名是否命中路由匹配规则
         * 命中匹配规则的类加载,将会从此ClassLoader中完成对应的加载行为
         *
         * @param javaClassName 参与匹配的Java类名
         * @return true:命中;false:不命中;
         */
        private boolean isHit(final String javaClassName) {
            for (final String regexExpress : regexExpresses) {
                try {
                    if (javaClassName.matches(regexExpress)) {
                        return true;
                    }
                } catch (Throwable cause) {
                    logger.warn("routing {} failed, regex-express={}.", javaClassName, regexExpress, cause);
                }
            }
            return false;
        }

    }

ModuleClassLoader的父类RoutingURLClassLoader中重写了loadClass(String javaClassName)方法,在module中引用sandbox-core的类由SandboxClassLoader负责加载:

public class RoutingURLClassLoader extends URLClassLoader {
  
    @Override
    protected Class<?> loadClass(final String javaClassName, final boolean resolve) throws ClassNotFoundException {
        return classLoadingLock.loadingInLock(javaClassName, new ClassLoadingLock.ClassLoading() {
            @Override
            public Class<?> loadClass(String javaClassName) throws ClassNotFoundException {
                //优先查询类加载路由表,如果命中路由规则,则优先从路由表中的ClassLoader完成类加载
                if (ArrayUtils.isNotEmpty(routingArray)) {
                    for (final Routing routing : routingArray) {
                        if (!routing.isHit(javaClassName)) {
                            continue;
                        }
                        final ClassLoader routingClassLoader = routing.classLoader;
                        try {
                            return routingClassLoader.loadClass(javaClassName);
                        } catch (Exception cause) {
                            //如果在当前routingClassLoader中找不到应该优先加载的类(应该不可能,但不排除有就是故意命名成同名类)
                            //此时应该忽略异常,继续往下加载
                            //ignore...
                        }
                    }
                }

                //先走一次已加载类的缓存,如果没有命中,则继续往下加载
                final Class<?> loadedClass = findLoadedClass(javaClassName);
                if (loadedClass != null) {
                    return loadedClass;
                }

                try {
                    Class<?> aClass = findClass(javaClassName);
                    if (resolve) {
                        resolveClass(aClass);
                    }
                    return aClass;
                } catch (Exception cause) {
                    DelegateBizClassLoader delegateBizClassLoader = BusinessClassLoaderHolder.getBussinessClassLoader();
                    try {
                        if(null != delegateBizClassLoader){
                            return delegateBizClassLoader.loadClass(javaClassName,resolve);
                        }
                    } catch (Exception e) {
                        //忽略异常,继续往下加载
                    }
                    return RoutingURLClassLoader.super.loadClass(javaClassName, resolve);
                }
            }
        });
    }  

ModuleClassLoader类加载流程如下

在这里插入图片描述

4)、SandBox初始化流程

SandBox初始化流程如下图

在这里插入图片描述

5)、类增强策略

在这里插入图片描述

SandBox通过在BootstrapClassLoader中埋藏的Spy类完成目标类和沙箱内核的通讯,最终执行到用户模块的AOP方法

6)、字节码增强和撤销流程

在这里插入图片描述

字节码增强时,通过Instrumentation的addTransformer(ClassFileTransformer transformer)方法注册一个ClassFileTransformer,从此之后的类加载都会被ClassFileTransformer拦截,然后调用Instrumentation的retransformClasses(Class<?>... classes)对JVM已经加载的类重新触发类加载,类加载时会被ClassFileTransformer拦截

字节码增强撤销时,通过Instrumentation的removeTransformer(ClassFileTransformer transformer)方法移除相应的ClassFileTransformer,然后调用Instrumentation的retransformClasses(Class<?>... classes)重新触发类加载

推荐JVM SandBox文章

JVM源码分析之javaagent原理完全解读

JVM SandBox模块编写example

JVM SandBox之调用方式(命令行和http)

JVM SandBox的技术原理与应用分析

JVM Sandbox源码分析–启动简析

JVM Sandbox源码分析–启动时加载模块

JVM Sandbox源码分析–增强目标类

JVM Sandbox源码分析–模块刷新和卸载

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值