乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

SpEL — Spring 表达式语言

一句话定位:SpEL(Spring Expression Language)是 Spring 框架内置的"动态计算器",让你在 XML、注解或代码中编写表达式,在运行时查询对象属性、调用方法、操作集合,甚至做逻辑判断。


为什么需要 SpEL?

想象乐途公司的小崔在配置邮件服务时,需要根据不同环境动态设置 SMTP 地址。如果没有 SpEL,他只能写死配置或写大量 if-else。有了 SpEL,一行表达式搞定:

@Value("#{environment['spring.profiles.active'] == 'prod' ? 'smtp.learnto.cn' : 'localhost'}")
private String smtpHost;

SpEL 贯穿 Spring 的各个角落:

  • @Value("#{...}") 注入动态值
  • Spring Security 的 @PreAuthorize("hasRole('ADMIN')")
  • 缓存注解的 key = "#userId"
  • XML 配置中的条件属性

核心语法速览

SpEL 表达式以 #{} 包裹(在 @Value 中),支持以下操作:

类型示例结果
字面量'Hello SpEL'字符串
属性访问#user.name访问 user 对象的 name 属性
方法调用#user.getName()调用方法
静态方法T(java.lang.Math).random()调用 Math.random()
运算符#age > 18 ? '成年' : '未成年'三元运算
正则#email matches '[a-z]+@learnto.cn'正则匹配
集合#orders.?[status == 'PAID']过滤集合
投影#orders.![totalAmount]提取字段形成新列表
选择#orders.^[status == 'PAID']找第一个匹配元素

在 Spring 中的使用场景

@Value 注入

@Service
public class FeixiangConfigService {

    // 注入系统属性
    @Value("#{systemProperties['os.name']}")
    private String osName;

    // 注入环境变量
    @Value("#{systemEnvironment['HOME']}")
    private String homePath;

    // 条件表达式:生产环境用真实网关,测试用假网关
    @Value("#{${feixiang.payment.mock:false} ? 'fakeGateway' : 'alipayGateway'}")
    private String gatewayBeanName;

    // 数学计算
    @Value("#{T(java.lang.Math).PI * 2}")
    private double twoPi;
}

缓存 Key 生成

乐途公司的商品详情页访问量巨大,小崔加了缓存:

@Cacheable(value = "product", key = "#productId + '_' + #region")
public Product getProduct(String productId, String region) {
    return productRepository.findById(productId);
}

@CacheEvict(value = "product", key = "#product.id + '_*'")
public void updateProduct(Product product) {
    productRepository.save(product);
}

安全权限控制

@RestController
public class SalaryController {

    // 只有 HR 或本人能查看工资
    @PreAuthorize("hasRole('HR') or #userId == authentication.principal.id")
    @GetMapping("/api/salary/{userId}")
    public Salary getSalary(@PathVariable Long userId) {
        return salaryService.findByUserId(userId);
    }

    // CEO 大翔能看所有人的工资
    @PreAuthorize("hasRole('CEO')")
    @GetMapping("/api/salary/all")
    public List<Salary> getAllSalaries() {
        return salaryService.findAll();
    }
}

编程式使用

除了注解,SpEL 也可以在代码中直接调用:

@Service
public class PromotionEngine {

    private final ExpressionParser parser = new SpelExpressionParser();
    private final StandardEvaluationContext context;

    public PromotionEngine() {
        this.context = new StandardEvaluationContext();
        // 注册自定义函数
        try {
            context.registerFunction("discount",
                PromotionEngine.class.getDeclaredMethod("calculateDiscount", double.class, int.class));
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

    // 自定义折扣函数:满 100 减 10,满 200 减 30
    public static double calculateDiscount(double amount, int quantity) {
        if (amount >= 200) return 30;
        if (amount >= 100) return 10;
        return 0;
    }

    public double evaluatePromotion(Order order, String expression) {
        // 将 order 设为根对象
        context.setRootObject(order);
        // 也可以设置变量
        context.setVariable("vipLevel", order.getUser().getVipLevel());

        Expression exp = parser.parseExpression(expression);
        return exp.getValue(context, Double.class);
    }
}

乐途公司双十一促销,小崔用 SpEL 让运营自己写促销规则:

// 运营配置的规则表达式
String rule = "#vipLevel >= 3 ? totalAmount * 0.8 : totalAmount * 0.95";

Order order = new Order();
order.setTotalAmount(188.88f * 2);  // 买两把乐途机械键盘
order.setUser(new User(2024001L, 3));  // 钻石会员

double finalAmount = promotionEngine.evaluatePromotion(order, rule);
// 结果:377.76 * 0.8 = 302.208

集合操作

SpEL 对集合的支持非常强大:

// 过滤:找出所有已支付订单
List<Order> paidOrders = parser.parseExpression(
    "#orders.?[status == T(com.feixiang.order.OrderStatus).PAID]"
).getValue(context, List.class);

// 投影:提取所有订单金额
List<Float> amounts = parser.parseExpression(
    "#orders.![totalAmount]"
).getValue(context, List.class);

// 选择第一个匹配
Order firstPaid = parser.parseExpression(
    "#orders.^[status == T(com.feixiang.order.OrderStatus).PAID]"
).getValue(context, Order.class);

// 选择最后一个匹配
Order lastPaid = parser.parseExpression(
    "#orders.$[status == T(com.feixiang.order.OrderStatus).PAID]"
).getValue(context, Order.class);

注意事项

注意点说明
安全性不要直接执行用户输入的 SpEL 表达式,存在代码注入风险。使用 SimpleEvaluationContext 限制权限
性能频繁执行的表达式应编译缓存(Spring 4.1+ 支持 SpelCompilerMode.IMMEDIATE)
类型转换SpEL 自动进行类型转换,但复杂场景建议显式指定 getValue(context, TargetType.class)
空安全使用 ?. 安全导航:#user?.address?.city,避免 NPE
与 @Value("${...}") 的区别${...} 是占位符解析(PropertyPlaceholder),#{...} 是 SpEL 表达式。两者可嵌套:@Value("${app.name:feixiang}") vs @Value("#{systemProperties['user.name']}")

常见面试题

Q1:@Value("${key}") 和 @Value("#{expression}") 有什么区别?

${...} 是属性占位符解析,从 Environment / PropertySource 中查找 key 对应的值,不支持运算。#{...} 是 SpEL 表达式,支持属性访问、方法调用、运算、集合操作等完整表达式能力。两者可组合使用:@Value("#{${app.timeout} * 1000}")。

Q2:SpEL 的 EvaluationContext 是什么?

EvaluationContext 是 SpEL 表达式的执行上下文,提供变量解析、方法解析、类型转换等服务。StandardEvaluationContext 功能最全但权限大;SimpleEvaluationContext 受限但更安全,适合解析不可信表达式。

Q3:如何在 SpEL 中调用 Spring Bean 的方法?

使用 @beanName.method() 语法,例如 @orderService.findById(8888)。需要在 BeanResolver 可用的上下文中执行。

Q4:SpEL 表达式编译模式有什么用?

默认 SpEL 是解释执行,每次都要解析 AST。开启 SpelCompilerMode.IMMEDIATE 后,表达式会被编译成 JVM 字节码,执行速度提升 10~100 倍,适合高频执行的表达式(如缓存 key 生成)。

上一页
@EventListener
下一页
校验 Validation — JSR-303 / JSR-380 Bean Validation