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. 如果是listarray将返回匹配的元素,而不是listarray,原始类型将自动装箱,如果没找到会返回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则返回的是一个只有一个entrymap,如果没有找到返回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].

遍历每一个元素并计算然后返回一个listarray,注意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();
}
相关推荐
Young55663 分钟前
还不了解工作流吗(基础篇)?
java·workflow·工作流引擎
让我上个超影吧5 分钟前
黑马点评【缓存】
java·redis·缓存
ajassi200013 分钟前
开源 java android app 开发(十一)调试、发布
android·java·linux·开源
YuTaoShao27 分钟前
Java八股文——MySQL「存储引擎篇」
java·开发语言·mysql
白露与泡影29 分钟前
springboot + nacos + k8s 优雅停机
spring boot·后端·kubernetes
crud33 分钟前
Java 中的 synchronized 与 Lock:深度对比、使用场景及高级用法
java
王德博客38 分钟前
【Java课堂笔记】Java 入门基础语法与面向对象三大特性详解
java·开发语言
seventeennnnn1 小时前
Java大厂面试真题:谢飞机的技术挑战
java·spring boot·面试·aigc·技术挑战·电商场景·内容社区
wkj0011 小时前
接口实现类向上转型和向上转型解析
java·开发语言·c#
qqxhb1 小时前
零基础设计模式——行为型模式 - 观察者模式
java·观察者模式·设计模式·go