【小家Spring】SpEL你感兴趣的实现原理浅析spring-expression~(SpelExpressionParser、EvaluationContext、rootObject)

每篇一句

万丈高楼平地起,勿在浮沙筑高台。技术永无止境,加油!

相关阅读

【小家Spring】Spring中@Value注解如此强大?从原理层面去剖析为何它有这么大的“能耐“

前言

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外部化配置注入了更多的活力,它让我们在运行时赋值、改变值都轻松的成为了可能~

另外,它有一个很大的特点是:需求驱动设计,所以只有多用才能记得牢,才能理解深~相信后续它还会持续发展,祝好。


关注A哥

AuthorA哥(YourBatman)
个人站点www.yourbatman.cn
E-mailyourbatman@qq.com
微 信fsx641385712
活跃平台
公众号BAT的乌托邦(ID:BAT-utopia)
知识星球BAT的乌托邦
每日文章推荐每日文章推荐

BAT的乌托邦

  • 25
    点赞
  • 85
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值