Java中使用代码反射模拟 C# LINQ


本文解释了如何使用代码反射在 Java 中模拟 C# 语言集成查询 ( LINQ ) 的各个方面。通过使用代码反射,可以

  • 在 Java 中模拟 C# 的 LINQ 查询,将 LINQ 查询转换为 SQL 语句,
  • 构建符号表示形式的Java代码模型,以模拟LINQ查询的各个方面,包括Lambda表达式和方法调用的转换。

代码反射是 OpenJDK 项目Babylon下正在研究和开发的 Java 平台功能。

Babylon巴比伦计划
Babylon 的主要目标是将 Java 的应用范围扩展到 SQL、可微分编程、机器学习模型和 GPU 等外部编程模型。 Babylon 将通过增强 Java 反射编程(称为代码反射)来实现这一目标。

代码反射由三部分组成:

  1. 将 Java 程序建模为代码模型,适合访问、分析和转换。
  2. Java 反射的增强功能,允许在编译时和运行时访问代码模型。
  3. 用于构建、分析和转换代码模型的 API。

什么是C# LINQ
语言集成查询(LINQ)是基于将查询功能直接集成到 C# 语言中的一组技术的名称。

传统上,针对数据的查询是以简单字符串的形式表达的,没有编译时的类型检查或 IntelliSense 支持。

此外,您还必须为每种类型的数据源学习不同的查询语言:SQL 数据库、XML 文档、各种 Web 服务等等。

有了 LINQ,查询就像类、方法和事件一样,是一种一流的语言构造。

DB db = ...;

// Query for customers in London.
IQueryable<String> custQuery =
    from cust in db.Customers
    where cust.City ==
"London"
    select cust.ContactName;

查询语法是便于读写 LINQ 查询的语法糖,但实际上只是方法调用的快捷方式。

DB 类包含模拟 SQL 表的封闭类,如 Customers,其属性 City 和 ContactName 模拟表中的行。

我们可以使用方法语法编写相同的 LINQ 查询。

DB db = ...;

// Query for customers in London.
IQueryable<String> custQuery =
    db.Customers
    .Where(cust => cust.City ==
"London")
    .Select(cust => cust.ContactName);


LINQ 查询是 IQuerable 的一个实例,它有一个 C# 属性 Expression,其值是 LINQ 查询的符号表示。在 C# 中,这种符号表示被称为表达式树。在上面的代码中,查询 custQuery 的表达式树由传递给 Where 和 Select 方法的 lambda 表达式树以及这些方法的调用表达式树组成。

LINQ 查询的表达式属性(表达式树)有一个令人愉悦的特性。也就是说,对于数学爱好者来说,假设 Q 是查询,Q.e 是查询的表达式,E 是评估函数(从表达式到查询),那么 E(Q. e) = Q。这种表达式树旨在进行符号化处理和转换。例如,转换到不同的编程领域,如 SQL,其中查询的表达式树被转换为 SQL 语句。

我们将重点讨论 Java 中的这方面问题,即构建一个查询,其等价表达式属性是一个代码模型,即 Java 代码的符号表示,它是通过使用代码反射功能生成的。

在 Java 中模拟类似 LINQ 的查询表达式
为了便于模拟,我们可以用 Java 来表达相同的查询,如下所示。

// record 建模的表有三列,每一列代表一个组件
record Customer(String contactName, String phone, String city) {
}

QueryProvider qp = new linq.TestQueryProvider();

// 查找伦敦的所有客户,并返回他们的姓名
QueryResult<Stream<String>> results = qp.newQuery(Customer.class)
        .where(c -> c.city.equals(
"London"))
        .select(c -> c.contactName)
        .elements();


我们使用客户记录来模拟 SQL 表,其中记录的组件对应于该表中的行。我们可以看到,Java 代码与使用方法语法的 C# LINQ 查询非常相似。

在下面的章节中,我们将解释如何使用代码反射来实现这一点。

在 Babylon 代码库中有一个概念验证实现的测试。该实现远未完成。

让我们将流畅查询扩展为单个语句,以便查看其类型。

Queryable<Customer> allCustomers = qp.query(Customer.class);
Queryable<Customer> londonCustomers = allCustomers.where(c -> c.city.equals("London"));
Queryable<String> londonCustomerNames = londonCustomers.select(c -> c.contactName);
QueryResult<Stream<String>> results = londonCustomerNames.elements();

前三个方法调用产生 Queryable 实例,最后一个方法调用产生 QueryResult 实例。每个实例都有一个查询的符号表示,即代码模型。我们稍后会看到。

  • 首先,我们创建一个新查询,它返回一个 Queryable<Customer> 的实例。
  • 在此基础上,我们通过调用 where 来 "过滤 "位于伦敦市的客户,并接受一个 lambda 表达式,如果客户的城市组件等于字符串 "London",则返回 true。
  • 然后,我们通过调用 select 将客户 "映射 "到他们的联系人姓名,接受一个 lambda 表达式,返回客户的 contactName 组件。
  • 最后,我们调用元素生成查询结果,并告知我们如何使用查询结果,在本例中,查询结果是客户联系人姓名流。

where 和 select 方法的特征如下。

default Queryable<T> where(QuotablePredicate<T> f) { /* ... */ }

default <R> Queryable<R> select(QuotableFunction<T, R> f) {
/* ... */ }

QuotablePredicate 和 QuotableFunction 是由 Predicate 和 Function 以及代码反射接口 java.lang.reflect.code.Quotable 扩展而来的函数式接口。

下面是 Quotable 的声明。

@FunctionalInterface
public interface QuotablePredicate<T> extends Quotable, Predicate<T> {
}

当 lambda 表达式针对一个可引用的功能接口时,源代码编译器将为该 lambda 表达式建立代码模型,并在运行时通过可引用实例进行访问。与我们获取可序列化的 lambda 表达式(使用 Serializable)的方式类似,我们也可以获取其代码模型可以被获取的 quotable lambda 表达式。