Java Agent之ByteBuddy

news2024/11/15 21:42:00

1:前言

在上一篇文章介绍 Java Agent 技术时,结合 Byte Buddy 技术实现了统计方法执行时间的功能。本次分享深入介绍 Byte Buddy 的一些基础知识,SkyWalking Agent 强大的地方就是重度使用该工具实现探针数据动态生成代码填充参数的。

2:为什么需要运行时代码生成

我们知道,Java 是一种强类型的编程语言,即要求所有变量和对象都有一个确定的类型,如果在赋值操作中出现类型不兼容的情况,就会抛出异常。强类型检查在大多数情况下是可行的,然而在某些特殊场景下,强类型检查则成了巨大的障碍。

例如,在对外提供一个通用 jar 包时,我们通常不能引用用户应用中定义的任何类型,因为当这个通用 jar 包被编译时,我们还不知道这些用户的自定义类型。为了调用用户自定义的类,访问其属性或方法,Java 类库提供了一套反射 API 帮助我们查找未知类型,以及调用其方法或字段。但是 Java 反射 API 有两个明显的缺点:

在早期 JDK 版本中,反射 API 性能很差。
反射 API 能绕过类型安全检查,反射 API 自身并不是类型安全的。
运行时代码生成在 Java 应用启动之后再动态生成一些类定义,这样就可以模拟一些只有使用动态编程语言编程才有的特性,同时也不丢失 Java 的强类型检查。在运行时生成代码需要特别注意的是 Java 类型被 JVM 加载之后,一般不会被垃圾被回收,因此不应该过度使用代码生成。

3:为什么选择 Byte Buddy

在 Java 的世界中,代码生成库不止 Byte Buddy 一个,以下代码生成库在 Java 中也很流行:

Java Proxy
Java Proxy 是 JDK 自带的一个代理工具,它允许为实现了一系列接口的类生成代理类。Java Proxy 要求目标类必须实现接口是一个非常大限制,例如,在某些场景中,目标类没有实现任何接口且无法修改目标类的代码实现,Java Proxy 就无法对其进行扩展和增强了。

CGLIB
CGLIB 诞生于 Java 初期,但不幸的是没有跟上 Java 平台的发展。虽然 CGLIB 本身是一个相当强大的库,但也变得越来越复杂。鉴于此,导致许多用户放弃了 CGLIB 。

Javassist
Javassist 的使用对 Java 开发者来说是非常友好的,它使用Java 源代码字符串和 Javassist 提供的一些简单 API ,共同拼凑出用户想要的 Java 类,Javassist 自带一个编译器,拼凑好的 Java 类在程序运行时会被编译成为字节码并加载到 JVM 中。Javassist 库简单易用,而且使用 Java 语法构建类与平时写 Java 代码类似,但是 Javassist 编译器在性能上比不了 Javac 编译器,而且在动态组合字符串以实现比较复杂的逻辑时容易出错。

Byte Buddy
Byte Buddy 提供了一种非常灵活且强大的领域特定语言,通过编写简单的 Java 代码即可创建自定义的运行时类。与此同时,Byte Buddy 还具有非常开放的定制性,能够应付不同复杂度的需求。

下表是 Byte Buddy 官网给出的数据,显示了上述代码生成库的基本性能,以纳秒为单位,标准偏差在括号内附加:

在 Java 的世界中,代码生成库不止 Byte Buddy 一个,以下代码生成库在 Java 中也很流行:

Java Proxy
Java Proxy 是 JDK 自带的一个代理工具,它允许为实现了一系列接口的类生成代理类。Java Proxy 要求目标类必须实现接口是一个非常大限制,例如,在某些场景中,目标类没有实现任何接口且无法修改目标类的代码实现,Java Proxy 就无法对其进行扩展和增强了。

CGLIB
CGLIB 诞生于 Java 初期,但不幸的是没有跟上 Java 平台的发展。虽然 CGLIB 本身是一个相当强大的库,但也变得越来越复杂。鉴于此,导致许多用户放弃了 CGLIB 。

