alibaba / alibaba/arthas

Proposal: 表达式体验优化与引擎更换

Open
#2,849 11 comments 5 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
37.6k
Forks
7.6k
Avg merge
1d 22h
Merged PRs (30d)
4

Description

## 背景

@hengyunabc 在去年发起过一个关于考虑不同的表达式引擎 #2747

我们在阿里内部进行了一些简单的交流, 希望能够借着 Arthas 4 的机会, 进行一次表达式体验的优化, 让用户能够更加方便地在黑屏情况下操作 Arthas 表达式。本次优化的主要目标有三个:

- 让表达式更加接近 Java 程序员的使用习惯
- 对于复杂表达式, 可以分多步调试和写出, 边写边调才更加顺畅
- 疑难问题解决: 比如泛型方法调用等

本文希望就这三个问题表达想法, 并与社区的朋友们一起讨论。

## 提议一: 更符合 Java 程序员的习惯

ognl 语法简洁, 但终究和 Java 的语法相差甚远。虽说现在有工具和 AI 的加持的, 但在需要黑屏操作, 或者微调的时候, 还是相当麻烦。

我是 [QLExpress](https://github.com/alibaba/QLExpress) 的开源支持者之一, 并且也计划在今年推出 4.0 版本。我提议将 Arthas 的表达式更换为 QLExpress, 考虑如下:

- QLExpress 的语法更加贴近 Java, 符合 Java 程序员习惯, Java 开发者可以不需要太多思考就能写出
- QLExpress 和 Arthas 一样, 都是阿里内外部广泛使用的开源项目, 经过多年检验。相关开发者在阿里座位也邻近, 有利于形成技术和运营上的合力
- 另外, 通过 QLExpress 自定义函数/操作符/宏的能力, 也更方便丰富 Arthas 表达式的写法

在 [Arthas的一些特殊用法文档说明](https://github.com/alibaba/arthas/issues/71) 这个 issue 中, 大家讨论了 ognl 在各种特殊场景下的写法, 借助 issue 中的一些例子, 展示在 QLExpress 中的写法:

> 以下写法还处于草稿状态, 修改的弹性很大, 欢迎社区的朋友们提意见

| 操作 | OGNL | QLExpress |
| ------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| 投影操作 | `l.{ #this.name }` | `l*.name` |
| 按条件过滤 | `l.{? #this.name == null }` | `l.filter(i -> i.name == null)` |
| 过滤后统计 | `l.{? #this.age > 10 }.size()` | `l.filter(i -> i.name == null).size()` |
| 复合表达式 | `l.{? #this.age > 10 }.size().(#this > 20 ? #this - 10 : #this + 10)` | `l.filter(i -> i.age > 10).size().let(i -> i > 20? i - 10: i + 10)` |
| 找到第一个符合条件的元素 | `l.{^ #this.name != null}` | `l.findFirst(i -> i.name != null)` |
| 找到最后一个符合条件的元素 | `l.{$ #this.name != null}` | `l.findLast(i -> i.name != null)` |
| 静态变量访问 | `@com.taobao.container.Test@m` | `com.taobao.container.Test.m` |
| 静态方法调用 | `@java.lang.Thread@currentThread()` | `java.lang.Thread.currentThread()` |
| 内部类 | `@com.taobao.container.Test$InnerClassName@m` | `com.taobao.container.Test.InnerClassName.m` |
| 复杂对象调用 | `@com.alibaba.fastjson.JSON@parseObject("{\"name\":\"aaa\"}", xxxx.class)` | `service.method({'a':1,'b':2, '@class':a.b.c}, {'a':1,'b':2, '@class':a.b.c.d})` |

## 提议二: 多步表达式

即使使用了 QLExpress, 以上的某些写法也有些太复杂。我们在咨询了一些用户后发现, 表达式难以编写的原因, 只有部分是因为对 ognl 语法不了解, **更深层次的原因还是在于: 让开发者一次性写对复杂的逻辑**。

因此我们提议在 Arthas 中支持多步表达式: 即支持将某个命令的结果保存下来, 然后用类似 REPL(Read–eval–print loop) 的表达式交互界面逐步完成复杂的数据操作。

这里给两个例子说明操作过程。

> 多步表达式的用法也还处于草稿状态, 环境社区的朋友们一起脑洞大开, 提意见

### getStatic 获取静态变量并处理

假设在 `com.alibaba.arthas.Test` 类中有一个静态变量 n, 它是一个 Person 列表, 我从中筛选出姓名为 `li` 的人的年龄。

原本 OGNL 表达式的写法如下:

```bash
$ getstatic com.alibaba.arthas.Test n 'iterator.{? #this.name=="li"}.age'
```

如果使用 QLExpress 一次性完成, 则写法如下:

```bash
$ getstatic com.alibaba.arthas.Test n 'this.filter(i -> i.name=="li").age'
```

整个处理过程, 其实分为筛选元素, 和取 age 属性两步完成。下面演示两种多步表达式用法:

多步表达式用法一:

```bash
# 将 getstatic 命令的结果赋值给变量 n
# 变量 n 会被保存在会话中
$ n=$(getstatic com.alibaba.arthas.Test n)
[
{
"name": "li",
"age": 30
},
{
"name": "wang",
"age": 11
}
]
# 新增一个单独的 eval 命令用于计算表达式
$ p=$(eval 'n.filter(it.name=="li")')
[
{
"name": "li",
"age": 30
}
]
$ eval 'p.age'
30
```

多步表达式用法二:

```bash
$ n=$(getstatic com.alibaba.arthas.Test n)
$ eval
# 进入一个表达式上下文, 编写表达式更加方便
eval$ p=n.filter(it.name=='li')
{
"name": "li",
"age": 30
}
eval$ p.age
30
eval$ exit()
```

### watch 获得方法参数/返回值并且处理

实践中, 有时候会碰到, 业务列表/对象实在太大了, 连一屏幕都显示不下, 此时去抠关键信息就如同大海捞针。有了多步表达式, 就可以一点点去筛选关键信息的位置。

watch 也比较特殊, 因为它执行后会等待在那里, 直到用户按下 Ctrl + C 或者超过最大数目限制。因为我们将 watch 命令的返回值定义为最近 5 个 expression 参数的返回值(小于 5 个则保存全部请求)。

举个例子, 对于

```bash
$ n=${watch demo.MathGame primeFactors "{params,returnObj}" -x 2}
```

假设 watch 到 10 个请求后, 用户按 Ctrl + C 停止, 则 n 中保存的值为:

> 下方为伪代码, 比如 params10 表示第 10 次请求的参数, returnObj10 表示第 10 次请求的返回值

```
[
{params10,returnObj10},
{params9,returnObj9},
{params8,returnObj8},
{params7,returnObj7},
{params6,returnObj6}
]
```

假设 `com.taobao.container.Test` 类的 `test` 方法的第一个参数为 Person 类型的列表。 我们想从最近一次调用该方法的参数中, 筛选出姓名为 `li` 的人的年龄, 则多步式写法如下:

```bash
$ n=$(watch com.taobao.container.Test test "{params,returnObj}" "returnObj.age==10&¶ms[0]==3" -x 2)
[
//...
]
# 等待,直到用户 Ctrl+C 暂停,只保存最近 5 个,作为一个 list 保存在变量 n 中
$ eval
# 取最近一次 watch 的结果
eval$ last=n[0]
[
[
{
"name": "wang",
"age": 11
},
{
"name": "li",
"age": 23
}
]
]
eval$ p=last[0].filter(it.name.age>20)
[
{
"name": "li",
"age": 23
}
]
eval$ p[0].age
23
```

## 提议三: 疑难问题解决

和 [arthas-idea-plugin](https://github.com/WangJi92/arthas-idea-plugin) 的作者 @WangJi92, 以及其他用户的交流中, 我们找到下面几个 arthas 表达式使用起来复杂, 甚至无法解决的问题。

> 这里罗列的不一定完整, 欢迎大家在继续补充

### 调用含有复杂对象的方法

假设有一个 `a.b.c.MyService` 中有一个方法为 `myMethod`, 它接受单个类型为 Person 的参数, Person 中只有一个属性为 name。则调用该方法在目前 Arthas 中只有两个方式:

- 方式一: 新建 Person 类, 逐个 set 属性完成后, 再调用方法

```bash
vmtool -x 3 --action getInstances --className a.b.c.MyService --express '#p=new a.b.c.Person(),#p.setName("li"),instances[0].myMethod(#p)'
```

这个方法仅仅是可行, 实际用起来体验很差。特别是对象嵌套层次复杂时, 因为没法格式化, 让广大程序员的近视度数又增加了。

- 方式二: 利用应用中 json 序列化库, 比如 fastjson, 将 json 反序列化成需要的对象, 再调用方法

```bash
vmtool -x 3 --action getInstances --className a.b.c.MyService --express 'instances[0].myMethod(@com.alibaba.fastjson.JSON@parseObject("{\"name\":\" li\"}",@a.b.c.Person@class))'
```

这里的表达式也很复杂, 需要转义 Json, 并且依赖应用中包含的 Json 序列化库。但是比较模板化, 只需要在别的编辑工具中将 Json 编辑好, 转义后复制到模板中即可。

[arthas-idea-plugin](https://github.com/WangJi92/arthas-idea-plugin) 就利用类似的表达式模板, 自动生成包含复杂对象的调用, 如下图:

![image](https://github.com/alibaba/arthas/assets/23725000/4cf5f589-8ce6-4b08-9623-2c999d8042d8)

在表达式替换为 QLExpress4 后, 我们计划的写法如下:

```bash
vmtool -x 3 --action getInstances --className a.b.c.MyService --express 'instances[0].myMethod({"name":"li", "@class": "a.b.c.Person"}))'
```

模板化的表达式更少了, 不再需要烦人的转义, 也不依赖应用中的 Json 序列化框架。书写嵌套对象也更加 easy, 一个嵌套对象的案例如下:

```json
{
"name": "li",
"age": {
"year": 31,
"mouth": 5,
"@class": "a.b.c.Age"
},
"@class": "a.b.c.Person"
}
```

### 方法参数为泛型类

OGNL 不支持泛型, 如果参数类中含有泛型, 甚至连序列化也帮不了你。

因为泛型的序列化需要借助带泛型的 `TypeReference`, 而 OGNL 中无法书写泛型。相关的表达式生成工具只能绕道, 先将属性序列化出来后, 再调用 set 方法设置进对象中。

假设方法签名是这样的:

```java
public class MyService {
public String myMethodGeneric(Pair pair) {
return "";
}
}
```

Pair 类的定义如下:

```java
public class Pair {

private int num;

private L left;

private R right;
// 省略 get set ...
}
```

想用 vmtool 调用它, 采用 OGNL + 序列化的写法就特别麻烦。必须要先序列化 left 属性, 再序列化 right 属性, 最后再设置进 Pair 对象中:

```bash
vmtool -x 3 --action getInstances --className a.b.c.MyService --express 'instances[0].myMethodGeneric((#p=@com.alibaba.fastjson.JSON@parseObject("{\"num\":2}",@a.b.c.Pair@class),(#p.setLeft(@com.alibaba.fastjson.JSON@parseObject("{\"name\":\" li\"}",@a.b.c.Person@class))),(#p.setRight(@com.alibaba.fastjson.JSON@parseObject("{\"address\":\"street 404\"}",@a.b.c.Home@class))),#p))'
```

这还只是在嵌套了一层的情况下, 如果对象层次更多, 即使工具能够生成, 也复杂得根本看不明白, 修改不了, 也没有任何工具可以将其结构化。

如果使用 QLExpress4, 则可以将其写得非常清晰:

```bash
vmtool -x 3 --action getInstances --className a.b.c.MyService --express 'instances[0].myMethodGeneric({"name":"li","age":{"year":31,"mouth":5,"@class":"a.b.c.Age"},"@class":"a.b.c.Person"})'
```

### 调用泛型方法

上一种情况虽然复杂, 至少还能借助工具生成。

和相关工具作者交流后, 他们表示, 如果调用的方法本身带有泛型方法的话, 则工具无法生成, 只能让开发者手工做微调, 比如下面的泛型方法:

```java
public class MyService {
public String myMethodGeneric2(Pair pair) {
return "";
}
}
```

因为不知道开发者想要赋予泛型的类型是什么, 工具无法生成准确的表达式。

如果让开发者手工微调的话, 因为表达式复杂度很高, 也几乎不可能, 于是陷入了两难, 几乎是不可用的状态。

当然, 这个问题如果工具作者想解的话, 也不是不能解, 比如加一个交互步骤, 让用户先手动选择想要的泛型类, 之后再执行生成。

不过直接采用上面 QLExpress4 的写法更简洁。

## 常见 Q & A

**问: Arthas 经常会在线上使用, 较复杂的表达式体验能够防止线上的误操作, 或者某些恶意操作。多步式表达式会不会导致这些行为过于容易呢?**

**答**: Arthas 能够做任何想得到的 Java 调用, 不是不能做, 只是做起来比较复杂。那自然会有人开发脚本生成工具来简化操作, 比如 @WangJi92 的 [arthas-idea-plugin](https://github.com/WangJi92/arthas-idea-plugin), 就能够自动生成任意复杂的 Java 调用脚本。与其让别人来简化, 不如 Arthas 自己来简化, 还更可控一些, 未来用更有效的机制保证安全。

**问: 多步式体验优化和 QLExpress 的关系是什么?**

**答**: 没有技术上的关系, OGNL 也可以开发多步式的交互。但是既然已经用了 QLExpress4 做表达式体验优化, 就希望一步到位, 将体验优化到极致。提供一个新的功能特性, 也能够吸引用户升级, 并且尝试了解新的表达式引擎。

**问: 复杂表达式编写是个低频操作, 有必要优化低频操作的体验吗?**

**答**: 高频的简单表达式写法我们维持不变, 比如最常用的 watch 表达式 `{params,returnObj,throwExp}`, 或者简单的对象取属性, 写法和原来保持一致。仅仅在需要更加复杂的处理时, 才需要了解 QLExpress4 的写法, 整体上是一个帕累托改进, 即在没有损失高频操作体验的情况下, 提升低频操作体验。而且之前的 "低频" 也不一定是真的 "低频", 可能是因为太麻烦, 开发者懒得这么用, 现在我们将它变简单了, 用户说不定能想出更多玩法呢。

**如果有更多问题, 也欢迎大家在评论区讨论。**

Contributor guide

Open the contributing guide

Research direction

Start with issue #2747 and review the expression-related entry points used by getstatic, watch, vmtool, and the proposed eval command. Before coding, establish an agreed scope for the QLExpress replacement and multi-step interaction; done requires a decided design covering compatibility and the listed use cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
cli, devtools
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.