本文正在参与「金石计划 . 分割6万现金大奖」

在日常事务代码开发中,咱们常常接触到AOP,比方熟知的Spring AOP。咱们用它来做事务切面,比方登录校验,日志记载,功用监控,大局过滤器等。但Spring AOP有一个局限性,并不是一切的类都保管在 Spring 容器,例如许多中间件代码、三方包代码,Java原生代码,都不能被Spring AOP署理到。如此一来,一旦你想要做的切面逻辑并不归于Spring的统辖规模,或许你想完结脱离Spring约束的切面功用,就无法完结了。

那关于Java后端运用,有没有一种更为通用的AOP办法呢?答案是有的,Java本身供给了JVM TI,Instrumentation等功用,允许运用者以经过一系列API完结对JVM的杂乱操控。自此衍生出了许多闻名的结构,比方Btrace,Arthas等等,协助开发者们完结更多更杂乱的Java功用。

JVM Sandbox也是其间的一员。当然,不同结构的规划意图和使命是不一样的,JVM-Sandbox的规划意图是完结一种在不重启、不侵入方针JVM运用状况下的AOP解决方案。

是不是看到这儿还是不清楚我在讲什么?别急,我举几个典型的JVM-Sandbox运用场景:

  • 流量回放:怎么录制线上运用每次接口恳求的入参和出参?改动运用代码固然能够,但成本太大,经过JVM-Sandbox,能够直接在不修正代码的状况下,直接抓取接口的出入参。
  • 安全漏洞热修正:假定某个三方包(例如出名的fastjson)又呈现了漏洞,集团内那么多运用,一个个发布新版本修正,漏洞现已形成了许多破坏。经过JVM-Sandbox,直接修正替换有漏洞的代码,及时止损。
  • 接口故障模仿:想要模仿某个接口超时5s后回来false的状况,JVM-Sandbox很轻松就能完结。
  • 故障定位:像Arthas相似的功用。
  • 接口限流:动态对指定的接口做限流。
  • 日志打印

能够看到,借助JVM-Sandbox,你能够完结许多之前在事务代码中做不了的事,大大拓宽了可操作的规模。

本文围绕JVM SandBox打开,主要介绍如下内容:

  • JVM SandBox诞生布景
  • JVM SandBox架构规划
  • JVM SandBox代码实战
  • JVM SandBox底层技能
  • 总结与展望

JVM Sandbox诞生布景

JVM Sandbox诞生的技能布景在引言中现已赘述完毕,下面是作者开发该结构的一些事务布景,以下描绘引证自文章:

JVM SandBox 是阿里开源的一款 JVM 平台非侵入式运转期 AOP 解决方案,本质上是一种 AOP 落地办法。那么或许有同学会问:已有老练的 Spring AOP 解决方案,阿里巴巴为什么还要“重复造轮子”?这个问题要回到 JVM SandBox 诞生的布景中来答复。在 2016 年中,天猫双十一催动了阿里巴巴内部许多事务体系的改动,恰逢徐冬晨(阿里巴巴测验开发专家)地点的团队调整,测验资源保证严峻不足,迫使他们有必要考虑更精准、更便捷的老事务测验回归验证方案。开发团队面临的是新接手的老体系,老的事务代码架构难以满足可测性的要求,许多现有测验结构也无法运用到老的事务体系架构中,所以需求新的测验思路和测验结构。

为什么不采用 Spring AOP 方案呢?Spring AOP 方案的痛点在于不是一切事务代码都保管在 Spring 容器中,并且更底层的中间件代码、三方包代码无法归入到回归测验规模,更糟糕的是测验结构会引入本身所依赖的类库,常常与事务代码的类库产生抵触,因而,JVM SandBox 应运而生。

JVM Sandbox全体架构

本章节不具体叙述JVM SandBox的一切架构规划,只讲其间几个最重要的特性。具体的架构规划能够看原结构代码仓库的Wiki。

类阻隔

许多结构经过破坏双亲派遣(我更愿意称之为直系亲属派遣)来完结类阻隔,SandBox也不破例。它经过自界说的SandboxClassLoader破坏了双亲派遣的约好,完结了几个阻隔特性:

  • 和方针运用的类阻隔:不必担心加载沙箱会引起原运用的类污染、抵触。
  • 模块之间类阻隔:做到模块与模块之间、模块和沙箱之间、模块和运用之间互不搅扰。

