SpEL實現原理淺析【轉】


原文:https://cloud.tencent.com/developer/article/1497676

前言

Spring Expression Language(簡稱 SpEL)是一個支持查詢和操作運行時對象導航圖功能的強大的表達式語言。它的語法類似於傳統 EL,但提供額外的功能,最出色的就是函數調用和簡單字符串的模板函數。

這不得不介紹的SpEL的概念。Sp:Spring,EL:Expression Language。我們熟悉的還有比如JSP中的EL表達式、Struts中的OGNL等等。那那那既然有了它們,為何還要SpEL呢?

SpEL 創建的初衷是給 Spring 社區提供一種簡單而高效的表達式語言,一種可貫穿整個 Spring 產品組的語言。這種語言的特性基於 Spring 產品的需求而設計,這是它出現的一大特色。

在我們離不開Spring框架的同時,其實我們也已經離不開SpEL了,因為它太好用、太強大了。此處我貼出官網的這張圖:

從圖中可以看出SpEL的重要,它在Spring家族中如同基石一般的存在。 SpEL是spring-expression這個jar提供給我們的功能,它從Spring3.x版本開始提供~

備注:SpEL並不依附於Spring容器,它也可以獨立於容器解析。因此,我們在書寫自己的邏輯、框架的時候,也可以借助SpEL定義支持一些高級表達式~ 需注意一點若看到這么用:#{ systemProperties['user.dir'] },我們知道systemProperties是Spring容器就內置的,至於為何?之前在分析容器原理的的時候有介紹過~ 還有systemEnvironment等等等等都是可以直接使用的~

關於systemPropertiessystemEnvironment具體取值可參考:【小家Java】Java環境變量(Env)和系統屬性(Property)詳解—工具文章

閱讀前准備

需要說明是:本文着眼於SpEL原理、源碼層面的剖析,因此閱讀本文之前,我默認小伙伴已經是掌握和可以熟練使用SpEL的的,這里貼出兩個文檔型兄弟博文供以參考: Spring學習總結(四)——表達式語言 Spring Expression Language Spring Expression Language(SpEL) 4 學習筆記

SpEL的使用基本總結如下:

  • SpEL 字面量: - 整數:#{8} - 小數:#{8.8} - 科學計數法:#{1e4} - String:可以使用單引號或者雙引號作為字符串的定界符號。 - Boolean:#{true}
  • SpEL引用bean , 屬性和方法: - 引用其他對象:#{car} - 引用其他對象的屬性:#{car.brand} - 調用其它方法 , 還可以鏈式操作:#{car.toString()} - 調用靜態方法靜態屬性:#{T(java.lang.Math).PI}
  • SpEL支持的運算符號: - 算術運算符:+,-,*,/,%,^(加號還可以用作字符串連接) - 比較運算符:< , > , == , >= , <= , lt , gt , eg , le , ge - 邏輯運算符:and , or , not , | - if-else 運算符(類似三目運算符):?:(temary), ?:(Elvis) - 正則表達式:#{admin.email matches ‘[a-zA-Z0-9._%±]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}’}

基本原理

