动态编程的利器 —— Spring 3.0 新特性:EL表达式

一、Spring EL 基础入门

1.1 什么是 Spring EL

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 来定义切点表达式,精确地控制切面的作用范围。

1.2 核心组件解析

Spring EL 的核心组件主要包括ExpressionParserExpressionEvaluationContext,它们相互协作,共同实现了 Spring EL 强大的表达式解析和求值功能。

ExpressionParserExpressionParser是 Spring EL 的表达式解析器,它的主要职责是将我们编写的表达式字符串解析成可执行的Expression对象。在使用 Spring EL 时,我们首先需要创建一个ExpressionParser实例,通常使用SpelExpressionParser类来实现。例如:

ExpressionParser parser = new SpelExpressionParser();

ExpressionExpression接口代表了一个已解析的表达式,它提供了getValue方法用于对表达式进行求值。当ExpressionParser将表达式字符串解析成Expression对象后,我们就可以通过调用getValue方法来获取表达式的计算结果。getValue方法可以接受一个EvaluationContext参数,用于提供表达式求值时所需的上下文信息。例如:

Expression expression = parser.parseExpression("1 + 2");

int result = expression.getValue(Integer.class);

System.out.println(result); // 输出3

EvaluationContextEvaluationContext提供了表达式求值的上下文环境,它包含了属性解析器、方法解析器和类型转换器等组件,用于在表达式求值过程中解析对象的属性、方法以及进行类型转换。在实际应用中,我们通常使用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 应用程序中轻松地进行各种复杂的表达式操作。

二、基础用法详解

2.1 表达式基础语法

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

布尔值直接使用truefalse表示,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

2.2 安全访问模式

在实际应用中,我们经常会遇到访问对象属性或方法时,对象可能为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,不会抛出空指针异常

在上述代码中,由于usernull,如果不使用安全访问模式,直接调用#user.getName()会抛出空指针异常。而使用#user?.getName(),则会安全地返回null,避免了程序因空指针异常而崩溃。这种安全访问模式在处理复杂对象图时非常有用,可以大大提高代码的健壮性和稳定性。

2.3 集合操作技巧

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。

三、高级应用场景

3.1 动态业务规则引擎

在复杂的业务系统中,业务规则往往需要根据不同的条件和场景进行动态调整,传统的硬编码方式难以满足这种灵活性需求。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 与规则引擎相结合,我们可以实现一个灵活、可扩展的动态业务规则引擎,满足复杂业务系统中不断变化的业务规则需求。这种方式不仅提高了系统的灵活性和可维护性,还使得业务人员能够更加方便地管理和调整业务规则,而无需依赖开发人员进行代码修改。

3.2 配置注入优化

在 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}用于注入otherBeanproperty属性。通过这种方式,我们可以在配置注入时进行各种运算和逻辑判断,使配置更加灵活。

Spring EL 还可以与外部配置文件(如propertiesyaml文件)以及环境变量相结合,实现配置的动态加载和替换。在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 使得配置注入更加灵活和可管理,能够满足不同环境和业务需求下的配置要求。

四、实战案例解析

4.1 电商促销计算

在电商系统中,促销活动的计算规则往往复杂多变,使用 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 代码,大大提高了代码的可维护性和灵活性。

4.2 权限动态校验

在企业级应用中,权限管理是一个重要的功能,需要根据用户的角色和权限来动态控制对资源的访问。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,我们还可以在表达式中访问自定义的方法,进一步扩展权限校验的逻辑。

五、性能优化与注意事项

5.1 表达式缓存策略

在使用 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对象,避免了重复解析,从而提高了性能。

5.2 常见问题处理

在使用 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删除。

编辑于

关注时代Java

关注时代Java