集合选择(Collection Selection)
Selection 是一个强大的表达式语言功能,它让你通过从一个源集合的条目中进行选择,将其转化为另一个集合。
选择使用的语法是 .?[selectionExpression]
。它对集合进行过滤,并返回一个新的集合,其中包含原始元素的一个子集。
常规使用
- 如果
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();
}
}
- 如果是
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);
}
}
- 如果是
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]
获取最后一个匹配元素.
- 如果是
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);
}
}
- 如果是
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();
}