AOP基础知识及简单应用(日志记录)

201 阅读5分钟

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

一、AOP基本原理

本文主要总结了AOP(面向切面编程)的基本原理,其中包括AOP特点、相关术语等;其次介绍的AOP在Spring、SpringBoot中的使用,并提供了一个日志记录切面的实例。

1.1. 什么是AOP

AOP是Aspect Oriented Programming的缩写,翻译为面向切面编程。

举个生活中的例子,在没有ETC之前,车辆通过高速时需要先取卡、还卡、付费,这个过程相当于在通过高速这个业务方法中又调用的抽取出来的取卡还卡以及付费的通用方法。在使用ETC之后,我们只需要注册一张ETC卡片,然后只需要通过ETC通道即可以完成通过高速目的,司机省去了繁琐的取、还卡和付费的操作,这个过程中ETC通道相当于是一个切面,而ETC卡片则是切点。

在实际开发中可以发现,就像通过高速时取还卡、付费这样的,一些功能通常会被多个地方使用,传统的面向对象(OOP)开发中一般会将实现这些功能的方法抽取出来,使用时再去调用(没有ETC)。但是,有的情况下我们又不想在每个需要它的地方去明确的调用它,例如日志、安全和事务管理都很重要,但是这些不应该是在业务代码中需要考虑并主动解决的问题。如果我们可以将这些不属于业务领域的问题交由其他对象来处理,是不是会更好(有ETC)?

1.2. AOP特点

1)切

与OOP不同AOP是横切逻辑,可以在不改变原有代码的基础上,通过动态代理模式将一些通用的逻辑加到代码中。

2)面

一个切点可能会横切多个对象,这些对象组合起来就是面。

1.3. AOP术语

1) 通知(Advice)

通知定义了切面是什么以及什么时候使用,Spring中切面共有五种类型的通知:

  • 前置通知(Before):在方法调用前执行通知的功能;
  • 后置通知(After):在方法调用后执行通知的功能;
  • 返回通知(After-returning):在方法成功执行后执行通知的功能;
  • 异常通知(After-throwing):在方法抛出异常后执行通知的功能;
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行,因为在一个方法中可以参与到方法执行的多个阶段,所以在特定时候会比较方便使用(详见3.2)。

2) 连接点(Join Point)

在程序执行中某个特定的点,可以是调用方法时、抛出异常时、甚至修改字段时,通过在这些点拦截来执行自定义的逻辑。

3) 切点(Poincut)

定义了在什么地方切入,切点的定义会通知所要织入的一个或多个连接点。

4) 切面(Aspect)

切面是通知和切点的组合,即在何时和何处完成其的功能。

5) 引入(Introduction)

引入是指向现有的类中添加新的方法或者属性。

6) 织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程,分为编译期织入、类加载期织入和运行期织入。可以在编译期类加载期或者运行期织入。

二、AOP基本使用

2.1. SpringBoot整合AOP

在maven管理的项目中,可以在pom.xml文件中加入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.2. 切点的设置(AspectJ切点表达式)

execution (* com.example.*.*(..))

其中第一个*表示返回任意的类型;从com到第二个*为方法所属的类;第三个*表示任意的方法,如需要指定方法则直接写方法名;最后的(..)表示使用任意的参数。

2.3. 注解方式创建切面

package com.example.aspect;

import org.apache.logging.slf4j.Log4jLogger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * @Classname AopAdvice
 * @Description 切面
 * @Date 2021/7/12 22:15
 * @Created by Default.W
 */

@Aspect
@Component
public class AopAdvice {

    private Logger logger = LoggerFactory.getLogger(AopAdvice.class);

    @Pointcut("execution (* com.example.*.*.*(..))")
    public void printLogger() {}

    @Before("test()")
    public void beforeAdvice() {
         System.out.println("beforeAdvice...");
    }

    @After("test()")
    public void afterAdvice() {
         System.out.println("afterAdvice...");
    }

    @AfterThrowing(pointcut = "printLogger()", throwing = "e")
    public void afterThrowing (Exception e) {
        if (e != null)
            System.out.println(e.getMessage());
    }

    @Around("printLogger()")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
        logger.info("方法开始");
        try {
            proceedingJoinPoint.proceed();
        } catch (Throwable t) {
            t.printStackTrace();
        } finally {
            logger.info("方法结束");
        }
    }
}

三、AOP的应用:日志切面

3.1 基本思路

在系统上线后,如果需要排查查询接口查询缓慢问题,需要阅读日志分析原因。本实例可以在指定方法执行完后输出执行时长,并且,每次进入同一个方法时,在日志中都会以不同的UUID标识出来,方便问题的排查和日志的迅速定位。

3.2 Demo演示

切面完整代码如下:

package com.example.aspect;

import org.apache.logging.slf4j.Log4jLogger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.UUID;

/**
 * @Classname AopAdvice
 * @Description 系统监控切面
 * @Date 2021/7/12 22:15
 * @Created by Default.W
 */

@Aspect
@Component
public class AopAdvice {

    private Logger logger = LoggerFactory.getLogger(AopAdvice.class);

    @Pointcut("execution (* com.example.*.*.*(..))")
    public void printLogger() {}

    @Around("printLogger()")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
        long startTime = System.currentTimeMillis();
        String uuid = UUID.randomUUID().toString().replace("-", "");
        Method method = this.getMethod(proceedingJoinPoint);
        try {
            proceedingJoinPoint.proceed();
        } catch (Throwable t) {
            t.printStackTrace();
        } finally {
            String methodFullName =  method.getDeclaringClass()
                .getName() + "." +method.getName();
            logger.info("["+ uuid + "] Method \"" + methodFullName + "\" start >>>");
            int paramCount = this.getParams(proceedingJoinPoint).length;
            if (paramCount > 0) {
                StringBuilder sb = new StringBuilder("[").
                    append(uuid).append("] Params is {");
                for (int i = 0; i < paramCount; i ++) {
                    sb.append(this.getParams(proceedingJoinPoint)[i])
                        .append(":").append(proceedingJoinPoint.getArgs()[i]);
                    if (i != paramCount - 1)
                        sb.append(", ");
                }
                sb.append("}");
                logger.info(sb.toString());
            }
            logger.info("["+ uuid + "] Method \""+ methodFullName 
                        +"\" end, elapsed time "
                        + (System.currentTimeMillis() - startTime) + "ms");
        }
    }


    private MethodSignature loadMethodSignature(JoinPoint jp) {
        Signature sig = jp.getSignature();
        return (MethodSignature) sig;
    }

    /**
     * 通过切入点获取当前执行方法对象
     *
     * @Param [jp]
     * @return java.lang.reflect.Method
     * @Date 2021/7/17 19:51
     * @Author Default.W
     **/
    private Method getMethod(JoinPoint jp)  {
        MethodSignature methodSignature = this.loadMethodSignature(jp);
        try {
            return jp.getTarget().getClass()
                .getMethod(methodSignature.getName(), 		
                           methodSignature.getParameterTypes());
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
            return null;
        }
    }

    public String[] getParams(JoinPoint jp) {
        MethodSignature methodSignature = loadMethodSignature(jp);
        return methodSignature.getParameterNames();
    }
}

四、参考文献

Spring实战(第4版)

Spring 核心概念——AOP 理解及运用

面试被问了几百遍的 IoC 和 AOP