如何在JVM层写切面?JVM Sandbox入门教程与原理浅谈

无侵入AOP与作业驱动

JVM-SANDBOX归于根据Instrumentation的动态编织类的AOP结构,经过精心结构了字节码增强逻辑,使得沙箱的模块能在不违反JDK约束状况下完结对方针运用办法的无侵入运转时AOP拦截

如何在JVM层写切面?JVM Sandbox入门教程与原理浅谈

从上图中,能够看到一个办法的整个履行周期都被代码“加强”了,能够带来的优点便是你在运用JVM SandBox只需求关于办法的作业进行处理。

// BEFORE
try {
​
  /*
   * do something...
   */// RETURN
   return;
​
} catch (Throwable cause) {
   // THROWS
}

在沙箱的世界观中,任何一个Java办法的调用都能够分解为BEFORERETURNTHROWS三个环节,由此在三个环节上引申出对应环节的作业探测和流程操控机制。

根据BEFORERETURNTHROWS三个环节作业分离,沙箱的模块能够完结许多类AOP的操作。

  1. 能够感知和改变办法调用的入参

  2. 能够感知和改变办法调用回来值和抛出的反常

  3. 能够改变办法履行的流程

    • 在办法体履行之前直接回来自界说成果目标,原有办法代码将不会被履行
    • 在办法体回来之前从头结构新的成果目标,乃至能够改变为抛出反常
    • 在办法体抛出反常之后从头抛出新的反常,乃至能够改变为正常回来

一切都是作业驱动的,这一点你或许很模糊,可是在下文的实战环节中,能够协助你了解。

JVM Sandbox代码实战

我将实战章节提早到这儿,意图是便利我们快速了解运用JVM SandBox开发是一件多么舒服的作业(比较于自己运用字节码替换等东西)。

运用版本:JVM-Sandbox 1.2.0

官方源码:github.com/alibaba/jvm…

咱们来完结一个小东西,在日常作业中,咱们总会遇到一些巨大的Spring工程,里边有苍茫多的Bean和事务代码,发动一个工程或许需求5分钟乃至更久,严峻连累开发效率。

咱们尝试运用JVM Sandbox来开发一个东西,对运用的Spring Bean发动耗时进行一次计算。这样能一目了然的发现工程发动慢的主要原因,避免去盲人摸象的优化。

终究效果如图:

如何在JVM层写切面?JVM Sandbox入门教程与原理浅谈

图中计算了一个运用从发动开端到一切SpringBean的发动耗时,依照从高到低排序,我由所以demo运用,Bean的耗时都偏低(也没有太多事务Bean),但在实践运用中会有十分多几秒乃至十几秒才完结初始化的Bean,能够进行针对性优化。

在JVMSandBox中怎么完结上面的东西?其实十分简略。

先贴上思路的全体流程:

如何在JVM层写切面?JVM Sandbox入门教程与原理浅谈

首要新建Maven工程,在Maven依赖中引证JVM SandBox,官方推荐独立工程运用parent办法。

<parent>
   <groupId>com.alibaba.jvm.sandbox</groupId>
   <artifactId>sandbox-module-starter</artifactId>
   <version>1.2.0</version>
</parent>

新建一个类作为一个JVM SandBox模块,如下图:

如何在JVM层写切面?JVM Sandbox入门教程与原理浅谈

运用@Infomation声明mode为AGENT形式,一共有两种形式Agent和Attach。

  • Agent:跟着JVM发动一同发动
  • Attach:在现已运转的JVM进程中,动态的插入

咱们由所以监控JVM发动数据,所以需求AGENT形式。

其次,继承com.alibaba.jvm.sandbox.api.Module和com.alibaba.jvm.sandbox.api.ModuleLifecycle。

