Spring EL(Spring Expression Language)即 Spring 表达式语言,是 Spring 3.0 引入的一个重要特性,它是一种功能强大的轻量级表达式语言,旨在为 Spring 应用程序提供运行时查询和操作对象图的能力。与传统的 Java 编程方式相比,Spring EL 允许开发者以一种更简洁、灵活的方式来处理对象的属性、方法调用以及各种逻辑运算。
从本质上讲,Spring EL 支持在运行时动态解析对象图,这意味着我们可以在程序运行过程中,根据实际情况对对象的属性和方法进行动态访问和调用。例如,在一个电商系统中,我们可能需要根据用户的不同等级来动态计算商品的折扣价格,使用 Spring EL 就可以很方便地实现这一功能。它的语法类似于 JSP EL,但在功能上有了进一步的增强,不仅支持基本的属性访问和算术运算,还支持方法调用、字符串模板等高级功能。
Spring EL 与 Spring 框架深度集成,这使得它在 Spring 的各种场景中都能发挥重要作用。无论是在 XML 配置文件中,还是在注解配置中,我们都可以轻松地使用 Spring EL 来进行值的注入、条件判断等操作。同时,Spring EL 还在 AOP(面向切面编程)、Spring MVC 等模块中有着广泛的应用,为这些模块提供了更加灵活和强大的功能支持。例如,在 AOP 中,我们可以使用 Spring EL 来定义切点表达式,精确地控制切面的作用范围。
Spring EL 的核心组件主要包括ExpressionParser
、Expression
和EvaluationContext
,它们相互协作,共同实现了 Spring EL 强大的表达式解析和求值功能。
ExpressionParser:ExpressionParser
是 Spring EL 的表达式解析器,它的主要职责是将我们编写的表达式字符串解析成可执行的Expression
对象。在使用 Spring EL 时,我们首先需要创建一个ExpressionParser
实例,通常使用SpelExpressionParser
类来实现。例如:
ExpressionParser parser = new SpelExpressionParser();
Expression:Expression
接口代表了一个已解析的表达式,它提供了getValue
方法用于对表达式进行求值。当ExpressionParser
将表达式字符串解析成Expression
对象后,我们就可以通过调用getValue
方法来获取表达式的计算结果。getValue
方法可以接受一个EvaluationContext
参数,用于提供表达式求值时所需的上下文信息。例如:
Expression expression = parser.parseExpression("1 + 2");
int result = expression.getValue(Integer.class);
System.out.println(result); // 输出3
EvaluationContext:EvaluationContext
提供了表达式求值的上下文环境,它包含了属性解析器、方法解析器和类型转换器等组件,用于在表达式求值过程中解析对象的属性、方法以及进行类型转换。在实际应用中,我们通常使用StandardEvaluationContext
类来创建上下文对象,并通过它来设置根对象和变量。例如:
// 创建一个User对象
User user = new User();
user.setName("John");
// 创建一个StandardEvaluationContext对象,并将User对象设置为根对象
StandardEvaluationContext context = new StandardEvaluationContext(user);
// 解析表达式并求值,这里的表达式是访问User对象的name属性
Expression expression = parser.parseExpression("name");
String name = expression.getValue(context, String.class);
System.out.println(name); // 输出John
在上述代码中,StandardEvaluationContext
对象将User
对象设置为根对象,这样在表达式求值时,就可以直接访问User
对象的属性。
Spring EL 的核心组件通过紧密协作,为我们提供了一个强大而灵活的表达式语言环境,使得我们可以在 Spring 应用程序中轻松地进行各种复杂的表达式操作。
Spring EL 的基础语法涵盖了文本表达式、数学运算、逻辑判断和类型操作等多个方面,这些基础语法是我们使用 Spring EL 进行表达式编写的基石。
文本表达式:文本表达式用于表示字符串、数字、布尔值和null
等常量。在 Spring EL 中,字符串需要用单引号包裹,例如:
ExpressionParser parser = new SpelExpressionParser();
String result = parser.parseExpression("'Hello World'").getValue(String.class);
System.out.println(result); // 输出Hello World
数字支持负数、指数及小数,默认情况下实数使用Double.parseDouble()
进行表达式类型转换,例如:
Long number = parser.parseExpression("1.024E+3").getValue(Long.class);
System.out.println(number); // 输出1024
布尔值直接使用true
或false
表示,null
则直接用null
表示,例如:
Boolean bool = parser.parseExpression("true").getValue(Boolean.class);
System.out.println(bool); // 输出true
Object nullValue = parser.parseExpression("null").getValue();
System.out.println(nullValue); // 输出null
数学运算:Spring EL 支持常见的数学运算符,如加(+
)、减(-
)、乘(*
)、除(/
)、取模(%
)和幂指数(^
)。这些运算符可以用于数值类型的表达式计算,例如:
Integer sum = parser.parseExpression("10 + 20 \* 3").getValue(Integer.class);
System.out.println(sum); // 先计算乘法20 \* 3 = 60,再计算加法10 + 60 = 70,输出70
Integer remainder = parser.parseExpression("10 % 3").getValue(Integer.class);
System.out.println(remainder); // 输出1
Integer power = parser.parseExpression("2 ^ 3").getValue(Integer.class);
System.out.println(power); // 输出8
逻辑判断:逻辑判断用于对条件进行判断,返回布尔值。Spring EL 支持常见的逻辑运算符,如与(and
或&&
)、或(or
或||
)、非(not
或!
)。这些运算符可以用于组合多个条件进行逻辑判断,例如:
Boolean result1 = parser.parseExpression("true && false").getValue(Boolean.class);
System.out.println(result1); // 输出false
Boolean result2 = parser.parseExpression("true or false").getValue(Boolean.class);
System.out.println(result2); // 输出true
Boolean result3 = parser.parseExpression("!true").getValue(Boolean.class);
System.out.println(result3); // 输出false
类型操作:T
操作符用于获取类型,可以调用对象的静态方法和访问静态成员。通过T(Type)
的形式来表示java.lang.Class
实例,其中Type
必须是类全限定名,java.lang
包除外,即该包下的类可以不指定包名,例如:
Class\<?> dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);
System.out.println(dateClass); // 输出class java.util.Date
Integer absValue = parser.parseExpression("T(java.lang.Math).abs(-1)").getValue(Integer.class);
System.out.println(absValue); // 输出1
Boolean isString = parser.parseExpression("'asdf' instanceof T(String)").getValue(Boolean.class);
System.out.println(isString); // 输出true
在实际应用中,我们经常会遇到访问对象属性或方法时,对象可能为null
的情况,如果直接访问,就会抛出空指针异常。Spring EL 提供了安全访问模式,通过在对象后面加?.
来避免空指针异常。当对象为null
时,使用安全访问模式会返回null
,而不是抛出异常。
假设我们有一个User
类,其中包含getName
方法,当我们不确定user
对象是否为null
时,可以使用安全访问模式来调用getName
方法:
// 假设user可能为null
User user = null;
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("user", user);
String name = parser.parseExpression("#user?.getName()").getValue(context, String.class);
System.out.println(name); // 输出null,不会抛出空指针异常
在上述代码中,由于user
为null
,如果不使用安全访问模式,直接调用#user.getName()
会抛出空指针异常。而使用#user?.getName()
,则会安全地返回null
,避免了程序因空指针异常而崩溃。这种安全访问模式在处理复杂对象图时非常有用,可以大大提高代码的健壮性和稳定性。
Spring EL 提供了丰富的集合操作技巧,包括集合选择、投影和#this
变量的使用,这些技巧使得我们可以方便地对集合进行各种操作。
集合选择:集合选择允许我们根据指定的条件从集合中筛选出符合条件的元素。Spring EL 提供了三种集合选择表达式:?[expression]
选择符合条件的元素、^[expression]
选择符合条件的第一个元素、$[expression]
选择符合条件的最后一个元素。
假设我们有一个包含整数的集合{1, 3, 5, 7}
,我们可以使用集合选择表达式来筛选出大于 3 的元素:
ExpressionParser parser = new SpelExpressionParser();
List\<Integer> list = parser.parseExpression("{1, 3, 5, 7}").getValue(List.class);
// 选择大于3的元素
List\<Integer> result1 = parser.parseExpression("#list.?\[#this > 3]").getValue(context, List.class);
System.out.println(result1); // 输出\[5, 7]
// 选择大于3的第一个元素
Integer result2 = parser.parseExpression("#list.^\[#this > 3]").getValue(context, Integer.class);
System.out.println(result2); // 输出5
// 选择大于3的最后一个元素
Integer result3 = parser.parseExpression("#list.$\[#this > 3]").getValue(context, Integer.class);
System.out.println(result3); // 输出7
集合投影:集合投影是指对集合中的每个元素进行某种操作,并返回一个新的集合。在 Spring EL 中,我们可以使用![expression]
来实现集合投影。例如,对一个包含整数的集合中的每个元素进行平方操作:
ExpressionParser parser = new SpelExpressionParser();
List\<Integer> list = parser.parseExpression("{1, 2, 3, 4}").getValue(List.class);
// 对集合中的每个元素进行平方操作
List\<Integer> result = parser.parseExpression("#list.!\[#this \* #this]").getValue(context, List.class);
System.out.println(result); // 输出\[1, 4, 9, 16]
#this
变量:#this
变量表示当前集合中的元素,常用于集合的过滤和操作。在集合选择和投影表达式中,#this
变量可以方便地引用当前元素,进行条件判断和操作。例如,在前面的集合选择示例中,#this > 3
就是使用#this
变量来判断当前元素是否大于 3。
在复杂的业务系统中,业务规则往往需要根据不同的条件和场景进行动态调整,传统的硬编码方式难以满足这种灵活性需求。Spring EL 与规则引擎(如 Drools)的结合,为实现动态业务规则引擎提供了强大的解决方案。通过这种结合,我们可以将业务规则以表达式的形式进行定义和管理,在运行时根据实际情况动态加载和执行这些规则,从而大大提高系统的灵活性和可维护性。
以电商系统的促销规则为例,系统可能需要根据不同的促销活动、用户等级、商品类别等条件来动态计算商品的折扣价格。使用 Spring EL 和规则引擎,我们可以将这些促销规则定义为一系列的表达式,例如:
// 规则1:新用户购买商品享受8折优惠
String rule1 = "#user.isNewUser() && #product.category == 'electronics' ? #product.price \* 0.8 : #product.price";
// 规则2:老用户购买商品满1000元享受9折优惠
String rule2 = "#user.isNewUser() == false && #totalAmount >= 1000 ? #totalAmount \* 0.9 : #totalAmount";
在上述代码中,#user
和#product
是上下文变量,分别代表用户对象和商品对象。通过这些表达式,我们可以根据用户和商品的具体情况动态计算折扣价格。
在实际应用中,我们可以将这些规则存储在数据库或配置文件中,通过 Spring EL 的表达式解析功能在运行时动态加载和解析这些规则。例如,我们可以创建一个RuleEngineService
类,用于加载和执行规则:
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
public class RuleEngineService {
private ExpressionParser parser = new SpelExpressionParser();
public double applyRule(String ruleExpression, Object contextObject) {
StandardEvaluationContext context = new StandardEvaluationContext(contextObject);
return parser.parseExpression(ruleExpression).getValue(context, Double.class);
}
}
在上述代码中,applyRule
方法接收一个规则表达式和一个上下文对象,通过StandardEvaluationContext
将上下文对象设置到表达式求值环境中,然后使用SpelExpressionParser
解析并执行规则表达式,返回计算结果。
通过将 Spring EL 与规则引擎相结合,我们可以实现一个灵活、可扩展的动态业务规则引擎,满足复杂业务系统中不断变化的业务规则需求。这种方式不仅提高了系统的灵活性和可维护性,还使得业务人员能够更加方便地管理和调整业务规则,而无需依赖开发人员进行代码修改。
在 Spring 应用中,配置注入是一项常见的操作,传统的配置注入方式往往较为繁琐,且缺乏灵活性。Spring EL 在配置注入方面提供了强大的功能,能够实现更灵活、高效的配置注入,同时结合外部配置文件和环境变量,进一步提升了配置的可管理性和可扩展性。
在使用@Value
注解进行配置注入时,Spring EL 可以解析各种表达式,从而实现更复杂的配置逻辑。例如,我们可以通过@Value
注解注入操作系统属性、表达式运算结果以及其他 bean 的属性等:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ConfigBean {
// 注入操作系统属性
@Value("#{systemProperties\['os.name']}")
private String osName;
// 注入表达式运算结果
@Value("#{T(java.lang.Math).random() \* 100}")
private double randomNumber;
// 注入其他bean的属性
@Value("#{otherBean.property}")
private String otherBeanProperty;
}
在上述代码中,#{systemProperties['os.name']}
用于注入操作系统的名称,#{T(java.lang.Math).random() * 100}
用于注入一个随机数,#{otherBean.property}
用于注入otherBean
的property
属性。通过这种方式,我们可以在配置注入时进行各种运算和逻辑判断,使配置更加灵活。
Spring EL 还可以与外部配置文件(如properties
或yaml
文件)以及环境变量相结合,实现配置的动态加载和替换。在application.properties
文件中,我们可以定义如下配置:
app.name=MyApp
app.version=1.0
然后在 Java 类中通过@Value
注解和 Spring EL 来获取这些配置:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class AppConfig {
@Value("\${app.name}")
private String appName;
@Value("\${app.version}")
private String appVersion;
}
在上述代码中,${app.name}
和${app.version}
用于从application.properties
文件中获取配置值。同时,Spring EL 还支持在表达式中引用环境变量,例如:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class EnvConfig {
@Value("#{systemEnvironment\['JAVA\_HOME']}")
private String javaHome;
}
在上述代码中,#{systemEnvironment['JAVA_HOME']}
用于获取系统环境变量JAVA_HOME
的值。通过结合外部配置文件和环境变量,Spring EL 使得配置注入更加灵活和可管理,能够满足不同环境和业务需求下的配置要求。
在电商系统中,促销活动的计算规则往往复杂多变,使用 Spring EL 表达式可以将这些复杂的计算逻辑以一种清晰、灵活的方式表达出来。以满减促销活动为例,假设我们有一个订单类Order
,其中包含订单金额amount
属性,促销活动的规则是订单金额满 500 元减 100 元,满 1000 元减 300 元。我们可以使用 Spring EL 表达式来实现这一计算逻辑:
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
public class Order {
private double amount;
public Order(double amount) {
this.amount = amount;
}
public double getAmount() {
return amount;
}
}
public class PromotionCalculator {
public static void main(String\[] args) {
Order order = new Order(800);
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext(order);
// 使用Spring EL表达式计算促销后的金额
String expression = "amount >= 1000? amount - 300 : amount >= 500? amount - 100 : amount";
double finalAmount = parser.parseExpression(expression).getValue(context, Double.class);
System.out.println("订单原始金额: " + order.getAmount());
System.out.println("促销后金额: " + finalAmount);
}
}
在上述代码中,首先创建了一个Order
对象并设置订单金额为 800 元。然后创建了SpelExpressionParser
解析器和StandardEvaluationContext
上下文,并将Order
对象设置为上下文的根对象。接着定义了一个 Spring EL 表达式,根据订单金额判断满足的促销条件并计算出促销后的金额。最后通过解析表达式并求值,得到最终的订单金额并输出。通过这种方式,当促销规则发生变化时,我们只需要修改表达式字符串,而不需要修改大量的 Java 代码,大大提高了代码的可维护性和灵活性。
在企业级应用中,权限管理是一个重要的功能,需要根据用户的角色和权限来动态控制对资源的访问。Spring EL 在 Spring Security 中有着广泛的应用,通过结合 Spring Security 和 Spring EL,我们可以实现基于表达式的权限动态校验,使权限管理更加灵活和强大。
假设我们有一个用户类User
,其中包含roles
属性,表示用户的角色集合。我们可以使用 Spring EL 表达式在 Spring Security 的配置中定义权限校验规则,例如:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.expression.WebSecurityExpressionRoot;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.http.HttpServletRequest;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 只有拥有ADMIN角色的用户才能访问/admin路径下的资源
.antMatchers("/admin/\*\*").access("@webSecurityExpressionRoot.hasRole('ADMIN')")
// 拥有USER角色的用户可以访问/user路径下的资源
.antMatchers("/user/\*\*").access("@webSecurityExpressionRoot.hasRole('USER')")
// 其他路径的资源允许所有用户访问
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.permitAll();
return http.build();
}
// 自定义WebSecurityExpressionRoot,用于在表达式中访问自定义方法
@Bean
public WebSecurityExpressionRoot webSecurityExpressionRoot(HttpServletRequest request, Authentication authentication) {
return new WebSecurityExpressionRoot(authentication, request);
}
}
在上述代码中,通过@EnableWebSecurity
注解开启 Spring Security 的功能。在filterChain
方法中,使用authorizeRequests
配置请求的权限规则。例如,对于/admin/**
路径的请求,使用@webSecurityExpressionRoot.hasRole('ADMIN')
表达式来判断用户是否拥有ADMIN
角色,只有拥有该角色的用户才能访问。对于/user/**
路径的请求,使用@webSecurityExpressionRoot.hasRole('USER')
表达式来判断用户是否拥有USER
角色。通过这种方式,我们可以根据用户的角色动态地控制对不同资源的访问权限,使权限管理更加灵活和细粒度。同时,通过自定义WebSecurityExpressionRoot
,我们还可以在表达式中访问自定义的方法,进一步扩展权限校验的逻辑。
在使用 Spring EL 表达式时,表达式的解析和求值是有一定开销的,特别是在频繁使用相同表达式的场景下,这种开销可能会对系统性能产生影响。为了提高性能,我们可以采用表达式缓存策略,避免重复解析相同的表达式。
Spring EL 本身并没有提供内置的表达式缓存机制,但我们可以通过自定义缓存来实现这一功能。例如,我们可以使用 Guava Cache 来缓存已解析的Expression
对象。下面是一个简单的示例:
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import java.util.concurrent.TimeUnit;
public class SpelExpressionCache {
private static final Cache\<String, Expression> EXPRESSION\_CACHE = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
private static final ExpressionParser PARSER = new SpelExpressionParser();
public static Expression getExpression(String expressionString) {
return EXPRESSION\_CACHE.get(expressionString, () -> PARSER.parseExpression(expressionString));
}
}
在上述代码中,我们使用CacheBuilder
创建了一个缓存EXPRESSION_CACHE
,设置最大缓存数量为 1000,缓存过期时间为 10 分钟。getExpression
方法首先尝试从缓存中获取已解析的Expression
对象,如果缓存中不存在,则使用SpelExpressionParser
解析表达式字符串,并将解析后的Expression
对象存入缓存。通过这种方式,当再次使用相同的表达式时,就可以直接从缓存中获取已解析的Expression
对象,避免了重复解析,从而提高了性能。
在使用 Spring EL 表达式的过程中,可能会遇到一些常见问题,下面我们将介绍如何处理这些问题。
空指针异常防护:在表达式中访问对象的属性或方法时,如果对象为null
,可能会抛出空指针异常。为了避免这种情况,我们可以使用?.
操作符进行安全访问。例如:
// 假设user可能为null
User user = null;
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("user", user);
// 使用安全访问模式,避免空指针异常
String name = parser.parseExpression("#user?.getName()").getValue(context, String.class);
System.out.println(name); // 输出null,不会抛出空指针异常
在上述代码中,#user?.getName()
表示如果#user
不为null
,则调用getName
方法,否则返回null
,从而避免了空指针异常的抛出。
性能敏感场景:在性能敏感的场景中,应避免在循环中重复解析表达式。因为表达式的解析是一个相对耗时的操作,在循环中重复解析会显著降低系统性能。例如,下面的代码在每次循环中都解析表达式,是一种不好的做法:
ExpressionParser parser = new SpelExpressionParser();
for (int i = 0; i < 1000; i++) {
Expression expression = parser.parseExpression("1 + 2");
int result = expression.getValue(Integer.class);
System.out.println(result);
}
为了优化性能,我们可以将表达式解析移到循环外部,只解析一次:
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。