SpringBoot整合AOP

1. AOP介绍

1.1 什么是AOP

aop全称Aspect Oriented Programming,即为面向切面编程。实现对业务程序非侵入式的扩展。底层是通过动态代理实现的,具体实现有基于jdk的动态代理和基于CGLib的动态代理。

  • JDK动态代理是基于反射机制实现代理接口的匿名类,在调用具体方法前调用invokeHandler来处理。有具体实现接口的情况下可以使用jdk动态代理或者cglib动态代理
  • cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码文件生产子类来处理。如果没有具体实现接口的情况下,只能使用基于字节码文件的动态代理cglib

1.2 AOP中的名词介绍

先介绍一些AOP中的各种名词

  • 切面(Aspect):切入点和通知的集合
  • 连接点(Joinpoint):目标对象中可以被增强的所有方法
  • 通知(Advice):增强的代码(逻辑),分为前置,后置,最终,异常,环绕
  • 切入点(Pointcut):目标对象中经过匹配最终增强的方法
  • 引入(Introduction):动态的为某个类增加和减少方法
  • 目标对象(Target Object):被代理的对象
  • AOP代理对象(AOP Proxy):AOP框架创建的代理对象,用于实现切面,调用方法
  • 织入(Weaving):将通知应用到切入点的过程

1.3 注解介绍

  • @EnableAspectJautoProxy 用于springboot启动类,代表开启注解aop功能支持
    • proxyTargetClass 是否强制使用CGlib的动态代理,默认false
    • exposeProxy 是否通过aop框架暴露该代理对象,aopContext能够访问
  • @Aspect 用于标注切面类
  • @Pointcut 用于标识切入点
    • value 切入点表达式
  • @Before 前置通知
  • @AfterReturning 后置通知
  • @AfterThrowing 异常通知
  • @After 最终通知
  • @Around 环绕通知,环绕通知代表了一个完整的流程,因此环绕通知和上面的四个通知任选其一使用

1.4 切入点表达式

  • execution - 根据表达式匹配,使用最多

    execution([修饰符] 返回类型 [包名.类名].方法名(参数列表) [异常])

    支持的通配符有 *:匹配所有。..:匹配多级包或者多个参数。+表示类以及子类

  • within - 匹配方法所在的包或者类

  • this - 用于向通知方法中传入代理对象的引用

  • target - 用于向通知方法中传入目标对象的引用

  • args - 用于向通知方法中传入参数,并且匹配参数个数

  • @args - 和args都是匹配参数,但是@args要求传入切入点的参数必须标注指定注解,且不能是SOURCE源码注解,比如Lombok的

  • @within - 匹配加了某个注解的类中的所有方法

  • @target - 与@within类似,但是要求标注到类上的注解,必须为RUNTIME的

  • @annotation - 匹配加了某个注解的方法

  • bean 通过spring容器中的beName匹配

    可以使用通配符*来标识以什么开头,以什么结尾

2. 测试

本文对应的源码地址:03-spring-boot-aop · master · csdn / spring-boot-csdn · GitLab (sea-clouds.cn)

pom.xml

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.5.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

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

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.6.0</version>
    </dependency>
</dependencies>

2.1 简单案例

/**
 * @author HLH
 * @description 简单的测试
 * @email 17703595860@163.com
 * @date Created in 2021/8/7 上午11:25
 */
@Component // 注册组件
@Aspect // 开启aop
public class MyAspect {

    /**
     * 匹配所有service中的所有方法
     */
    @Pointcut(value = "execution(public * xyz.hlh.boot3..service..*(..))")
    public void pointCut() {}

    @Before(value = "pointCut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("前置通知");
    }

    @AfterReturning(value = "pointCut()", returning = "returnObj")
    public void afterReturning(JoinPoint joinPoint, Object returnObj) {
        System.out.println("后置通知");
    }

    @AfterThrowing(value = "pointCut()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Exception e) {
        System.out.println("异常通知");
    }

    @After(value = "pointCut()")
    public void afterThrowing(JoinPoint joinPoint) {
        System.out.println("最终通知");
    }

