1. 概述

这是 系列教程 的第五篇文章,我们将借助一个非常实用的开源库 —— **rsql-parser**,来实现一个功能完整的 REST API 查询语言。

RSQL(RESTful Service Query Language)是 FIQL(Feed Item Query Language)的超集,语法简洁清晰,特别适合用于 REST 接口的过滤查询场景。✅ 它天生就和 REST 风格 API 很搭,用起来顺手,表达能力强。

相比自己造轮子设计查询语法,使用 RSQL 能避免很多“踩坑”问题,比如参数解析歧义、嵌套逻辑混乱等。


2. 准备工作

首先,在 pom.xml 中引入 rsql-parser 的 Maven 依赖:

<dependency>
    <groupId>cz.jirutka.rsql</groupId>
    <artifactId>rsql-parser</artifactId>
    <version>2.1.0</version>
</dependency>

接着定义本文示例中使用的实体类 —— User

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    private String firstName;
    private String lastName;
    private String email;
 
    private int age;
}

这个类很标准,后续所有查询都将基于它展开。


3. 解析请求

RSQL 的核心机制是将查询字符串解析成抽象语法树(AST),然后通过 访问者模式(Visitor Pattern) 遍历节点,生成对应的 JPA Specification

我们需要实现 RSQLVisitor 接口,创建自定义访问器 CustomRsqlVisitor

public class CustomRsqlVisitor<T> implements RSQLVisitor<Specification<T>, Void> {

    private GenericRsqlSpecBuilder<T> builder;

    public CustomRsqlVisitor() {
        builder = new GenericRsqlSpecBuilder<T>();
    }

    @Override
    public Specification<T> visit(AndNode node, Void param) {
        return builder.createSpecification(node);
    }

    @Override
    public Specification<T> visit(OrNode node, Void param) {
        return builder.createSpecification(node);
    }

    @Override
    public Specification<T> visit(ComparisonNode node, Void params) {
        return builder.createSpecification(node);
    }
}

接下来是构建 Specification 的核心类 —— GenericRsqlSpecBuilder

public class GenericRsqlSpecBuilder<T> {

    public Specification<T> createSpecification(Node node) {
        if (node instanceof LogicalNode) {
            return createSpecification((LogicalNode) node);
        }
        if (node instanceof ComparisonNode) {
            return createSpecification((ComparisonNode) node);
        }
        return null;
    }

    public Specification<T> createSpecification(LogicalNode logicalNode) {        
        List<Specification> specs = logicalNode.getChildren()
          .stream()
          .map(node -> createSpecification(node))
          .filter(Objects::nonNull)
          .collect(Collectors.toList());

        Specification<T> result = specs.get(0);
        if (logicalNode.getOperator() == LogicalOperator.AND) {
            for (int i = 1; i < specs.size(); i++) {
                result = Specification.where(result).and(specs.get(i));
            }
        } else if (logicalNode.getOperator() == LogicalOperator.OR) {
            for (int i = 1; i < specs.size(); i++) {
                result = Specification.where(result).or(specs.get(i));
            }
        }

        return result;
    }

    public Specification<T> createSpecification(ComparisonNode comparisonNode) {
        Specification<T> result = Specification.where(
          new GenericRsqlSpecification<T>(
            comparisonNode.getSelector(), 
            comparisonNode.getOperator(), 
            comparisonNode.getArguments()
          )
        );
        return result;
    }
}

