基于AOP+ Spel实现优雅记录日志实战
2024-10-30 09:21 阅读(238)

基本概念

Spring表达式语言(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。

虽然SpEL作为Spring产品组合中表达式评估的基础,但并不直接与Spring绑定,可以独立使用。为了自包含,本章中的许多示例将SpEL用作独立表达式语言。

基本使用

最简单的case:

// 创建一个解析器
ExpressionParser parser = new SpelExpressionParser();
// 解析一个表达式
Expression exp = parser.parseExpression("'Hello World'");
// 通过表达式获取值
String message = (String) exp.getValue();

稍微复杂:调用方法


ExpressionParser parser = new SpelExpressionParser();
// 调用字符串的方法
Expression exp = parser.parseExpression("'Hello World'.concat('!')");
String message = (String) exp.getValue();

再次进阶:读取外部bean的属性、方法


// 创建并设置日历
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);

// 构造函数参数分别为姓名、生日和国籍。
Inventor tesla = new Inventor("尼古拉·特斯拉", c.getTime(), "塞尔维亚人");

ExpressionParser parser = new SpelExpressionParser();

Expression exp = parser.parseExpression("name"); // 将姓名解析为表达式
String name = (String) exp.getValue(tesla);
// name == "尼古拉·特斯拉"

exp = parser.parseExpression("name == '尼古拉·特斯拉'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true

理解 EvaluationContext

EvaluationContext 接口用于在评估表达式时解析属性、方法或字段,并帮助执行类型转换。Spring 提供了两种实现。



SimpleEvaluationContext:公开了一组基本的 SpEL 语言特性和配置选项,适用于不需要 SpEL 语言语法的全部功能并且应该有意义地受限制的表达式类别。示例包括但不限于数据绑定表达式和基于属性的过滤器。



StandardEvaluationContext:公开了完整的 SpEL 语言特性和配置选项。您可以使用它指定默认根对象,并配置每个可用的与评估相关的策略



另外,可以通过StandardEvaluationContext进行变量setVariable、函数的注入registerFunction,实现Spel表达式可以进行读取。

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted",
		MethodType.methodType(String.class, Object[].class));
context.setVariable("message", mh);

// 评估为 "Simple message: <Hello World>"
String message = parser.parseExpression("#message('Simple message: <%s>', 'Hello World', 'ignored')")
		.getValue(context, String.class);

实战

依赖

引入基本的依赖

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
}

代码实现

日志注解

@Target(ElementType.METHOD)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    String value();
}

切面实现

@Component
@Aspect
public class EvaluationContextAOP {
    private final Logger logger = LoggerFactory.getLogger(EvaluationContextAOP.class);
    private final DefaultParameterNameDiscoverer nameDiscovery = new DefaultParameterNameDiscoverer();
    private final ExpressionParser parser = new SpelExpressionParser();
    // 表达式缓存
    private final Map<Method, Expression> expressionCache = new ConcurrentHashMap<>();
    // 函数缓存
    private final Map<String, Method> functionCache = new ConcurrentHashMap<>();

    @Around("@annotation(cn.mj.springboottest.log.Log)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        boolean skip = false;
        Object target = joinPoint.getTarget();
        Object[] args = joinPoint.getArgs();
        String methodName = joinPoint.getSignature().getName();
        Class<?>[] argTypes = new Class[args.length];
        for (int i = 0; i < args.length; i++) {
            argTypes[i] = args[i].getClass();
        }
        Method method = ReflectionUtils.findMethod(target.getClass(), methodName, argTypes);
        Log annotation = AnnotationUtils.findAnnotation(method, Log.class);
        String expr = annotation.value();
        Expression expression = null;
        if (expr.isEmpty()){
            skip = true;
        }else{
            expression = expressionCache.computeIfAbsent(method, k -> parser.parseExpression(expr));
        }
        Object result = null;
        Throwable throwable = null;
        try {
            result = joinPoint.proceed();

        }catch (Throwable e) {
            throwable = e;
            throw e;
        }finally {
            if (!skip){
                StandardEvaluationContext context = new MethodBasedEvaluationContext(target, method, args, nameDiscovery);
                // 注册函数可以进行使用
//                context.registerFunction();
                context.setVariable("result",result);
                String logContent = expression.getValue(context, String.class);
                logger.info(logContent);
            }
        }
        return result;
    }
}

使用注解进行标记

@RestController
@Validated
public class TestController {

    @Resource
    TestService testService;
    @PostMapping("/test")
    @Log("'测试:name is ' + #testBean.name")
    public ResponseEntity<String> test(@Validated @RequestBody  TestBean testBean,
                                       @RequestParam("id") @NotNull(message = "id不能为空") @Min(value = 1,message = "id必须大于1") Long id)
    {
        testService.test(new TestBean("",1));
        return ResponseEntity.ok("ok");
    }
    @GetMapping("/query")
    @Log("'测试:result name is ' + #result.name")
    public TestBean query(){
        return new TestBean("query",1);
    }

}

总结

以上基于AOP+Spel实现了一个丐版的优雅日志记录工具,在实际应用中可能还需要记录其他的信息:日志记录、异常、执行时长、链路日志等等,都可以基于此进行扩展(定义一个日志Bean、使用ThreadLocal等等实现)。