Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README-EN-source.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,24 @@ The following example code adds a `hello()` extension function to the String cla
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=extensionFunction]
----

=== Extend Field Handler

By default, `obj.field` can only read fields or getters of standard Java Beans. For non-standard containers such as Flink Row, JDBC ResultSet or user-defined MapLike/CollectionLike structures, field access is closed, and users usually have to convert and copy the data before they can access it in scripts.

With `addExtendFieldHandler(Class, ExtendFieldHandler)` you can register a custom field-access handler for a given type, so that these non-standard containers can also be accessed with the regular `obj.field` syntax:

[source,java,indent=0]
----
include::./src/test/java/com/alibaba/qlexpress4/ExtendFieldHandlerTest.java[tag=extendFieldHandler]
----

Once a bean is an instance of the binding class (`bindingClass`), the handler becomes the *authoritative* source for that type's fields: whatever it returns is taken as the field value, and returning `null` means the field value itself is `null` rather than falling back to the default Java reflection logic. The default logic only applies when no handler's binding class matches the current bean. Therefore the two cases of `obj.field` are distinguished as follows:

* the bean type is not bound to any handler → the default reflection logic applies;
* the bean type is bound but the field value is `null` → `null` is returned.

If a container should signal "field does not exist" rather than return `null`, throw an exception from within the handler.

=== Java Class Object, Field, and Method Aliases

QLExpress supports defining one or more aliases for objects, fields, or methods through the `QLAlias` annotation, making it convenient for non-technical personnel to use expressions to define rules.
Expand Down
31 changes: 30 additions & 1 deletion README-EN.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -788,9 +788,38 @@ The following example code adds a `hello()` extension function to the String cla
params -> ((Number)params[0]).intValue() + ((Number)params[1]).intValue());
QLResult resultAdd = express4Runner.execute("1.add(2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS);
assertEquals(3, resultAdd.getResult());

----

=== Extend Field Handler

By default, `obj.field` can only read fields or getters of standard Java Beans. For non-standard containers such as Flink Row, JDBC ResultSet or user-defined MapLike/CollectionLike structures, field access is closed, and users usually have to convert and copy the data before they can access it in scripts.

With `addExtendFieldHandler(Class, ExtendFieldHandler)` you can register a custom field-access handler for a given type, so that these non-standard containers can also be accessed with the regular `obj.field` syntax:

[source,java,indent=0]
----
Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);

// RowLike is a non-standard container whose fields can only be read via getValue(name);
// register a handler so it can be accessed with the regular obj.field syntax in scripts.
runner.addExtendFieldHandler(RowLike.class, (bean, fieldName) -> ((RowLike) bean).getValue(fieldName));

RowLike row = new RowLike(new String[] { "name", "age" }, new Object[] { "张三", 30 });
Map<String, Object> context = new HashMap<>();
context.put("row", row);

Object name = runner.execute("row.name", context, QLOptions.DEFAULT_OPTIONS).getResult();
Assert.assertEquals("张三", name);
----

Once a bean is an instance of the binding class (`bindingClass`), the handler becomes the *authoritative* source for that type's fields: whatever it returns is taken as the field value, and returning `null` means the field value itself is `null` rather than falling back to the default Java reflection logic. The default logic only applies when no handler's binding class matches the current bean. Therefore the two cases of `obj.field` are distinguished as follows:

* the bean type is not bound to any handler → the default reflection logic applies;
* the bean type is bound but the field value is `null` → `null` is returned.

If a container should signal "field does not exist" rather than return `null`, throw an exception from within the handler.

=== Java Class Object, Field, and Method Aliases

QLExpress supports defining one or more aliases for objects, fields, or methods through the `QLAlias` annotation, making it convenient for non-technical personnel to use expressions to define rules.
Expand Down
18 changes: 18 additions & 0 deletions README-source.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,24 @@ include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=scri
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=extensionFunction]
----

=== 扩展字段取值

默认情况下,`obj.field` 只能读取标准 Java Bean 的字段或 getter。对于 Flink Row、JDBC ResultSet 或者自定义的 MapLike/CollectionLike 等非标准容器,字段取值逻辑是封闭的,用户往往需要先做一次转换拷贝才能在脚本中访问。