    /**
     * 环绕通知因为包含显示的方法调用,所以必须要有返回参数
     *  因为显示调用,处理异常,所以和上面的四个通知只能选择一种使用
     */
    @Around(value = "pointCut()")
    public Object around(ProceedingJoinPoint pjp) {
        Object result = null;
        try {
            System.out.println("前置处理");
            // 调用方法
            result = pjp.proceed(pjp.getArgs());
            System.out.println("后置处理");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("异常处理");
        } finally {
            System.out.println("最终处理");
        }
        return result;
    }

}

2.2 execution

用于表达式匹配

/**
 * 匹配所有service中的所有方法
 */
@Pointcut(value = "execution(public * xyz.hlh.boot3..service..*(..))")
public void pointCut() {}

/**
 * 匹配所有service中的所有方法
 * 修饰符和抛出异常可以省略
 */
@Pointcut(value = "execution(* xyz.hlh.boot3..service..*(..))")
public void pointCut() {}

2.3 within

用于匹配切入点所在的包或者类

/**
 * 匹配所有UserService类中的所有方法
 */
@Pointcut(value = "within(xyz.hlh.boot3.service.UserService)")
public void pointCut() {}

/**
 * 匹配所有service包中的所有类中的所有方法
 */
@Pointcut(value = "within(xyz.hlh.boot3.service.*)")
public void pointCut1() {}

/**
 * 匹配所有service包以及所有子包中的所有类中的所有方法
 */
@Pointcut(value = "within(xyz.hlh.boot3.service..*)")
public void pointCut2() {}

2.4 this

用于向通知方法中传入代理对象的引用

/**
 * this 向切入点中传入代理对象,argNames指定此方法中的参数名词列表
 */
@Before(value = "pointCut() && this(proxy)", argNames = "joinPoint,proxy")
public void before(JoinPoint joinPoint, Object proxy) {
    // 代理对象
    System.out.println(proxy);
    System.out.println("前置通知");
}

2.5 target

用于向通知方法中传入目标对象

/**
 * target 向切入点中传入目标对象,argNames指定此方法中的参数名词列表
 */
@Before(value = "pointCut() && target(obj)", argNames = "joinPoint,obj")
public void before(JoinPoint joinPoint, Object obj) {
    // 目标对象
    System.out.println(obj);
    System.out.println("前置通知");
}

2.6 args

用于向通知方法中传入参数,并且匹配参数个数

/**
 * args 向切入点中传入参数(匹配参数),argNames指定此方法中的参数名词列表
 *  该测试方法为修改User的方法 入参有两个 User user,Integer userId
 *  args中参数名词随意,但是参数个数需要匹配
 *  此处通知方法中的参数类型可以指定具体的类型,spring自动强转,但是如果类型不对,会造成进不去该通知
 */
@Before(value = "pointCut() && args(a, b)", argNames = "joinPoint,a,b")
public void before(JoinPoint joinPoint, User a, Integer b) {
    System.out.println(a);
    System.out.println("前置通知");
}

2.7 @args

和args都是匹配参数,但是@args要求传入切入点的参数必须标注指定注解,且不能是SOURCE源码注解,比如Lombok的

/**
 * @args 和args都是匹配参数,但是@args要求传入切入点的参数必须标注指定注解,且不能是SOURCE源码注解,比如Lombok的
 *  该测试方法为修改User的方法 入参有两个 User user,Integer userId
 *  @args中指定对应的标注注解的的类型,参数个数需要匹配,不校验的参数用*代替
 *  该处要求第一个参数User对象必须标注MyAnno的自定义注解
 */
@Before(value = "pointCut() && @args(xyz.hlh.boot3.annotation.MyAnno, *)")
public void before(JoinPoint joinPoint) {
    System.out.println("前置通知");
}

2.8 @within

匹配加了某个注解的类中的所有方法

/**
 * @within 用于匹配标书指定注解的类中的所有方法
 *  匹配标注了MyAnno注解的类中的所有方法
 */
@Pointcut(value = "@within(xyz.hlh.boot3.annotation.MyAnno)")
public void pointCut() {}

2.9 @target

与@within类似,但是要求标注到类上的注解,必须为RUNTIME的

