文章 62
浏览 15135
spring SpEL 结合AOP玩出花样

spring SpEL 结合AOP玩出花样

image.png

背景

最近接到一个需求,需要收集系统产生的行为日志和用户操作行为的日志,有个详情字段,可以需要自定义扩展,按简单方式实现可以通过 AOP 注解,如果仅仅是这样实现的话,日志详情需要耦合在业务代码逻辑中,不太优雅,想着能不能直接有个表达式,通过方法参数组装,这时候想到了 SpEL 表达式机制

Spel 概述

Spring 表达式语言全称为“Spring Expression Language”,缩写为“SpEL”,类似于 Struts2x 中使用的 OGNL 表达式语言,能在运行时构建复杂表达式、存取对象图属性、对象方法调用等等,并且能与 Spring 功能完美整合,如能用来配置 Bean 定义。

一.SpEl 基础

简述:

** SpEL 是一个支持运行时查询和设置属性值、方法调用、访问数组、属性、构造器等的表达式语言。通过使用标准化语法,SpEL 集成了对象图导航、运算符重载、列表投影、选择和聚合等丰富特性。 **

示例:

  1. 简单写个 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 方法根据上下文获得表达式值。

  1. 方法调用

方法调用"指的是 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 可能需要考虑以下几点以优化性能:

  1. 缓存解析表达式 **:SpEL 表达式解析可以是一个昂贵的操作。一旦表达式被解析,它可以被缓存并重用,这样可以避免重复解析带来的性能开销。 **
  2. 避免过度使用 **:在性能关键的代码路径中避免过度使用 SpEL,因为每次计算表达式都会带来额外的开销。 **
  3. 预编译表达式 :Spring SpEL 支持预编译表达式到字节码,这可以显著提高执行速度。可以通过设置 <span class="ne-text">SpelCompilerMode</span> 来启用编译。
  4. 简化表达式 **:复杂的表达式需要更多的解析和计算时间。尽量简化 SpEL 表达式,并去掉不必要的复杂度。 **
  5. 优化上下文访问 **:减少在表达式中访问上下文中的变量,特别是如果这涉及到复杂的查找或是多次重复的操作。 **
  6. 适时的评估 **:仅在需要时才评估表达式,如果有可能的话,尽量在应用的生命周期中尽早计算并存储结果,而不是每次都重新计算。 **
  7. 监控和日志记录 **:对 SpEL 表达式的使用进行监控,记录表达式的评估时间,以便识别和优化潜在的性能瓶颈。 **
  8. 使用其他替代方案 **:在某些情况下,可以考虑使用其他方式替代 SpEL,比如直接 Java 方法调用或者使用其他表达式语言,这些可能有更好的性能。 **
  9. 合理的应用设计 **:在应用设计时就考虑到性能影响,尽可能地减少动态表达式的使用,特别是在循环或频繁调用的代码块中。 **

高级特性

