这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战
自定义函数和调用 Java 方法
如果你想在 AviatorScript 中调用 Java 方法,除了内置的函数库之外,你还可以通过下列方式来实现:
- 自定义函数
- 自动导入 java 类方法
- 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