其间ModuleLifecycle包含了整个模块的生命周期回调函数。

  • onLoad:模块加载,模块开端加载之前调用!模块加载是模块生命周期的开端,在模块生命中期中有且只会调用一次。 这儿抛出反常将会是阻挠模块被加载的仅有办法,假如模块断定加载失败,将会开释掉一切预恳求的资源,模块也不会被沙箱所感知
  • onUnload:模块卸载,模块开端卸载之前调用!模块卸载是模块生命周期的完毕,在模块生命中期中有且只会调用一次。 这儿抛出反常将会是阻挠模块被卸载的仅有办法,假如模块断定卸载失败,将不会形成任何资源的提早封闭与开释,模块将能继续正常作业
  • onActive:模块被激活后,模块所增强的类将会被激活,一切com.alibaba.jvm.sandbox.api.listener.EventListener将开端收到对应的作业
  • onFrozen:模块被冻结后,模块所持有的一切com.alibaba.jvm.sandbox.api.listener.EventListener将被静默,无法收到对应的作业。 需求注意的是,模块冻结后尽管不再收到相关作业,但沙箱给对应类织入的增强代码依然还在。
  • loadCompleted:模块加载完结,模块完结加载后调用!模块完结加载是在模块完结一切资源加载、分配之后的回调,在模块生命中期中有且只会调用一次。 这儿抛出反常不会影响模块被加载成功的成果。模块加载完结之后,一切的根据模块的操作都能够在这个回调中进行

最常用的是loadCompleted,所以咱们重写loadCompleted类,在里边敞开咱们的监控类SpringBeanStartMonitor线程。

而SpringBeanStartMonitor的核心代码如下图:

如何在JVM层写切面?JVM Sandbox入门教程与原理浅谈

运用Sandbox的doClassFilter过滤出匹配的类,这儿咱们是BeanFactory。

运用doMethodFilter过滤出要监听的办法,这儿是initializeBean。

里取initializeBean作为计算耗时的切入办法。具体为什么挑选该办法,涉及到SpringBean的发动生命周期,不在本文赘述规模内。

如何在JVM层写切面?JVM Sandbox入门教程与原理浅谈

接着运用moduleEventWatcher.watch(springBeanFilter, springBeanInitListener, Event.Type.BEFORE, Event.Type.RETURN);

将咱们的springBeanInitListener监听器绑定到被观测的办法上。这样每次initializeBean被调用,都会走到咱们的监听器逻辑。

监听器的主要逻辑如下:

如何在JVM层写切面?JVM Sandbox入门教程与原理浅谈

代码有点长,不必细看,主要便是在原办法的BeforeEvent(进入前)和ReturnEvent(履行正常回来后)履行上述的切面逻辑,我这儿便是运用了一个MAP存储每个Bean的初始化开端和完毕时间,终究计算出初始化耗时。

终究,咱们还需求一个办法来知道咱们的原始Spring运用现已发动完毕,这样咱们能够手动卸载咱们的Sandbox模块,究竟他现已完结了他的历史使命,不需求再依附在主进程上。

咱们经过一个简陋的办法,查看http://127.0.0.1:8080/是否会回来小于500的状况码,来判别Spring容器是否现已发动。当然假如你的Spring没有运用Web结构,就不能用这个办法来判别发动完结,你也许能够经过Spring自己的生命周期钩子函数来完结,这儿我是偷了个懒。

整个SpringBean监听模块的开发就完结了,你能够感受到,你的开发和日常事务开发几乎没有差异,这便是JVM Sandbox带给你的最大优点。

上述源码放在了我的Github仓库:

github.com/monitor4all…

JVM Sandbox底层技能

整个JVM Sandbox的入门运用基本上讲完了,上文提到了一些JVM技能名词,或许小伙伴们听过但不是特别了解。这儿简略论述几个重要的概念,理清楚这几个概念之间的关系,以便我们更好的了解JVM Sandbox底层的完结。

JVMTI

JVMTI(JVM Tool Interface)是 Java 虚拟机所供给的 native 编程接口,JVMTI能够用来开发并监控虚拟机,能够查看JVM内部的状况,并操控JVM运用程序的履行。可完结的功用包含但不限于:调试、监控、线程剖析、覆盖率剖析东西等。

许多java监控、诊断东西都是根据这种办法来作业的。假如arthas、jinfo、brace等,尽管这些东西底层是JVM TI,可是它们还运用到了上层东西JavaAgent。

JavaAgent和Instrumentation

Javaagent是java指令的一个参数。参数 javaagent 能够用于指定一个 jar 包。

-agentlib:<libname>[=<选项>] 加载本机署理库 <libname>, 例如 -agentlib:hprof
  另请参看 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<选项>]
  按完好路径名加载本机署理库
-javaagent:<jarpath>[=<选项>]
  加载 Java 编程言语署理, 请参看 java.lang.instrument

