SpringBoot SpEL語法掃盲與查詢手冊的實現
Spring 表示式語言簡稱為 SpEL,一種類似 Ognl 的物件圖導航語言(對於 ognl 不熟悉的同學可以參考一下: Ognl 系列博文)
SeEL 為 Spring 提供了豐富的想象空間,除了一些基本的表示式操作之外,還支援
- 訪問 bean 物件
- 呼叫方法,訪問(修改)類(物件)屬性
- 計算表示式
- 正則匹配
- ...
I. 語法百科
以下內容均來自官方文件: https://docs.spring.io/spring-framework/docs/5.2.1.RELEASE/spring-framework-reference/core.html#expressions
1. 字面表示式
Spel 支援strings,numeric values (int,real,hex),boolean,and null等基本型別,例項如下
ExpressionParser parser = new SpelExpressionParser(); // evals to "Hello World" String helloWorld = (String) parser.parseExpression("'Hello World'").getValue(); // double 型別 double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue(); // evals to 2147483647 int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue(); boolean trueValue = (Boolean) parser.parseExpression("true").getValue(); Object nullValue = parser.parseExpression("null").getValue();
請注意,字串需要用單引號包括,浮點數預設為 double 型別,用null表示null object
輸出結果
str: Hello World
double: 6.0221415E23
int: 2147483647
bool: true
null: null
2. Inline List
通過{}來表明 List 表示式,一個空的列表直接用{}表示
ExpressionParser parser = new SpelExpressionParser(); // Integer列表 List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(); System.out.println("list: " + numbers); // List的元素為List List<List> listlOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(); System.out.println("List<List> : " + listlOfLists);
輸出結果
list: [1,4]
List<List> : [[a,b],[x,y]]
3. Inline map
{key:value}來表示 map 表示式,空 Map 直接用{:}表示
private void map() { ExpressionParser parser = new SpelExpressionParser(); Map map = (Map) parser.parseExpression("{txt:'Nikola',dob:'10-July-1856'}").getValue(); System.out.println("map: " + map); Map mapOfMaps = (Map) parser.parseExpression("{txt:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}") .getValue(); System.out.println("Map<Map>: " + mapOfMaps); }
輸出結果
map: {txt=Nikola,dob=10-July-1856}
Map<Map>: {txt={first=Nikola,last=Tesla},dob={day=10,month=July,year=1856}}
4. 陣列
陣列可以藉助new構造方法來實現,通過下標ary[index]的方式訪問陣列中的元素
private void array() { ExpressionParser parser = new SpelExpressionParser(); int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(); System.out.println("array: " + JSON.toJSONString(numbers1)); // Array with initializer int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,3}").getValue(); System.out.println("array: " + JSON.toJSONString(numbers2)); // Multi dimensional array int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(); System.out.println("array: " + JSON.toJSONString(numbers3)); int[] nums = new int[]{1,5}; EvaluationContext context = new StandardEvaluationContext(); context.setVariable("num",nums); // 通過下標訪問陣列中的元素 Integer numVal = parser.parseExpression("#num[1]").getValue(context,Integer.class); System.out.println("numVal in array: " + numVal); }
輸出如下
array: [0,0]
array: [1,3]
array: [[0,0],[0,0]]
numVal in array: 3
5. 表示式
Spel 支援一些 Java 語法中常規的比較判斷,算數運算,三元表示式,型別判斷,matches正則匹配等基表表達式
下面給出一些簡單的例項
public void expression() { ExpressionParser parser = new SpelExpressionParser(); // 運算 System.out.println("1+2= " + parser.parseExpression("1+2").getValue()); // 比較 System.out.println("1<2= " + parser.parseExpression("1<2").getValue()); System.out.println("true ? hello : false > " + parser.parseExpression("3 > 2 ? 'hello': 'false' ").getValue()); // instanceof 判斷,請注意靜態類,用T進行包裝 System.out.println("instance : " + parser.parseExpression("'a' instanceof T(String)").getValue()); //正則表示式 System.out.println("22 是否為兩位數字 :" + parser.parseExpression("22 matches '\\d{2}'").getValue()); }
輸出結果
1+2= 3
1<2= true
true ? hello : false > hello
instance : true
22 是否為兩位數字 :true
6. Type 與靜態類
如果想獲取 Class 物件,或者訪問靜態成員/方法,可以藉助T()語法來實現
比如我們有一個靜態類
public static class StaClz { public static String txt = "靜態屬性"; public static String hello(String tag) { return txt + " : " + tag; } }
如果希望訪問靜態屬性txt,表示式可以寫成T(com.git.hui.boot.spel.demo.BasicSpelDemo.StaClz).txt,請注意圓括號中的是完整簽名;訪問靜態方法方式類似
public void type() { // class,靜態類 ExpressionParser parser = new SpelExpressionParser(); String name = parser.parseExpression("T(com.git.hui.boot.spel.demo.BasicSpelDemo.StaClz).txt").getValue(String.class); System.out.println("txt: " + name); String methodReturn = parser.parseExpression("T(com.git.hui.boot.spel.demo.BasicSpelDemo.StaClz).hello" + "('一灰灰blog')") .getValue(String.class); System.out.println("static method return: " + methodReturn); // class類獲取 Class stringClass = parser.parseExpression("T(String)").getValue(Class.class); System.out.println("class: " + stringClass.getName()); }
輸出結果如下
txt: 靜態屬性
static method return: 靜態屬性 : 一灰灰blog
class: java.lang.String
上面的寫法,請重點看一下T(String),這裡的 String 沒有用完整的包路徑,即直接位於java.lang包下的類,是可以省略掉完整包名的,就像我們平時寫程式碼時,也不需要顯示的加一個import java.lang.*
7. 構造方法
上面介紹 array 的時候,就介紹了使用new來建立陣列物件,當然也可以直接構造其他的普通物件,如我們新建一個測試類
public static class Person { private String name; private int age; public Person(String name,int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public String toString() { return "Person{" + "txt='" + name + '\'' + ",age=" + age + '}'; } }
通過 SpEl 建立一個物件的例項
public void construct() { ExpressionParser parser = new SpelExpressionParser(); Person person = parser.parseExpression("new com.git.hui.boot.spel.demo.BasicSpelDemo.Person('一灰灰',20)") .getValue(Person.class); System.out.println("person: " + person); }
輸出結果如下:
person: Person{txt='一灰灰',age=20}
請注意,構造方法中類的完整簽名
8. 變數引用
細心的小夥伴,在上面介紹陣列的成員演示的例項中,寫法如"#num[1]",這個 num 前面有一個#,這是一個語法定義,有#修飾的表示變數訪問
要理解這一小節,首先得理解EvaluationContext,在我們的 SpEL 表示式的解析中,getValue有一個引數就是這個 Context,你可以將他簡單理解為包含一些物件的上下文,我們可以通過 SpEL 的語法,來訪問操作 Context 中的某些成員、成員方法屬性等
一般的操作過程如下:
- context.setVariable("person",person); 向EvaluationContext中塞入成員變數
- parser.parseExpression(xxx).getValue(context) 解析 SpEL 表示式,context 必須作為傳參丟進去哦
一個簡單的例項
public void variable() { ExpressionParser parser = new SpelExpressionParser(); Person person = new Person("一灰灰blog",18); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("person",person); String name = parser.parseExpression("#person.getName()").getValue(context,String.class); System.out.println("variable name: " + name); Integer age = parser.parseExpression("#person.age").getValue(context,Integer.class); System.out.println("variable age: " + age); }
輸出結果如下
variable name: 一灰灰blog
variable age: 18
友情提示,如果訪問物件的私有 Field/method,會拋異常
9. 函式
Context 中的變數,除了是我們常見的基本型別,普通的物件之外,還可以是方法,在setVariable時,設定的成員型別為method即可
public void function() { try { ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); // 註冊一個方法變數,引數為method型別 context.setVariable("hello",StaClz.class.getDeclaredMethod("hello",String.class)); String ans = parser.parseExpression("#hello('一灰灰')").getValue(context,String.class); System.out.println("function call: " + ans); } catch (Exception e) { e.printStackTrace(); } }
輸出結果如下
function call: 靜態屬性 : 一灰灰
10. bean 訪問
在 Spring 中,什麼物件最常見?當然是 bean,那麼我們可以直接通過 SpEL 訪問 bean 的屬性、呼叫方法麼?
要訪問 bean 物件,所以我們的EvaluationContext中需要包含 bean 物件才行
藉助BeanResolver來實現,如context.setBeanResolver(new BeanFactoryResolver(applicationContext));
其次訪問 bean 的字首修飾為@符號
為了演示這種場景,首先建立一個普通的 Bean 物件
@Data @Component public class BeanDemo { private String blog = "https://spring.hhui.top"; private Integer num = 8; public String hello(String name) { return "hello " + name + ",welcome to my blog " + blog + ",now person: " + num; } }
接著我們需要獲取ApplicationContext,所以可以稍微改一下我們的測試類,讓它繼承自ApplicationContextAware
private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public void bean() { ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new BeanFactoryResolver(applicationContext)); // 獲取bean物件 BeanDemo beanDemo = parser.parseExpression("@beanDemo").getValue(context,BeanDemo.class); System.out.println("bean: " + beanDemo); // 訪問bean方法 String ans = parser.parseExpression("@beanDemo.hello('一灰灰blog')").getValue(context,String.class); System.out.println("bean method return: " + ans); }
上面的寫法和之前的並沒有太大的區別,實際輸出結果如下
bean: BeanDemo(blog=https://spring.hhui.top,num=8)
bean method return: hello 一灰灰blog,welcome to my blog https://spring.hhui.top,now person: 8
11. ifElse
SpEL 支援三元表示式,在上述的表示式中也給出了例項
public void ifThenElse() { // 三元表示式,? : ExpressionParser parser = new SpelExpressionParser(); String ans = parser.parseExpression("true ? '正確': '錯誤'").getValue(String.class); System.out.println("ifTheElse: " + ans); }
輸出結果如下
ifTheElse: 正確
12. elvis
xx != null ? xx : yy => xx?:yy
這個也屬於我們經常遇到的一種場景,如果 xx 為 null,則返回 yy;否則直接返回 xx;簡化寫法為 elvis 寫法: xx?:yy
public void elvis() { // xx != null ? xx : yy => xx?:yy ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("name",null); String name = parser.parseExpression("#name?:'Unknown'").getValue(context,String.class); System.out.println("elvis-before " + name); context.setVariable("name","Exists!"); name = parser.parseExpression("#name?:'Unknown'").getValue(context,String.class); System.out.println("elvis-after " + name); }
輸出結果如下
elvis-before Unknown
elvis-after Exists!
13. 安全表示式
在 java 中,最常見最討厭的是一個就是 NPE 的問題,SpEL 中當然也可能出現這種情況,但是若在 SpEL 中進行非空判斷,那就很不優雅了,SpEL 提供了xx?.yy的寫法來避免 npe,即
xx == null ? null : xx.yy => xx?.yy
舉例說明
public void safeOperate() { // 防npe寫法,xx == null ? null : xx.yy => xx?.yy ExpressionParser parser = new SpelExpressionParser(); Person person = new Person(null,18); String name = parser.parseExpression("name?.length()").getValue(person,String.class); System.out.println("safeOperate-before: " + name); person.name = "一灰灰blog"; name = parser.parseExpression("name?.length()").getValue(person,String.class); System.out.println("safeOperate-after: " + name); }
輸出結果如下
safeOperate-before: null
safeOperate-after: 7
14. 容器擷取
遍歷容器,獲取子集,相當於 jdk8 Stream 中 filter 用法,語法格式如下
xx.?[expression],請注意中括弧中的表示式必須返回 boolean
舉例說明
public void collectionSelection() { // 容器擷取,返回滿足條件的子集 // xx.?[expression],將滿足expression的子元素保留,返回一個新的集合,類似容器的 filter List<Integer> list = new ArrayList<>(Arrays.asList(1,4,6,7,8,9)); ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); context.setVariable("list",list); // 用 #this 來指代列表中的迭代元素 List<Integer> subList = (List<Integer>) parser.parseExpression("#list.?[#this>5]").getValue(context); System.out.println("subList: " + subList); Map<String,Integer> map = new HashMap<>(); map.put("a",1); map.put("b",10); map.put("c",4); map.put("d",7); context.setVariable("map",map); // 表示式內部用key,value 來指代map的k,v Map subMap = parser.parseExpression("#map.?[value < 5]").getValue(context,Map.class); System.out.println("subMap: " + subMap); subMap = parser.parseExpression("#map.?[key == 'a']").getValue(context,Map.class); System.out.println("subMap: " + subMap); }
輸出結果如下
subList: [6,9]
subMap: {a=1,c=4}
subMap: {a=1}
注意
- 在列表表示式中,可以通過#this來指代列表中的每一個元素
- 在 map 表示式中,通過key,value來分別指代 map 中的k,v
15. 容器對映
將一個集合通過某種規則,對映為另一種集合,相當於 jdk8 Stream 中的 map 用法,語法如下
xx.![expression],將表示式計算的結果作為輸出容器中的成員
舉例如下
public void collectionProjection() { // 容器操作之後,生成另一個容器,類似lambda中的map方法 // xx.![expression] List<Integer> list = new ArrayList<>(Arrays.asList(1,9)); ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); context.setVariable("list",list); // 用 #this 來指代列表中的迭代元素 List newList = parser.parseExpression("#list.![#this * 2]").getValue(context,List.class); System.out.println("newList: " + newList); Map<String,map); List newListByMap = parser.parseExpression("#map.![value * 2]").getValue(context,List.class); System.out.println("newListByMap: " + newListByMap); }
輸出結果如下:
newList: [2,12,14,16,18]
newListByMap: [2,20,14]
16. 表示式模板
SpEL 還提供了一種自定義表示式模板的方式,將字面量和表示式放在一起使用,比如下面這一條語句
"random number is #{T(java.lang.Math).random()}"
其中#{T(java.lang.Math).random()}是一個 SpEL 表示式,左邊的是普通字串,這種寫法也常見於@Value註解中的屬性寫法,當然直接通過上面的寫法執行這個語句會報錯,這個時候需要指定ParserContext
舉例說明
public void template() { // 模板,混合字面文字與表示式,使用 #{} 將表示式包裹起來 ExpressionParser parser = new SpelExpressionParser(); String randomPhrase = parser.parseExpression("random number is #{T(java.lang.Math).random()}",ParserContext.TEMPLATE_EXPRESSION).getValue(String.class); System.out.println("template: " + randomPhrase); }
輸出結果如下
template: random number is 0.10438946298113871
17. 小結
SpEL 屬於非常強大的表示式語言了,就我個人的感覺而言,它和 OGNL 有些像,當它們的上下文中包含了 Spring 的上下文時,可以訪問任何的 bean,而你可以藉助它們的語法規範,做各種事情
推薦我之前的一個專案,https://github.com/liuyueyi/quick-fix,利用 ognl 結合ApplicationContext,可以隨心所欲的訪問控制應用中的任何 bean 物件
II. 其他
0. 專案
工程:https://github.com/liuyueyi/spring-boot-demo
原始碼:https://github.com/liuyueyi/spring-boot-demo/spring-boot/013-spel
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。