Javassist
Javassist 的使用对 Java 开发者来说是非常友好的,它使用Java 源代码字符串和 Javassist 提供的一些简单 API ,共同拼凑出用户想要的 Java 类,Javassist 自带一个编译器,拼凑好的 Java 类在程序运行时会被编译成为字节码并加载到 JVM 中。Javassist 库简单易用,而且使用 Java 语法构建类与平时写 Java 代码类似,但是 Javassist 编译器在性能上比不了 Javac 编译器,而且在动态组合字符串以实现比较复杂的逻辑时容易出错。

Byte Buddy
Byte Buddy 提供了一种非常灵活且强大的领域特定语言,通过编写简单的 Java 代码即可创建自定义的运行时类。与此同时,Byte Buddy 还具有非常开放的定制性,能够应付不同复杂度的需求。

下表是 Byte Buddy 官网给出的数据,显示了上述代码生成库的基本性能,以纳秒为单位,标准偏差在括号内附加:
在这里插入图片描述
代码生成库需要在“生成快速的代码”与“快速生成代码”之间进行折中。Byte Buddy 折中的考虑是:类型动态创建不是程序中的常见步骤,并不会对长期运行的应用程序产生重大性能影响,但方法调用等操作却在程序中随处可见。所以,Byte Buddy 的主要侧重点在于生成更快速的代码。

4:Byte Buddy Demo

我们需要了解的第一个类就是 ByteBuddy 类,任何一个由 Byte Buddy 创建/增强的类型都是通过 ByteBuddy 类的实例来完成的,如下所示:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
        .subclass(Object.class) // 生成 Object的子类
        .name("com.xxx.Type")   // 生成类的名称为"com.xxx.Type"
        .make();

包括 subclass 在内,Byte Buddy 动态增强代码总共有三种方式:

  1. subclass:对应 ByteBuddy.subclass() 方法。这种方式比较好理解,就是为目标类(即被增强的类)生成一个子类,在子类方法中插入动态代码。

  2. rebasing:对应 ByteBuddy.rebasing() 方法。当使用 rebasing 方式增强一个类时,Byte Buddy 保存目标类中所有方法的实现,也就是说,当 Byte Buddy 遇到冲突的字段或方法时,会将原来的字段或方法实现复制到具有兼容签名的重新命名的私有方法中,而不会抛弃这些字段和方法实现。从而达到不丢失实现的目的。这些重命名的方法可以继续通过重命名后的名称进行调用。例如:

class Foo { // Foo的原始定义
  String bar() { return "bar"; }
}
class Foo { // 增强后的Foo定义
  String bar() { return "foo" + bar$original(); }
  private String bar$original() { return "bar"; }
}
  1. redefinition:对应 ByteBuddy.redefine() 方法。当重定义一个类时,Byte Buddy 可以对一个已有的类添加属性和方法,或者删除已经存在的方法实现。如果使用其他的方法实现替换已经存在的方法实现,则原来存在的方法实现就会消失。例如,这里依然是增强 Foo 类的 bar() 方法使其直接返回 “unknow” 字符串,增强结果如下:
class Foo { // 增强后的Foo定义
  String bar() { return "unknow"; }
}

通过上述三种方式完成类的增强之后,我们得到的是 DynamicType.Unloaded 对象,表示的是一个未加载的类型,我们可以使用 ClassLoadingStrategy 加载此类型。Byte Buddy 提供了几种类加载策略,这些策略定义在 ClassLoadingStrategy.Default 中,其中:

  1. WRAPPER 策略:创建一个新的 ClassLoader 来加载动态生成的类型。
  2. CHILD_FIRST 策略:创建一个子类优先加载的 ClassLoader,即打破了双亲委派模型。
  3. INJECTION 策略:使用反射将动态生成的类型直接注入到当前 ClassLoader 中。

具体使用方式如下所示:

Class<?> loaded = new ByteBuddy()
        .subclass(Object.class)
        .name("com.xxx.Type")
        .make()
        // 使用 WRAPPER 策略加载生成的动态类型
        .load(Main2.class.getClassLoader(), 
              ClassLoadingStrategy.Default.WRAPPER)
        .getLoaded();