Spring Expression Language (SpEL) 提供了一系列高级特性,允许你编写强大而灵活的表达式。以下是一些 SpEL 的高级特性:

  1. 复杂表达式 **: **
  • 支持算术、关系和逻辑运算符。
  • 字符串拼接、正则表达式匹配。
  • 类型比较和实例化。
  1. 变量和属性访问 **: **
  • 可以访问对象图中的属性、数组内容、列表元素、字典/映射内容。
  • 支持变量定义并在表达式中使用。
  1. 方法调用 **: **
  • 能够调用字符串、集合、数组上的方法。
  • 支持自定义函数和对象的方法调用。
  1. 类型操作 **: **
  • **使用 **<span class="ne-text">T(Type)</span> 运算符访问类对象。
  • 支持类的静态方法和属性调用。
  1. 构造函数调用 **: **
  • **使用 **<span class="ne-text">new</span> 关键字调用构造函数创建新对象。
  1. 集合选择(Projection and Selection) **: **
  • 集合选择 (<span class="ne-text">?.</span>) 从集合中选择匹配特定条件的元素。
  • 集合投影 (<span class="ne-text">!.</span>) 对集合中的每个元素应用表达式,并返回新的集合。
  1. 集合操作 **: **
  • 表达式可以处理列表、集合、字典和数组类型。
  1. 内联列表/字典 **: **
  • **使用 **<span class="ne-text">{}</span> 语法直接在表达式中定义列表或字典。
  1. 条件表达式(三元运算符) **: **
  • **支持 **<span class="ne-text">(condition) ? trueExpression : falseExpression</span> 的条件逻辑。
  1. 模板表达式 **: **
  • **使用 **<span class="ne-text">#{expression}</span> 作为表达式占位符,可以嵌入在文本中。
  1. Bean 引用 **: **
  • **可以通过 **<span class="ne-text">@</span> 符号引用 Spring 容器中的 bean。
  1. 安全导航运算符 **: **
  • **防止空指针异常的 **<span class="ne-text">?.</span> 运算符。
  1. 集合过滤 **: **
  • **使用 **<span class="ne-text">?[selectionExpression]</span> 语法对集合进行过滤。
  1. 表达式链(Chaining) **: **
  • 表达式可以被链接和嵌套。
  1. Lambda 表达式和闭包 **: **
  • SpEL 支持 Lambda 表达式的定义和调用。
  1. 用户定义的函数 **: **
  • 可以注册并在表达式中调用用户定义的函数。
  1. 流畅的 API **: **
  • SpEL 的 API 支持链式调用,使得编写和组装表达式更加流畅。

常见问题

在使用 Spring Expression Language (SpEL) 时,可能会遇到一些常见的问题和挑战:

  1. 性能问题 **: **
  • 如前所述,SpEL 可以产生性能开销,尤其是在大量计算表达式的情况下。为了提高性能,可以考虑缓存解析后的表达式,或避免在性能关键的路径中使用 SpEL。
  1. 复杂表达式的维护性 **: **
  • 随着表达式变得越来越复杂,它们可能变得难以理解和维护。确保你的表达式简洁明了,必要时添加适当的注释。
  1. 安全风险 **: **
  • 如果表达式的某些部分是由用户提供的,那么可能存在安全风险,因为用户可能注入恶意代码。确保对用户输入进行适当的验证和清理。
  1. 上下文管理 **: **
  • 确保 SpEL 表达式中使用的所有对象和属性都在评估上下文中正确设置和可用。
  1. 类型转换错误 **: **
  • 当 SpEL 表达式的结果类型与预期的类型不匹配时,会出现类型转换错误。要确保正确处理类型转换。
  1. 属性或方法不存在 **: **
  • 如果尝试访问在上下文对象中不存在的属性或方法,将抛出异常。要确保所有引用的属性和方法都可访问。
  1. 权限限制 **: **
  • 当使用 SpEL 访问类型、方法或属性时,需要相应的权限。例如,私有字段和方法默认无法通过 SpEL 访问。
  1. 不正确的字面量表示 **: **
  • 字符串、数字、布尔值和其他字面量在 SpEL 中有特定的表示方法。不正确的表示可能导致解析错误。
  1. 嵌套属性访问问题 **: **
  • 在访问嵌套属性时,如果中间的某个属性为 <span class="ne-text">null</span>,可能会抛出空指针异常。使用安全导航运算符 <span class="ne-text">?.</span> 可以避免这种情况。
  1. 集合处理错误 **: **
  • 在处理集合类型时,确保使用正确的语法来引用元素、调用方法或进行过滤。
  1. 模板表达式限制 **: **
  • 在模板表达式中,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();
    }

image.png

image.png

总结

  1. Spel 功能还是比较强大的,可以脱离 Spring 环境独立运行
  2. spel 可以用在一些动态规则的匹配方面,比如监控系统中监控规则的动态匹配;其他的一些条件动态判断等等
  3. 但也有一些常见问题,性能问题,安全问题,需要结合自己的业务使用

标题:spring SpEL 结合AOP玩出花样
作者:xiaohugg
地址:https://xiaohugg.top/articles/2023/11/22/1700638861003.html

人民有信仰 民族有希望 国家有力量