文章目录
- 一、概述
-
- 1、什么是SpEL
- 2、SpEL能做什么
- 二、SpEL表达式使用
-
- 0、用到的类
- 1、文字表达式
- 2、属性, 数组, List, Map,和 索引
-
- (1)属性操作
- (2)数组和List
- (3)Map
- 3、内嵌List
- 4、内嵌Map
- 5、构建数组
- 6、调用类的方法
- 7、SpEL操作符
-
- (1)标准运算符
- (2)instanceof 和 正则表达式的匹配操作符
- (3)操作符的英文等价标识
- (4)逻辑运算符
- (5)数学运算符
- (6)赋值运算符
- 8、获取类的类型
- 9、调用类构造器
- 10、SpEL变量
-
- (1)基本使用
- (2)#this 和 #root变量
- 11、调用类静态方法
- 12、Bean引用
- 13、三元运算符(If-Then-Else)
- 14、Elvis操作符
- 15、安全导航操作员
- 16、集合选择
- 17、集合投影
- 18、表达式模板
- 参考资料
一、概述
1、什么是SpEL
SpEL(Spring Expression Language)是Spring框架中用于表达式语言的一种方式。它类似于其他编程语言中的表达式语言,用于在运行时计算值或执行特定任务。
SpEL提供了一种简单且强大的方式来访问和操作对象的属性、调用对象的方法,以及实现运算、条件判断等操作。它可以被用于XML和注解配置中,可以用于许多Spring框架中的特性,如依赖注入、AOP、配置文件等。
SpEL表达式可以在字符串中进行定义,使用特殊的语法和符号来表示特定的操作。例如,可以使用${expression}来表示一个SpEL表达式,其中expression是具体的SpEL语句。
SpEL支持各种操作和函数,包括算术运算、逻辑运算、条件判断、正则表达式匹配、集合操作等。它还支持访问上下文中的变量和参数,以及调用对象的方法。
2、SpEL能做什么
SpEL表达式具有广泛的功能,以下是一些SpEL表达式可以做的事情:
- 访问对象属性:SpEL表达式可以通过对象引用来访问对象的属性,例如
${object.property}
。 - 调用方法:SpEL表达式可以调用对象的方法,例如
${object.method()}
。 - 进行算术运算:SpEL表达式支持各种算术运算符,如加法、减法、乘法和除法。
- 进行逻辑运算:SpEL表达式支持逻辑运算符,如与、或、非等。
- 进行条件判断:SpEL表达式可以进行条件判断,例如通过if语句判断条件,并执行相应的操作。
- 访问集合元素和属性:SpEL表达式可以通过索引或键来访问集合中的元素或对象的属性。
- 执行正则表达式匹配:SpEL表达式可以执行正则表达式匹配,并返回匹配结果。
- 访问上下文变量和参数:SpEL表达式可以访问上下文中的变量和方法参数。
- 进行类型转换:SpEL表达式可以进行类型转换操作,将一个对象转换为另一种类型。
- 支持特殊操作符:SpEL表达式支持一些特殊的操作符,如Elvis操作符(
?:
)、安全导航操作符(?.
)等。
总的来说,SpEL表达式可以用于在运行时计算值、执行任务和操作对象,提供了灵活且强大的表达能力,广泛应用于Spring框架中的各种功能和配置中。
二、SpEL表达式使用
0、用到的类
public class PlaceOfBirth {
private String city;
private String country;
public PlaceOfBirth(String city) {
this.city=city;
}
public PlaceOfBirth(String city, String country) {
this(city);
this.country = country;
}
public String getCity() {
return city;
}
public void setCity(String s) {
this.city = s;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
}
import java.util.*;
public class Society {
private String name;
public static String Advisors = "advisors";
public static String President = "president";
private ListInventor> members = new ArrayListInventor>();
private Map officers = new HashMap();
public List getMembers() {
return members;
}
public Map getOfficers() {
return officers;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isMember(String name) {
for (Inventor inventor : members) {
if (inventor.getName().equals(name)) {
return true;
}
}
return false;
}
}
import java.util.Date;
import java.util.GregorianCalendar;
public class Inventor {
private String name;
private String nationality;
private String[] inventions;
private Date birthdate;
private PlaceOfBirth placeOfBirth;
public Inventor(String name, String nationality) {
GregorianCalendar c= new GregorianCalendar();
this.name = name;
this.nationality = nationality;
this.birthdate = c.getTime();
}
public Inventor(String name, Date birthdate, String nationality) {
this.name = name;
this.nationality = nationality;
this.birthdate = birthdate;
}
public Inventor() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNationality() {
return nationality;
}
public void setNationality(String nationality) {
this.nationality = nationality;
}
public Date getBirthdate() {
return birthdate;
}
public void setBirthdate(Date birthdate) {
this.birthdate = birthdate;
}
public PlaceOfBirth getPlaceOfBirth() {
return placeOfBirth;
}
public void setPlaceOfBirth(PlaceOfBirth placeOfBirth) {
this.placeOfBirth = placeOfBirth;
}
public void setInventions(String[] inventions) {
this.inventions = inventions;
}
public String[] getInventions() {
return inventions;
}
}
1、文字表达式
支持的文字表达式类型有字符串、数值(int、real、hex)、布尔和null。字符串由单引号
分隔。若要将单引号本身放在字符串中,请使用两个单引号字符。
通常来说,不会单纯的定义一个简单的文字表达式,而是通过方法调用等等复杂的操作,来完成一个功能:
// 定义Parser,可以定义全局的parser
ExpressionParser parser = new SpelExpressionParser();
// 获取字符串 "Hello World"
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();
// double类型 6.0221415E23
double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();
// int类型 2147483647
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();
// true
boolean trueValue = (Boolean) parser.parseExpression("true").getValue();
// null
Object nullValue = parser.parseExpression("null").getValue();
2、属性, 数组, List, Map,和 索引
(1)属性操作
注意!属性名的第一个字母不区分大小写
。
// 定义Parser,可以定义全局的parser
ExpressionParser parser = new SpelExpressionParser();
// 注意!属性名的第一个字母不区分大小写。 birthdate.year等效于Birthdate.Year
// 取出Inventor 中,birthdate属性的year属性
Inventor zhangsan = new Inventor("zhangsan", new Date(), "China");
// 定义StandardEvaluationContext ,传入一个操作对象
StandardEvaluationContext zhangsanContext = new StandardEvaluationContext(zhangsan);
int year = (Integer) parser.parseExpression("birthdate.year + 1900").getValue(zhangsanContext);
System.out.println(year); // 2023
//取出Inventor的placeOfBirth的city属性
PlaceOfBirth placeOfBirth = new PlaceOfBirth("长沙", "中国");
zhangsan.setPlaceOfBirth(placeOfBirth);
String city = (String) parser.parseExpression("placeOfBirth.City").getValue(zhangsanContext);
System.out.println(city); // 长沙
(2)数组和List
数组和List的内容是通过使用方括号符号获得的。
// 定义Parser,可以定义全局的parser
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// 省略数据初始化
// 取出tesla对象的inventions 第四个数据
String invention = parser.parseExpression("inventions[3]").getValue(
context, tesla, String.class);
// 取出ieee对象的第一个Member的name属性
String name = parser.parseExpression("Members[0].Name").getValue(
context, ieee, String.class);
// 取出ieee对象的第一个Member中的第七个Inventions
String invention = parser.parseExpression("Members[0].Inventions[6]").getValue(
context, ieee, String.class);
(3)Map
Map操作是通过key来获取的
// 取出societyContext的Officers中的key为president的值
Inventor pupin = parser.parseExpression("Officers['president']").getValue(
societyContext, Inventor.class);
String city = parser.parseExpression("Officers['president'].PlaceOfBirth.City").getValue(
societyContext, String.class);
// Officers中key为advisors的值取第一个
parser.parseExpression("Officers['advisors'][0].PlaceOfBirth.Country").setValue(
societyContext, "Croatia");
3、内嵌List
可以使用{}
符号在表达式中直接表示List。{}
本身意味着一个空列表。
// 定义Parser,可以定义全局的parser
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// [1, 2, 3, 4]
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);
System.out.println(numbers);
// 嵌套: [[a, b], [x, y]]
List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);
System.out.println(listOfLists);
4、内嵌Map
使用{key:value}
符号在表达式中表示Map。{:}
意味着空Map。
// 定义Parser,可以定义全局的parser
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// {name=Nikola, dob=10-July-1856}
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);
System.out.println(inventorInfo);
// 嵌套:{name={first=Nikola, last=Tesla}, dob={day=10, month=July, year=1856}}
Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);
System.out.println(mapOfMaps);
// List与Map可以嵌套使用,互相结合。
// 嵌套:[{name={first=Nikola, last=Tesla}}, {dob={day=10, month=July, year=1856}}]
List listOfMaps = (List) parser.parseExpression("{{name:{first:'Nikola',last:'Tesla'}},{dob:{day:10,month:'July',year:1856}}}").getValue(context);
System.out.println(listOfMaps);
5、构建数组
多维数组不提供初始化方式。
// 定义Parser,可以定义全局的parser
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);
// 数组并初始化
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);
// 多维数组
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);
6、调用类的方法
ExpressionParser parser = new SpelExpressionParser();
// 调用substring方法
String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class);
// 调用societyContext中对象的isMember方法,并传值。
StandardEvaluationContext societyContext = new StandardEvaluationContext(society);
boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(
societyContext, Boolean.class);
7、SpEL操作符
(1)标准运算符
使用标准运算符表示法支持关系运算符(等于、不等于、小于、小于或等于、大于和大于或等于)。
null不被视为任何东西(即不为零)。因此,任何其他值总是大于null (X > null总是为真),并且没有任何其他值小于零(X
ExpressionParser parser = newSpelExpressionParser();
// evaluates to true
boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class);
// evaluates to false
boolean falseValue = parser.parseExpression("2 ).getValue(Boolean.class);
// evaluates to true
boolean trueValue = parser.parseExpression("'black' ).getValue(Boolean.class);
(2)instanceof 和 正则表达式的匹配操作符
使用基本类型时要小心,因为它们会立即被装箱为包装器类型,所以1 instanceof T(int)
会计算为false,而1 instanceof T(Integer)
会计算为true。
// evaluates to false
boolean falseValue = parser.parseExpression(
"'xyz' instanceof T(Integer)").getValue(Boolean.class);
// evaluates to true
boolean trueValue = parser.parseExpression(
"'5.00' matches '^-?\d+(\.\d{2})?$'").getValue(Boolean.class);
//evaluates to false
boolean falseValue = parser.parseExpression(
"'5.0067' matches '^-?\d+(\.\d{2})?$'").getValue(Boolean.class);
(3)操作符的英文等价标识
每个符号操作符也可以被指定为纯字母的等价物
。这避免了所使用的符号对于嵌入表达式的文档类型具有特殊含义的问题(例如在XML文档中)。所有文本操作符都不区分大小写
。对应的文本是:
lt ( gt (>)
le ( ge (>=)
eq (==)
ne (!=)
div (/)
mod (%)
not (!)
(4)逻辑运算符
SpEL支持以下逻辑运算符:and、or、not
// 结果: false
boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class);
// 调用方法并根据方法返回值判断
String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
// -- OR --
boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class);
// 调用方法并根据方法返回值判断
String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
// -- NOT --
// 取反
boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class);
// -- AND and NOT --
String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')";
boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
(5)数学运算符
可以对数字和字符串使用加法运算符
。
只能对数字使用减法、乘法和除法运算符
。
也可以使用模数(%)和指数幂(^)
运算符。
强制执行标准运算符优先级。
// Addition
int two = parser.parseExpression("1 + 1").getValue(Integer.class); // 2
String testString = parser.parseExpression(
"'test' + ' ' + 'string'").getValue(String.class); // 'test string'
// Subtraction
int four = parser.parseExpression("1 - -3").getValue(Integer.class); // 4
double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class); // -9000
// Multiplication
int six = parser.parseExpression("-2 * -3").getValue(Integer.class); // 6
double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class); // 24.0
// Division
int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class); // -2
double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class); // 1.0
// Modulus
int three = parser.parseExpression("7 % 4").getValue(Integer.class); // 3
int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class); // 1
// Operator precedence
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class); // -21
(6)赋值运算符
若要给对象设置属性,请使用赋值运算符(=)
。这通常在对setValue的调用中完成,但也可以在对getValue的调用中完成。
// 定义Parser,可以定义全局的parser
ExpressionParser parser = new SpelExpressionParser();
Inventor inventor = new Inventor();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
parser.parseExpression("Name").setValue(context, inventor, "Aleksandar Seovic");
System.out.println(inventor.getName()); // Aleksandar Seovic
// 或者这样赋值
String aleks = parser.parseExpression(
"Name = 'Aleksandar Seovic2'").getValue(context, inventor, String.class);
System.out.println(inventor.getName()); // Aleksandar Seovic2
8、获取类的类型
可以使用特殊的T
运算符来指定java.lang.Class
的实例(类型)。静态方法也是通过使用这个操作符来调用的。
StandardEvaluationContext
使用TypeLocator
来查找类型,StandardTypeLocator(
可以替换)是基于对java.lang
包的理解而构建的。所以java.lang
中类型的T()
引用不需要使用全限定名,但是其他包中的类,必须使用全限定名。
ExpressionParser parser = new SpelExpressionParser();
Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);
Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);
boolean trueValue = parser.parseExpression(
"T(java.math.RoundingMode).CEILING )
.getValue(Boolean.class);
9、调用类构造器
使用new
运算符调用构造函数。除了基本类型(int、float等
)和String
之外,所有类型都应该使用完全限定的类名。
Inventor einstein = p.parseExpression(
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
.getValue(Inventor.class);
//创建一个新的Inventor,并且添加到members的list中
p.parseExpression(
"Members.add(new org.spring.samples.spel.inventor.Inventor(
'Albert Einstein', 'German'))").getValue(societyContext);
10、SpEL变量
(1)基本使用
可以使用#variableName
语法在表达式中引用变量。通过在EvaluationContext
实现上使用setVariable
方法来设置变量
ExpressionParser parser = new SpelExpressionParser();
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable("newName", "Mike Tesla"); // 设置变量
// 获取变量newName,并将其赋值给name属性
parser.parseExpression("Name = #newName").getValue(context, tesla);
System.out.println(tesla.getName()); // "Mike Tesla"
(2)#this 和 #root变量
#this变量引用当前的评估对象(根据该评估对象解析非限定引用)。
#root变量总是被定义并引用根上下文对象。虽然#this可能会随着表达式的组成部分的计算而变化,但是#root总是指根。
// 创建一个Integer数组
ListInteger> primes = new ArrayListInteger>();
primes.addAll(Arrays.asList(2,3,5,7,11,13,17));
// create parser and set variable 'primes' as the array of integers
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable("primes", primes);
// numbers > 10 的 list
// evaluates to [11, 13, 17]
ListInteger> primesGreaterThanTen = (ListInteger>) parser.parseExpression(
"#primes.?[#this>10]").getValue(context);
System.out.println(primesGreaterThanTen);
11、调用类静态方法
// 方法定义的方式
Method method = ...;
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("myFunction", method);
// 准备一个要调用的目标方法
public class StringUtils {
public static String reverseString(String input) {
StringBuilder backwards = new StringBuilder(input.length());
for (int i = 0; i input.length(); i++) {
backwards.append(input.charAt(input.length() - 1 - i));
}
return backwards.toString();
}
}
// 调用目标静态方法
public static void main(String[] args) throws NoSuchMethodException {
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// 获取要调用的方法
context.setVariable("reverseString",
StringUtils.class.getDeclaredMethod("reverseString", String.class));
// 调用
String helloWorldReversed = parser.parseExpression(
"#reverseString('hello')").getValue(context, String.class);
}
12、Bean引用
如果已经用bean解析器配置了评估上下文,则可以使用@
符号从表达式中查找bean。
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());
// 将调用MyBeanResolver 的 resolve(context,"something")
Object bean = parser.parseExpression("@something").getValue(context);
// 注意!MyBeanResolver 可以使用系统自带的BeanFactoryResolver,写成:
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
// BeanFactoryResolver的resolve方法,就是通过Bean的名称来获取Bean:
@Override
public Object resolve(EvaluationContext context, String beanName) throws AccessException {
try {
return this.beanFactory.getBean(beanName);
}
catch (BeansException ex) {
throw new AccessException("Could not resolve bean reference against BeanFactory", ex);
}
}
要访问工厂bean本身,应该在bean名称前加上&
符号:
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());
// 将调用MyBeanResolver 的 resolve(context,"something")
Object bean = parser.parseExpression("&foo").getValue(context);
13、三元运算符(If-Then-Else)
// 使用示例
String falseString = parser.parseExpression(
"false ? 'trueExp' : 'falseExp'").getValue(String.class);
// name属性设置值
parser.parseExpression("Name").setValue(societyContext, "IEEE");
// 设置变量
societyContext.setVariable("queryName", "Nikola Tesla");
// 三元运算符
expression = "isMember(#queryName)? #queryName + ' is a member of the ' " +
"+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'";
String queryResultString = parser.parseExpression(expression)
.getValue(societyContext, String.class);
// queryResultString = "Nikola Tesla is a member of the IEEE Society"
14、Elvis操作符
Elvis运算符是三元运算符语法的缩写,用于Groovy语言中。使用三元运算符语法时,通常需要将一个变量重复两次,如下例所示:
String name = "Elvis Presley";
String displayName = (name != null ? name : "Unknown");
可以使用Elvis运算符(因与Elvis的发型相似而得名)优化。以下示例显示了如何使用Elvis运算符:
ExpressionParser parser = new SpelExpressionParser();
String name = parser.parseExpression("name?:'Unknown'").getValue(String.class);
System.out.println(name); // 'Unknown'
更复杂的实例:
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
String name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name); // Nikola Tesla
tesla.setName(null);
name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name); // Elvis Presley
15、安全导航操作员
安全导航操作符用于避免NullPointerException
,来自Groovy语言。通常,当引用一个对象时,可能需要在访问该对象的方法或属性之前验证它不为null。为了避免这种情况,安全导航运算符返回null,而不是引发异常。以下示例显示了如何使用安全导航运算符:
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));
String city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city); // Smiljan
tesla.setPlaceOfBirth(null);
city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city); // null - does not throw NullPointerException!!!
16、集合选择
// 语法.?[selectionExpression]
ListInventor> list = (ListInventor>) parser.parseExpression(
"Members.?[Nationality == 'Serbian']").getValue(societyContext);
// 返回value小于27的值
Map newMap = parser.parseExpression("map.?[value).getValue();
除了返回所有选定的元素之外,还可以只检索第一个或最后一个值。要获得匹配选择的第一个条目,语法是。.^[selectionExpression]
.要获得最后一个匹配的选择,语法是。.$[选择表达式]
。
17、集合投影
// 语法:.![projectionExpression]
// returns ['Smiljan', 'Idvor' ]
List placesOfBirth = (List)parser.parseExpression("Members.![placeOfBirth.city]");
18、表达式模板
// 通常使用#{}作为模板,与字符串拼接起来
String randomPhrase = parser.parseExpression(
"random number is #{T(java.lang.Math).random()}",
new TemplateParserContext()).getValue(String.class);
// evaluates to "random number is 0.7038186818312008"
// TemplateParserContext 的定义
public class TemplateParserContext implements ParserContext {
public String getExpressionPrefix() {
return "#{";
}
public String getExpressionSuffix() {
return "}";
}
public boolean isTemplate() {
return true;
}
}
参考资料
https://docs.spring.io/spring-framework/docs/5.1.6.RELEASE/spring-framework-reference/core.html#expressions