在Spring中使用RSQL实现REST查询语言

  REST API提供一种搜索/查询语言能够类似SQL那样实现REST API的干净简单且强大的操作。rsql-parser是一个提供REST查询语言RSQL的强大库包。RSQL是Feed Item Query Language (简称:FIQL)的子集。

下面我们看看在Spring中实现RSQL的大概步骤:

首先,我们导入RSQL Maven包:

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

定义主要业务领域模型实体如下:

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

下面我们开始解析REST客户端的请求:

RSQL表达式内部以节点形式表达,所以,这里一般使用访问者模式作实现输入分析,必须记住,我们需要实现接口 RSQLVisitor interface, 并且创建我们自己的访问者实现 – CustomRsqlVisitor:

public class CustompRsqlVisitor<T> implements RSQLVisitor<Specification<User>, Void> {
 
    private UserRsqlSpecBuilder builder;
 
    public CustomRsqlVisitor() {
        builder = new UserRsqlSpecBuilder();
    }
 
    @Override
    public Specification<User> visit(AndNode node, Void param) {
        return builder.createSpecification(node);
    }
 
    @Override
    public Specification<User> visit(OrNode node, Void param) {
        return builder.createSpecification(node);
    }
 
    @Override
    public Specification<User> visit(ComparisonNode node, Void params) {
        return builder.createSecification(node);
    }
}

现在我们需要处理持久层,为这些每个节点构造查询。这里使用Spring Data JPA规格,实现一个规格构建器,用来构建我们要访问的每个节点的规格。

public class UserRsqlSpecBuilder {
 
    public Specifications<User> createSpecification(Node node) {
        if (node instanceof LogicalNode) {
            return createSpecification((LogicalNode) node);
        }
        if (node instanceof ComparisonNode) {
            return createSpecification((ComparisonNode) node);
        }
        return null;
    }
 
    public Specifications<User> createSpecification(LogicalNode logicalNode) {
        List<Specifications<User>> specs = new ArrayList<Specifications<User>>();
        Specifications<User> temp;
        for (Node node : logicalNode.getChildren()) {
            temp = createSpecification(node);
            if (temp != null) {
                specs.add(temp);
            }
        }
 
        Specifications<User> result = specs.get(0);
        if (logicalNode.getOperator() == LogicalOperator.AND) {
            for (int i = 1; i < specs.size(); i++) {
                result = Specifications.where(result).and(specs.get(i));
            }
        } else if (logicalNode.getOperator() == LogicalOperator.OR) {
            for (int i = 1; i < specs.size(); i++) {
                result = Specifications.where(result).or(specs.get(i));
            }
        }
 
        return result;
    }
 
    public Specifications<User> createSpecification(ComparisonNode comparisonNode) {
        Specifications<User> result = Specifications.where(
          new UserRsqlSpecification(
            comparisonNode.getSelector(),
            comparisonNode.getOperator(),
            comparisonNode.getArguments()
          )
        );
        return result;
    }
}

注意:

  • LogicalNode是AND/OR节点,有多个子节点
  • ComparisonNode 则是没有子节点,它保留有 Selector, Operator 和 参数Arguments

举例,对于查询“name==john” – 可得:

  1. Selector则是: “name”
  2. Operator则是: “==”
  3. Arguments则是:[john]

 

下面我们创建一个定制的规格:当构建查询时,我们需要利用一个定制的User Specification – “UserRsqlSpecification“:

public class UserRsqlSpecification implements Specification<User> {
    private String property;
    private ComparisonOperator operator;
    private List<String> arguments;
 
    public UserRsqlSpecification(
      String property, ComparisonOperator operator, List<String> arguments) {
        super();
        this.property = property;
        this.operator = operator;
        this.arguments = arguments;
    }
 
    @Override
    public Predicate toPredicate(
      Root<User> 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.<String> 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(Root<User> root) {
        List<Object> args = new ArrayList<Object>();
        Class<? extends Object> type = root.get(property).getJavaType();
 
        for (String argument : arguments) {
            if (type.equals(Integer.class)) {
                args.add(Integer.parseInt(argument));
            } else if (type.equals(Long.class)) {
                args.add(Long.parseLong(argument));
            } else {
                args.add(argument);
            }
        }
 
        return args;
    }
}

下面是枚举enum “RsqlSearchOperation ,其保留有默认的rsql-parser operator:

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;
    }
}

好了,REST 查询语言核心功能基本开发完毕,下面我们来测试一下:

首先,初始化一下数据:

@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.com");
        userJohn.setAge(22);
        repository.save(userJohn);
 
        userTom = new User();
        userTom.setFirstName("tom");
        userTom.setLastName("doe");
        userTom.setEmail("tom@doe.com");
        userTom.setAge(26);
        repository.save(userTom);
    }
}

 

测试操作符operator是等于号”=“的查询:

查询所有姓是john和名是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)));
}

 

测试“不等于"的查询:

查询姓不是"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)));
}

 

测试“大于>”的查询:

查询年龄大于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)));
}

 

测试"Like"查询

查询姓中有字符"jo"的用户

@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)));
}

 

测试IN:查询姓中是 “john” 或 “jack“的用户:

@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)));
}

 

最后一步,将所有的功能放入控制器:

@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 dao.findAll(spec);
}

通过浏览器访问:

http://localhost:8080/users?search=firstName==jo*;age<25

得到结果是:

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

 

完整源码见:the github project 

Spring更多源码