為了更好的敘述,以一個簡單例子作為參照:

    public static void main(String[] args) { String expressionStr = "1 + 2"; ExpressionParser parpser = new SpelExpressionParser(); //SpelExpressionParser是Spring內部對ExpressionParser的唯一最終實現類 Expression exp = parpser.parseExpression(expressionStr); //把該表達式,解析成一個Expression對象:SpelExpression // 方式一:直接計算 Object value = exp.getValue(); System.out.println(value.toString()); //3 // 若你在@Value中或者xml使用此表達式,請使用#{}包裹~~~~~~~~~~~~~~~~~ System.out.println(parpser.parseExpression("T(System).getProperty('user.dir')").getValue()); //E:\work\remotegitcheckoutproject\myprojects\java\demo-war System.out.println(parpser.parseExpression("T(java.lang.Math).random() * 100.0").getValue()); //27.38227555400853 // 方式二:定義環境變量,在環境內計算拿值 // 環境變量可設置多個值:比如BeanFactoryResolver、PropertyAccessor、TypeLocator等~~~ // 有環境變量,就有能力處理里面的占位符 ${} EvaluationContext context = new StandardEvaluationContext(); System.out.println(exp.getValue(context)); //3 }

任何語言都需要有自己的語法,SpEL當然也不例外。所以我們應該能夠想到,給一個字符串最終解析成一個值,這中間至少得經歷: 字符串 -> 語法分析 -> 生成表達式對象 -> (添加執行上下文) -> 執行此表達式對象 -> 返回結果

關於SpEL的幾個概念:

  1. 表達式(“干什么”)SpEL的核心,所以表達式語言都是圍繞表達式進行的
  2. 解析器(“誰來干”):用於將字符串表達式解析為表達式對象
  3. 上下文(“在哪干”):表達式對象執行的環境,該環境可能定義變量、定義自定義函數、提供類型轉換等等
  4. root根對象及活動上下文對象(“對誰干”):root根對象是默認的活動上下文對象,活動上下文對象表示了當前表達式操作的對象

這是對於解析一個語言表達式比較基本的一個處理步驟,為了更形象的表達出意思,繪制一幅圖友好展示如下:

步驟解釋:

  1. 按照SpEL支持的語法結構,寫出一個expressionStr
  2. 准備一個表達式解析器ExpressionParser,調用方法parseExpression()對它進行解析。這一步至少完成了如下三件事:
    1. 使用一個專門的斷詞器Tokenizer,將給定的表達式字符串拆分為Spring可以認可的數據格式
    2. 根據斷詞器處理的操作結果生成相應的語法結構
    3. 在這處理過程之中就需要進行表達式的對錯檢查(語法格式不對要精准報錯出來)
  3. 將已經處理好后的表達式定義到一個專門的對象Expression里,等待結果
  4. 由於表達式內可能存在占位符變量${},所以還不太適合馬上直接getValue()(若不需要解析占位符那就直接getValue()也是可以拿到值的)。所以在計算之前還得設置一個表達式上下文對象`EvaluationContext`(這一步步不是必須的)
  5. 替換好占位符內容后,利用表達式對象計算出最終的結果~~~~

相信從這個Demo可以了解到SpEL處理的一個過程邏輯,有處理流程有一個整體的認識了。那么接下來就是要拆分到各個核心組件的內部,一探究竟~

ExpressionParser:表達式解析器

將表達式字符串解析為可計算的已編譯表達式。支持分析模板(Template)標准表達式字符串。 它是一個抽象,並沒有要求具體的語法規則,Spring實現的語法規則是:SpEL語法。

// @since 3.0
public interface ExpressionParser { // 他倆都是把字符串解析成一個Expression對象~~~~ 備注expressionString都是可以被repeated evaluation的 Expression parseExpression(String expressionString) throws ParseException; Expression parseExpression(String expressionString, ParserContext context) throws ParseException; }

此處,ParserContext:提供給表達式分析器的輸入,它可能影響表達式分析/編譯例程。它會對我們解析表達式字符串的行為影響

ParserContext

public interface ParserContext { // 是否是模版表達式。 比如:#{3 + 4} boolean isTemplate(); // 模版的前綴、后綴 子類是可以定制化的~~~ String getExpressionPrefix(); String getExpressionSuffix(); // 默認提供的實例支持:#{} 的形式 顯然我們可以改變它但我們一般並不需要這么去做~ ParserContext TEMPLATE_EXPRESSION = new ParserContext() { @Override public boolean isTemplate() { return true; } @Override public String getExpressionPrefix() { return "#{"; } @Override public String getExpressionSuffix() { return "}"; } }; }

它只有一個實現類:TemplateParserContext。(ParserContext.TEMPLATE_EXPRESSION也是該接口的一個內部實現,我們可以直接引用)

關於StandardBeanExpressionResolver的內部類實現,也是一個非常基礎的實現。關於@Value的原理文章有提到

public class TemplateParserContext implements ParserContext { private final String expressionPrefix; private final String expressionSuffix; // 默認就是它了~~~ public TemplateParserContext() { this("#{", "}"); } @Override public final boolean isTemplate() { return true; } @Override public final String getExpressionPrefix() { return this.expressionPrefix; } @Override public final String getExpressionSuffix() { return this.expressionSuffix; } }

~ExpressionParser的繼承樹如下

TemplateAwareExpressionParser

它是一個支持解析模版Template的解析器。

// @since 3.0  它是一個抽象類
public abstract class TemplateAwareExpressionParser implements ExpressionParser { @Override public Expression parseExpression(String expressionString) throws ParseException { return parseExpression(expressionString, null); } @Override public Expression parseExpression(String expressionString, @Nullable ParserContext context) throws ParseException { // 若指定了上下文,並且是模版 就走parseTemplate if (context != null && context.isTemplate()) { return parseTemplate(expressionString, context); } else { // 抽象方法 子類去實現~~~ return doParseExpression(expressionString, context); } } private Expression parseTemplate(String expressionString, ParserContext context) throws ParseException { // 若解析字符串是空串~~~~~ if (expressionString.isEmpty()) { return new LiteralExpression(""); } // 若只有一個模版表達式,直接返回。否則會返回一個CompositeStringExpression,聚合起來的表達式~ Expression[] expressions = parseExpressions(expressionString, context); if (expressions.length == 1) { return expressions[0]; } else { return new CompositeStringExpression(expressionString, expressions); } } // ... parseExpressions的實現邏輯 還是稍顯復雜的~ 因為支持的case太多了~~~ }

它的子類實現有:InternalSpelExpressionParserSpelExpressionParser

SpelExpressionParser

SpEL parser該實例是可重用的和線程安全的(原因?此處賣個關子,小伙伴可自行想想)

public class SpelExpressionParser extends TemplateAwareExpressionParser { private final SpelParserConfiguration configuration; public SpelExpressionParser() { this.configuration = new SpelParserConfiguration(); } public SpelExpressionParser(SpelParserConfiguration configuration) { Assert.notNull(configuration, "SpelParserConfiguration must not be null"); this.configuration = configuration; } // 最終都是委托給了Spring的內部使用的類:InternalSpelExpressionParser--> 內部的SpEL表達式解析器~~~ public SpelExpression parseRaw(String expressionString) throws ParseException { return doParseExpression(expressionString, null); } // 這里需要注意:因為是new的,所以每次都是一個新對象,所以它是線程安全的~ @Override protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context) throws ParseException { return new InternalSpelExpressionParser(this.configuration).doParseExpression(expressionString, context); } }

這里的SpelParserConfiguration表示:顧名思義它表示SpEL的配置類。在構建SpelExpressionParser時我們可以給其傳遞一個SpelParserConfiguration對象以對SpelExpressionParser進行配置。其可以用於指定在遇到List或Array為null時是否自動new一個對應的實例(一般不建議修改此值~以保持語義統一)

// 它是個public類,因為`StandardBeanExpressionResolver`也使用到了它~~~
public class SpelParserConfiguration { // OFF IMMEDIATE(expressions are compiled as soon as possible) MIXED private static final SpelCompilerMode defaultCompilerMode; static { // 它的值可由`spring.properties`里面的配置改變~~~~ 所以你可以在你的類路徑下放置一個文件,通過`spring.expression.compiler.mode=IMMEDIATE`來控制編譯行為 String compilerMode = SpringProperties.getProperty("spring.expression.compiler.mode"); defaultCompilerMode = (compilerMode != null ? SpelCompilerMode.valueOf(compilerMode.toUpperCase()) : SpelCompilerMode.OFF); } // 調用者若沒指定,會使用上面的默認的~ private final SpelCompilerMode compilerMode; @Nullable private final ClassLoader compilerClassLoader; // 碰到為null的,是否給自動new一個對象,比如new String(),new ArrayList()等等~ private final boolean autoGrowNullReferences; // 專門針對於集合是否new private final boolean autoGrowCollections; // 集合能夠自動增長到的最大值~~~~ private final int maximumAutoGrowSize; // 省略get/set方法~~~后面會給一個自定義配置的示例~~~ }

InternalSpelExpressionParser

上面知道SpelExpressionParser最終都是委托它里做的,並且configuration也交給它,然后調用doParseExpression方法處理~

// 它是Spring內部使用的類~
class InternalSpelExpressionParser extends TemplateAwareExpressionParser { private static final Pattern VALID_QUALIFIED_ID_PATTERN = Pattern.compile("[\\p{L}\\p{N}_$]+"); private final SpelParserConfiguration configuration; //SpEL的配置 // 此處用一個雙端隊列 來保存表達式的每一個節點,每個節點都是一個SpelNode 該對象記錄着位置、子節點、父節點等等~~~ private final Deque<SpelNodeImpl> constructedNodes = new ArrayDeque<>(); private String expressionString = ""; // 帶解析的表達式字符串~ // Token流:token保存着符號類型(如int(,]+=?>=等等各種符號 非常之多) 然后記錄着它startPos和endPos private List<Token> tokenStream = Collections.emptyList(); // length of a populated token stream private int tokenStreamLength; // Current location in the token stream when processing tokens private int tokenStreamPointer; // 唯一的一個構造函數~ public InternalSpelExpressionParser(SpelParserConfiguration configuration) { this.configuration = configuration; } @Override protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context) throws ParseException { try { this.expressionString = expressionString; // Tokenizer就是分詞器。把待解析的表達式交給它分詞~~~ Tokenizer tokenizer = new Tokenizer(expressionString); // process處理,得到tokenStream 並且記錄上它的總長度 並且標記當前處理點為0 this.tokenStream = tokenizer.process(); this.tokenStreamLength = this.tokenStream.size(); this.tokenStreamPointer = 0; this.constructedNodes.clear(); // 顯然把當前節點清空~~ SpelNodeImpl ast = eatExpression(); Assert.state(ast != null, "No node"); Token t = peekToken(); if (t != null) { throw new SpelParseException(t.startPos, SpelMessage.MORE_INPUT, toString(nextToken())); } Assert.isTrue(this.constructedNodes.isEmpty(), "At least one node expected"); // 最終:每一個SpelNodeImpl 它就是一個SpelExpression表達式,但會出去。\ // 此時:只是把我們的字符串解析成為一個SpelExpression,還沒有參與賦值、計算哦~~~~ return new SpelExpression(expressionString, ast, this.configuration); } catch (InternalParseException ex) { throw ex.getCause(); } } ... // 解析表達式的邏輯非常的復雜,Spring團隊老牛逼了,竟然支持到了這么多的功能~~~~ }

這么一來,我們的ExpressionParser就算解釋完成了。絕大部分情況下我們最終都是使用了SpelExpressionParser去解析標准的語言表達式。

但是,但是,但是我們上面也說了,它還支持Template模式,下面以一個Demo加深了解:

SpEL對Template模式支持
    public static void main(String[] args) { String greetingExp = "Hello, #{#user} ---> #{T(System).getProperty('user.home')}"; ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("user", "fsx"); Expression expression = parser.parseExpression(greetingExp, new TemplateParserContext()); System.out.println(expression.getValue(context, String.class)); //Hello, fsx ---> C:\Users\fangshixiang }

這個功能就有點像加強版的字符串格式化了。它的執行步驟描述如下:

  1. 創建一個模板表達式,所謂模板就是帶字面量和表達式的字符串。其中#{}表示表達式的起止。上面的#user是表達式字符串,表示引用一個變量(注意這個寫法,有兩個#號)
  2. 解析字符串。其實SpEL框架的抽象是與具體實現無關的,只是我們這里使用的都是SpelExpressionParser
  3. 通過evaluationContext.setVariable可以在上下文中設定變量。
  4. 使用Expression.getValue()獲取表達式的值,這里傳入了Evalution上下文,第二個參數是類型參數,表示返回值的類型。

只有Template模式的時候,才需要#{},不然SpEL就是里面的內容即可,如1+2就是一個SpEL 至於@Value為何需要#{spel表示是內容}這樣包裹着,是因為它是這樣的expr = this.expressionParser.parseExpression(value, this.beanExpressionParserContext);,也就是說它最終是parseTemplate()這個去解析的~~~~ 如果parse的時候傳的context是null啥的,就不會解析外層#{}了


Expression

表示的是表達式對象。能夠根據上下文對象對自身進行計算的表達式。封裝以前分析的表達式字符串的詳細信息。

// @since 3.0   表達式計算的通用抽象。  該接口提供的方法非常非常之多~~~ 但不要怕大部分都是重載的~~~
public interface Expression { // 返回原始表達式的字符串~~~ String getExpressionString(); // 使用一個默認的標准的context執行計算~~~ @Nullable Object getValue() throws EvaluationException; // SpEL內部幫你轉換~~~ 使用的是默認的context @Nullable <T> T getValue(@Nullable Class<T> desiredResultType) throws EvaluationException; // 根據指定的根對象計算此表達式 @Nullable Object getValue(Object rootObject) throws EvaluationException; @Nullable <T> T getValue(Object rootObject, @Nullable Class<T> desiredResultType) throws EvaluationException; // 根據指定的上下文:EvaluationContext來計算值~~~ rootObject:跟對象 @Nullable Object getValue(EvaluationContext context) throws EvaluationException; // 以rootObject作為表達式的root對象來計算表達式的值。 // root對象:比如parser.parseExpression("name").getValue(person);相當於去person里拿到name屬性的值。這個person就叫root對象 @Nullable Object getValue(EvaluationContext context, Object rootObject) throws EvaluationException; @Nullable <T> T getValue(EvaluationContext context, @Nullable Class<T> desiredResultType) throws EvaluationException; @Nullable <T> T getValue(EvaluationContext context, Object rootObject, @Nullable Class<T> desiredResultType) throws EvaluationException; // 返回可傳遞給@link setvalue的最一般類型 @Nullable Class<?> getValueType() throws EvaluationException; @Nullable Class<?> getValueType(Object rootObject) throws EvaluationException; @Nullable Class<?> getValueType(EvaluationContext context) throws EvaluationException; @Nullable Class<?> getValueType(EvaluationContext context, Object rootObject) throws EvaluationException; @Nullable TypeDescriptor getValueTypeDescriptor() throws EvaluationException; @Nullable TypeDescriptor getValueTypeDescriptor(Object rootObject) throws EvaluationException; @Nullable TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException; @Nullable TypeDescriptor getValueTypeDescriptor(EvaluationContext context, Object rootObject) throws EvaluationException; // 確定是否可以寫入表達式,即可以調用setValue() boolean isWritable(Object rootObject) throws EvaluationException; boolean isWritable(EvaluationContext context) throws EvaluationException; boolean isWritable(EvaluationContext context, Object rootObject) throws EvaluationException; // 在提供的上下文中將此表達式設置為提供的值。 void setValue(Object rootObject, @Nullable Object value) throws EvaluationException; void setValue(EvaluationContext context, @Nullable Object value) throws EvaluationException; void setValue(EvaluationContext context, Object rootObject, @Nullable Object value) throws EvaluationException; }

它的繼承樹如下:

SpelExpression

這個是我們核心,甚至也是目前SpEL的唯一實現。 表達式可以獨立計算,也可以在指定的上下文中計算。在表達式計算期間,可能會要求上下文解析:對類型、bean、屬性和方法的引用。

public class SpelExpression implements Expression { // 在編譯表達式之前解釋該表達式的次數。 private static final int INTERPRETED_COUNT_THRESHOLD = 100; // 放棄前嘗試編譯表達式的次數 private static final int FAILED_ATTEMPTS_THRESHOLD = 100; private final String expression; // AST:抽象語法樹~ private final SpelNodeImpl ast; // SpelNodeImpl的實現類非常非常之多~~~ private final SpelParserConfiguration configuration; @Nullable private EvaluationContext evaluationContext; // 如果沒有指定,就會用默認的上下文 new StandardEvaluationContext() // 如果該表達式已經被編譯了,就會放在這里 @since 4.1 Spring內部並沒有它的實現類 尷尬~~~編譯是要交給我們自己實現??? @Nullable private CompiledExpression compiledAst; // 表達式被解釋的次數-達到某個限制時可以觸發編譯 private volatile int interpretedCount = 0; // 編譯嘗試和失敗的次數——使我們最終放棄了在似乎不可能編譯時嘗試編譯它的嘗試。 private volatile int failedAttempts = 0; // 唯一構造函數~ public SpelExpression(String expression, SpelNodeImpl ast, SpelParserConfiguration configuration) { this.expression = expression; this.ast = ast; this.configuration = configuration; } ... // 若沒有指定,這里會使用默認的StandardEvaluationContext上下文~ public EvaluationContext getEvaluationContext() { if (this.evaluationContext == null) { this.evaluationContext = new StandardEvaluationContext(); } return this.evaluationContext; } ... @Override @Nullable public Object getValue() throws EvaluationException { // 如果已經被編譯過,就直接從編譯后的里getValue即可~~~~ if (this.compiledAst != null) { try { EvaluationContext context = getEvaluationContext(); return this.compiledAst.getValue(context.getRootObject().getValue(), context); } catch (Throwable ex) { // If running in mixed mode, revert to interpreted if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { this.interpretedCount = 0; this.compiledAst = null; } else { throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); } } } ExpressionState expressionState = new ExpressionState(getEvaluationContext(), this.configuration); // 比如此處SeEl是加法+,所以ast為:OpPlus語法樹去處理的~~~ Object result = this.ast.getValue(expressionState); // 檢查是否需要編譯它~~~ checkCompile(expressionState); return result; } ... // 備注:數據轉換都是EvaluationContext.getTypeConverter() 來進行轉換 // 注意:此處的TypeConverter為`org.springframework.expression`的 只有一個實現類:StandardTypeConverter // 它內部都是委托給ConversionService去做的,具體是`DefaultConversionService`去做的~ @Override @Nullable public Class<?> getValueType() throws EvaluationException { return getValueType(getEvaluationContext()); } @Override @Nullable public Class<?> getValueType(Object rootObject) throws EvaluationException { return getValueType(getEvaluationContext(), rootObject); } @Override @Nullable public Class<?> getValueType(EvaluationContext context, Object rootObject) throws EvaluationException { ExpressionState expressionState = new ExpressionState(context, toTypedValue(rootObject), this.configuration); TypeDescriptor typeDescriptor = this.ast.getValueInternal(expressionState).getTypeDescriptor(); return (typeDescriptor != null ? typeDescriptor.getType() : null); } ... @Override public boolean isWritable(Object rootObject) throws EvaluationException { return this.ast.isWritable(new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), this.configuration)); } ... @Override public void setValue(Object rootObject, @Nullable Object value) throws EvaluationException { this.ast.setValue(new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), this.configuration), value); } ... }

這個是我們最主要的一個Expression表達式,AST是它的心臟。

LiteralExpression

Literal:字面意義的 它沒有計算的活,只是表示字面意思(字面量)。 so,它里面處理的類型:全部為String.class,並且和EvaluationContext無關

CompositeStringExpression

表示一個分為多個部分的模板表達式(它只處理Template模式)。每個部分都是表達式,但模板的純文本部分將表示為LiteralExpression對象。顯然它是一個聚合

public class CompositeStringExpression implements Expression { private final String expressionString; // 內部持有多個Expression~~~ private final Expression[] expressions; public CompositeStringExpression(String expressionString, Expression[] expressions) { this.expressionString = expressionString; this.expressions = expressions; } // 它是把每個表達式的值都拼接起來了 因為它只會運用於Template模式~~~~~ @Override public String getValue() throws EvaluationException { StringBuilder sb = new StringBuilder(); for (Expression expression : this.expressions) { String value = expression.getValue(String.class); if (value != null) { sb.append(value); } } return sb.toString(); } // 返回值的類型一樣的永遠是String.class @Override public Class<?> getValueType(Object rootObject) throws EvaluationException { return String.class; } // 不可set @Override public boolean isWritable(EvaluationContext context) { return false; } @Override public void setValue(Object rootObject, @Nullable Object value) throws EvaluationException { throw new EvaluationException(this.expressionString, "Cannot call setValue on a composite expression"); } }

這個表達式的計算中,和EvaluationContext這個上下文有莫大的關系,因此有必要看看它

EvaluationContext:評估/計算的上下文

表達式在計算上下文中執行。在表達式計算期間遇到引用時,正是在這種上下文中解析引用。它的默認實現為:StandardEvaluationContext

EvaluationContext可以理解為parser 在這個環境里執行parseExpression解析操作。 比如說我們現在往**ctx(一個EvaluationContext )**中放入一個 對象list (注:假設list里面已經有數據,即list[0]=true)

ctx.setVariable("list" , list); //可以理解為往ctx域 里放了一個list變量

接下來要想獲取或設置list的值都要在ctx范圍內才能找到:

parser.parseExpression("#list[0]").getValue(ctx);//在ctx這個環境里解析出list[0]的值 parser.parseExpression("#list[0]").setValue(ctx , "false");//在ctx這個環境中獎 list[0]設為false

假如我們又往ctx中放入一個person對象(假設person已經實例化並且person.name的值是fsx

ctx.setVariable("p", person);

那么取其中name屬性要像下面這樣:

parser.parseExpression("#p.name").getValue(ctx);//結果是 fsx

但是若是我們將ctx的root設為person 取name的時候就可以省略root對象這個前綴("#")了

//StandardEvaluationContext是EvaluationContext的子類 提供了setRootObject方法
((StandardEvaluationContext)ctx2).setRootObject(person); parser.parseExpression("name").getValue(ctx2); //訪問rootobject即person的屬性那么 結果:fsx // 這種方式同 parser.parseExpression("name").getValue(person); //它的意思是直接從root對象里找~~~~

這樣獲取name就會去root對象里直接找 而不用#p這樣子了~~~~ 這就是root對象的用處~它在后面的屬性訪問器中用處更大

public interface EvaluationContext { // 上下文可議持有一個根對象~~ TypedValue getRootObject(); // 返回屬性訪問器列表,這些訪問器將依次被要求讀取/寫入屬性 注意此處的屬性訪問器是el包自己的,不是bean包下的~~~ // ReflectivePropertyAccessor(DataBindingPropertyAccessor):通過反射讀/寫對象的屬性~ // BeanFactoryAccessor:這個屬性訪問器讓支持bean從bean工廠里獲取 // EnvironmentAccessor:可以從環境中.getProperty(name) // BeanExpressionContextAccessor:和BeanExpressionContext相關 // MapAccessor:可以從map中獲取值~~~ List<PropertyAccessor> getPropertyAccessors(); // ConstructorResolver它只有一個實現:ReflectiveConstructorResolver List<ConstructorResolver> getConstructorResolvers(); // 它的實現:ReflectiveMethodResolver/DataBindingMethodResolver List<MethodResolver> getMethodResolvers(); /** * Return a bean resolver that can look up beans by name. */ // 返回一個處理器:它能夠通過beanName找到bean // BeanResolver:唯一實現 BeanFactoryResolver 它內部持有BeanFactory的引用~ return this.beanFactory.getBean(beanName); @Nullable BeanResolver getBeanResolver(); // 返回一個類型定位器,該定位器可用於通過短名稱或完全限定名稱查找類型 唯一實現:StandardTypeLocator TypeLocator getTypeLocator(); // TypeConverter:唯一實現為StandardTypeConverter 其實還是依賴DefaultConversionService的 TypeConverter getTypeConverter(); TypeComparator getTypeComparator(); // 處理重載的 OperatorOverloader getOperatorOverloader(); // 這兩個方法,就是在這個上下文里設置值、查找值的~~~~ void setVariable(String name, @Nullable Object value); @Nullable Object lookupVariable(String name); }

EvaluationContext的繼承樹如下:

主要有兩個開箱即用的實現:SimpleEvaluationContextStandardEvaluationContext

SimpleEvaluationContext

公開僅支持部分的SpEL的支持。它有意限制的表達式類別~~ 旨在僅支持SpEL語言語法的一個子集,它不包括 Java類型引用,構造函數和bean引用等等。它還要求明確選擇對表達式中屬性和方法的支持級別。

StandardEvaluationContext

公開支持全套SpEL語言功能和配置選項。您可以使用它來指定默認的根對象並配置每個可用的評估相關策略。這也是

public class StandardEvaluationContext implements EvaluationContext { private TypedValue rootObject; @Nullable private volatile List<PropertyAccessor> propertyAccessors; @Nullable private volatile List<ConstructorResolver> constructorResolvers; @Nullable private volatile List<MethodResolver> methodResolvers; @Nullable private volatile ReflectiveMethodResolver reflectiveMethodResolver; @Nullable private BeanResolver beanResolver; @Nullable private TypeLocator typeLocator; @Nullable private TypeConverter typeConverter; private TypeComparator typeComparator = new StandardTypeComparator(); private OperatorOverloader operatorOverloader = new StandardOperatorOverloader(); // 上下文變量 就是一個Map private final Map<String, Object> variables = new ConcurrentHashMap<>(); */ public StandardEvaluationContext() { this.rootObject = TypedValue.NULL; } */ public StandardEvaluationContext(Object rootObject) { this.rootObject = new TypedValue(rootObject); } ... // 省略get/set方法 // 如果為null,就accessors.add(new ReflectivePropertyAccessor()); @Override public List<PropertyAccessor> getPropertyAccessors() { return initPropertyAccessors(); } // 意思是把添加進來的accessor 放在默認的前面。。。 public void addPropertyAccessor(PropertyAccessor accessor) { addBeforeDefault(initPropertyAccessors(), accessor); } // 默認 resolvers.add(new ReflectiveConstructorResolver()); @Override public List<ConstructorResolver> getConstructorResolvers() { return initConstructorResolvers(); } public void addConstructorResolver(ConstructorResolver resolver) { addBeforeDefault(initConstructorResolvers(), resolver); } ... // set null 有移除的效果 @Override public void setVariable(@Nullable String name, @Nullable Object value) { // For backwards compatibility, we ignore null names here... // And since ConcurrentHashMap cannot store null values, we simply take null // as a remove from the Map (with the same result from lookupVariable below). if (name != null) { if (value != null) { this.variables.put(name, value); } else { this.variables.remove(name); } } } public void setVariables(Map<String, Object> variables) { variables.forEach(this::setVariable); } // 注冊自定義函數,原理還是variables~~~~~~~~ public void registerFunction(String name, Method method) { this.variables.put(name, method); } @Override @Nullable public Object lookupVariable(String name) { return this.variables.get(name); } public void registerMethodFilter(Class<?> type, MethodFilter filter) throws IllegalStateException { initMethodResolvers(); ReflectiveMethodResolver resolver = this.reflectiveMethodResolver; if (resolver == null) { throw new IllegalStateException( "Method filter cannot be set as the reflective method resolver is not in use"); } resolver.registerMethodFilter(type, filter); } ... }

使用 setRootObject 方法來設置根對象,使用 setVariable 方法來注冊自定義變量,使用 registerFunction 來注冊自定義函數等等(registerFunction 方法進行注冊自定義函數,其實完全可以使用 setVariable 代替,兩者其實本質是一樣的

EvaluationContext的作用類似於OGNL中的StackContextEvaluationContext可以包含多個對象,但只能有一個root對象。 當表達式中包含變量時,SpEL就會根據EvaluationContext中變量的值對表達式進行計算。 往EvaluationContext里放入對象方法:setVariable(String name,Object value);向EvaluationContext中放入value對象,該對象名為name

需要注意的是EvaluationContext接口中並沒有定義設置root對象的方法,所以我們可以在StandardEvaluationContext里來設置root對象:setRootObject(Object rootObject) 默認它用#root取得此root對象,在SpEL中訪問root對象的屬性時,可以省略#root對象前綴,比如#root.name 可以簡寫成 name(注意不是寫成#name

setVariable()的使用
    public static void main(String[] args) { ExpressionParser parser = new SpelExpressionParser(); Person person = new Person("fsx", 30); List<String> list = new ArrayList<String>() {{ add("fsx"); add("周傑倫"); }}; Map<String, Integer> map = new HashMap<String, Integer>() {{ put("fsx", 18); put("周傑倫", 40); }}; EvaluationContext ctx = new StandardEvaluationContext(); //把list和map都放進環境變量里面去 ctx.setVariable("myPerson", person); ctx.setVariable("myList", list); ctx.setVariable("myMap", map); //============================================ System.out.println(parser.parseExpression("#myPerson").getValue(ctx)); //Person{name='fsx', age=30} System.out.println(parser.parseExpression("#myPerson.name").getValue(ctx)); //fsx // setVariable方式取值不能像root一樣,前綴不可省略~~~~~ System.out.println(parser.parseExpression("#name").getValue(ctx)); //null 顯然找不到這個key就返回null唄~~~ // 不寫前綴默認去root找,找出一個null。再訪問name屬性那可不報錯了嗎 //System.out.println(parser.parseExpression("name").getValue(ctx)); // Property or field 'name' cannot be found on null System.out.println(parser.parseExpression("#myList").getValue(ctx)); // [fsx, 周傑倫] System.out.println(parser.parseExpression("#myList[1]").getValue(ctx)); // 周傑倫 // 請注意對Map取值兩者的區別:中文作為key必須用''包起來 當然['fsx']也是沒有問題的 System.out.println(parser.parseExpression("#myMap[fsx]").getValue(ctx)); //18 System.out.println(parser.parseExpression("#myMap['周傑倫']").getValue(ctx)); //40 // =========若采用#key引用的變量不存在,返回的是null,並不會報錯哦============== System.out.println(parser.parseExpression("#map").getValue(ctx)); //null // 黑科技:SpEL內直接可以使用new方式創建實例 能創建數組、List 但不能創建普通的實例對象(難道是我還不知道)~~~~ System.out.println(parser.parseExpression("new String[]{'java','spring'}").getValue()); //[Ljava.lang.String;@30b8a058 System.out.println(parser.parseExpression("{'java','c語言','PHP'}").getValue()); //[java, c語言, PHP] 創建List System.out.println(parser.parseExpression("new Person()").getValue()); //A problem occurred whilst attempting to const }

需要注意一點:setVariable()進去的取值時,是必須指定前綴的。介紹的黑科技,也有它的使用注意事項哦~

#root表達式的使用

這個是SpEL中比較重要的一點,因為這個隱藏的表達式在Spring中有比較多的使用,例如:

  • @EventListener注解中condtion屬性:#root.event#root.args
  • @Cacheable等緩存相關注解:#root.method #root.target等等非常多

可能有伙伴會問,這些全靠死記?非也,一切都有因,先用一個例子看看效果:

    public static void main(String[] args) { ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.setRootObject(new Person("fsx", 18)); // 這個就是最終#root取出來的對象 若沒有設置 就不能使用#root System.out.println(parser.parseExpression("#root").getValue(context) instanceof Person); // true System.out.println(parser.parseExpression("#root").getValue(context)); //Person{name='fsx', age=18} System.out.println(parser.parseExpression("#root.name").getValue(context)); //fsx System.out.println(parser.parseExpression("#root.age").getValue(context)); // 18 // 若單純的想獲取屬性值,請不要使用# 直接使用name即可 // #root代表把root當作key先去查找出對象,再導航查找。。。。。而不用#類似全文查找(這個做法非常非常像JSP的el表達式的寫法) System.out.println(parser.parseExpression("#name").getValue(context)); // null System.out.println(parser.parseExpression("name").getValue(context)); // fsx // el參與計算時,取值方式也可以是#root 或者直接name屬性的方式 都是可以的 System.out.println(parser.parseExpression("name=='孫悟空'").getValue(context)); //false System.out.println(parser.parseExpression("name=='fsx'").getValue(context)); //true System.out.println(parser.parseExpression("#root.name=='fsx'").getValue(context)); //true System.out.println(parser.parseExpression("name.equals('fsx')").getValue(context)); //true //org.springframework.expression.spel.SpelEvaluationException: EL1004E: Method call: Method equalsXXX(java.lang.String) cannot be found on type java.lang.String System.out.println(parser.parseExpression("name.equalsXXX('fsx')").getValue(context)); // 報錯 }

相信看了我寫的這個Demo后,小伙伴有木有一種對神秘的#root豁然開朗的感覺。 此處我也列出兩個常用的場景下的參考類:

  • @EventListener可用屬性值參考:org.springframework.context.event.EventExpressionRootObject
  • @Cacheable可用屬性值參考:org.springframework.cache.interceptor.CacheExpressionRootObject

SpEL中的PropertyAccessor(重要)

PropertyAccessor屬性訪問器是SpEL中一個非常重要的概念。它內置的實現也有多種:

// @since 3.0
public interface PropertyAccessor { // 這個接口返回的類型相當於告訴SpEL它能夠處理的targetType類型,若返回null,表示它通處理所有的type @Nullable Class<?>[] getSpecificTargetClasses(); boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException; TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException; boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException; void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) throws AccessException; }

PropertyAccessor的繼承樹如下:

ReflectivePropertyAccessor/DataBindingPropertyAccessor
	// 它能夠處理所有的類型,所以Spring把它作為默認的處理器   放在最后一位(它通過反射處理的是字段Field、Method等的引用~)
	@Override
	@Nullable
	public Class<?>[] getSpecificTargetClasses() { return null; }
BeanFactoryAccessor
	@Override
	public Class<?>[] getSpecificTargetClasses() { return new Class<?>[] {BeanFactory.class}; } @Override public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { return (target instanceof BeanFactory && ((BeanFactory) target).containsBean(name)); } @Override public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { Assert.state(target instanceof BeanFactory, "Target must be of type BeanFactory"); return new TypedValue(((BeanFactory) target).getBean(name)); }
EnvironmentAccessor
	@Override
	public Class<?>[] getSpecificTargetClasses() { return new Class<?>[] {Environment.class}; } // 永遠返回true @Override public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { return true; } @Override public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { Assert.state(target instanceof Environment, "Target must be of type Environment"); return new TypedValue(((Environment) target).getProperty(name)); }
BeanExpressionContextAccessor
	//org.springframework.beans.factory.config.BeanExpressionContext  這個是Spring Bean工廠相關的默認使用的語言上線文
	@Override
	public Class<?>[] getSpecificTargetClasses() { return new Class<?>[] {BeanExpressionContext.class}; } @Override public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { return (target instanceof BeanExpressionContext && ((BeanExpressionContext) target).containsObject(name)); } @Override public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { Assert.state(target instanceof BeanExpressionContext, "Target must be of type BeanExpressionContext"); return new TypedValue(((BeanExpressionContext) target).getObject(name)); }
CompilablePropertyAccessor
// @since 4.1  org.springframework.asm.Opcodes:和asm相關的接口 
public interface CompilablePropertyAccessor extends PropertyAccessor, Opcodes { // Compilable:合適的 boolean isCompilable(); Class<?> getPropertyType(); // CodeFlow 和asm有關的對象~ void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf); }
MapAccessor
	@Override
	public Class<?>[] getSpecificTargetClasses() { return new Class<?>[] {Map.class}; } @Override public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { Object value = map.get(name); return new TypedValue(value); }

目前而言,只有使用PropertyOrFieldReference這一種ast的時候,才會用到PropertyAccessor去處理,如果只是簡單的加法、乘法等簡單運算啥的,和PropertyOrFieldReference是沒有啥關系的~ 例如#{person}這就是引用一個Bean,它使用到的AST是PropertyOrFieldReference,所以就和Accessor有關了(Spring容器內部和它有較大關系~)

下面用一個示例,表明PropertyAccessor的作用:

    public static void main(String[] args) { String expressionStr = "person"; ExpressionParser parpser = new SpelExpressionParser(); //SpelExpressionParser是Spring內部對ExpressionParser的唯一最終實現類 Expression exp = parpser.parseExpression(expressionStr); //把該表達式,解析成一個Expression對象:SpelExpression // 設置targetType為Person類型 StandardEvaluationContext context = new StandardEvaluationContext(new Person("fsx", 18)); context.addPropertyAccessor(new BeanExpressionContextAccessor()); System.out.println(exp.getValue(context)); }

假如我們直接運行就會報錯:

org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'person' cannot be found on object of type 'com.fsx.bean.Person' - maybe not public or not valid?

很明顯,因為我們指定的PropertyAccessor中,沒有一個能夠處理Person.class這個類型。然而最終會交給ReflectivePropertyAccessor處理,而它的canRead()是返回false的(Person類因為它既沒有person方法,也沒有person字段所以肯定返回false啊),所以就拋出異常了~

具體邏輯參見:PropertyOrFieldReference#getPropertyAccessorsToTry或者AstUtils#getPropertyAccessorsToTry~

自定義PropertyAccessor擴展SpEL的功能(重要)

為了解決上面那個問題,我們可以通過自定義PropertyAccessor來擴展支持。

   public static void main(String[] args) { String expressionStr = "person"; ExpressionParser parpser = new SpelExpressionParser(); //SpelExpressionParser是Spring內部對ExpressionParser的唯一最終實現類 Expression exp = parpser.parseExpression(expressionStr); //把該表達式,解析成一個Expression對象:SpelExpression // 設置targetType為Person類型 此處用Person對象~ StandardEvaluationContext context = new StandardEvaluationContext(new Person("fsx", 18)); // 通過自定義一個處理器,讓能從Person類里解析person這個Name引用~ context.addPropertyAccessor(new PropertyAccessor() { // 該處理器只處理Person類目標類型~ @Override public Class<?>[] getSpecificTargetClasses() { return new Class[]{Person.class}; } // 此處:要求Person對象必須有name和age才行 @Override public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { Person person = (Person) target; return person.getName() != null && person.getAge() != null; } @Override public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { Person person = (Person) target; return new TypedValue(person.getName() + ":" + name); } @Override public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { return false; } @Override public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException { } }); System.out.println(exp.getValue(context)); //fsx:person }

這樣子,就能正常輸出了~~~~ 完美。通過自定義的屬性訪問器,我們就能自己擴展SpEL的功能了

自定義PropertyAccessor是我們擴展Spring中@Value注解功能的最重要渠道之一。因為@Value強大的最主要原因是SpEL的功能強大~

演示其它PropertyAccessor生效的示例

環境准備:

@Configuration
public class RootConfig { @Bean public Person person() { return new Person("fsx", 18); } } @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {RootConfig.class}) public class TestSpringBean { @Autowired BeanFactory beanFactory; @Test public void test1() { String expressionStr = "person"; Expression exp = new SpelExpressionParser().parseExpression(expressionStr); // 設置執行上下文 StandardEvaluationContext context = new StandardEvaluationContext(beanFactory); context.addPropertyAccessor(new BeanFactoryAccessor()); System.out.println(expressionStr + "-->" + exp.getValue(context)); } }

以上。BeanFactoryAccessor生效,輸出:person-->Person{name='fsx', age=18}

@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {RootConfig.class}) public class TestSpringBean { @Autowired Environment environment; @Test public void test1() { String expressionStr = "['user.home']"; Expression exp = new SpelExpressionParser().parseExpression(expressionStr); // 設置執行上下文 StandardEvaluationContext context = new StandardEvaluationContext(environment); context.addPropertyAccessor(new EnvironmentAccessor()); System.out.println(exp.getValue(context)); } }

以上。EnvironmentAccessor生效,輸出:['user.home']-->C:\Users\fangshixiang

此處取值注意事項:若你是像本例一樣的聯合屬性,請參照本例書寫。而user.home或者'user.home'或者[user.home]等都是錯誤的寫法

BeanExpressionContextAccessor 關於它的示例這里就不演示了,它是處理BeanExpressionContext。而它是Spring的Bean工廠的對象,Spring內部處理bean相關的el都是通過它來處理的。(備注:它和bean強關聯~) 有興趣可參考類:StandardBeanExpressionResolver或者參考博文: 【小家Spring】Spring中@Value注解如此強大?從原理層面去剖析為何它有這么大的“能耐“

總結

Spring3.x引入的SpEL可謂非常的驚艷,它的實現非常的復雜,但它的使用卻異常的簡單和靈活。它給Spring外部化配置注入了更多的活力,它讓我們在運行時賦值、改變值都輕松的成為了可能~

另外,它有一個很大的特點是:需求驅動設計,所以只有多用才能記得牢,才能理解深~相信后續它還會持續發展,祝好。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM