SpEl表达式之强大的集合选择(Collection Selection)和集合投影(Collection Projection)

集合选择(Collection Selection)

Selection 是一个强大的表达式语言功能,它让你通过从一个源集合的条目中进行选择,将其转化为另一个集合。

选择使用的语法是 .?[selectionExpression]。它对集合进行过滤,并返回一个新的集合,其中包含原始元素的一个子集。

常规使用

  1. 如果list则循环的是每个元素,通过#this获取元素本身,也可以直接调用元素的方法和公共属性,调用方法或属性可省略#this,返回一个list
java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import java.util.List;
public class MainTests {
    @Test
    public void test(){
        var p = new SpelExpressionParser();
        // 调用this
        var res1 = p.parseExpression("#root.?[#this<2]").getValue(List.of(1,2,3));
        assert res1 instanceof List<?> l && l.size()==1 && l.getFirst().equals(1);
        // 调用方法
        var res2 = p.parseExpression("#root.?[#this.intValue()<3]").getValue(List.of(1,3),List.class);
        assert  res2.size()==1 && res2.getFirst().equals(1);
        // 调用方法或属性可省略this
        var res3 = p.parseExpression("#root.?[intValue()<3]").getValue(List.of(1,3),List.class);
        assert res3.getFirst().equals(res2.getFirst());
        // 没匹配到时不会返回null,而是返回空集合
        var res4 = p.parseExpression("#root.?[#this<1]").getValue(List.of(1));
        assert res4 instanceof List l && l.isEmpty();
    }
}
  1. 如果是array,则返回一个array,需要注意的是即便传入的是原始类型数组,在转换后会被自动包装,其他情况类似list
java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class MainTests {
    @Test
    public void test(){
        var p = new SpelExpressionParser();
        // 结果将自动包装
        var res1 = p.parseExpression("#root.?[#this<2]").getValue(new int[]{1,2,3});
        assert res1.getClass().isArray() && res1.getClass().getComponentType().equals(Integer.class) && ((Integer[])res1).length==1 && ((Integer[])res1)[0]==1;
        // 自身不会被包装
        var res2 = p.parseExpression("#root.class.componentType").getValue(new int[]{1});
        assert res2.equals(int.class);
    }
}
  1. 如果是map,则循环的是每个entry,也可以通过#this获取entry本身,或者通过getKey()(可简化为key),getValue()(可简化为value),返回一个map,其他情况和list相同
java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import java.util.Map;

public class MainTests {
    @Test
    public void test(){
        var p = new SpelExpressionParser();
        // 如果是map.则循环的是每个entry,也可以通过#this获取entry本身,或者通过getKey()或key,getValue()或value获取kv
        var res1 = p.parseExpression("#root.?[value=='aa']").getValue(Map.of('a',"aa",'b',"bb"));
        assert res1 instanceof Map<?,?> m && m.size()==1 && m.get('a').equals("aa");
        // 也可以用常规的方法表达式
        var res2 = p.parseExpression("#root.?[#this.getKey().toString()=='a']").getValue(Map.of('a',"aa"));
        assert res2 instanceof Map<?,?> m && m.size()==1 && m.containsKey('a');
    }
}

等价方法

投影功能类似于于stream.filter.collect操作

java 复制代码
// 数组类型等价方法
public <T> Object[] selection(T[] obj, Predicate<T> mapper){
    return Arrays.stream(obj).filter(mapper).toArray();
}
// 列表类型等价方法
public <T> List<T> selection(List<T> obj, Predicate<T> mapper){
    return obj.stream().filter(mapper).collect(Collectors.toList());
}
// 映射类型等价方法
public <K,V> Map<K, V> selection(Map<K,V> obj, Predicate<? super Map.Entry<K,V>> mapper){
    return obj.entrySet().stream().filter(mapper).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

选择首个匹配的元素或最后一个匹配的元素

使用表达式.^[selectionExpression]获取第一个匹配元素,使用表达式.$[selectionExpression]获取最后一个匹配元素.

  1. 如果是list或array将返回匹配的元素,而不是list或array,原始类型将自动装箱,如果没找到会返回null
java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import java.util.List;

public class MainTests {
    @Test
    public void test(){
        var p = new SpelExpressionParser();
        // 返回首个匹配的元素,如果是原始类型也会被自动包装
        var res1 = p.parseExpression("#root.^[#this<3]").getValue(List.of(1,3));
        assert res1 instanceof Integer i && i==1;
        // 如果找不到合适的元素,则返回null
        var res2 = p.parseExpression("#root.^[#this<1]").getValue(new int[]{1});
        assert res2 == null;
        // 使用条件true选择首个元素
        var res3 = p.parseExpression("#root.^[true]").getValue(new int[]{1});
        assert res3.equals(1);
        // 返回最后一个匹配的元素,只是把^换成$
        var res4 = p.parseExpression("#root.$[#this<3]").getValue(List.of(1,2,3));
        assert res4.equals(2);
    }
}
  1. 如果是map则返回的是一个只有一个entry的map,如果没有找到返回null,需要注意的是HashMap是不排序的,LinkedHashMap才是排序的
java 复制代码
import cn.hutool.core.map.MapUtil;
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;

import java.util.LinkedHashMap;
import java.util.Map;

public class MainTests {
    @Test
    public void test(){
        var p = new SpelExpressionParser();
        // 返回首个匹配的元素,如果是原始类型也会被自动包装,需要注意的是HashMap是不排序的,LinkedHashMap是排序的
        var res1 = p.parseExpression("#root.^[value<3]").getValue(
                MapUtil.builder(new LinkedHashMap<>()).put('a',1).put('b',2).put('c',3).build());
        assert res1 instanceof Map m && m.size()==1 && m.get('a').equals(1);
        // 返回最后一个匹配的元素
        var res2 = p.parseExpression("#root.$[value<3]").getValue(
                MapUtil.builder(new LinkedHashMap<>()).put('a',1).put('b',2).put('c',3).build());
        assert res2 instanceof Map m && m.size()==1 && m.get('b').equals(2);
        // 如果没有找到返回null
        var res3 = p.parseExpression("#root.$[value<3]").getValue(Map.of('a',3));
        assert res3 == null;
    }
}

等价方法

表达式.^[selectionExpression]类似于stream.filter.findFirst方法

java 复制代码
// 数组类型等价方法
public <T> T selectionFirst(T[] obj, Predicate<T> mapper){
    return Arrays.stream(obj).filter(mapper).findFirst().orElse(null);
}
// 列表类型等价方法
public <T> T selectionFirst(List<T> obj, Predicate<T> mapper){
    return obj.stream().filter(mapper).findFirst().orElse(null);
}
// 映射类型等价方法
public <K,V> Map<K, V> selectionFirst(Map<K,V> obj, Predicate<? super Map.Entry<K,V>> mapper){
    var s = obj.entrySet().stream().filter(mapper).findFirst().orElse(null);
    if (s==null) return null;
    return Map.of(s.getKey(),s.getValue());
}
// 列表类型获取最后一个匹配元素的等价方法
public <T> T selectionLast(List<T> obj, Predicate<T> mapper){
    return obj.stream().filter(mapper).toList().reversed().stream().findFirst().orElse(null);
}

不希望返回null

可以通过埃尔维斯(Elvis)运算符来避免返回null,其语法是objMayNull?:obj,如果objMayNull为空则返回obj,否则返回objMayNull

java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.util.List;

public class MainTests {
    @Test
    public void test(){
        var p = new SpelExpressionParser();
        var ctx = new StandardEvaluationContext();
        ctx.setRootObject(List.of(1));
        ctx.setVariable("defaultV",0);
        // 当无法筛选到合适的元素时,返回默认值0(defaultV)
        var res1 = p.parseExpression("#root.^[#this<1]?:#defaultV").getValue(ctx);
        assert Integer.valueOf(0).equals(res1);
    }
}

集合投影(Collection Projection)

投影让一个集合驱动一个子表达式的评估,其结果是一个新的集合.投射的语法是 .![projectionExpression].

遍历每一个元素并计算然后返回一个list或array,注意list投影后返回list,map投影后也是返回list,只有array投影后返回array,且返回值内的原始类型会被自动装箱.

java 复制代码
import cn.hutool.core.collection.ListUtil;
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;

import java.util.List;
import java.util.Map;

public class MainTests {
    @Test
    public void test(){
        var p = new SpelExpressionParser();
        // 投影list返回list,将每个元素乘2,返回[2,4],int会被包装为Integer
        var res1 = p.parseExpression("#root.![#this*2]").getValue(ListUtil.of(1,2));
        assert res1 instanceof List<?> l && l.size()==2 && l.getFirst().equals(2) && l.getLast().equals(4) && l.getFirst().getClass().equals(Integer.class);
        // 投影array返回array
        var res2 = p.parseExpression("#root.![#this*2]").getValue(new int[]{3});
        assert res2.getClass().isArray() && ((Integer[])res2)[0]==6;
        // 投影map返回list,需要注意的是如果map不是已排序的,返回的list也将不是
        var res3 = p.parseExpression("#root.![value]").getValue(Map.of('a',1,'b',2));
        assert res3 instanceof List<?> l && l.contains(1) && l.contains(2);
    }
}

等价方法

投影功能类似于于stream.map.collect操作

java 复制代码
// 数组类型等价方法
public <T> Object[] projection(T[] obj, Function<T,Object> mapper){
    return Arrays.stream(obj).map(mapper).toArray();
}
// 列表类型等价方法
public <T,R> List<R> projection(List<T> obj, Function<T,R> mapper){
    return obj.stream().map(mapper).collect(Collectors.toList());
}
// 映射类型等价方法
public <K,V,R> List<R> projection(Map<K,V> obj, Function<? super Map.Entry<K,V>, R> mapper){
    return obj.entrySet().stream().map(mapper).toList();
}
相关推荐
聪明的笨猪猪32 分钟前
Java 工程案例:AI 生成 @Async 异步线程池踩坑
java·经验分享·面试·ai编程
学长毕设1 小时前
基于SpringBoot公务员考试交流平台-附源码
java·spring boot·后端
SL_staff3 小时前
私有化低代码:面向强监管行业的可控数字化实践
java·低代码
三翼鸟数字化技术团队4 小时前
高性能存储引擎RocksDB:原理与Java生态实践
java
AI开源软件4 小时前
SpringBoot3+AI Agent 智能报销:怎样把对话接进现有财务审批系统
spring boot·ai agent·人工确认·ruoyi office·agentos·智能报销·财务审批
SL_staff4 小时前
JVS-APS有限产能建模的七个刚性支点:面向产线真实约束的技术实现解析
java·开源·全栈
特立独行的猫A4 小时前
少 3 个环节、少 1 次 ABI、少 3500 行代码:cj-tauri 与 Ionic/Capacitor 的鸿蒙桥接机制对比
java·后端·python
学长毕设4 小时前
基于SpringBoot的高校后勤报修管理系统-附源码
java·spring boot·后端
特立独行的猫A4 小时前
cj-tauri vs Rust/Tauri 同款播放器实测:编译 21 秒 vs 11 分钟,磁盘 12 MB vs 1.2 GB
java·后端·python
杨凯凡5 小时前
Spring AI RAG 实战:向量检索、租户过滤、引用溯源与防幻觉边界
java·人工智能·spring