乐趣区

关于java:硬核资源清华博士的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 曾经提供了大部分性能, 咱们只须要手动解决如下局部:

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

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

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

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
@Slf4j
public 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
@AllArgsConstructor
public class LogRootObject {
    /**
     * 办法参数
     */
    private final Object[] args;}

但须要留神的是, 它的构造间接决定了业务形容 SpEL 表达式翻译实现的后果, 所以务必提前和需要沟通好业务形容最全面的数据范畴

比方, 咱们还须要记录指标办法 / 指标类的信息, 那这个设计是不满足, 应该是:

@Getter
@AllArgsConstructor
public 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, 返回真正冀望的数据内容

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

@Getter
public 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
@Slf4j
public 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 是专一于运行状态下的数据处理, 在简略的业务中, 齐全没有必须应用的必要

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

  • 以上就是《Spring Boot 中 AOP 与 SpEL 的利用》的分享。
  • 也欢送大家交换探讨,该文章若有不正确的中央,心愿大家多多包涵。
  • 你们的反对就是我最大的能源,如果对大家有帮忙给个赞哦~~~
退出移动版