通过 `addExtendFieldHandler(Class, ExtendFieldHandler)` 可以给某个类型注册一个自定义的字段取值处理器,让这些非标准容器也能直接用 `obj.field` 语法访问:

[source,java,indent=0]
----
include::./src/test/java/com/alibaba/qlexpress4/ExtendFieldHandlerTest.java[tag=extendFieldHandler]
----

一旦 bean 是绑定类(`bindingClass`)的实例,该处理器就是这个类型字段取值的**权威**来源:它返回什么就是什么,返回 `null` 表示字段的值就是 `null`,而不会回退到默认的 Java 反射逻辑。只有当没有任何处理器的绑定类匹配当前 bean 时,才走默认取值逻辑。因此 `obj.field` 的两种情况可以这样区分:

* bean 的类型没有绑定任何处理器 → 走默认反射逻辑;
* bean 的类型绑定了处理器但字段值为 `null` → 返回 `null`。

如果某个容器希望对「字段不存在」报错而不是返回 `null`,可以在处理器内部主动抛出异常。

=== Java类的对象,字段和方法别名

QLExpress 支持通过 `QLAlias` 注解给对象,字段或者方法定义一个或多个别名,方便非技术人员使用表达式定义规则。
Expand Down
31 changes: 30 additions & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -789,9 +789,38 @@ QLExpress 使用 ANTLR4 作为解析引擎,ANTLR4 在运行时会构建 DFA (
params -> ((Number)params[0]).intValue() + ((Number)params[1]).intValue());
QLResult resultAdd = express4Runner.execute("1.add(2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS);
assertEquals(3, resultAdd.getResult());

----

=== 扩展字段取值

默认情况下,`obj.field` 只能读取标准 Java Bean 的字段或 getter。对于 Flink Row、JDBC ResultSet 或者自定义的 MapLike/CollectionLike 等非标准容器,字段取值逻辑是封闭的,用户往往需要先做一次转换拷贝才能在脚本中访问。

通过 `addExtendFieldHandler(Class, ExtendFieldHandler)` 可以给某个类型注册一个自定义的字段取值处理器,让这些非标准容器也能直接用 `obj.field` 语法访问:

[source,java,indent=0]
----
Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);

// RowLike is a non-standard container whose fields can only be read via getValue(name);
// register a handler so it can be accessed with the regular obj.field syntax in scripts.
runner.addExtendFieldHandler(RowLike.class, (bean, fieldName) -> ((RowLike) bean).getValue(fieldName));

RowLike row = new RowLike(new String[] { "name", "age" }, new Object[] { "张三", 30 });
Map<String, Object> context = new HashMap<>();
context.put("row", row);

Object name = runner.execute("row.name", context, QLOptions.DEFAULT_OPTIONS).getResult();
Assert.assertEquals("张三", name);
----

一旦 bean 是绑定类(`bindingClass`)的实例,该处理器就是这个类型字段取值的**权威**来源:它返回什么就是什么,返回 `null` 表示字段的值就是 `null`,而不会回退到默认的 Java 反射逻辑。只有当没有任何处理器的绑定类匹配当前 bean 时,才走默认取值逻辑。因此 `obj.field` 的两种情况可以这样区分:

* bean 的类型没有绑定任何处理器 → 走默认反射逻辑;
* bean 的类型绑定了处理器但字段值为 `null` → 返回 `null`。

如果某个容器希望对「字段不存在」报错而不是返回 `null`,可以在处理器内部主动抛出异常。

=== Java类的对象,字段和方法别名

