0302实现-AOP-spring6

文章目录

    • 1 概述
      • 1.1 AOP实现方案
      • 1.2 底层技术
    • 2 Spring+AspectJ基于注解实现
      • 2.1 依赖jar包
      • 2.2 切面及通知
    • 3 开发中应用场景
      • 3.1 事务处理
      • 3.2 安全日志记录
    • 结语

1 概述

1.1 AOP实现方案

Spring对于AOP的实现包括以下3种方式:

  • 第一种方式:Spring框架结合AspectJ框架实现的AOP,基于注解方式;
  • 第二种方式:Spring框架结合AspectJ框架实现的AOP,基于XML方式;
  • 第三种方式:Spring框架自己实现的AOP,基于XML配置方式;

实际开发中,都是Spring+Aspectj来实现AOP,目前主流上通过注解方式。我们讲解基于注解方式 ,XML自行查阅相关文档。

1.2 底层技术

那么它们实现AOP底层啥基于啥实现的呢?这里暂时给个概述,以后有机会学习底层原理在详细讲解。

Spring AOP的实现主要基于动态代理技术和字节码增强技术

  1. 动态代理技术

Spring AOP中使用了JDK动态代理和CGLIB动态代理两种方式来实现AOP的功能。JDK动态代理是基于接口的代理,只能为接口创建代理对象;而CGLIB动态代理则是通过继承的方式来实现代理,可以为任意类创建代理对象。

对于使用JDK动态代理的目标类,在运行时会生成一个实现了目标类接口的代理类,代理类中会调用一个InvocationHandler接口的invoke()方法,在该方法中进行拦截和增强操作。

对于无法使用JDK动态代理的目标类,Spring AOP则会使用CGLIB动态代理来创建代理对象。CGLIB动态代理是通过继承目标类来实现代理的,代理类会重写目标类的方法并在其中加入拦截和增强代码。

  1. 字节码增强技术

除了动态代理技术,Spring AOP还可以通过字节码增强技术来实现AOP的功能。字节码增强技术是通过修改类的字节码来实现对目标类的增强,可以在编译期或运行时对类进行修改。

在Spring AOP中,字节码增强技术主要是通过AspectJ来实现的。AspectJ是一个基于Java语言的切面编程框架,可以在编译期或运行时对Java类进行字节码增强。

通过AspectJ的注解或XML配置,可以指定哪些类和方法需要被拦截和增强,然后通过AspectJ提供的编译器或运行时织入工具,在编译期或运行时对目标类进行字节码增强。

即Spring 自己实现的AOP底层基于我们之前讲解的动态代理;AspectJ基于字节码增强来实现。

2 Spring+AspectJ基于注解实现

2.1 依赖jar包

构建一个新练习项目,pom.xml引入依赖2-1如下所示:

    <dependencies>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-contextartifactId>
            <version>6.0.6version>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-aspectsartifactId>
            <version>6.0.6version>
        dependency>
        <dependency>
            <groupId>org.junit.jupitergroupId>
            <artifactId>junit-jupiter-apiartifactId>
            <version>5.9.0version>
            <scope>testscope>
        dependency>
        
        <dependency>
            <groupId>org.apache.logging.log4jgroupId>
            <artifactId>log4j-coreartifactId>
            <version>2.19.0version>
        dependency>
        <dependency>
            <groupId>org.apache.logging.log4jgroupId>
            <artifactId>log4j-slf4j2-implartifactId>
            <version>2.19.0version>
        dependency>
    dependencies>

2.2 切面及通知

新建简单的目标类OrderService,代码如下2-1所示:

package com.gaogzhen.spring6.aop.service;

import org.springframework.stereotype.Service;

/**
 * 订单服务
 *  目标类
 * @author gaogzhen
 */
@Service
public class OrderService {

    public void generate() {
        System.out.println("系统正在生成订单。。。");

        // if (true) {
        //     throw new RuntimeException("运行时异常");
        // }
    }
}

新建一个配置类AopConfig,用于指定包扫描和开启AspectJ的自动代理,代码如下2-2所示:

package com.gaogzhen.spring6.aop.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

/**
 * 配置aop
 * @author gaogzhen
 */
@Configuration
@ComponentScan({"com.gaogzhen.spring6.aop.service"})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopConfig {
}
  • @EnableAspectJAutoProxy注解用于开启AspectJ自动代理,参数proxyTargetClass = true用于指定CGLIB代理;默认false,即JDK动态代理。

新建切面类LogAspect,里面包括我们常用的通知类型,代码如下2-3所示:

package com.gaogzhen.spring6.aop.service;


import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * 日志切面
 *  切面=通知+切点
 *     通知:方法增强
 *
 * @author gaogzhen
 */
@Component
@Aspect
@Order(3)
public class LogAspect {

    @Pointcut("execution(* com.gaogzhen.spring6.aop.service..*(..))")
    public void common() {}

    /**
     * 前置通知
     */
    @Before("common()")
    public void beforeAdvice(JoinPoint joinPoint) {
        System.out.println("前置通知");
        Signature signature = joinPoint.getSignature();
        System.out.println("目标方法名:" + signature.getName());
    }