前文动态生成的 com.xxx.Type 类型只是简单的继承了 Object 类,在实际应用中动态生成新类型的一般目的就是为了增强原始的方法,下面通过一个示例展示 Byte Buddy 如何增强 toString() 方法:

String str = new ByteBuddy() // 创建ByteBuddy对象
        .subclass(Object.class) // subclass增强方式
        .name("com.xxx.Type") // 新类型的类名
        // 拦截其中的toString()方法
        .method(ElementMatchers.named("toString")) 
        // 让toString()方法返回固定值
        .intercept(FixedValue.value("Hello World!")) 
        .make()
        // 加载新类型,默认WRAPPER策略
        .load(ByteBuddy.class.getClassLoader()) 
        .getLoaded()
        .newInstance() // 通过 Java反射创建 com.xxx.Type实例
        .toString(); // 调用 toString()方法
System.out.println(str);

首先需要关注这里的 method() 方法,method() 方法可以通过传入的 ElementMatchers 参数匹配多个需要修改的方法,这里的 ElementMatchers.named(“toString”) 即为按照方法名匹配 toString() 方法。如果同时存在多个重载方法,则可以使用 ElementMatchers 其他 API 描述方法的签名,如下所示:

ElementMatchers.named("toString") // 指定方法名称
    .and(ElementMatchers.returns(String.class)) // 指定方法的返回值
    .and(ElementMatchers.takesArguments(0)) // 指定方法参数

接下来需要关注的是 intercept() 方法,通过 method() 方法拦截到的所有方法会由 Intercept() 方法指定的 Implementation 对象决定如何增强。这里的 FixValue.value() 会将方法的实现修改为固定值,上例中就是固定返回 “Hello World!” 字符串。

Byte Buddy 中可以设置多个 method() 和 Intercept() 方法进行拦截和修改,Byte Buddy 会按照栈的顺序来进行拦截。下面通过一个示例进行说明,首先我们定一个 Foo 类,其中有三个方法,如下:

class Foo { // Foo 中定义了三个方法
  public String bar() { return null; }
  public String foo() { return null; }
  public String foo(Object o) { return null; }
}

接下来使用 Byte Buddy 动态生成一个 Foo 的子类,并修改其中的方法:

Foo dynamicFoo = new ByteBuddy()
  .subclass(Foo.class) 
  .method(isDeclaredBy(Foo.class)) // 匹配 Foo中所有的方法
  .intercept(FixedValue.value("One!")) 
  .method(named("foo")) // 匹配名为 foo的方法
  .intercept(FixedValue.value("Two!"))
  .method(named("foo").and(takesArguments(1))) // 匹配名为foo且只有一个
                                               // 参数的方法
  .intercept(FixedValue.value("Three!"))
  .make()
  .load(getClass().getClassLoader(), INJECTION)
  .getLoaded()
  .newInstance();
System.out.println(dynamicFoo.bar());
System.out.println(dynamicFoo.foo());
System.out.println(dynamicFoo.foo(null));

这里 method() 方法出现了三次,且每次出现后面都跟着的 intercept() 方法使用的 Implementation 参数都不同。Byte Buddy 会按照栈的方式将后定义 method() 方法在栈顶,先定义的方法在栈底。在匹配方法的时候,按照下图执行出栈流程逐一匹配:
在这里插入图片描述
前面的示例中,目标方法都被修改成了返回固定值,在实际应用中意义不大,实践中最常用的是通过 MethodDelegation 将拦截到的目标方法委托为另一个类去处理。下面通过一个示例对 MethodDelegation 的使用进行分析,首先创建一个名为 DB 的类作为目标类:

class DB {
    public String hello(String name) {
        System.out.println("DB:" + name);
        return null;
    }
}

下面来看 Interceptor 这个类的定义:

class Interceptor {
    public static String intercept(String name) { return "String"; }
    public static String intercept(int i) { return "int"; }
    public static String intercept(Object o) { return "Object";}
}

Interceptor 中有三个方法,最终会委托给哪个方法呢?答案是 intercept(String name) 方法,委托并不是根据名称来的,而是和 Java 编译器在选重载时用的参数绑定类似。如果我们将 Intercept(String) 这个重载去掉,则 Byte Buddy 会选择 Intercept(Object) 方法。你可以尝试执行一下该示例,得到的输出分别是 String 和 Object。

