Java使用Aviator表达式学习记录(十四)自定义函

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

自定义函数和调用 Java 方法

如果你想在 AviatorScript 中调用  Java 方法,除了内置的函数库之外,你还可以通过下列方式来实现:

  1. 自定义函数
  2. 自动导入 java 类方法
  1. FunctionMissing 机制

我们将一一介绍。

自定义函数

可以通过 java 代码实现并往引擎中注入自定义函数,在 AviatorScript 中就可以使用,事实上所有的内置函数也是通过同样的方式实现的:

public class TestAviator {
    public static void main(String[] args) {
            //注册函数
            AviatorEvaluator.addFunction(new AddFunction());
            System.out.println(AviatorEvaluator.execute("add(1, 2)"));           // 3.0
            System.out.println(AviatorEvaluator.execute("add(add(1, 2), 100)")); // 103.0
        }
    }
    class AddFunction 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 AviatorDouble(left.doubleValue() + right.doubleValue());
        }
        public String getName() {
            return "add";
        }
    }
复制代码

所有的函数都实现了 AviatorFunction 接口:

public interface AviatorFunction {
  /**
   * Get the function name
   *
   * @return
   */
  public String getName();

  /**
   * call function
   *
   * @param env Variable environment
   * @return
   */
  public AviatorObject call(Map<String, Object> env);


  public AviatorObject call(Map<String, Object> env, AviatorObject arg1);
  
  public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2);

  ......
 }
复制代码

它有一系列 call 方法,根据参数个数不同而重载,其中 getName() 返回方法名。一般来说都推荐继承 com.googlecode.aviator.runtime.function.AbstractFunction  ,并覆写对应参数个数的方法即可,例如上面例子中定义了 add 方法,它接受两个参数

 public AviatorObject call(Map<String, Object> env, 
                                  AviatorObject arg1, AviatorObject arg2)
复制代码

第一个参数是当前执行的上下文,arg1 和 arg2 分别表示从左到右的两个参数,最终结果是另一个 AviatorObject ,这个实现中是将两个参数相加,返回浮点结果 AviatorDouble 。

注意,哪怕结果为 null,也必须返回 AviatorNil.NIL 表示结果为 null,而不是直接返回 java 的 null。

我们再看一个内置的函数例子,比如 map(seq, fn) 函数,它将 fn 这个函数作用在集合 seq 里的每个元素上,并返回结果组成的新集合, 比如我们可以将数组的元素都递增 1:

## examples/map.fn

let a = tuple(1, 2, 3);

map(a, println);

println("after map:");

a = map(a, lambda(x) -> x + 1 end);

map(a, println);
复制代码

我们同时用 map 来遍历数组 a 并打印每个元素:

1
2
3
after map:
2
3
4
复制代码

我们看下 map 的函数实现 SeqMapFunction.java

public class SeqMapFunction extends AbstractFunction {

  @Override
  @SuppressWarnings({"unchecked", "rawtypes"})
  public AviatorObject call(final Map<String, Object> env, final AviatorObject arg1,
      final AviatorObject arg2) {

    Object first = arg1.getValue(env);
    AviatorFunction fun = FunctionUtils.getFunction(arg2, env, 1);
    if (fun == null) {
      throw new FunctionNotFoundException(
          "There is no function named " + ((AviatorJavaType) arg2).getName());
    }
    if (first == null) {
      throw new NullPointerException("null seq");
    }
    Sequence seq = RuntimeUtils.seq(first, env);
    Collector collector = seq.newCollector(seq.hintSize());
    for (Object obj : seq) {
      collector.add(fun.call(env, AviatorRuntimeJavaType.valueOf(obj)).getValue(env));
    }
    return AviatorRuntimeJavaType.valueOf(collector.getRawContainer());
  }


  @Override
  public String getName() {
    return "map";
  }

}
复制代码

我们将第一个参数通过 RuntimeUtils.seq(first, env) 转成了 sequence 抽象(见 10.2 节),然后遍历这个集合,并对每个元素调用第二个参数传入的函数 fun ,将结果添加到 collector ,最终返回 collector 集合。

自定义可变参数函数

要实现可变参数的函数,如果是直接继承 AbstractFunction ,要实现一系列的 call 方法,未免太繁琐了,因此 AviatorScript 还提供了 AbstractVariadicFunction ,可以更方便地实现可变参数函数,比如内置的 tuple(x, y, z,...) 创建 Object 数组的函数就是基于它实现:

import java.util.Map;
import com.googlecode.aviator.runtime.function.AbstractVariadicFunction;
import com.googlecode.aviator.runtime.type.AviatorObject;
import com.googlecode.aviator.runtime.type.AviatorRuntimeJavaType;

/**
 * tuple(x,y,z, ...) function to return an object array.
 *
 * @author dennis
 * @since 4.0.0
 *
 */
public class TupleFunction extends AbstractVariadicFunction {


  private static final long serialVersionUID = -7377110880312266008L;

  @Override
  public String getName() {
    return "tuple";
  }

  @Override
  public AviatorObject variadicCall(Map<String, Object> env, AviatorObject... args) {
    Object[] tuple = new Object[args.length];
    for (int i = 0; i < args.length; i++) {
      tuple[i] = args[i].getValue(env);
    }
    return AviatorRuntimeJavaType.valueOf(tuple);
  }

}
复制代码

