前端页面在运行过程中出现ElementPlusError错误,报[ElOnlyChild] no valid child node found错误消息

本文记录一个前端页面开发出现的问题,涉及的前端框架版本为,

  • “vue”: “^3.2.22”
  • “element-plus”: “^2.0.4”

1. 问题

近期在使用vue 3 + element plus进行前端页面开发,在前端页面运行过程浏览器后台中出现如下错误消息,

error.ts:12 ElementPlusError: [ElOnlyChild] no valid child node found
    at debugWarn (error.ts:12:18)
    at Proxy.<anonymous> (only-child.ts:35:9)
    at renderComponentRoot (runtime-core.esm-bundler.js:893:44)
    at ReactiveEffect.componentUpdateFn [as fn] (runtime-core.esm-bundler.js:5098:34)
    at ReactiveEffect.run (reactivity.esm-bundler.js:167:25)
    at updateComponent (runtime-core.esm-bundler.js:4968:26)
    at processComponent (runtime-core.esm-bundler.js:4901:13)
    at patch (runtime-core.esm-bundler.js:4489:21)
    at ReactiveEffect.componentUpdateFn [as fn] (runtime-core.esm-bundler.js:5107:17)
    at ReactiveEffect.run (reactivity.esm-bundler.js:167:25)

出现这个错误消息概率比较高,但不是每次都必现。页面上各个组件按钮都运行正常,出现这个问题感觉有些纳闷,上面的错误消息也没有直接给出哪里不对,一时不知道问题在哪里。

2. 定位

从错误消息[ElOnlyChild] no valid child node found来看,报的是子组件无效,估计是什么组件使用的不当,没有映射到相应的模型数据,是不是哪里变量命名有拼写错误,使用Ctrl+F检查了一番代码,没有发现任何变量拼写错误。

为了发现问题,开始对页面组件进行二分法排查,把页面组件逐一过滤,定位问题出现在如下一个气泡确认框组件上,

<el-popconfirm title="确定要删除吗?" @confirm="deleteConfig(row)">
   <template #reference>
       <el-button type="text" v-if="row.version != 'new'">删除</el-button>
   </template>
</el-popconfirm>

这个组件没有什么特别,之前已经在很多地方用的很好,唯一不同的是添加了v-if控制,稍微判断了一下,应该就是这个v-if导致。

若把上面的el-button去掉,则直接可以复现这个问题,

<el-popconfirm title="确定要删除吗?" @confirm="deleteConfig(row)">
   <template #reference>
   </template>
</el-popconfirm>

气泡确认框组件的用途,主要是在点击某个元素弹出一个简单的气泡确认框,其需要绑定在一个子组件上,若子组件为空,则会报出如下的错误信息,

[ElOnlyChild] no valid child node found...

在了解到如上的问题后,将v-if移到el-popconfirm组件,正确的书写方法为,

<el-popconfirm title="确定要删除吗?" v-if="row.version != 'new'" @confirm="deleteConfig(row)">
   <template #reference>
       <el-button type="text">删除</el-button>
   </template>
</el-popconfirm>

至此问题解决。

Java字节码增强技术Bytebuddy探路篇

图片来自pixabay.com的Alexas_Fotos会员

最近为了实现Java应用RPC调用的录制和Mock回放,需要以无侵入方式获取到RPC方法的出入参数和返回响应消息等数据,于是踏上了Java字节码增强技术的道路摸索,这个非常类似Trace所使用的相关技术,不过需要深入到RPC方法级别,对指定方法进行无侵入方式切面处理。先后对ASM/Javaassist/Bytebuddy等技术进行了调研等,本文是对所做摸索探路工作的总结,若读者有类似Trace场景需求,可以进行借鉴参考。

在众多比较之后最后选择的是Bytebuddy技术。

1. Java字节码简介

Java字节码是众多字节码增强技术的知识基础。Java语言写出的源代码首先需要编译成class文件,即字节码文件,然后被JVM加载并运行,每个class文件具有如下固定的数据格式,