关键点说明:

  • LogicalNode:表示逻辑操作(AND / OR),可包含多个子节点
  • ComparisonNode:表示单个比较条件,包含三个要素:
    • Selector:字段名(如 firstName
    • Operator:操作符(如 ==, =gt=
    • Arguments:参数值(如 ["john"]

例如,查询 firstName==john 的结构为:

组成部分
Selector firstName
Operator ==
Arguments ["john"]

4. 创建自定义 Specification

为了将 RSQL 条件映射到 JPA 查询,我们实现通用的 GenericRsqlSpecification<T> 类:

public class GenericRsqlSpecification<T> implements Specification<T> {

    private String property;
    private ComparisonOperator operator;
    private List<String> arguments;

    @Override
    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        List<Object> args = castArguments(root);
        Object argument = args.get(0);
        switch (RsqlSearchOperation.getSimpleOperator(operator)) {

        case EQUAL: {
            if (argument instanceof String) {
                return builder.like(root.get(property), argument.toString().replace('*', '%'));
            } else if (argument == null) {
                return builder.isNull(root.get(property));
            } else {
                return builder.equal(root.get(property), argument);
            }
        }
        case NOT_EQUAL: {
            if (argument instanceof String) {
                return builder.notLike(root.<String> get(property), argument.toString().replace('*', '%'));
            } else if (argument == null) {
                return builder.isNotNull(root.get(property));
            } else {
                return builder.notEqual(root.get(property), argument);
            }
        }
        case GREATER_THAN: {
            return builder.greaterThan(root.<String> get(property), argument.toString());
        }
        case GREATER_THAN_OR_EQUAL: {
            return builder.greaterThanOrEqualTo(root.<String> get(property), argument.toString());
        }
        case LESS_THAN: {
            return builder.lessThan(root.<String> get(property), argument.toString());
        }
        case LESS_THAN_OR_EQUAL: {
            return builder.lessThanOrEqualTo(root.<String> get(property), argument.toString());
        }
        case IN:
            return root.get(property).in(args);
        case NOT_IN:
            return builder.not(root.get(property).in(args));
        }

        return null;
    }

    private List<Object> castArguments(final Root<T> root) {
        
        Class<? extends Object> type = root.get(property).getJavaType();
        
        List<Object> args = arguments.stream().map(arg -> {
            if (type.equals(Integer.class)) {
               return Integer.parseInt(arg);
            } else if (type.equals(Long.class)) {
               return Long.parseLong(arg);
            } else {
                return arg;
            }            
        }).collect(Collectors.toList());

        return args;
    }

    // 构造函数、getter、setter 省略
}

⚠️ 注意:

  • 使用泛型,不绑定具体实体,复用性强
  • 支持类型自动转换(String → Integer/Long)
  • * 通配符被转换为 SQL 的 %,实现模糊匹配

再来看 RsqlSearchOperation 枚举,用于映射 RSQL 操作符:

public enum RsqlSearchOperation {
    EQUAL(RSQLOperators.EQUAL), 
    NOT_EQUAL(RSQLOperators.NOT_EQUAL), 
    GREATER_THAN(RSQLOperators.GREATER_THAN), 
    GREATER_THAN_OR_EQUAL(RSQLOperators.GREATER_THAN_OR_EQUAL), 
    LESS_THAN(RSQLOperators.LESS_THAN), 
    LESS_THAN_OR_EQUAL(RSQLOperators.LESS_THAN_OR_EQUAL), 
    IN(RSQLOperators.IN), 
    NOT_IN(RSQLOperators.NOT_IN);

    private ComparisonOperator operator;

    private RsqlSearchOperation(ComparisonOperator operator) {
        this.operator = operator;
    }

    public static RsqlSearchOperation getSimpleOperator(ComparisonOperator operator) {
        for (RsqlSearchOperation operation : values()) {
            if (operation.getOperator() == operator) {
                return operation;
            }
        }
        return null;
    }
}

这个枚举起到了“翻译表”的作用,把 rsql-parser 的原生操作符转为我们熟悉的语义。


5. 测试查询功能

编写单元测试验证查询逻辑是否正确。先准备测试数据:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { PersistenceConfig.class })
@Transactional
@TransactionConfiguration
public class RsqlTest {

    @Autowired
    private UserRepository repository;

    private User userJohn;
    private User userTom;

    @Before
    public void init() {
        userJohn = new User();
        userJohn.setFirstName("john");
        userJohn.setLastName("doe");
        userJohn.setEmail("john.doe@example.com");
        userJohn.setAge(22);
        repository.save(userJohn);

        userTom = new User();
        userTom.setFirstName("tom");
        userTom.setLastName("doe");
        userTom.setEmail("tom.doe@example.com");
        userTom.setAge(26);
        repository.save(userTom);
    }
}

5.1 测试等于条件

查找 firstName == johnlastName == doe 的用户:

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("firstName==john;lastName==doe");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);

    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));
}

✅ 使用 ; 表示 AND 逻辑。


5.2 测试否定条件

查找 firstName != john 的用户:

@Test
public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("firstName!=john");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);

    assertThat(userTom, isIn(results));
    assertThat(userJohn, not(isIn(results)));
}

5.3 测试大于条件

查找 age > 25 的用户:

@Test
public void givenMinAge_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("age>25");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);

    assertThat(userTom, isIn(results));
    assertThat(userJohn, not(isIn(results)));
}

5.4 测试模糊匹配

查找 firstNamejo 开头的用户:

@Test
public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("firstName==jo*");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);

    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));
}

* 是通配符,会被转成 % 实现 LIKE 查询。


5.5 测试 IN 查询

查找 firstNamejohnjack 的用户:

@Test
public void givenListOfFirstName_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("firstName=in=(john,jack)");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);

    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));
}

in= 操作符非常实用,避免前端拼多个 or 条件。


6. UserController 接口实现

最后,把整个链路串起来,暴露一个支持 RSQL 查询的接口:

@RequestMapping(method = RequestMethod.GET, value = "/users")
@ResponseBody
public List<User> findAllByRsql(@RequestParam(value = "search") String search) {
    Node rootNode = new RSQLParser().parse(search);
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    return repository.findAll(spec);
}

📌 示例请求 URL:

http://localhost:8082/spring-rest-query-language/auth/users?search=firstName==jo*;age<25

响应结果:

[{
    "id": 1,
    "firstName": "john",
    "lastName": "doe",
    "email": "john.doe@example.com",
    "age": 24
}]

简单粗暴,一行参数搞定复杂查询。


7. 总结

本文展示了如何基于 rsql-parser 快速构建一个强大、灵活的 REST 查询语言,无需重复造轮子。

✅ 优势总结:

  • ✅ 语法标准,学习成本低
  • ✅ 支持 AND/OR、IN、LIKE、比较操作
  • ✅ 与 Spring Data JPA 无缝集成
  • ✅ 易于扩展自定义操作符

项目完整代码可在 GitHub 获取:
👉 https://github.com/eugenp/tutorials/tree/master/spring-web-modules/spring-rest-query-language

Maven 项目,导入即用,适合中大型系统做通用查询模块的技术选型参考。


原始标题:REST Query Language with RSQL

« 上一篇: Baeldung每周评论15
» 下一篇: 构建REST查询语言