写点什么

硬核资源!清华博士的 Spring Boot 中 AOP 与 SpEL 笔记,码农:膜拜

发布于: 2021 年 05 月 06 日
硬核资源!清华博士的Spring Boot中AOP与SpEL笔记,码农:膜拜

该文章是介绍《Spring Boot 中 AOP 与 SpEL 的应用》,根据我以前的学习笔记以及工作中长时间积累所整理的一份笔记,纯干货,希望对大家有帮助。


Spring Boot 中 AOP 与 SpEL 的应用

定义

我不想在这里去摘抄百度百科的内容, 以下内容纯属个人理解:

  • AOP: 面向切面编程?NO, 我们低端点, 它就是一个非常厉害的装饰器, 可以和业务逻辑平行运行, 适合处理一些日志记录/权限校验等操作

  • SpEL: 全称 SpEL 表达式, 可以理解为 JSP 的超级加强版, 使用得当可以为我们节省代码(此话为抄袭), 大家使用它最多的地方其实是引入配置, 例如:

// 你看我熟悉不?@Value("#{file.root}")复制代码
复制代码

那么什么时候会一起使用它们呢?

其实很多经验丰富的大佬们下意识就能回答, 记录系统日志

没错, AOP 是与业务逻辑平行, SpEL 是与业务数据平行, 把它们结合起来, 就能让我们在传统的面向对象/过程编程的套路中更上一层楼

接下来我就用一个实际的记录业务日志功能的实现来记录如何在 Spring Boot 中使用 AOP 与 SpEL

了解它们

想要使用它们, 作为先行者列出其中的重点是个人义务, 我们先来看看其中需要特别在意的几点概念:

AOP

明确概念:

@Aspect: 切面

@Poincut: 切点

JoinPoint: 普通连接点

ProceedingJoinPoint: 环绕连接点

切入时机:

before: 目标方法开始执行之前

after: 目标方法开始执行之后

afterReturning: 目标方法返回之后

afterThrowing: 目标方法抛出异常之后

around: 环绕目标方法, 最为特殊

我们用代码来展示下各个切入时机的位置:

try{    try{        @around        @before        method.invoke();        @around    }catch(){        throw new Exception();    }finally{        @after    }    @afterReturning}catch(){    @afterThrowing}
复制代码

其中的 around 是最为特殊的切入时机, 它的切入点也必须为 ProceedingJoinPoint, 其它均为 JoinPoint

我们需要手动调用 ProceedingJoinPoint 的 proceed 方法, 它会去执行目标方法的业务逻辑

around 最麻烦, 却也是最强的

SpEL

要使用 SpEL, 肯定难不住每一位小伙伴, 但它到底是如何从一个简单的文字表达式转换为运行中的数据内容呢?

其实 Spring 和 Java 已经提供了大部分功能, 我们只需要手动处理如下部分:

  1. TemplateParserContext: 表达式解析模板, 即如何提取 SpEL 表达式

  2. RootObject: 业务数据内容, SpEL 表达式解析过程中需要的数据都从这其中获取

  3. ParameterNameDiscoverer: 参数解析器, 在 SpEL 解析的过程中, 尝试从 rootObject 中直接获取数据

  4. EvaluationContext: 解析上下文, 包含 RootObject, ParameterNameDiscoverer 等数据, 是整个 SpEL 解析的环境

那么 SpEL 的过程我们可以粗略概括为:

设计 RootObject->设计 SpEL 表达式的运行上下文->设计 SpEL 解析器(包括表达式解析模板和参数解析器)

实践出真知

业务场景

实现一个业务日志记录的功能, 需要记录如下内容:

  • 功能模块

  • 业务描述, 包含业务数据 id

  • 目标方法详情

  • 目标类详情

  • 入参

  • 返回值

  • 用户信息, 如用户 id/ip 等

一目了然, 这些数据都需要在运行的过程中进行获取, 还记得吗?在刚才的介绍里, 运行这个状态词是 AOP 和 SpEL 的特点

所以, 我们将使用 AOP 和 SpEL, 来完成这个需求

业务分析