QLExpress 支持通过 `QLAlias` 注解给对象,字段或者方法定义一个或多个别名,方便非技术人员使用表达式定义规则。
Expand Down
19 changes: 18 additions & 1 deletion src/main/java/com/alibaba/qlexpress4/Express4Runner.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import com.alibaba.qlexpress4.runtime.context.ObjectFieldExpressContext;
import com.alibaba.qlexpress4.runtime.context.QLAliasContext;
import com.alibaba.qlexpress4.runtime.function.CustomFunction;
import com.alibaba.qlexpress4.runtime.function.ExtendFieldHandler;
import com.alibaba.qlexpress4.runtime.function.ExtensionFunction;
import com.alibaba.qlexpress4.runtime.function.QMethodFunction;
import com.alibaba.qlexpress4.runtime.instruction.QLInstruction;
Expand Down Expand Up @@ -516,7 +517,23 @@ public boolean addCompileTimeFunction(String name, CompileTimeFunction compileTi
public void addExtendFunction(ExtensionFunction extensionFunction) {
this.reflectLoader.addExtendFunction(extensionFunction);
}


/**
* Register a custom field-access handler bound to {@code bindingClass}, used to access
* fields of non-standard containers (e.g. Flink Row, JDBC ResultSet) with the regular
* {@code obj.fieldName} syntax. Delegates to {@link ReflectLoader#addExtendFieldHandler}.
* <p>
* Once a bean is assignable to {@code bindingClass} the handler is authoritative for its
* fields: its return value (including {@code null}) is taken as the field value rather than
* falling back to Java reflection.
*
* @param bindingClass the receiver type the handler is bound to
* @param fieldHandler the field-access handler
*/
public void addExtendFieldHandler(Class<?> bindingClass, ExtendFieldHandler fieldHandler) {
this.reflectLoader.addExtendFieldHandler(bindingClass, fieldHandler);
}

/**
* add an extension function with variable arguments.
* @param name the name of the extension function
Expand Down
80 changes: 77 additions & 3 deletions src/main/java/com/alibaba/qlexpress4/runtime/ReflectLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.alibaba.qlexpress4.runtime.data.DataValue;
import com.alibaba.qlexpress4.runtime.data.FieldValue;
import com.alibaba.qlexpress4.runtime.data.MapItemValue;
import com.alibaba.qlexpress4.runtime.function.ExtendFieldHandler;
import com.alibaba.qlexpress4.runtime.function.ExtensionFunction;
import com.alibaba.qlexpress4.runtime.function.FilterExtensionFunction;
import com.alibaba.qlexpress4.runtime.function.MapExtensionFunction;
Expand Down Expand Up @@ -48,7 +49,15 @@ public class ReflectLoader {
*/
private final List<ExtensionFunction> extensionFunctions =
new CopyOnWriteArrayList<>(Arrays.asList(FilterExtensionFunction.INSTANCE, MapExtensionFunction.INSTANCE));


/**
* Custom field-access handlers registered by the user. Each entry binds a handler to a
* receiver type (e.g. Flink Row, JDBC ResultSet or other non-standard containers).
* The list is iterated in insertion order; the first handler whose binding class is
* assignable from the bean's class is considered authoritative.
*/
private final List<ExtendFieldHandlerHolder> fieldHandlers = new CopyOnWriteArrayList<>();

public ReflectLoader(QLSecurityStrategy securityStrategy, boolean allowPrivateAccess) {
this.securityStrategy = securityStrategy;
this.allowPrivateAccess = allowPrivateAccess;
Expand All @@ -57,7 +66,24 @@ public ReflectLoader(QLSecurityStrategy securityStrategy, boolean allowPrivateAc
public void addExtendFunction(ExtensionFunction extensionFunction) {
extensionFunctions.add(extensionFunction);
}


/**
* Register a custom field-access handler bound to {@code bindingClass}, used to access
* fields of non-standard containers (e.g. Flink Row, JDBC ResultSet or user-defined
* MapLike/CollectionLike) during the field-access stage of a QL expression.
* <p>
* Once the bean is assignable to {@code bindingClass}, the handler becomes the authoritative
* source for that bean's fields: whatever it returns (including {@code null}) is taken as the
* field value. Handlers are consulted in registration order, so an earlier registration for an
* assignable type wins.
*
* @param bindingClass the receiver type the handler is bound to
* @param fieldHandler the field-access handler
*/
public void addExtendFieldHandler(Class<?> bindingClass, ExtendFieldHandler fieldHandler) {
fieldHandlers.add(new ExtendFieldHandlerHolder(bindingClass, fieldHandler));
}

public Constructor<?> loadConstructor(Class<?> cls, Class<?>[] paramTypes) {
if (securityStrategy instanceof StrategyIsolation) {
return null;
Expand All @@ -81,6 +107,12 @@ public Constructor<?> loadConstructor(Class<?> cls, Class<?>[] paramTypes) {
}

public Value loadField(Object bean, String fieldName, boolean skipSecurity, ErrorReporter errorReporter) {
// first try the user-registered custom field handlers (e.g. Flink Row, JDBC ResultSet)
Value extended = loadExtendField(bean, fieldName);
if (extended != null) {
return extended;
}

if (bean.getClass().isArray() && BasicUtil.LENGTH.equals(fieldName)) {
return new DataValue(((Object[])bean).length);
}
Expand All @@ -104,7 +136,27 @@ else if (bean instanceof MetaClass) {
return loadJavaField(bean.getClass(), bean, fieldName, skipSecurity, errorReporter);
}
}


/**
* Dispatch the field access to the first user-registered handler whose binding class is
* assignable from the bean's class. Such a handler is authoritative for the bean type, so its
* result is wrapped and returned even when it is {@code null} (meaning the field value itself
* is {@code null}). Returns {@code null} only when no handler's binding class matches, so that
* the caller falls back to the default field-access logic.
*/
private Value loadExtendField(Object bean, String fieldName) {
if (fieldHandlers.isEmpty()) {
return null;
}
Class<?> beanClass = bean.getClass();
for (ExtendFieldHandlerHolder holder : fieldHandlers) {
if (holder.getBindingClass().isAssignableFrom(beanClass)) {
return new DataValue(holder.getHandler().getField(bean, fieldName));
}
}
return null;
}

public IMethod loadMethod(Object bean, String methodName, Class<?>[] argTypes) {
boolean isStaticMethod = bean instanceof MetaClass;
Class<?> clz = isStaticMethod ? ((MetaClass)bean).getClz() : bean.getClass();
Expand Down Expand Up @@ -367,6 +419,28 @@ else if (ex instanceof InvocationTargetException) {
}
}

/**
* Binds an {@link ExtendFieldHandler} to the receiver type it handles.
*/
private static class ExtendFieldHandlerHolder {
private final Class<?> bindingClass;

private final ExtendFieldHandler handler;

private ExtendFieldHandlerHolder(Class<?> bindingClass, ExtendFieldHandler handler) {
this.bindingClass = bindingClass;
this.handler = handler;
}

public Class<?> getBindingClass() {
return bindingClass;
}

public ExtendFieldHandler getHandler() {
return handler;
}
}

private static class FieldReflectCache {
private final BiFunction<ErrorReporter, Object, Supplier<Object>> getterSupplier;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.alibaba.qlexpress4.runtime.function;

/**
* Custom field-access handler bound to a specific receiver type.
* <p>
* It extends the behaviour of {@link com.alibaba.qlexpress4.runtime.ReflectLoader#loadField}
* so that non-standard containers (such as Flink Row, JDBC ResultSet or user-defined
* MapLike/CollectionLike structures) can be accessed with the regular {@code obj.fieldName}
* syntax in QL expressions.
* <p>
* A handler is registered against a binding class via
* {@link com.alibaba.qlexpress4.Express4Runner#addExtendFieldHandler(Class, ExtendFieldHandler)}
* and is only invoked when the bean is assignable to that binding class. Binding to a class
* keeps each registration isolated and frees the caller from dealing with low-level runtime
* structures: just return the raw field value.
* <p>
* Once the bean matches the binding class the handler is <em>authoritative</em> for that bean's
* fields: whatever it returns is taken as the field value, so returning {@code null} means the
* field value itself is {@code null} (it does <b>not</b> fall back to Java reflection). This is
* how the two cases below are distinguished:
* <ul>
* <li>the bean type is not bound to any handler &rarr; the default reflection logic applies;</li>
* <li>the bean type is bound but the field value is {@code null} &rarr; {@code null} is returned.</li>
* </ul>
* If a bound container should signal "field does not exist" rather than yield {@code null}, throw
* an exception from the handler.
*
* <p>Example —— supporting Flink Row:
* <pre>{@code
* runner.addExtendFieldHandler(org.apache.flink.types.Row.class,
* (bean, fieldName) -> ((Row) bean).getField(fieldName));
* }</pre>
*
* @author ayasaz
* @since QLExpress4
*/
@FunctionalInterface
public interface ExtendFieldHandler {

/**
* Resolve the value of {@code fieldName} from the given bean.
*
* @param bean the receiver object, guaranteed to be assignable to the binding class
* @param fieldName the field name being accessed
* @return the raw field value; {@code null} means the field value itself is {@code null}
*/
Object getField(Object bean, String fieldName);
}
Loading