除了通过上述 API 拦截方法并将方法实现委托给 Interceptor 增强之外,Byte Buddy 还提供了一些预定义的注解,通过这些注解我们可以告诉 Byte Buddy 将哪些需要的数据注入到 Interceptor 中,下面依然通过一个示例来介绍 Byte Buddy 中常用的注解:

class Interceptor {
    @RuntimeType
    public Object intercept(
      @This Object obj, // 目标对象
      @AllArguments Object[] allArguments, // 注入目标方法的全部参数
      @SuperCall Callable<?> zuper, // 调用目标方法,必不可少哦
      @Origin Method method, // 目标方法
      @Super DB db // 目标对象
    ) throws Exception {
        System.out.println(obj); 
        System.out.println(db);
        // 从上面两行输出可以看出,obj和db是一个对象
        try {
          return zuper.call(); // 调用目标方法
        } finally {
        }
}
// 输出:
// com.xxx.DB$ByteBuddy$8AV3B7GI@2d127a61
// com.xxx.DB$ByteBuddy$8AV3B7GI@2d127a61
// DB:World
// null

这里详细说明每个注解的作用:

  • @RuntimeType 注解:告诉 Byte Buddy 不要进行严格的参数类型检测,在参数匹配失败时,尝试使用类型转换方式(runtime type casting)进行类型转换,匹配相应方法。
  • @This 注解:注入被拦截的目标对象(即前面示例的 DB 对象)。
  • @AllArguments 注解:注入目标方法的全部参数,是不是感觉与 Java 反射的那套 API 有点类似了?
  • @Origin 注解:注入目标方法对应的 Method 对象。如果拦截的是字段的话,该注解应该标注到 Field 类型参数。
  • @Super 注解:注入目标对象。通过该对象可以调用目标对象的所有方法。
  • @SuperCall:这个注解比较特殊,我们要在 intercept() 方法中调用目标方法的话,需要通过这种方式注入,与 Spring AOP 中的 ProceedingJoinPoint.proceed() 方法有点类似,需要注意的是,这里不能修改调用参数,从上面的示例的调用也能看出来,参数不用单独传递,都包含在其中了。另外,@SuperCall 注解还可以修饰 Runnable 类型的参数,只不过目标方法的返回值就拿不到了。

有一个地方需要注意,这里定义的 Interceptor.intercept() 方法不是静态方法,而是一个实例方法。前面示例中要委托到 Interceptor 的静态方法,在 MethodDelegation.to() 方法中传递的参数是 Interceptor.class,这里要委托到 Interceptor 的实例方法需要在 MethodDelegation.to() 方法中传递 Interceptor 实例:

MethodDelegation.to(Interceptor.class) // 委托到 Interceptor的静态方法
MethodDelegation.to(new Interceptor()) // 委托到 Interceptor的实例方法

前面示例中,使用 @SuperCall 注解注入的 Callable 参数来调用目标方法时,是无法动态修改参数的,如果想要动态修改参数,则需要用到 @Morph 注解以及一些绑定操作,示例如下:

String hello = new ByteBuddy()
    .subclass(DB.class)
    .method(named("hello"))
    .intercept(MethodDelegation.withDefaultConfiguration()
        .withBinders( 
            // 要用@Morph注解之前,需要通过 Morph.Binder 告诉 Byte Buddy 
            // 要注入的参数是什么类型
            Morph.Binder.install(OverrideCallable.class)
        )
        .to(new Interceptor()))
    .make()
    .load(Main.class.getClassLoader(), INJECTION)
    .getLoaded()
    .newInstance()
    .hello("World");

Byte Buddy 相关的基础入门就差不多介绍完了,SkyWalking Agent 使用到的Byte Buddy知识我们先对他有个大概了解,这样再去看源码的时候就不会太模糊,当你看Skywalking源码的时候你会发现很多代码都是定义在那里,你根本不知道他的结果是咋出来的,尤其是生成TraceID的时候看的云里雾里。如果你想更深入地了解 Byte Buddy 的使用,可以参考其官网教程(http://bytebuddy.net/#/tutorial)进行学习。

5:总结

本次分享首先说明了运行时生成代码技术存在的必要性,然后简单介绍了当前市面上流行的 Java 代码生成库,并简单比较了它们各自优缺点,最后通过多组示例演示了 Byte Buddy 库的基本使用方式,对其中的 API 以及常用注解进行了详细的介绍。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1078769.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于springboot实现在线动漫信息交流分享平台项目【项目源码+论文说明】

基于springboot实现在线动漫信息交流分享平台演示 摘要 随着社会互联网技术的快速发展&#xff0c;每个行业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff1b;对于在线动漫信息平台当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#xf…

公司文件防泄密软件——「天锐绿盾」@德人合科技

天锐绿盾是一款企业级数据安全解决方案&#xff0c;主要用于保护企业的知识产权、客户资料、财务数据、技术图纸、应用系统等机密信息化数据不外泄。 PC访问地址&#xff1a; &#x1f517;isite.baidu.com/site/wjz012xr/2eae091d-1b97-4276-90bc-6757c5dfedee 该软件解决方案…

Three.js如何计算3DObject的2D包围框?

推荐&#xff1a;用 NSDT编辑器 快速搭建可编程3D场景 在Three.js应用开发中&#xff0c;有时你可能需要为3D场景中的网格绘制2D的包围框&#xff0c;应该怎么做&#xff1f; 朴素的想法是把网格的3D包围框投影到屏幕空间&#xff0c;例如&#xff0c;下图中的绿色框 3D包围框…

Linux 部署1Panel 现代化运维管理面板进行公网远程访问

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏:《速学数据结构》 《C语言进阶篇》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 文章目录 前言1. Linux 安装1Panel2. 安装cpolar内网穿透2.1 使用一键脚本安装命令 2.2向系统添加服务2.3 启动cpolar服务…

【名城优企游学】国轩高科,用数字化带来强劲发展动力

成立于2006 年5月&#xff0c;系中国动力电池产业最早进入资本市场的民族企业&#xff1b;2015年5月上市&#xff0c;股票代码SZ.002074&#xff0c;拥有新能源汽车动力锂电池、储能、输配电设备等业务板块&#xff0c;建有独立成熟的研发、采购、生产、销售体系。 它就是新能…

五分钟了解一下什么是「贪心算法 」‼️‼️‼️

五分钟了解一下什么是「贪心算法 」‼️‼️‼️ 1 概念 贪心的意思在于在作出选择时&#xff0c;每次都要选择对自身最为有利的结果&#xff0c;保证自身利益的最大化。贪心算法就是利用这种贪心思想而得出一种算法。 贪心算法作为五大算法之一&#xff0c;在数据结构中的应…

如何进行lidar和imu的外参标定

我们使用的lidar_align这个算法来进行标定。 1.下载源码 在ros工作空间下的src文件夹下运行这个命令。 git clone https://github.com/ethz-asl/lidar_align.gitsudo apt-get install libnlopt-devcd ..catkin_make 这里编译的时候会爆一些错误。我遇到的是&#xff1a; 这…

渗透测试成功的8个关键

渗透测试 (penetration test)并没有一个标准的定义&#xff0c;国外一些安全组织达成共识的通用说法是&#xff1a;渗透测试是通过模拟恶意黑客的攻击方法&#xff0c;来评估计算机网络系统安全的一种评估方法。这个过程包括对系统的任何弱点、技术缺陷或漏洞的主动分析&#x…

考试中心|学习资料|学习情况|纯净无广|在线组卷刷题

土著刷题Plus专业版v1.2版本已全面对其个人版功能&#xff0c;完全满足学员培训/刷题考察全套流程&#xff0c;提供完整的服务流程。接下来将主要介绍一下这一版的新功能 考试中心 满足培训机构/刷题组织者考察刷题用户的管理需求&#xff0c;【围绕考试展开】&#xff0c;提供…

SQLite数据库使用时碰到的问题

背景 1、最近使用sqlite数据库&#xff0c;因为轻量&#xff0c;所以&#xff0c;不需要特别的部署服务器环境。 开发的平台使用的是Qt 碰到的问题 1、我在向数据库中插入字段的时候&#xff0c;少写了一个字段的内容&#xff0c;报这个错误 QSqlError("", "…

火焰原子吸收光谱法、容量法和电感耦合等离子体发射光谱法

声明 本文是学习GB-T 1871.5-2022 磷矿石和磷精矿中氧化镁含量的测定 火焰原子吸收光谱法、容量法和电感耦合等离子体发射光谱法. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本文件描述了在磷矿石和磷精矿中测定氧化镁含量的火焰原子吸收…

史上最全 结构型模式之 代理 适配器 装饰者 模式

创建型设计模式 原型模式 建造者模式 创建者模式对比_软工菜鸡的博客-CSDN博客 5&#xff0c;结构型模式 day03 结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式&#xff0c;前者采用继承机制来组织接口和类&#xff0c;后者釆用组…

大模型部署手记(8)LLaMa2+Windows+llama.cpp+英文文本补齐

1.简介&#xff1a; 组织机构&#xff1a;Meta&#xff08;Facebook&#xff09; 代码仓&#xff1a;https://github.com/facebookresearch/llama 模型&#xff1a;llama-2-7b 下载&#xff1a;使用download.sh下载 硬件环境&#xff1a;暗影精灵7Plus Windows版本&#…

制药企业纯化水设备的应用及维护:设备健康管理软件的关键作用

纯化水设备在制药企业中扮演着重要角色&#xff0c;为生产高纯度水提供支持。然而&#xff0c;为了确保设备的可靠性和稳定性&#xff0c;有效的维护和管理至关重要。设备健康管理软件PreMaint成为实现这一目标的关键工具。本文将介绍PreMaint软件如何优化纯化水设备的维护&…

Qt_C++读写FM1208 CPU卡源码、支持windows、Linux系统

本示例使用的发卡器&#xff1a; Android Linux RFID读写器NFC发卡器WEB可编程NDEF文本/智能海报/-淘宝网 (taobao.com) #include "mainwindow.h" #include "./ui_mainwindow.h" #include <QDebug> #include "QLibrary" #include "Q…

政府部门搭建的数字化工具平台有哪些?

政府部门搭建的数字化工具平台多样且广泛&#xff0c;旨在提高行政效能、优化公共服务和促进政务创新。以下是一些常见的数字化工具平台及其功能的详细介绍。 一、电子政务平台 1. 政务门户网站&#xff1a;作为政府对外的窗口&#xff0c;提供政府机构、政策信息、在线办事、…

Redis 的过期键 | Navicat 技术干货

Redis 是一种高性能的内存数据存储&#xff0c;以其速度和多功能性而闻名。其中一个有用的特性是为键设置过期时间的功能。在 Redis 中&#xff0c;为键设置过期时间对于管理数据和确保过时或临时数据自动从数据库中删除是至关重要的。在本文中&#xff0c;我们将探讨在 redis-…

SpringBoot整合DataWay配置前端查询

Dataway介绍 Dataway 是基于 DataQL 服务聚合能力&#xff0c;为应用提供的一个接口配置工具。使得使用者无需开发任何代码就配置一个满足需求的接口。整个接口配置、测试、冒烟、发布。一站式都通过 Dataway 提供的 UI 界面完成。UI 会以 Jar 包方式提供并集成到应用中并和应…

Springboot学习笔记——3

Springboot学习笔记——3 一、热部署1.1、手动启动热部署1.2、自动启动热部署1.3、热部署范围配置1.4、关闭热部署 二、配置高级2.1、第三方bean属性绑定2.2、松散绑定2.3、常用计量单位应用2.4、bean属性校验2.5、进制数据转换规则 三、测试3.1、加载测试专用属性3.2、加载测试…

mysql面试题32:MySQL数据库服务器性能分析的方法命令有哪些?

该文章专注于面试,面试只要回答关键点即可,不需要对框架有非常深入的回答,如果你想应付面试,是足够了,抓住关键点 面试官:MySQL数据库服务器性能分析的方法命令有哪些? MySQL数据库服务器性能分析的方法和命令有以下几种: EXPLAIN命令:用于分析查询语句的执行计划,…