在上面-javaagent参数中提到了参看java.lang.instrument,这是在rt.jar 中界说的一个包,该包供给了一些东西协助开发人员在 Java 程序运转时,动态修正体系中的 Class 类型。其间,运用该软件包的一个关键组件便是 Javaagent。从名字上看,似乎是个 Java 署理之类的,而实践上,他的功用更像是一个Class 类型的转换器,他能够在运转时承受从头外部恳求,对Class类型进行修正。

Instrumentation的底层完结依赖于JVMTI。

JVM 会优先加载 带 Instrumentation 签名的办法,加载成功疏忽第二种,假如第一种没有,则加载第二种办法。

Instrumentation支持的接口:

public interface Instrumentation {
    //增加一个ClassFileTransformer
    //之后类加载时都会经过这个ClassFileTransformer转换
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
    void addTransformer(ClassFileTransformer transformer);
    //移除ClassFileTransformer
    boolean removeTransformer(ClassFileTransformer transformer);
    boolean isRetransformClassesSupported();
    //将一些现已加载过的类从头拿出来经过注册好的ClassFileTransformer转换
    //retransformation能够修正办法体,可是不能变更办法签名、增加和删去办法/类的成员特点
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    boolean isRedefineClassesSupported();
    //从头界说某个类
    void redefineClasses(ClassDefinition... definitions)
        throws  ClassNotFoundException, UnmodifiableClassException;
    boolean isModifiableClass(Class<?> theClass);
    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();
    @SuppressWarnings("rawtypes")
    Class[] getInitiatedClasses(ClassLoader loader);
    long getObjectSize(Object objectToSize);
    void appendToBootstrapClassLoaderSearch(JarFile jarfile);
    void appendToSystemClassLoaderSearch(JarFile jarfile);
    boolean isNativeMethodPrefixSupported();
    void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}

Instrumentation的局限性:

  • 不能经过字节码文件和自界说的类名从头界说一个原本不存在的类
  • 增强类和老类有必要遵从许多约束:比方新类和老类的父类有必要相同;新类和老类完结的接口数也要相同,并且是相同的接口;新类和老类拜访符有必要一致。 新类和老类字段数和字段名要一致;新类和老类新增或删去的办法有必要是private static/final润饰的;

更具体的原理论述能够看下文:

www.cnblogs.com/rickiyang/p…

再谈Attach和Agent

上面的实战章节中现已提到了attach和agent两者的差异,这儿再打开聊聊。

在Instrumentation中,Agent形式是经过-javaagent:<jarpath>[=<选项>]从运用发动时候就插桩,跟着运用一同发动。它要求指定的类中有必要要有premain()办法,并且对premain办法的签名也有要求,签名有必要满足以下两种格局:

public static void premain(String agentArgs, Instrumentation inst)
  
public static void premain(String agentArgs)

一个java程序中-javaagent参数的个数是没有约束的,所以能够增加恣意多个javaagent。一切的java agent会依照你界说的顺序履行,例如:

java -javaagent:agent1.jar -javaagent:agent2.jar -jar MyProgram.jar

上面介绍Agent形式的Instrumentation是在 JDK 1.5中供给的,在1.6中,供给了attach办法的Instrumentation,你需求的是agentmain办法,并且签名如下:

public static void agentmain (String agentArgs, Instrumentation inst)public static void agentmain (String agentArgs)

这两种办法各有不同用处,一般来说,Attach办法适合于动态的对代码进行功用修正,在排查问题的时候用的比较多。而Agent形式跟着运用发动,所以常常用于提早完结一些增强功用,比方我上面实战中的发动观测,运用防火墙,限流策略等等。

总结

本文花了较短的篇幅重点介绍了JVM Sandbox的功用,实践用法,以及根底原理。它经过封装一些底层JVM操控的结构,使得对JVM层面的AOP开发变的反常简略,就像作者自己所说“JVM-SANDBOX还能协助你做许多许多,取决于你的脑洞有多大了。

笔者在公司内部也经过它完结了许多小东西,比方上面的运用发动数据观测(公司内部是一个更为安稳杂乱的版本,还监控了许多中间件的数据),协助了许多部门搭档,优化他们运用的发动速度。所以假如对JVM感兴趣,无妨大开脑洞,想一想JVM Sandbox还能在哪里协助到你的作业,给自己的作业添彩。

参阅

www.infoq.cn/article/tsy…

www.cnblogs.com/rickiyang/p…

www.jianshu.com/p/eff047d44…

本文正在参与「金石计划 . 分割6万现金大奖」