仔细观察需要记录的数据内容, 我们可以分析它们从那里得到:

  • 功能模块: 通过 AOP 中切入点的注解获得

  • 业务描述: 将 SpEL 表达式写入 AOP 切入点的注解, 在 AOP 运行过程中翻译表达式获得

  • 目标方法详情: 通过 AOP 切入点获得

  • 目标类详情: 通过 AOP 切入点获得

  • 入参: 通过 AOP 切入点获得

  • 返回值: 通过 AOP 切入点获得

  • 用户信息: 在 AOP 运行的过程中通过代码获得

在明确了数据来源后, 我们先进行 AOP 相关的设计

AOP 注解设计

AOP 注解的目的是在代码层面记录数据, 并提供切入点, 上面提及的功能模块和业务描述需要在这里写入, 开始写代码:

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface KoalaLog {
/** * 功能模块 * * @return 功能模块 */ String type() default "";
/** * 业务描述 * * @return 业务描述 */ String content() default "";
}
复制代码

AOP 切面设计

切面设计其实就两个内容:

  • 切入点

  • 切入时机

有没有发现这和刚才的介绍是差不多的呢?

回归正题, 我们的切入点就是刚才设计的注解, 切入时机是目标方法执行之后, 即 afterReturning

细心的小伙伴肯定知道, 方法的执行是可能会出错的, 所以除了 afterReturning 之外, 我们还需要再加一个关于异常的切入时机, 即 afterThrowing