    /**
     * 后置通知
     */
    @AfterReturning("common()")
    public void afterReturningAdvice() {
        System.out.println("后置通知");
    }

    /**
     * 环绕通知(环绕在前置通知之前,在后置通知之后)
     */
    @Around("common()")
    public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        // 前处理
        System.out.println("前环绕");
        // 目标方法
        joinPoint.proceed();
        // 后处理
        System.out.println("后环绕");
    }

    /**
     * 异常通知
     *  发生异常之后通知
     */
    @AfterThrowing("common()")
    public void afterThrowingAdvice() {
        System.out.println("异常通知");
    }

    /**
     * 最终通知
     *  fianlly 语句快中的通知
     */
    @After("common()")
    public void afterAdvice() {
        System.out.println("最终通知");
    }
}
  • @Aspect 标记为切面
  • @Pointcut:通用切点

通用切点只是一个标记,方法名随意,不需要方法体。

  • @Before:前置通知
  • @AfterReturning:后置通知
  • @Around:环绕通知
  • @AfterThrowing:异常通知
  • @After:最终通知

几种通知参数,可以说切点表达式,也可以说通用切点;如果想要在其他切面中使用通用切点,需要加上类的全路径。

通知注解标记的方法,可以接收连接点参数,环绕通知为ProceedingJoinpoint,其他通知为Joinpoint;连接点用于获取目标方法相关信息。

测试类代码2-4如下所示:

@Test
public void testAnnotation() {
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AopConfig.class);
    OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
    orderService.generate();
}

OrderService在注释异常时,我们看下通知的执行顺序如下所示:

前环绕
前置通知
目标方法名:generate
系统正在生成订单。。。
后置通知
最终通知
后环绕

程序正常的通知执行顺序:前环绕通知=>前置通知=>目标方法=>后置通知=>最终通知=>后环绕通知;前环绕通知即环绕通知中目标方法之前的部分,后环绕通知即环绕通知中目标方法执行之和的部分。

OrderService添加异常后,通知执行顺序如下:

前环绕
前置通知
目标方法名:generate
系统正在生成订单。。。
异常通知
最终通知
Tests run: 2, Failures: 2, Errors: 0, Skipped: 0, Time elapsed: 0.758 sec <<< FAILURE!
com.gaogzhen.spring6.test.SpringAOPTest.testBefore()  Time elapsed: 0.728 sec  <<< FAILURE!
java.lang.RuntimeException: 运行时异常

程序产生异常后,执行异常通知,因为最终通知在finally代码块中,会执行。

不同切面的执行顺序指定,下面我们在添加一个SecurityAspect。通过在类上添加@Order(数值)注解,来指定执行顺序。数值越小,优先级越高,即越早执行。

测试我们指定LogAspect @Order(3),SecurityAspect @Order(1),看下执行顺序,异常通知不在测试,测试结果如下:

前环绕:安全切面----
前置通知:安全切面----
前环绕
前置通知
目标方法名:generate
系统正在生成订单。。。
后置通知
最终通知
后环绕
后置通知:安全切面----
最终通知:安全切面----
后环绕:安全切面----

3 开发中应用场景

日常开发中,事务处理和安全日志记录是我们经常要用到的交叉业务。

3.1 事务处理

事务的四个处理过程:

  1. 开启事务
  2. 执行核心业务代码
  3. 提交事务(核心业务执行没有异常)
  4. 回滚事务(核心业务执行有异常)

应用AOP解决方案:

  • 前置通知开启事务,后置通知提交事务,异常通知用于回滚事务;
  • 前环绕通知开启事务,后环绕通知提交事务,异常通知用于回滚事务。

3.2 安全日志记录

通常我们安全记录用于记录信息如下:

  • 耗时:可用于分析优化
  • 谁:当前用户信息
  • 位置:客户端IP地址
  • 操作:执行的操作(方法),参数
  • 结果:执行结果

应用AOP传统解决方案:

  • 耗时:后置通知记录当前时间-前置通知记录的当前时间或者环绕通知后环绕记录当前时间-前环绕记录的当前时间
  • 谁:一般我们通过应用上下文可以获取
  • 位置:通过请求对象可以获取
  • 操作:通过连接点Joinpoint或者ProceedingJointpoint获取方法相关信息
  • 结果:接收返回结果即可。
  • 记录相应信息,存入日志表即可。

一般情况下我们应用环绕通知比较多。

上面解决方法为同步方法,记录信息,存储都需要消耗一定时间,在实时性要求高场景下不适用。这时可以通过消息队列来异步解耦,这时候耗时需要拆分出去;异步获取单一结果可以采用Mono异步背压方式或者消息队列解决。

结语

如果小伙伴什么问题或者指教,欢迎交流。

❓QQ:806797785

⭐️源代码仓库地址:https://gitee.com/gaogzhen/spring6-study

参考:

[1]Spring框架视频教程[CP/OL].P106-117.

[2]ChatGPT

你可能感兴趣的:(#,spring全家桶,java,spring,spring,boot)