核心就是实现 variadicCall(Map<String, Object> env, AviatorObject... args) 方法,其中的 args 就是可变的参数列表。对于 tuple 函数就可以传入任意个数的函数:

tuple();  ##空的 object 数组
tuple(1);
tuple(1, 2, "hello");
复制代码

lambda 自定义函数

除了用 java 代码实现自定义函数之外,你也可以使用 lambda 来定义,比如上面 add 的例子可以修改为:

AviatorEvaluator.defineFunction("add", "lambda (x,y) -> x + y end");
AviatorEvaluator.execute("add(1,2)"); // 结果为 3
复制代码

当然,这个例子跟上面的 add 实现还是稍有区别的,这里并没有限定 x, y 必须为数字,并且返回类型也没有限定为 double。因此它也可以将字符串相加。

调用 Java 静态方法

从 5.2.1 版本开始,支持直接用 Class.Method(..args) 的语法直接调用 Java 类的静态方法(仅限 public static 方法),基于反射实现(尽量做了缓存等优化),例如:

## examples/static_methods.av

use java.util.regex.Pattern;

let p = Pattern.compile("\d+");

if "123" =~ p {
  p("matched");
  p($0);
}

if "a123" =~ p {
  p("matched");
  p($0);
}
复制代码

我们通过 use 导入了 Pattern 类(在 aviator 中内置了正则类型,这里仅是为了举例),然后调用 Pattern.compile(String) 方法编译了一个正则表达式,接下里使用匹配运算符 =~ 来测试字符串是否跟正则匹配,如果匹配,就打印整个匹配的结果。

导入 java 方法

AviatorScript 还支持快捷地导入 Java 类方法,包括静态方法和实例方法,下面我们来详细介绍下。

导入静态方法

假设你有一个工具类 StringUtils ,里面有一系列 public 的静态方法,如 StringUtils.isBlank 等,那么通过:

AviatorEvaluator.addStaticFunctions("str", StringUtils.class);
复制代码

的方式,就可以将这个类所有公开的静态方法批量导入到 str 这个 namespace 下,接下来就可以直接调用这些方法:

str.isBlank('')
复制代码

导入实例方法

AviatorScript 同样支持将 java 某个类的实例方法导入 aviator 求值器作为自定义函数。但是跟通常的 java 方法调用方式 instance.method(args) 的方式不一样的是, aviator 要求将 instance 这个 this 指针作为第一个参数明确传入,转成 method(instance, args) 的调用方式。

例如 String 类有很多方法,我们可以批量导入:

AviatorEvaluator.addInstanceFunctions("s", String.class);
复制代码

通过 addInstanceFunctions(namespace, clazz) 方法导入后,你就可以对所有字符串使用 String 的方法,只是字符串实例要求作为第一个参数明确传入,比如调用 String#indexOf 方法:

s.indexOf("hello", "l")
复制代码

输出结果 2

其他调用方法类似,也就是 instance.method(args) 调用需要转成 namespace.method(instance, args) 的方式

调用可变参数方法

对于 java 的可变参数方法,本质上转成一个数组来调用,例如下面这个可变参数的 join 方法:

  public static String join(final String... args) {
    if (args == null || args.length == 0) {
      return "";
    }
    StringBuilder sb = new StringBuilder();
    boolean wasFirst = true;
    for (int i = 0; i < args.length; i++) {
      if (wasFirst) {
        sb.append(args[i]);
        wasFirst = false;
      } else {
        sb.append(",").append(args[i]);
      }
    }
    return sb.toString();
  }
复制代码

在使用上面方式导入后,在表达式里必须先用 seq.array 创建数组来调用:

test.join(seq.array(java.lang.String, 'hello','dennis'))
复制代码

返回 hello,world 字符串。 seq.array 的第一个参数是数组里的元素类型 class 名称,如果是基本类型,可以是 int, float, double 等等。

批量导入方法和注解支持

如果要同时导入静态方法和实例方法,可以使用 importFunctions 方法:

AviatorEvaluator.importFunctions(StringUtils.class);
复制代码

默认的 namespace 是类名 StringUtils,因此就可以在表达式里这样用 StringUtils.isBlank('hello world')

如果想要更多定制化的东西,可以使用注解 annotation。

例如想要定制导入的 namespace 和范围,可以对 java 类使用 Import 标注:

@Import(ns = "test", scopes = {ImportScope.Static})
public class StringUtils {
  ...
 
}
复制代码
  • ns 指定导入后的 namespace,
  • scopes 指定导入的方法范围。

如果想忽略某个方法,可以对方法用 Ignore 标注:

@Import(ns = "test", scopes = {ImportScope.Static})
public class StringUtils {
  ...

  @Ignore
  public static double test(final double a, final double b) {
    return a + b;
  }
}
复制代码

同时可以用 Function 标注导入的函数名字,默认都是原来的方法名:

@Import(ns = "test", scopes = {ImportScope.Static})
public class StringUtils {
  ...

  @Function(rename = "is_empty")
  public boolean isEmpty(final String s) {
    return s.isEmpty();
  }
}
复制代码

后面将介绍的 Io 模块就是通过导入的方式实现,参见 IoModule.java