• 【java表达式引擎】四、高性能、轻量级的AviatorScript


    一、Aviator 介绍

    github:(https://github.com/killme2008/aviatorscript%60)
    参考文档1:https://www.yuque.com/boyan-avfmj/aviatorscript
    参考博客2:https://blog.csdn.net/ZhangQingmu/article/details/125087255

    Aviator起源于2011年,由国内的开发者开源的,表达式引擎
    表达式引擎当时国内开源的已经有 IKExpression,可惜是纯解释执行的,效率很一般,Groovy 刚开始流行,性能不错,但是整体很重量级,更重要的原因是我们希望控制用户能使用的语法和函数,需要一个定制的“子集”,因此 Aviator 就诞生了。

    原理和特点

    Aviator 的基本过程是将表达式直接翻译成对应的 java 字节码执行,整个过程最多扫两趟(开启执行优先模式,如果是编译优先模式下就一趟),这样就保证了它的性能超越绝大部分解释性的表达式引擎,测试也证明如此;其次,除了依赖 commons-beanutils 这个库之外(用于做反射)不依赖任何第三方库,因此整体非常轻量级,整个 jar 包大小哪怕发展到现在 5.0 这个大版本,也才 430K。同时, Aviator 内置的函数库非常“节制”,除了必须的字符串处理、数学函数和集合处理之外,类似文件 IO、网络等等你都是没法使用的,这样能保证运行期的安全,如果你需要这些高阶能力,可以通过开放的自定义函数来接入。因此总结它的特点是:

    ● 高性能
    ● 轻量级
    ● 一些比较有特色的特点:
    ○ 支持运算符重载
    ○ 原生支持大整数和 BigDecimal 类型及运算,并且通过运算符重载和一般数字类型保持一致的运算方式。
    ○ 原生支持正则表达式类型及匹配运算符 =~
    ○ 类 clojure 的 seq 库及 lambda 支持,可以灵活地处理各种集合
    ● 开放能力:包括自定义函数接入以及各种定制选项
    ● Java Scripting API 支持
    ● 数组的 支持

    二、依赖

    目前github上的最新版本是5.3.2
    大家可以依赖以下版本,也可以下载源码直接在源码中测试

    <dependency>
      <groupId>com.googlecode.aviator</groupId>
      <artifactId>aviator</artifactId>
      <version>5.3.2</version>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    三、样例使用

    1、需求

    这次现提出一些需求看是否都可以满足

    • 常规的 加减乘除
      1+1+3+5-6=4
      (9.88+12.24)-5.88/(5.88+6.14)* (9.88+12.24)= 11.299234608985

    • 逻辑判断
      if(3<6,1,0)
      IFERROR((MIN(5/0,3)),0) = 0

    2、上手使用

    我们主要使用这两个方法就可以满足上述需求

     AviatorEvaluator.compile
    AviatorEvaluator.execute
    
    • 1
    • 2

    3、加减乘除

       @Test
       public void test1() {
          Expression exp1 = AviatorEvaluator.compile("1+1+3+5-6");
          Expression exp2 = AviatorEvaluator.compile(" (9.88+12.24)-5.88/(5.88+6.14)* (9.88+12.24)");
          Object execute = exp1.execute();
          Object execute2 = exp2.execute();
          assertEquals(4,execute);
          System.out.println( execute);//4
          System.out.println(execute2);//11.299234608985023
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    4、逻辑判断

    执行test2报错了不支持excel格式的逻辑判断

       @Test
       public void test2() {
          Expression exp1 = AviatorEvaluator.compile("if(3<6,1,0)");
          Expression exp2 = AviatorEvaluator.compile("IFERROR((MIN(5/0,3)),0)");
          Object execute = exp1.execute();
          Object execute2 = exp2.execute();
          System.out.println( execute);//
          System.out.println(execute2);//
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    支持if else 和三母运算

       /**
        * if else
        */
       @Test
       public void test3() {
          Expression exp1 = AviatorEvaluator.compile("if(1<2) { 3 } else { 4}");
          Expression exp2 = AviatorEvaluator.compile("1<2?  3 : 4 ");
          Object execute = exp1.execute();
          System.out.println( execute);//3
          Object execute2 = exp2.execute();
          System.out.println( execute2);//3
       }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    max和min

       /**
        * max和min函数
        */
       @Test
       public void test4() {
          Expression exp1 = AviatorEvaluator.compile("min(3,4)");
          Expression exp2 = AviatorEvaluator.compile("max(3,4)");
          Object execute = exp1.execute();
          System.out.println( execute);//3
          Object execute2 = exp2.execute();
          System.out.println( execute2);//4
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    小编的一些常见的需求基本可以满足,下面就看一下还支持那些吧。

    四、其余介绍

    1、基本数据类型

    AviatorScript 支持常见的类型,如数字、布尔值、字符串等等,同时将大整数、BigDecimal、正则表达式也作为一种基本类型来支持。

    加减乘除对应的运算符就是 +,-,*,/ 这都比较好理解,取模运算符就是 % ,规则和语法和 java 是一样的。

    • 整数
      整数例如 -99、0、1、2、100……等等,对应的类型是 java 中的 long 类型。AviatorScript 中并没有 byte/short/int 等类型,统一整数类型都为 long,支持的范围也跟 java 语言一样:-9223372036854774808~9223372036854774807。

    • 浮点数
      仅支持 double 类型
      浮点数和浮点数的算术运算结果为浮点数,浮点数和整数的运算结果仍然为浮点数。

    • 高精度计算(Decimal)
      浮点数是无法用于需要精确运算的场景,比如货币运算或者物理公式运算等,这种情况下如果在 Java 里一般推荐使用 BigDecimal 类型,调用它的 add/sub 等方法来做算术运算。
      AviatorScript 将 BigDecimal 作为基本类型来支持(下文简称 decimal 类型),只要浮点数以 M 结尾就会识别类型为 deicmal,例如 1.34M 、 0.333M 或者科学计数法 2e-3M 等等。
      如果你觉的为浮点数添加 M 后缀比较麻烦,希望所有浮点数都解析为 decimal ,可以开启 Options.ALWAYS_PARSE_FLOATING_POINT_NUMBER_INTO_DECIMAL

    • 字符串
      在任何语言中,字符串都是最基本的类型,比如 java 里就是 String 类型。AviatorScript 中同样支持字符串,只要以单引号或者双引号括起来的连续字符就是一个完整的字符串对象,例如:

    "a"  或者 'a' 
    
    • 1

    2、逻辑判断

    支持if esle elseif 等连续判断

    3、循环语句

    支持 for 和 while 两种循环语句

    五、函数列表

    只要是下方的函数,都是可以直接使用的
    例如

       /**
        * 使用系统函数
        */
       @Test
       public void test6() {
          Expression exp1 = AviatorEvaluator.compile("min(3,4)");
          Expression exp2 = AviatorEvaluator.compile("rand(100)");
          Object execute = exp1.execute();
          System.out.println( execute);//3
          Object execute2 = exp2.execute();
          System.out.println( execute2);//59
       }
    
       /**
        * 日期函数
        */
       @Test
       public void test8() {
          Expression exp1 = AviatorEvaluator.compile("date_to_string(date,format)");
          Map<String, Object> env = new HashMap<>();
          env.put("date", new Date());
          env.put("format", "yyyy-MM-dd");
          Object result =  exp1.execute(env);
          System.out.println( result);//2022-09-12
    
    
          Expression exp2 = AviatorEvaluator.compile("string_to_date(source,format)");
          Map<String, Object> env2 = new HashMap<>();
          env2.put("source", "2022-01-04");
          env2.put("format", "yyyy-MM-dd");
          Object result2 =  exp2.execute(env2);
          System.out.println( result2);//Tue Jan 04 00:00:00 CST 2022
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    1、系统函数

    函数名称
    说明
    assert(predicate, [msg])
    断言函数,当 predicate 的结果为 false 的时候抛出 AssertFailed 异常, msg 错误信息可选。
    sysdate()
    返回当前日期对象 java.util.Date
    rand()
    返回一个介于 [0, 1) 的随机数,结果为 double 类型
    rand(n)
    返回一个介于 [0, n) 的随机数,结果为 long 类型
    cmp(x, y)
    比较 x 和 y 大小,返回整数,0 表示相等, 1 表达式 x > y,负数则 x < y。
    print([out],obj)
    打印对象,如果指定 out 输出流,向 out 打印, 默认输出到标准输出
    println([out],obj)
    或者
    p([out], obj)
    与 print 类似,但是在输出后换行
    pst([out], e);
    等价于 e.printStackTrace(),打印异常堆栈,out 是可选的输出流,默认是标准错误输出
    now()
    返回 System.currentTimeMillis() 调用值
    long(v)
    将值转为 long 类型
    double(v)
    将值转为 double 类型
    boolean(v)
    将值的类型转为 boolean,除了 nil 和 false,其他都值都将转为布尔值 true。
    str(v)
    将值转为 string 类型,如果是 nil(或者 java null),会转成字符串 ‘null’
    bigint(x)
    将值转为 bigint 类型
    decimal(x)
    将值转为 decimal 类型
    identity(v)
    返回参数 v 自身,用于跟 seq 库的高阶函数配合使用。
    type(x)
    返回参数 x 的类型,结果为字符串,如 string, long, double, bigint, decimal, function 等。Java  类则返回完整类名。
    is_a(x, class)
    当 x 是类 class 的一个实例的时候,返回 true,例如 is_a(“a”, String) ,class 是类名。
    is_def(x)
    返回变量 x 是否已定义(包括定义为 nil),结果为布尔值
    undef(x)
    “遗忘”变量  x,如果变量 x 已经定义,将取消定义。
    range(start, end, [step])
    创建一个范围,start 到 end 之间的整数范围,不包括 end, step 指定递增或者递减步幅。
    tuple(x1, x2, …)
    创建一个 Object 数组,元素即为传入的参数列表。
    eval(script, [bindings], [cached])
    对一段脚本文本 script 进行求值,等价于 AviatorEvaluator.execute(script, env, cached)
    comparator(pred)
    将一个谓词(返回布尔值)转化为 java.util.Comparator 对象,通常用于 sort 函数。
    max(x1, x2, x3, …)
    取所有参数中的最大值,比较规则遵循逻辑运算符规则。
    min(x1, x2, x3, …)
    取所有参数中的最小值,比较规则遵循逻辑运算符规则。
    constantly(x)
    用于生成一个函数,它对任意(个数)参数的调用结果 x。

    2、数学函数

    math.abs(d)
    求 d 的绝对值
    math.round(d)
    四舍五入
    math.floor(d)
    向下取整
    math.ceil(d)
    向上取整
    math.sqrt(d)
    求 d 的平方根
    math.pow(d1,d2)
    求 d1 的 d2 次方
    math.log(d)
    求 d 的自然对数
    math.log10(d)
    求 d 以 10 为底的对数
    math.sin(d)
    正弦函数
    math.cos(d)
    余弦函数
    math.tan(d)
    正切函数
    math.atan(d)
    反正切函数
    math.acos(d)
    反余弦函数
    math.asin(d)
    反正弦函数

    3、字符串函数

    date_to_string(date,format)
    将 Date 对象转化化特定格式的字符串,2.1.1 新增
    string_to_date(source,format)
    将特定格式的字符串转化为 Date 对 象,2.1.1 新增
    string.contains(s1,s2)
    判断 s1 是否包含 s2,返回 Boolean
    string.length(s)
    求字符串长度,返回 Long
    string.startsWith(s1,s2)
    s1 是否以 s2 开始,返回 Boolean
    string.endsWith(s1,s2)
    s1 是否以 s2 结尾,返回 Boolean
    string.substring(s,begin[,end])
    截取字符串 s,从 begin 到 end,如果忽略 end 的话,将从 begin 到结尾,与 java.util.String.substring 一样。
    string.indexOf(s1,s2)
    java 中的 s1.indexOf(s2),求 s2 在 s1 中 的起始索引位置,如果不存在为-1
    string.split(target,regex,[limit])
    Java 里的 String.split 方法一致,2.1.1 新增函数
    string.join(seq,seperator)
    将集合 seq 里的元素以 seperator 为间隔 连接起来形成字符串,2.1.1 新增函数
    string.replace_first(s,regex,replacement)
    Java 里的 String.replaceFirst 方法, 2.1.1 新增
    string.replace_all(s,regex,replacement)
    Java 里的 String.replaceAll 方法 , 2.1.1 新增

    4、集合函数

    函数名称
    说明
    repeat(n, x)
    返回一个 List,将元素 x 重复 n 次组合而成。
    repeatedly(n, f)
    返回一个 List,将函数 f 重复调用 n 次的结果组合而成。
    seq.array(clazz, e1, e2,e3, …)
    创建一个指定 clazz 类型的数组,并添加参数 e1,e2,e3 …到这个数组并返回。 clazz 可以是类似 java.lang.String 的类型,也可以是原生类型,如 int/long/float 等
    seq.array_of(clazz, size1, size2, …sizes)
    创建 clazz 类型的一维或多维数组,维度大小为 sizes 指定。clazz 同 seq.array 定义。
    seq.list(p1, p2, p3, …)
    创建一个 java.util.ArrayList 实例,添加参数到这个集合并返回。
    seq.set(p1, p2, p3, …)
    创建一个 java.util.HashSet 实例,添加参数到这个集合并返回。
    seq.map(k1, v1, k2, v2, …)
    创建一个 java.util.HashMap 实例,参数要求偶数个,类似 k1,v1 这样成对作为 key-value 存入 map,返回集合。
    seq.entry(key, value)
    创建 Map.Entry 对象,用于 map, filter 等函数
    seq.keys(m)
    返回 map 的 key 集合
    seq.vals(m)
    返回 map 的 value 集合
    into(to_seq, from_seq)
    用于 sequence 转换,将 from sequence 的元素使用 seq.add 函数逐一添加到了 to sequence 并返回最终的 to_seq
    seq.contains_key(map, key)
    当 map 中存在 key 的时候(可能为 null),返回 true。对于数组和链表,key 可以是 index,当 index 在有效范围[0…len-1],返回 true,否则返回 false
    seq.add(coll, element)
    seq.add(m, key, value)
    往集合 coll 添加元素,集合可以是 java.util.Collection,也可以是 java.util.Map(三参数版本)
    seq.put(coll, key, value)
    类似 List.set(i, v)。用于设置 seq 在 key 位置的值为 value,seq 可以是 map ,数组或者 List。 map 就是键值对, 数组或者 List 的时候, key 为索引位置整数,value 即为想要放入该索引位置的值。
    seq.remove(coll, element)
    从集合或者 hash map 中删除元素或者 key
    seq.get(coll, element)
    从 list、数组或者 hash-map 获取对应的元素值,对于 list 和数组, element 为元素的索引位置(从 0 开始),对于 hash map 来说, element 为 key。
    map(seq,fun)
    将函数 fun 作用到集合 seq 每个元素上, 返回新元素组成的集合
    filter(seq,predicate)
    将谓词 predicate 作用在集合的每个元素 上,返回谓词为 true 的元素组成的集合
    count(seq)
    返回集合大小,seq 可以是数组,字符串,range ,List 等等
    is_empty(seq)
    等价于 count(seq) == 0,当集合为空或者 nil,返回 true
    distinct(seq)
    返回 seq 去重后的结果集合。
    is_distinct(seq)
    当 seq 没有重复元素的时候,返回 true,否则返回 false
    concat(seq1, seq2)
    将 seq1 和 seq2 “连接”,返回连接后的结果,复杂度 O(m+n), m 和 n 分别是两个集合的长度。
    include(seq,element)
    判断 element 是否在集合 seq 中,返回 boolean 值,对于 java.uitl.Set 是 O(1) 时间复杂度,其他为 O(n)
    sort(seq, [comparator])
    排序集合,仅对数组和 List 有效,返回排序后的新集合,comparator 是一个 java.util.Comparator 实例,可选排序方式。
    reverse(seq)
    将集合元素逆序,返回新的集合。
    reduce(seq,fun,init)
    fun 接收两个参数,第一个是集合元素, 第二个是累积的函数,本函数用于将 fun 作用在结果值(初始值为 init 指定)和集合的每个元素上面,返回新的结果值;函数返回最终的结果值
    take_while(seq, pred)
    遍历集合 seq,对每个元素调用 pred(x),返回 true则加入结果集合,最终返回收集的结果集合。也就是说从集合 seq 收集 pred 调用为 true 的元素。
    drop_while(seq, pred)
    与 take_while 相反,丢弃任何 pred(x) 为 true 的元素并返回最终的结果集合。
    group_by(seq, keyfn)
    对集合 seq 的元素按照 keyfn(x) 的调用结果做分类,返回最终映射 map。具体使用见 文档
    zipmap(keys, values)
    返回一个 HashMap,其中按照 keys 和 values 两个集合的顺序映射键值对。具体使用见 文档
    seq.every(seq, fun)
    fun 接收集合的每个元素作为唯一参数,返回 true 或 false。当集合里的每个元素调用 fun 后都返回 true 的时候,整个调用结果为 true,否则为 false。
    seq.not_any(seq, fun)
    fun 接收集合的每个元素作为唯一参数,返回 true 或 false。当集合里的每个元素调用 fun 后都返回 false 的时候,整个调用结果为 true,否则为 false。
    seq.some(seq, fun)
    fun 接收集合的每个元素作为唯一参数,返回 true 或 false。当集合里的只要有一个元素调用 fun 后返回 true 的时候,整个调用结果立即为该元素,否则为 nil。
    seq.eq(value)
    返回一个谓词,用来判断传入的参数是否跟 value 相等,用于 filter 函数,如filter(seq,seq.eq(3)) 过滤返回等于3 的元素组成的集合
    seq.neq(value)
    与 seq.eq 类似,返回判断不等于的谓词
    seq.gt(value)
    返回判断大于 value 的谓词
    seq.ge(value)
    返回判断大于等于 value 的谓词
    seq.lt(value)
    返回判断小于 value 的谓词
    seq.le(value)
    返回判断小于等于 value 的谓词
    seq.nil()
    返回判断是否为 nil 的谓词
    seq.exists()
    返回判断不为 nil 的谓词
    seq.and(p1, p2, p3, …)
    组合多个谓词函数,返回一个新的谓词函数,当今仅当 p1、p2、p3 …等所有函数都返回 true 的时候,新函数返回 true
    seq.or(p1, p2, p3, …)
    组合多个谓词函数,返回一个新的谓词函数,当 p1, p2, p3… 其中一个返回 true 的时候,新函数立即返回 true,否则返回 false。
    seq.min(coll)
    返回集合中的最小元素,要求集合元素可比较(实现 Comprable 接口),比较规则遵循 aviator 规则。
    seq.max(coll)
    返回集合中的最大元素,要求集合元素可比较(实现 Comprable 接口),比较规则遵循 aviator 规则。

    5、自定义函数的使用

    
       /**
        * 自定义函数minn
        */
       @Test
       public void test7() {
          AviatorEvaluator.addFunction(new MinnFunction());
          String expression = "minn(a,b)";
          Expression compiledExp = AviatorEvaluator.compile(expression);
          Map<String, Object> env = new HashMap<>();
          env.put("a", 5.5);
          env.put("b", 55);
          Double result = (Double) compiledExp.execute(env);
          System.out.println(result);//5.5
       }
    
       static class MinnFunction extends AbstractFunction {
          @Override public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
             Number left = FunctionUtils.getNumberValue(arg1, env);
             Number right = FunctionUtils.getNumberValue(arg2, env);
             return new AviatorBigInt(Math.min(left.doubleValue(), right.doubleValue()));
          }
          public String getName() {
             return "minn";
          }
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    六、对象中的属性引用

    你要访问变量a中的某个属性b, 那么你可以通过a.b访问到, 更进一步, a.b.c将访问变量a的b属性中的c属性值, 推广开来也就是说 Aviator 可以将变量声明为嵌套访问的形式。

      /**
       * 类型转换,及对象中的属性引用
       */
      @Test
      public void testTypeConversation() {
        Map<String, Object> env = new HashMap<String, Object>();
        env.put("foo", new Foo(100));
        env.put("bar", new Bar(99, 999));
        env.put("date", new Date());
    
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("key", "aviator");
        env.put("tmap", map);
        env.put("bool", Boolean.FALSE);
    
        // long op long=long
        System.out.println(this.instance.execute("3+3") instanceof Long);
        System.out.println(this.instance.execute("3+3/2") instanceof Long);
        System.out.println(this.instance.execute("foo.a+bar.a", env) instanceof Long);
        System.out.println(this.instance.execute("bar.a+bar.b", env));
    
        // double op double=double
        System.out.println(this.instance.execute("3.2+3.3") instanceof Double);
        System.out.println(this.instance.execute("3.01+3.1/2.1") instanceof Double);
        System.out.println(this.instance.execute("3.19+3.1/2.9-1.0/(6.0002*7.7+8.9)") instanceof Double);
    
        // double + long=double
         System.out.println(this.instance.execute("3+0.02") instanceof Double);
         System.out.println(this.instance.execute("3+0.02-100") instanceof Double);
         System.out.println(this.instance.execute("3+3/2-1/(6*7+8.0)") instanceof Double);
         System.out.println(this.instance.execute("foo.a+3.2-1000", env) instanceof Double);
    
        // object + string =string
        System.out.println( this.instance.execute("'hello '+ 'world'"));
        System.out.println( this.instance.execute("'hello '+tmap.key", env));
        System.out.println(this.instance.execute("true+' '+tmap.key", env));
        System.out.println( this.instance.execute("foo.a+tmap.key", env));
        System.out.println( this.instance.execute("/\\d+/+'hello'"));
        System.out.println( this.instance.execute("3.2+tmap.key", env));
        System.out.println( this.instance.execute("bool+' is false'", env));
    
    }
    public class Foo {
        int a;
        public Foo() {
        }
        public Foo(final int a) {
          super();
          this.a = a;
        }
        public int getA() {
          return this.a;
        }
        public void setA(final int a) {
          this.a = a;
        }
      }
      public class Bar extends Foo {
        int b;
        public Bar() {
        }
        public Bar(final int a, final int b) {
          super(a);
          this.b = b;
        }
        public int getB() {
          return this.b;
        }
        public void setB(final int b) {
          this.b = b;
        }
    
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
  • 相关阅读:
    抓包day3
    C Primer Plus(6) 中文版 第11章 字符串和字符串函数 11.6 字符串示例:字符串排序
    2023-10-16 itoa函数的局限以及实现
    Leetcode43 字符串相乘以及字符串相加
    半道转嵌入式开发适合吗?(从事十年的我建议不要,你会后悔的)
    新学期|除了认真学习的“flag”,你还立了啥?
    Java SpringBoot实现PDF转图片
    PageOffice 在线编辑 office文件,回调父页面
    C语言实现“队列“
    vue3 + vite + ts + setup , 第九练 自定义指令directive的使用,简单封装一个拖动指令
  • 原文地址:https://blog.csdn.net/weixin_43333483/article/details/126817731