/**
 * @target 用于匹配标书指定注解的类中的所有方法,要求注解必须是RUNTIME的
 *  匹配UserService类,并且标注了MyAnno注解的类中的所有方法
 *  @target 不能单独使用,网上并没有类似说明,可能是我的springboot原因,单独使用会报错。合并使用效果正常
 */
@Pointcut("within(xyz.hlh.boot3.service.UserService) && @target(xyz.hlh.boot3.annotation.MyAnno)")
public void pointCut() {}

2.10 @annotation

匹配加了某个注解的方法

/**
 * @annotation 匹配加了某个注解的方法
 *  匹配标注了MyAnno注解的方法
 *  此处测试方法为insertUser
 */
@Pointcut("@annotation(xyz.hlh.boot3.annotation.MyAnno)")
public void pointCut() {}

2.10 bean

通过spring容器中的beName匹配

/**
 * 匹配spring容器中为指定beanName的对象中的所有方法
 *  此处匹配spring容器中名称为userService的对象中的所有方法
 */
@Pointcut(value = "bean(userService)")
public void pointCut() {}

/**
 * 匹配spring容器中为指定beanName的对象中的所有方法
 *  此处匹配spring容器中名称为Service结尾的对象中的所有方法
 */
@Pointcut(value = "bean(*Service)")
public void pointCut1() {}

3. 案例

使用AOP的例子很多

  • 比如spring的事务管理器
  • 日志
  • 敏感字过滤
  • 登录用户参数注入

等等,这儿主要介绍AOP日志和登录用户或参数注入

3.1 AOP日志

项目中一般都会有日志打印,比如方法的入参,出参,有没有异常。通常使用非侵入式的日志打印。aop可以完成非侵入式的日志打印

/**
 * @author HLH
 * @description Service 日志打印
 * @email 17703595860@163.com
 * @date Created in 2021/8/7 上午11:25
 */
@Component // 注册组件
@Aspect // 开启aop
@Slf4j(topic = "service") // 修改logger name
public class ServiceLogAspect {

    /**
     * 拦截所有service中的所有方法
     */
    @Pointcut(value = "execution(* xyz.hlh.boot3..service..*(..))")
    public void pointCut() {}

    /**
     * 前置打印
     */
    @Before(value = "pointCut()")
    public void before(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取方法名
        String methodName = signature.getName();
        // 获取类全限定类名
        String declaringTypeName = signature.getDeclaringTypeName();
        // 获取类名
        String className = declaringTypeName.substring(declaringTypeName.lastIndexOf(".") + 1);
        // 记录日志
        Object[] args = joinPoint.getArgs();
        log.info("{}.{} is called, args list is {}", className, methodName, JSONUtil.toJsonStr(args));
    }

    /**
     * 后置打印
     */
    @AfterReturning(value = "pointCut()", returning = "returnObj")
    public void afterReturning(JoinPoint joinPoint, Object returnObj) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取方法名
        String methodName = signature.getName();
        // 获取类全限定类名
        String declaringTypeName = signature.getDeclaringTypeName();
        // 获取类名
        String className = declaringTypeName.substring(declaringTypeName.lastIndexOf(".") + 1);
        // 记录日志
        log.info("{}.{} is call to complete, return value is {}", className, methodName, JSONUtil.toJsonStr(returnObj));
    }

    /**
     * 异常打印
     */
    @AfterThrowing(value = "pointCut()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Exception e) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取方法名
        String methodName = signature.getName();
        // 获取类全限定类名
        String declaringTypeName = signature.getDeclaringTypeName();
        // 获取类名
        String className = declaringTypeName.substring(declaringTypeName.lastIndexOf(".") + 1);
        // 记录日志
        log.error("{}.{} throws exception '{}' because of '{}'"
                , className, methodName, e.getClass(), e.getMessage(), e);
    }

}

3.2 登录用户参数注入

登录成功之后,很多方法中需要使用登录用户信息,但是需要写代码从redis中根据token获取登录用户信息。这是一个通用的操作,可以使用AOP+自定义注解+springMVC的参数解析器实现mvc中的方法中,自动注入一个登录用户

具体实现请查看另外一篇文章:https://blog.csdn.net/HLH_2021/article/details/119491890

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值