@Aspect@Component@Slf4jpublic class KoalaLogAspect {
@Pointcut(value = "@annotation(cn.houtaroy.springboot.koala.starter.log.annotations.KoalaLog)") public void logPointCut() {} @AfterReturning(value = "logPointCut()", returning = "returnValue") public void log(JoinPoint joinPoint, Object returnValue) { // 记录正常日志 } @AfterThrowing(pointcut = "logPointCut()", throwing = "error") public void errorLog(JoinPoint joinPoint, Exception error) { // 记录异常日志 } }
复制代码

以上, AOP 的全部设计都结束了, 至于如何实现记录日志的逻辑, 我们要等 SpEL 设计结束后再进行, 暂且搁置

SpEL 模型设计

为了实现 SpEL, 我们需要如下几个模型:

  • LogRootObject: 运行数据来源

  • LogEvaluationContext: 解析上下文, 用于整个解析环境

  • LogEvaluator: 解析器, 解析 SpEL, 获取数据

LogRootObject

LogRootObject 是 SpEL 表达式的数据来源, 即业务描述

上文提及在业务描述中需要记录业务数据的 id, 它可以通过方法参数获得, 那么:

@Getter@AllArgsConstructorpublic class LogRootObject {
/** * 方法参数 */ private final Object[] args;
}
复制代码

但需要注意的是, 它的结构直接决定了业务描述 SpEL 表达式翻译完成的结果, 所以务必提前和需求沟通好业务描述最全面的数据范围

比如, 我们还需要记录目标方法/目标类的信息, 那这个设计是不满足, 应该是:

@Getter@AllArgsConstructorpublic class LogRootObject {
/** * 目标方法 */ private final Method method;
/** * 方法参数 */ private final Object[] args;
/** * 目标类的类型信息 */ private final Class<?> targetClass;
}
复制代码

LogEvaluationContext

Spring 提供了 MethodBasedEvaluationContext, 我们只需要继承它, 并实现对应的构造方法:

public class LogEvaluationContext extends MethodBasedEvaluationContext {
/** * 构造方法 * * @param rootObject 数据来源对象 * @param discoverer 参数解析器 */ public LogEvaluationContext(LogRootObject rootObject, ParameterNameDiscoverer discoverer) { super(rootObject, rootObject.getMethod(), rootObject.getArgs(), discoverer); }
}
复制代码

LogEvaluator

这是我们最核心的解析器, 用于解析 SpEL, 返回真正期望的数据内容

我们需要初始化表达式编译器/表达式编译模板/参数解析器

@Getterpublic class LogEvaluator {
/** * SpEL解析器 */ private final SpelExpressionParser parser = new SpelExpressionParser();
/** * 参数解析器 */ private final ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
/** * 表达式模板 */ private final ParserContext template = new TemplateParserContext("${", "}");
/** * 解析 * * @param expression 表达式 * @param context 日志表达式上下文 * @return 表达式结果 */ public Object parse(String expression, LogEvaluationContext context) { return getExpression(expression).getValue(context); }
/** * 获取翻译后表达式 * * @param expression 字符串表达式 * @return 翻译后表达式 */ private Expression getExpression(String expression) { return getParser().parseExpression(expression, template); } }
复制代码

到此为止, 整个 SpEL 表达式的全部内容搞定, 再次强调下它的逻辑:

设计 RootObject->设计 SpEL 表达式的运行上下文->设计 SpEL 解析器(包括表达式解析模板和参数解析器)

AOP 业务逻辑

完成了 SpEL 的设计, 我们可以把目光回归到刚才 AOP 中没有实现的业务代码, 这里的流程非常简单:

解析 SpEL->生成日志实体->保存日志

这里的内容不再赘述, 小伙伴们只需要认真看一遍代码就全部明白了

@Aspect@Slf4jpublic class KoalaLogAspect {
/** * 日志SpEL解析器 */ private final LogEvaluator evaluator = new LogEvaluator();
/** * jackson */ @Autowired private ObjectMapper objectMapper;
/** * 日志切入点 */ @Pointcut(value = "@annotation(cn.houtaroy.springboot.koala.starter.log.annotations.KoalaLog)") public void logPointCut() { }
/** * 方法返回后切入点 * * @param joinPoint 切入点 * @param returnValue 返回值 */ @AfterReturning(value = "logPointCut()", returning = "returnValue") public void log(JoinPoint joinPoint, Object returnValue) { // 记录正常日志 Log koalaLog = generateLog(joinPoint); try { koalaLog.setReturnValue(objectMapper.writeValueAsString(returnValue)); // 记录日志代码... } catch (JsonProcessingException e) { log.error("KOALA-LOG: 序列化返回值失败", e); } }
/** * 方法抛出异常后切入点 * * @param joinPoint 切入点 * @param error 异常 */ @AfterThrowing(pointcut = "logPointCut()", throwing = "error") public void errorLog(JoinPoint joinPoint, Exception error) { // 记录异常日志 Log koalaLog = generateLog(joinPoint); koalaLog.setReturnValue(error.getMessage()); // 记录日志代码... }
/** * 生成日志实体 * * @param joinPoint 切入点 * @return 日志实体 */ private Log generateLog(JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); Object[] args = joinPoint.getArgs(); Class<?> targetClass = AopProxyUtils.ultimateTargetClass(joinPoint.getTarget()); KoalaLog annotation = method.getAnnotation(KoalaLog.class); LogRootObject rootObject = new LogRootObject(method, args, targetClass); LogEvaluationContext context = new LogEvaluationContext(rootObject, evaluator.getDiscoverer()); Object content = evaluator.parse(annotation.content(), context); Log koalaLog = Log.builder().type(annotation.type()).content(content.toString()).createTime(new Date()).build(); try { koalaLog.setArguments(objectMapper.writeValueAsString(args)); } catch (JsonProcessingException e) { log.error("KOALA-LOG: 序列化方法参数失败", e); } return koalaLog; }}
复制代码

上面的内容缺少记录日志的具体代码, 各位根据实际情况进行补充(我不会承认是自己拖延症还没有将 ORM 封装写完)

总结

AOP 和 SpEL 是专注于运行状态下的数据处理, 在简单的业务中, 完全没有必须使用的必要

代码是我们的工具, 如何正确和便捷地使用它们也是一种能力

  • 以上就是《MyBatis 实现多表查询》的分享,上述所有代码可以在我的练习项目中找到, 代码十分简陋。

  • 也欢迎大家交流探讨,该文章若有不正确的地方,希望大家多多包涵。

  • 你们的支持就是我最大的动力,如果对大家有帮忙给个赞哦~~~

用户头像

还未添加个人签名 2021.04.26 加入

还未添加个人简介

评论

发布
暂无评论
硬核资源!清华博士的Spring Boot中AOP与SpEL笔记,码农:膜拜