背景
最近接到一个需求,需要收集系统产生的行为日志和用户操作行为的日志,有个详情字段,可以需要自定义扩展,按简单方式实现可以通过 AOP 注解,如果仅仅是这样实现的话,日志详情需要耦合在业务代码逻辑中,不太优雅,想着能不能直接有个表达式,通过方法参数组装,这时候想到了 SpEL 表达式机制
Spel 概述
Spring 表达式语言全称为“Spring Expression Language”,缩写为“SpEL”,类似于 Struts2x 中使用的 OGNL 表达式语言,能在运行时构建复杂表达式、存取对象图属性、对象方法调用等等,并且能与 Spring 功能完美整合,如能用来配置 Bean 定义。
一.SpEl 基础
简述:
** SpEL 是一个支持运行时查询和设置属性值、方法调用、访问数组、属性、构造器等的表达式语言。通过使用标准化语法,SpEL 集成了对象图导航、运算符重载、列表投影、选择和聚合等丰富特性。 **
示例:
- 简单写个 helloWorld
@Test
void testHelloWorld() {
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("('Hello' + ' World').concat(#end)");
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("end", "!");
System.out.println(expression.getValue(context));
}
** 最终输出 Hello World! **
简单分析下代码
**1)创建解析器:**SpEL 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默认实现;
2)解析表达式:使用 ExpressionParser 的 parseExpression 来解析相应的表达式为 Expression 对象。
3)构造上下文:准备比如变量定义等等表达式需要的上下文数据。
4)求值:通过 Expression 接口的 getValue 方法根据上下文获得表达式值。
- 方法调用
方法调用"指的是 SpEL 表达式中的一种能力,可以调用 Java 对象的方法。你可以在 SpEL 中编写表达式,这些表达式在评估时会调用 Java 对象的公共方法,并使用该方法的返回值。
方法调用的语法类似于 Java 中的方法调用,这是一个 SpEL 表达式调用方法的简单例子:
@Test
void testMethod() {
SpELTest spELTest = new SpELTest();
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
Expression expression = parser.parseExpression("#spELTest.add(5, 3)");
context.setVariable("spELTest",spELTest);
System.out.println("number = " + expression.getValue(context));
}
public int add(int number1, int number2) {
return number1+number2;
}
结果输出 8 需要注意被调用的方法需要是 public
二.SpEL 语法特性
- 字面量表达式
类型 | 示例 |
---|---|
字符串 | String str1 = parser.parseExpression("'Hello World!'").getValue(String.class); |
数字类型 | int int1 = parser.parseExpression("1").getValue(Integer.class);long long1 = parser.parseExpression("-1L").getValue(long.class);float float1 = parser.parseExpression("1.1").getValue(Float.class);double double1 = parser.parseExpression("1.1E+2").getValue(double.class);int hex1 = parser.parseExpression("0xa").getValue(Integer.class);long hex2 = parser.parseExpression("0xaL").getValue(long.class); |
布尔类型 | boolean true1 = parser.parseExpression("true").getValue(boolean.class);boolean false1 = parser.parseExpression("false").getValue(boolean.class); |
null 类型 | Object null1 = parser.parseExpression("null").getValue(Object.class); |
@Test
public void test2() {
ExpressionParser parser = new SpelExpressionParser();
String str1 = parser.parseExpression("'Hello World!'").getValue(String.class);
int int1 = parser.parseExpression("1").getValue(Integer.class);
long long1 = parser.parseExpression("-1L").getValue(long.class);
float float1 = parser.parseExpression("1.1").getValue(Float.class);
double double1 = parser.parseExpression("1.1E+2").getValue(double.class);
int hex1 = parser.parseExpression("0xa").getValue(Integer.class);
long hex2 = parser.parseExpression("0xaL").getValue(long.class);
boolean true1 = parser.parseExpression("true").getValue(boolean.class);
boolean false1 = parser.parseExpression("false").getValue(boolean.class);
Object null1 = parser.parseExpression("null").getValue(Object.class);
System.out.println("str1=" + str1);
System.out.println("int1=" + int1);
System.out.println("long1=" + long1);
System.out.println("float1=" + float1);
System.out.println("double1=" + double1);
System.out.println("hex1=" + hex1);
System.out.println("hex2=" + hex2);
System.out.println("true1=" + true1);
System.out.println("false1=" + false1);
System.out.println("null1=" + null1);
}
- 属性、数组、方法、构造函数
修改属性
@Test
void test5() {
User user = new User();
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("user", user);
user.setUsername("测试");
ExpressionParser parser = new SpelExpressionParser();
//使用.符号,访问user.car.name会报错,原因:user.car为空
try {
System.out.println(parser.parseExpression("#user.username").getValue(context, String.class));
} catch (EvaluationException | ParseException e) {
System.out.println("出错了:" + e.getMessage());
}
user.setUsername(null);
//使用安全访问符号?.,可以规避null错误
System.out.println(parser.parseExpression("#user.username?:'default'").getValue(context, String.class));
}
**输出 **<span class="ne-text">测试</span>``<span class="ne-text">default</span>
数组集合
@Test
void test7() {
ExpressionParser parser = new SpelExpressionParser();
//将返回不可修改的空List
List<Integer> result2 = parser.parseExpression("{}").getValue(List.class);
//对于字面量列表也将返回不可修改的List
List<Integer> result1 = parser.parseExpression("{1,2,3}").getValue(List.class);
System.out.println("result2 是空集合 " + result2.isEmpty());
try {
result1.set(0, 2);
} catch (Exception e) {
//对于字面量列表也将返回不可修改的List
System.out.println("result1 不可修改");
}
//对于列表中只要有一个不是字面量表达式,将只返回原始List,
//不会进行不可修改处理
String expression3 = "{{1+2,2+4},{3,4+4}}";
List<List<Integer>> result3 = parser.parseExpression(expression3).getValue(List.class);
result3.get(0).set(0, 1);
System.out.println(result3);
//声明二维数组并初始化
int[] result4 = parser.parseExpression("new int[2]{1,2}").getValue(int[].class);
System.out.println(result4[1]);
//定义一维数组并初始化
int[] result5 = parser.parseExpression("new int[1]").getValue(int[].class);
System.out.println(result5[0]);
}
- 类型操作
** instanceof 运算符**
@Test
void testInstanceOfExpression() {
ExpressionParser parser = new SpelExpressionParser();
Boolean value = parser.parseExpression("'啊啊啊啊啊啊' instanceof T(String)").getValue(Boolean.class);
System.out.println(value);
}
如果要引入方法静态方法 通过 T
类型字面量
@Test
void testClassTypeExpression() {
ExpressionParser parser = new SpelExpressionParser();
//java.lang包类访问
Class<String> result1 = parser.parseExpression("T(String)").getValue(Class.class);
System.out.println(result1);
//其他包类访问
String expression2 = "T(com.dtsw.tenant.api.config.security.SpELTest)";
Class<SpelTest> value = parser.parseExpression(expression2).getValue(Class.class);
System.out.println(value == SpelTest.class);
//类静态字段访问
int result3 = parser.parseExpression("T(Integer).MAX_VALUE").getValue(int.class);
System.out.println(result3 == Integer.MAX_VALUE);
//类静态方法调用
int result4 = parser.parseExpression("T(Integer).parseInt('1')").getValue(int.class);
System.out.println(result4);
}
class java.lang.String
false
true
**1
SpEL 运算符
算数运算表达式
SpEL 支持加(+)、减(-)、乘(*)、除(/)、求余(%)、幂(^)运算。
类型 | 示例 |
---|---|
加减乘除 | int result1 = parser.parseExpression("1+2-3*4/2").getValue(Integer.class);//-3 |
求余 | int result2 = parser.parseExpression("4%3").getValue(Integer.class);//1 |
幂运算 | int result3 = parser.parseExpression("2^3").getValue(Integer.class);//8 |
SpEL 还提供求余(MOD)和除(DIV)而外两个运算符,与“%”和“/”等价,不区分大小写。
关系表达式
**等于(==)、不等于(!=)、大于(>)、大于等于(>=)、小于(<)、小于等于(<=),区间(between)运算。**
如 parser.parseExpression("1>2").getValue(boolean.class);将返回 false;
而 parser.parseExpression("1 between {1, 2}").getValue(boolean.class);将返回 true。
between 运算符右边操作数必须是列表类型,且只能包含 2 个元素。第一个元素为开始,第二个元素为结束,区间运算是包含边界值的,即 xxx>=list.get(0) && xxx<=list.get(1)
SpEL 同样提供了等价的“EQ” 、“NE”、 “GT”、“GE”、 “LT” 、“LE”来表示等于、不等于、大于、大于等于、小于、小于等于,不区分大小写。
@Test
public void test3() {
ExpressionParser parser = new SpelExpressionParser();
boolean v1 = parser.parseExpression("1>2").getValue(boolean.class);
boolean between1 = parser.parseExpression("1 between {1,2}").getValue(boolean.class);
System.out.println("v1=" + v1);
System.out.println("between1=" + between1);
}
逻辑表达式
且(and 或者&&)、或(or 或者 ||)、非(!或 NOT)。
@Test
public void test4() {
ExpressionParser parser = new SpelExpressionParser();
boolean result1 = parser.parseExpression("2>1 and (!true or !false)").getValue(boolean.class);
boolean result2 = parser.parseExpression("2>1 && (!true || !false)").getValue(boolean.class);
boolean result3 = parser.parseExpression("2>1 and (NOT true or NOT false)").getValue(boolean.class);
boolean result4 = parser.parseExpression("2>1 && (NOT true || NOT false)").getValue(boolean.class);
System.out.println("result1=" + result1);
System.out.println("result2=" + result2);
System.out.println("result3=" + result3);
System.out.println("result4=" + result4);
}
字符串连接及截取表达式
使用“+”进行字符串连接,使用“'String'[0] [index]”来截取一个字符,目前只支持截取一个,如“'Hello ' + 'World!'”得到“Hello World!”;而“'Hello World!'[0]”将返回“H”。
三目运算
**三目运算符 **“表达式 1?表达式 2:表达式 3”用于构造三目运算表达式,如“2>1?true:false”将返回 true;
Elivis 运算符
Elivis 运算符“表达式 1?:表达式 2”从 Groovy 语言引入用于简化三目运算符的,当表达式 1 为非 null 时则返回表达式 1,当表达式 1 为 null 时则返回表达式 2,简化了三目运算符方式“表达式 1? 表达式 1:表达式 2”,如“null?:false”将返回 false,而“true?:false”将返回 true;
正则表达式
使用“str matches regex,如“'123' matches '\d{3}'”将返回 true;
括号优先级表达式
使用“(表达式)”构造,括号里的具有高优先级。
在 Spring 配置中使用 SpEL 在 Spring 配置中使用 SpEL
@Value 注解
- 字段注入
- 方法注入动态
动态配置
- Bean 定义条件化
- Bean 的创建
性能考虑
Spring Expression Language(SpEL)在表达式计算上有一定的性能开销,因为它涉及到动态语言特性,如反射和运行时解析。在性能敏感的应用中,使用 SpEL 可能需要考虑以下几点以优化性能:
- 缓存解析表达式 **:SpEL 表达式解析可以是一个昂贵的操作。一旦表达式被解析,它可以被缓存并重用,这样可以避免重复解析带来的性能开销。 **
- 避免过度使用 **:在性能关键的代码路径中避免过度使用 SpEL,因为每次计算表达式都会带来额外的开销。 **
- 预编译表达式 :Spring SpEL 支持预编译表达式到字节码,这可以显著提高执行速度。可以通过设置
<span class="ne-text">SpelCompilerMode</span>
来启用编译。 - 简化表达式 **:复杂的表达式需要更多的解析和计算时间。尽量简化 SpEL 表达式,并去掉不必要的复杂度。 **
- 优化上下文访问 **:减少在表达式中访问上下文中的变量,特别是如果这涉及到复杂的查找或是多次重复的操作。 **
- 适时的评估 **:仅在需要时才评估表达式,如果有可能的话,尽量在应用的生命周期中尽早计算并存储结果,而不是每次都重新计算。 **
- 监控和日志记录 **:对 SpEL 表达式的使用进行监控,记录表达式的评估时间,以便识别和优化潜在的性能瓶颈。 **
- 使用其他替代方案 **:在某些情况下,可以考虑使用其他方式替代 SpEL,比如直接 Java 方法调用或者使用其他表达式语言,这些可能有更好的性能。 **
- 合理的应用设计 **:在应用设计时就考虑到性能影响,尽可能地减少动态表达式的使用,特别是在循环或频繁调用的代码块中。 **
高级特性
Spring Expression Language (SpEL) 提供了一系列高级特性,允许你编写强大而灵活的表达式。以下是一些 SpEL 的高级特性:
- 复杂表达式 **: **
- 支持算术、关系和逻辑运算符。
- 字符串拼接、正则表达式匹配。
- 类型比较和实例化。
- 变量和属性访问 **: **
- 可以访问对象图中的属性、数组内容、列表元素、字典/映射内容。
- 支持变量定义并在表达式中使用。
- 方法调用 **: **
- 能够调用字符串、集合、数组上的方法。
- 支持自定义函数和对象的方法调用。
- 类型操作 **: **
- **使用 **
<span class="ne-text">T(Type)</span>
运算符访问类对象。 - 支持类的静态方法和属性调用。
- 构造函数调用 **: **
- **使用 **
<span class="ne-text">new</span>
关键字调用构造函数创建新对象。
- 集合选择(Projection and Selection) **: **
- 集合选择 (
<span class="ne-text">?.</span>
) 从集合中选择匹配特定条件的元素。 - 集合投影 (
<span class="ne-text">!.</span>
) 对集合中的每个元素应用表达式,并返回新的集合。
- 集合操作 **: **
- 表达式可以处理列表、集合、字典和数组类型。
- 内联列表/字典 **: **
- **使用 **
<span class="ne-text">{}</span>
语法直接在表达式中定义列表或字典。
- 条件表达式(三元运算符) **: **
- **支持 **
<span class="ne-text">(condition) ? trueExpression : falseExpression</span>
的条件逻辑。
- 模板表达式 **: **
- **使用 **
<span class="ne-text">#{expression}</span>
作为表达式占位符,可以嵌入在文本中。
- Bean 引用 **: **
- **可以通过 **
<span class="ne-text">@</span>
符号引用 Spring 容器中的 bean。
- 安全导航运算符 **: **
- **防止空指针异常的 **
<span class="ne-text">?.</span>
运算符。
- 集合过滤 **: **
- **使用 **
<span class="ne-text">?[selectionExpression]</span>
语法对集合进行过滤。
- 表达式链(Chaining) **: **
- 表达式可以被链接和嵌套。
- Lambda 表达式和闭包 **: **
- SpEL 支持 Lambda 表达式的定义和调用。
- 用户定义的函数 **: **
- 可以注册并在表达式中调用用户定义的函数。
- 流畅的 API **: **
- SpEL 的 API 支持链式调用,使得编写和组装表达式更加流畅。
常见问题
在使用 Spring Expression Language (SpEL) 时,可能会遇到一些常见的问题和挑战:
- 性能问题 **: **
- 如前所述,SpEL 可以产生性能开销,尤其是在大量计算表达式的情况下。为了提高性能,可以考虑缓存解析后的表达式,或避免在性能关键的路径中使用 SpEL。
- 复杂表达式的维护性 **: **
- 随着表达式变得越来越复杂,它们可能变得难以理解和维护。确保你的表达式简洁明了,必要时添加适当的注释。
- 安全风险 **: **
- 如果表达式的某些部分是由用户提供的,那么可能存在安全风险,因为用户可能注入恶意代码。确保对用户输入进行适当的验证和清理。
- 上下文管理 **: **
- 确保 SpEL 表达式中使用的所有对象和属性都在评估上下文中正确设置和可用。
- 类型转换错误 **: **
- 当 SpEL 表达式的结果类型与预期的类型不匹配时,会出现类型转换错误。要确保正确处理类型转换。
- 属性或方法不存在 **: **
- 如果尝试访问在上下文对象中不存在的属性或方法,将抛出异常。要确保所有引用的属性和方法都可访问。
- 权限限制 **: **
- 当使用 SpEL 访问类型、方法或属性时,需要相应的权限。例如,私有字段和方法默认无法通过 SpEL 访问。
- 不正确的字面量表示 **: **
- 字符串、数字、布尔值和其他字面量在 SpEL 中有特定的表示方法。不正确的表示可能导致解析错误。
- 嵌套属性访问问题 **: **
- 在访问嵌套属性时,如果中间的某个属性为
<span class="ne-text">null</span>
,可能会抛出空指针异常。使用安全导航运算符<span class="ne-text">?.</span>
可以避免这种情况。
- 集合处理错误 **: **
- 在处理集合类型时,确保使用正确的语法来引用元素、调用方法或进行过滤。
- 模板表达式限制 **: **
- 在模板表达式中,SpEL 表达式需要与文本内容适当分隔。如果分隔不当,可能导致解析错误。
为了解决这些常见问题:
- 充分了解和熟悉 SpEL 的语法和特性。
- 在开发和维护过程中编写单元测试来验证 SpEL 表达式的行为。
- 监控运行时表达式的评估性能,以便及时发现并解决性能问题。
- 采用好的编码实践,如避免使用过于复杂的表达式,保持代码的可读性和可维护性。
- 遵循安全最佳实践,不要允许未经验证的用户输入构成 SpEL 表达式部分,以防止潜在的注入攻击。
AOP+SPEL 业务使用
package com.dtsw.tenant.api.common;
import cn.hutool.core.text.CharSequenceUtil;
import com.dtsw.tenant.api.config.security.DtswUserDetails;
import com.dtsw.tenant.api.domain.audit.AuditDomainService;
import com.dtsw.tenant.api.domain.audit.common.AuditType;
import com.dtsw.tenant.api.domain.audit.dto.AuditDto;
import com.dtsw.tenant.api.domain.user.UserDomainService;
import com.dtsw.tenant.api.domain.user.dto.UserDto;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.*;
/**
* @Version 1.0
* @Author xiaohugg
* @Description UserApprovalAspect 收集审计信息,审计信息执行出错,不会影响到业务逻辑方法
* @Date 2023/11/21 16:58
**/
@Aspect
@Component
@RequiredArgsConstructor
public class AuditAspect {
private static final Logger logger = LoggerFactory.getLogger(AuditAspect.class);
private final ExpressionParser parser = new SpelExpressionParser();
private final AuditDomainService auditDomainService;
private final UserDomainService userDomainService;
private final ApplicationContext applicationContext;
/**
* 保证即使aop切面执行逻辑出现异常,不能影响到目标方法,只会影响到本次信息的收集,可能性很小,就基于日志打印排查
*
* @param joinPoint joinPoint
* @param auditAction 行为注解
* @return 业务方法返回值
* @throws Throwable 业务逻辑异常
*/
@Around("@annotation(auditAction)")
public Object logUserAction(ProceedingJoinPoint joinPoint, AuditAction auditAction) throws Throwable {
String paramValue = "";
boolean success = true;
//如果是 expression 解析错误或者aop方法解析错误,不是业务出错,本次信息不收集
boolean isParseError = false;
try {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = signature.getParameterNames();
Object[] parameterValues = joinPoint.getArgs();
StandardEvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], parameterValues[i]);
}
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
String paramExp = auditAction.expression();
if (CharSequenceUtil.isNotEmpty(paramExp)) {
Expression exp = parser.parseExpression(paramExp);
paramValue = exp.getValue(context, String.class);
}
} catch (Exception e) {
isParseError = true;
logger.error("Exception occurred while parsing parameters for auditing {}", e.getMessage());
}
//保证即使aop切面执行逻辑出现异常,不能影响到目标方法,只会影响到本次信息的收集,可能性很小,就基于日志打印排查
try {
return joinPoint.proceed();
} catch (Throwable ex) {
success = false;
doCatchFailure(auditAction, ex, isParseError, paramValue);
// 抛出业务目标方法异常
throw ex;
} finally {
doCatchSuccess(auditAction, success, isParseError, paramValue);
}
}
private void doCatchSuccess(AuditAction auditAction, boolean success, boolean isParseError, String paramValue) {
if (success && !isParseError) {
try {
logUserActionSuccess(auditAction, paramValue);
} catch (Exception e) {
logger.error("收集审计信息失败,失败的原因 {}, 操作对象 {}", e.getMessage(), paramValue);
}
}
}
private void doCatchFailure(AuditAction auditAction, Throwable ex, boolean isParseError, String paramValue) {
try {
if (!isParseError) {
logUserActionFailure(auditAction, ex, paramValue);
}
} catch (Exception e) {
logger.error("收集审计信息失败,失败的原因 {}, 操作对象 {} ", e.getMessage(), paramValue);
}
}
public void logUserActionSuccess(AuditAction auditAction, String returnValue) {
String operator = getCurrentUsername(auditAction, returnValue);
String detail = buildDetail(auditAction.actionType(), returnValue);
doSave(detail, operator, auditAction.actionType(), true, null, auditAction.systemAudit());
}
public void logUserActionFailure(AuditAction auditAction, Throwable exception, String paramValue) {
String operator = getCurrentUsername(auditAction, paramValue);
String detail = buildDetail(auditAction.actionType(), paramValue);
doSave(detail, operator, auditAction.actionType(), false, exception.getMessage(), auditAction.systemAudit());
}
private String buildDetail(AuditType actionType, String additionalInfo) {
return String.format("%s %s",
actionType.getMessage(), additionalInfo == null ? "" : additionalInfo);
}
private void doSave(String message, String operator, AuditType type, boolean isSuccess, String errorMessage, boolean systemLog) {
//通过用户名获取租户名称
String tenant = null;
if (CharSequenceUtil.isNotEmpty(operator)) {
UserDto userDto = userDomainService.findByUsername(operator).orElseThrow(() -> new IllegalArgumentException("用户不存在: " + operator));
tenant = userDto.getTenant();
}
AuditDto auditDto = AuditDto.create(type, isSuccess, message, tenant, operator, systemLog ? null : IpAddressUtils.getIpByRequest(), errorMessage);
auditDomainService.save(auditDto, systemLog);
}
private String getCurrentUsername(AuditAction auditAction, String returnValue) {
if (auditAction.actionType() == AuditType.LOGIN) {
return returnValue.split("@")[0];
}
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.map(UsernamePasswordAuthenticationToken.class::cast)
.map(UsernamePasswordAuthenticationToken::getPrincipal)
.map(DtswUserDetails.class::cast)
.map(DtswUserDetails::getUsername).orElse(null);
}
}
@Override
@AuditAction(actionType = AuditType.RETRY_TRANSACTION, expression = "@transactionMessageServiceImpl.generateAuditDetail(#ids)")
public void retryMsg(List<Integer> ids) {
transactionMessageDomainService.retryMsg(ids);
}
@Override
@AuditAction(actionType = AuditType.SKIP_TRANSACTION, expression = "@transactionMessageServiceImpl.generateAuditDetail(#ids)")
public void skipMsg(List<Integer> ids) {
transactionMessageDomainService.skipMsg(ids);
}
@Override
public String generateAuditDetail(String idStr) {
List<Integer> ids = Stream.of(Objects.requireNonNull(idStr).split(",")).map(Integer::valueOf).toList();
List<PageTransactionMsgParam> transactionMessage = transactionMessageDomainService.findByIds(ids);
if (CollectionUtils.isEmpty(transactionMessage)) {
throw new IllegalArgumentException("获取不到事务信息,参数异常: " + idStr);
}
StringJoiner stringJoiner = new StringJoiner("\n");
transactionMessage.forEach(item -> stringJoiner.add(item.toString()));
return stringJoiner.toString();
}
总结
- Spel 功能还是比较强大的,可以脱离 Spring 环境独立运行
- spel 可以用在一些动态规则的匹配方面,比如监控系统中监控规则的动态匹配;其他的一些条件动态判断等等
- 但也有一些常见问题,性能问题,安全问题,需要结合自己的业务使用