ClassFile {
    u4             magic;           // 魔数,固定为0xCAFEBABE
    u2             minor_version;   // 次版本
    u2             major_version;   // 主版本,常见版本:52对应1.8,51对应1.7,其他依次类推
    u2             constant_pool_count;                     // 常量池个数
    cp_info        constant_pool[constant_pool_count-1];    // 常量池定义
    u2             access_flags;    // 访问标志:ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT等
    u2             this_class;      // 类索引
    u2             super_class;     // 父类索引
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

可以看到,class文件总是一个魔数开头,后面跟着版本号,然后就是常量定义、访问标志、类索引、父类索引、接口个数和索引表、字段个数和索引表、方法个数和索引表、属性个数和索引表。

class文件本质上是一个字节码流,每个字节码所处的位置代表着一定的指令和含义。如何对class文件中定义的指令和字节码进行解读、增强定义、编排,这是字节码增强技术所要完成的事情。

了解Java字节码有助于字节码增强的开发,但并不是实现字节码增强开发的必要条件,最新主流的众多字节码增强工具框架类库都将字节码的编排进行了不同程度封装,在可读性、易编排性、排错性上提供开发便利性,学习曲线和开发难度得到了较好的改善。

2. Java字节码增强支持

对于字节码增强的开发来说,JVMTI是一个在实践中应该被熟悉的工具技术。JVM从1.5版本开始提供JVM Tool Interface,这是JVM对外的、用于Java应用监控和调试的一系列工具接口,是JVM平台调试架构的重要组成部分。

下图是JVM平台调试架构图

The Java™ Platform Debugger Architecture is structured as follows:
           Components                          Debugger Interfaces

                /    |--------------|
               /     |     VM       |
 debuggee ----(      |--------------|  <------- JVM TI - Java VM Tool Interface(Jvm服务端调试接口)
               \     |   back-end   |
                \    |--------------|
                /           |
 comm channel -(            |  <--------------- JDWP - Java Debug Wire Protocol (Java调试通信协议)
                \           |
                     |--------------|
                     | front-end    |
                     |--------------|  <------- JDI - Java Debug Interface (客户端调试接口和调试应用)
                     |      UI      |
                     |--------------|

JVM启动支持加载agent代理,而agent代理本身就是一个JVM TI的客户端,其通过监听事件的方式获取Java应用运行状态,调用JVM TI提供的接口对应用进行控制。

我们可以看下Java agent代理的两个入口函数定义,

// 用于JVM刚启动时调用,其执行时应用类文件还未加载到JVM
public static void premain(String agentArgs, Instrumentation inst);

// 用于JVM启动后,在运行时刻加载
public static void agentmain(String agentArgs, Instrumentation inst);

这两个入口函数定义分别对应于JVM TI专门提供了执行字节码增强(bytecode instrumentation)的两个接口。

  • 加载时刻增强,类字节码文件在JVM加载的时候进行增强,。
  • 动态增强,已经被JVM加载的class字节码文件,当被修改或更新时进行增强,从JDK 1.6开始支持。

这两个接口都是从JDK 1.6开始支持。

我们无需对上面JVM TI提供的两个接口规范了解太多,Java Agent和Java Instrument类包封装好了字节码增强的上述接口通信。我们需要了解的是,上述入口函数传入的第二个参数Instrumentation实例,即Java Instrument类java.lang.instrument.Instrumentation,查看其类定义,可以看到其提供的核心方法只有一个addTransformer,用于添加多个ClassFileTransformer,

// 说明:添加ClassFileTransformer
// 第一个参数:transformer,类转换器
// 第二个参数:canRetransform,经过transformer转换过的类是否允许再次转换
void Instrumentation.addTransformer(ClassFileTransformer transformer, boolean canRetransform)

ClassFileTransformer则提供了tranform()方法,用于对加载的类进行增强重定义,返回新的类字节码流。需要特别注意的是,若不进行任何增强,当前方法返回null即可,若需要增强转换,则需要先拷贝一份classfileBuffer,在拷贝上进行增强转换,然后返回拷贝。

// 说明:对类字节码进行增强,返回新的类字节码定义
// 第一个参数:loader,类加载器
// 第二个参数:className,内部定义的类全路径
// 第三个参数:classBeingRedefined,待重定义/转换的类
// 第四个参数:protectionDomain,保护域
// 第五个参数:classfileBuffer,待重定义/转换的类字节码(不要直接在这个classfileBuffer对象上修改,需拷贝后进行)
// 注:若不进行任何增强,当前方法返回null即可,若需要增强转换,则需要先拷贝一份classfileBuffer,在拷贝上进行增强转换,然后返回拷贝。
byte[] ClassFileTransformer.transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte classfileBuffer)

下面简单的演示样例,通过Java agent打印出JVM加载的类列表,

  1. 下载演示代码,见这里
  2. 编译项目 mvn clean package
  3. 运行命令

杳杳寒山道

杳杳寒山道

唐代:寒山 杳杳寒山道,落落冷涧滨。 啾啾常有鸟,寂寂更无人。 淅淅风吹面,纷纷雪积身。 朝朝不见日,岁岁不知春。
一条幽暗寂静的寒山道上,旁边流淌着飘飘落落的山水小溪。 驻足而立,可以闻见山谷中鸟儿啾啾地啼鸣,四周远望,山中空无一人。 风淅淅沥沥地吹向脸庞,突然空中飘起了雪花,纷纷扬扬地落在了我身上。在这里,一年四季难得见一束温暖的阳关,终年也不知春天的花香鸟语,却是一片静心之地。