# 前言 🚀本章代码示例

# 关于 OOP 的概念

OOP 是面向对象的程序设计,在谈到 OOP 程序设计前,我们不得不了解一下 POP 即面向过程的程序设计, POP 它是以功能为中心进行思考和组织的一种编程方式,强调的是系统数据被加工处理的过程,而 OOP 则注重封装,强调整体性的概念,以对象为中心,将对象内部组织与外部环境区分开来。

至于 OOP 的出现对 POP 存在很多颠覆性的,不能说 POP 没有了价值,只是不同时代的产物,从方法论来讲,更喜欢将面向对象与面向过程看做是事物的两个方面,那就是局部与整体 (必须注意局部与整体是相对的),在实际应用中,两者方法都同样重要。

Java 程序设计过程中,我们享受到了 OOP 设计思想带来的甜头,以至于在这个一切皆对象众生平等的世界里欢乐不已,而 OOP 确实也遵循自身的宗旨,即将数据及数据的操作行为放在一起相互依存,是不可分割的整体,利用该定义对于相同类型的对象进行分类并抽象后得出共同的特性,从而行成了类,在 Java 程序设计中这些类对象就是 class ,它使程序设计更简单,更易于维护。但是随着应用程序规模的增加,慢慢地 OOP 程序设计也开始暴露出了一些问题。

# 关于 AOP 的概念

JavaAop 详细介绍点击查看🚀

# Aop 领跑者 - AspectJ

在进行下方测试时请务必先在 IntelliJ IDEA 中配置 AspectJ ,不然将无法进行后续操作,AspectJ 详细配置🚀

先看下方案例,首先编写一个 HelloWord 类,然后通过 AspectJ 的技术切入该类并执行相应操作。

HelloWord.java
package top.rem.rain.aspect;
/**
 * @Author: LightRain
 * @Description: 使用 Aspect 进行事务围绕
 * @DateTime: 2024-01-04 00:34
 * @Version:1.0
 **/
public class HelloWord {
    
    private void helloWord(){
        System.out.println("Hello Word!");
    }
    public static void main(String[] args) {
        HelloWord helloWord = new HelloWord();
        helloWord.helloWord();
        /*
        执行结果:
          helloWord () 方法执行前验证权限
          Hello Word!
          helloWord () 方法执行后记录日志
         */
    }
}

下面将来编写 AspectJ 类,注意关键字 aspect , 而不是 class ,其后缀名称为 .aj 代表它是属于 AspectJ 类,含义与 class 相同,即定义一个 AspectJ 类。

HelloWordAspect.aj
package top.rem.rain.aspect;
public aspect HelloWordAspect {
    /*
      定义切点,日志记录切点
     */
    pointcut recordLog():call(* HelloWord.helloWord(..));
    /*
      定义切点,权限验证 (实际开发中日志和权限一般会放在不同的切面中,这里仅为方便演示)
     */
    pointcut authCheck():call(* HelloWord.helloWord(..));
    /*
      定义前置通知!
     */
    before():authCheck(){
        System.out.println("helloWord()方法执行前验证权限");
    }
    /*
      定义后置通知
     */
    after():recordLog(){
        System.out.println("helloWord()方法执行后记录日志");
    }
}

下面是运行 main 方法后的执行结果:

helloWord () 方法执行前验证权限
Hello Word!
helloWord () 方法执行后记录日志

执行结果是意料之中的,我们可以发现明明只运行了 main 方法,却在 helloWord() 方法前后分别进行了权限验证和日志记录,这就是 AspectJ 的功劳了, AspectJ 是一个 AOP 框架,它能对 Java 代码进行编译,让 Java 代码具有 AspectJAOP 功能, AspectJ 是目前实现 AOP 最成熟的框架, AspectJJava 程序完全兼容。

使用 aspect 关键字来定义一个切面类,它可以是单独的日志切面、权限切面等,在切面内部使用 pointcut 定义了两个切点,一个用于权限验证,一个用于日志记录,而所谓的切点就是那些需要应用切面的方法,如:需要在 helloWord() 方法执行前后进行权限验证和日志记录,那么就需要捕捉该方法,而 pointcut 就是定义这些需要捕捉的方法,这些方法也被称为目标方法,最后定义两个通知,通知就是那些需要在目标方法前后执行的函数,如 before() 即前置通知,会在目标方法之前执行, after() 是后置通知,会在目标方法执行之后再执行此方法。

切点定义语法:使用关键字 pointcut 定义切点,后面跟函数名称,最后编写匹配方式,函数一般使用 call()execution() 进行匹配。

pointcut 函数名称:匹配表达式

在上述示例中的 recordLog() 是自定义函数名称,其中 * 表示任意返回值,紧接着就是需要拦截目标函数了, helloWord(..) 表示任意参数类型。

pointcut recordLog():call(* HelloWord.helloWord(..));
  • 关于通知定义语法有以下五种方式:
    • before() :在目标方法执行前,执行前置通知。
    • after() :在目标方法执行后,执行后置通知。
    • after() returning :在目标方法返回时,执行后置返回通知。
    • after() throwing :在目标方法抛出异常时,执行异常通知。
    • around() :在目标方法执行中执行,可控制目标方法是否执行的环绕通知,即目标方法的前后。
[返回值类型] 通知函数名称 (参数)[returning/throwing]: 连接点函数 (切点函数){}
如:after () returning:recordLog (){}

示例代码如下:注意其中 around 通知即环绕通知,可以通过 proceed() 方法来控制目标函数是否执行。

HelloWordAspect.java
package top.rem.rain.aspect;
public aspect HelloWordAspect {
    /*
      定义前置通知
      before (参数): 连接点函数 {
          函数体
      }
     */
    before():authCheck(){
        System.out.println("sayHello方法执行前验证权限");
    }
    /*
      定义后置通知
      after (参数): 连接点函数 {
          函数体
      }
     */
    after():recordLog(){
        System.out.println("sayHello方法执行后记录日志");
    }
    
    /*
      定义后置通知带返回值
      after (参数) returning (返回值类型): 连接点函数 {
          函数体
      }
     */
    after()returning(int x): get(){
        System.out.println("返回值为:"+x);
    }
    /*
      异常通知
      after (参数) throwing (返回值类型): 连接点函数 {
          函数体
      }
     */
    after() throwing(Exception e):helloWord(){
        System.out.println("抛出异常:"+e.toString());
    }
    /*
      环绕通知 可通过 proceed () 控制目标函数是否执行
      Object around (参数): 连接点函数 {
          函数体
          Object result=proceed ();// 执行目标函数
          return result;
      }
     */
    Object around():aroundAdvice(){
        System.out.println("sayAround 执行前执行");
        Object result=proceed();// 执行目标函数
        System.out.println("sayAround 执行后执行");
        return result;
    }
    
}

现在已经有了对 pointcut(切入点)advice(通知) 的概念,而切面则是调用切入点和通知的组合如上述使用的 aspect 关键字定义的 HelloWordAspect 类,把切面应用到目标函数的过程称为 weaving(织入)

UserServiceImpl.java
public class UserServiceImpl{
    public void addUser(){}
    public void deleteUser(){}
}
/*
  joinPoint (连接点):指的是哪些目标函数可以被拦截如:addUser ()、deleteUser ()
  pointcut (切入点):指的是对 joinPoint 中哪些目标函数进行切入
  advice (通知):在某个特定的切入点上执行操作,如:日志、权限验证等具体要应用于切入点的代码
  weaving (织入):把切面的代码应用到目标函数的过程被称为织入      
  Aspect (切面):由切点和通知结合而成,定义通知应用到哪些切入点上
 */

# Aspect 织入原理

我们需要了解 AspectJ 是如何应用到 Java 代码中的,其中这个过程被称为织入,对于这个过程分为动态织入和静态织入,动态织入是在运行期时动态将要增强的代码织入到目标类中,这样的操作都是通过动态代理来完成的,如: ProxyCGLIB

ApectJ 采用的是静态织入方式,它是在编译期间进行织入,在这个期间使用 AspectJajc 编译器把 aspect 类编译成 class 字节码后,在 Java 目标类编译时织入,即先编译 aspect 类再编译目标类。

pixPLiq.png

ajc 编译器它是一种可以识别 aspect 语法的编译器,采用 Java 语言编写的,由于 javac 编译器并不能识别 aspect 语法,便有了 ajc 编译器, ajc 编译器是可以编译 Java 文件的。

为了更佳直观了解 aspect 织入方式,现在打开刚才已编译完成的 HelloWord.class 文件,反编译后的代码如下:

HelloWord.class
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package top.rem.rain.aspect;
public class HelloWord {
    public HelloWord() {
    }
    private void helloWord() {
        System.out.println("Hello Word!");
    }
    public static void main(String[] args) {
        HelloWord helloWord = new HelloWord();
        HelloWord var10000 = helloWord;
        try {
            // 切面类前置通知织入
            HelloWordAspect.aspectOf().ajc$before$top_rem_rain_HelloWordAspect$1$22c5541();
            // 目标函数的调用
            var10000.helloWord();
        } catch (Throwable var3) {
            HelloWordAspect.aspectOf().ajc$after$top_rem_rain_HelloWordAspect$2$4d789574();
            throw var3;
        }
        // 切面类后置通知织入
        HelloWordAspect.aspectOf().ajc$after$top_rem_rain_HelloWordAspect$2$4d789574();
    }
}

当然除了编译期织入,还存在链接期 (编译后) 织入,即:将 aspect 类和 java 目标类同时编译成字节码文件后,再进行织入处理,这种方式有助于已编译好的第三方 jarclass 文件进行织入操作。

# SpringAop-AspectJ

SpringAOPAspectJ 目的是一致的,它们都是为了统一处理横切业务,但与 AspectJ 不同的是 SpringAOP 更注重的是与 SpringIOC 容器结合,利用该优势来处理横切业务问题,因此在 AOP 功能完善方面来讲 AspectJ 更具有优势。

此时 Spring 注意到了 AspectJ 在实现 AOP 的方式上依赖于特殊编译器 (ajc编译器) ,因此 Spring 采用了动态代理技术来构建 SpringAOP 内部机制,其 SpringAOP(动态织入)AspectJ(静态织入) 最大的区别就是动态织入和静态织入。

AspectJ 1.5 之后引入了 @Aspect 注解开发, Spring 也很快的跟进了这种方式,因此 Spring 2.0 后便使用了与 AspectJ 相同名称的注解,注意: Spring 只是使用了与 AspectJ 5 相同的注解,并没有使用 AspectJ 的编译器,其底层实现依旧是动态代理技术,Spring 官方描述

# SpringAop 快速入门

首先在普通 Maven 项目中添加以下依赖。

pom.xml
<properties>
  <spring.version>6.1.0</spring.version>
</properties>
<dependencies>
  <!--    添加 Aspectj 依赖    -->
  <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.14</version>
  </dependency>
  <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjtools</artifactId>
    <version>1.8.14</version>
  </dependency>
  <!--    添加 Spring 依赖    -->
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>${spring.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>${spring.version}</version>
  </dependency>
  <!--    添加 junit 单元测试    -->
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>compile</scope>
  </dependency>
  <!--    添加 Spring 测试    -->
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>6.0.6</version>
  </dependency>
</dependencies>

定义 UserDao 接口。

UserDao.java
package top.rem.rain.spring.aop;
/**
 * @author LightRain
 */
public interface UserDao {
  /**
   * 添加用户
   */
  String addUser();
  /**
   * 删除用户
   */
  void deleteUser();
}

定义 UserDao 接口的实现类。

UserDaoImpl.java
package top.rem.rain.spring.aop;
import org.springframework.stereotype.Repository;
/**
 * @Author: LightRain
 * @Description: 实现类
 * @DateTime: 2024-01-04 21:17
 * @Version:1.0
 **/
@Repository
public class UserDaoImpl implements UserDao{
  /**
   * 添加用户
   *
   * @return
   */
  @Override
  public String addUser() {
    System.out.println("添加了一条用户记录");
    return "成功添加一条用户记录";
  }
  /**
   * 删除用户
   */
  @Override
  public void deleteUser() {
    System.out.println("删除了一条用户记录");
  }
}

定义 SpringAopaspect

UserDaoAspect.java
package top.rem.rain.spring.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
 * @Author: LightRain
 * @Description: SpringAop 切面类
 * @DateTime: 2024-01-04 21:28
 * @Version:1.0
 **/
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理
public class UserDaoAspect {
  /**
   * 前置通知
   */
  @Before("execution(* top.rem.rain.spring.aop.UserDao.addUser(..))")
  public void before() {
    System.out.println("前置通知....");
  }
  /**
   * 后置通知
   * returnVal, 切点方法执行后的返回值
   */
  @AfterReturning(value = "execution(* top.rem.rain.spring.aop.UserDao.addUser(..))", returning = "returnVal")
  public void afterReturning(Object returnVal) {
    System.out.println("后置通知:" + returnVal);
  }
  /**
   * 环绕通知
   *
   * @param joinPoint 可用于执行切点的类
   * @return
   * @throws Throwable
   */
  @Around("execution(* top.rem.rain.spring.aop.UserDao.addUser(..))")
  public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("环绕通知前....");
    Object obj = (Object) joinPoint.proceed();
    System.out.println("环绕通知后....");
    return obj;
  }
  /**
   * 抛出通知
   *
   * @param e
   */
  @AfterThrowing(value = "execution(* top.rem.rain.spring.aop.UserDao.addUser(..))", throwing = "e")
  public void afterThrowable(Throwable e) {
    System.out.println("出现异常:msg=" + e.getMessage());
  }
  /**
   * 无论在什么情况下都会执行的方法
   */
  @After(value = "execution(* top.rem.rain.spring.aop.UserDao.addUser(..))")
  public void after() {
    System.out.println("最终通知....");
  }
}

编写配置文件并交由 SpringIOC 来管理。

spring-aop-aspectj.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 定义目标对象 -->
    <bean id="userDao" class="top.rem.rain.spring.aop.UserDaoImpl" />
    <!-- 定义 aspect 类 -->
    <bean name="userDaoAspect" class="top.rem.rain.spring.aop.UserDaoAspect"/>
</beans>

定义一个测试类来测试是否可以正确切入,测试类代码如下:

UserDaoTest.java
package top.rem.rain.spring.aop;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
 * @Author: LightRain
 * @Description: 测试类
 * @DateTime: 2024-01-04 21:54
 * @Version:1.0
 **/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations= "classpath:spring-aop-aspectj.xml")
public class UserDaoTest {
  @Autowired
  private UserDao userDao;
  @Test
  public void testUserDao(){
    userDao.addUser();
        /*
        执行结果:
          环绕通知前....
          前置通知....
          添加了一条用户记录
          后置通知:成功添加一条用户记录
          最终通知....
          环绕通知后....
         */
  }
}

在此说明一下 UserDaoAspect 类中的定义,在该类中编写了五种注解类型的通知方法,分别是 @Before(前置通知) @AfterReturning(后置通知) @Around(环绕通知) @AfterThrowing(异常通知) @After(最终通知) ,这五种通知与 AspectJ 通知类型几乎是一样的,并在注解通知上使用 execution 关键字来定义切点表达式,即指明该通知要应用到哪个目标方法,当只有一个 execution 参数时 value 属性可以省略,当含有两个或以上参数时 value 属性就不可以省略了。

当然除了把切点表达式直接传递给注解通知类型外,还可以使用 @pointcut 来定义切点表达式,这个与 AspectJ 使用的关键字 pointcut 是一样的,在将目标类和 aspect 类定义完成后,还需在 XML 配置文件中进行配置并将它们交由 SpringIOC 来管理。

# 关于 SpringAop 术语

SpringAop 实现是遵循的 AOP 规范,特别是以 AspectJ 为参考可以与 Java 无缝整合,因此在 AOP 术语的概念上与 AspectJAOP 术语是一样的。

如: pointcut(切点) 定义需要应用通知的目标方法,通知则是那些需要应用到目标方法而编写的方法体, Aspect(切面) 则是通知与切点的结合, weaving(织入) 则是将 aspect 类应用到目标方法的过程,只不过 SpringAop 底层是通过动态代理技术来实现的。

# SpringAop - 注解开发

# 定义切点方法

在上述案例中,定义过滤切点方法时,是直接把 execution 已定义匹配表达式作为值传递给通知类型的。

UserDaoAspect.java
public class UserDaoAspect {
  @After(value = "execution(* top.rem.rain.spring.aop.UserDao.addUser(..))")
  public void after() {
    System.out.println("最终通知....");
  }
}

在除了上述方式外,还可以采用与 AspectJ 中使用 pointcut 关键字类似的定义切点表达式,使用 @Pointcut 注解,代码如下:

UserDaoAspect.java
public class UserDaoAspect {
    /**
     * 使用 @Pointcut 注解定义切点
     */
    @Pointcut("execution(* top.rem.rain.spring.aop.UserDao.addUser(..))")
    private void customPointcut(){}
    /**
     * 应用切入点方法
     */
    @After(value="customPointcut()")
    public void afterDemo(){
        System.out.println("最终通知....");
    }
}

使用 @Pointcut 注解进行定义并应用到通知方法 afterDemo 时直接传递切点表达式的方法名称即可。

# 切入点指示符

为了使方法通知应用到相应过滤的目标方法上, SpringAop 提供了匹配表达式,这些表达式也被称为切入点指示符,Spring 官方文档

# 方法签名表达式

如果想要根据方法签名进行过滤使用关键字 execution 即可,语法表达式如下:

//scope:方法作用域,如 public,private,protect
//returnt-type:方法返回值类型
//fully-qualified-class-name:表方法所在类的完全限定名称
//parameters:方法参数
execution (<scope> <return-type> <fully-qualified-class-name>.*(parameters))

对于给定的作用域、返回值类型、完全限定名以及参数匹配的方法将会应用切点函数指定的通知,示例如下:

// 匹配 UserDaoImpl 类中的所有方法
@Pointcut("execution(* top.rem.rain.spring.aop.UserDaoImpl.*(..))")
// 匹配 UserDaoImpl 类中的所有公共方法
@Pointcut("execution(public * top.rem.rain.spring.aop.UserDaoImpl.*(..))")
// 匹配 UserDaoImpl 类中的所有公共方法并且返回值为 String 类型
@Pointcut("execution(public String top.rem.rain.spring.aop.UserDaoImpl.*(..))")
// 匹配 UserDaoImpl 类中第一个参数为 int 类型的所有公共方法
@Pointcut("execution(public * top.rem.rain.spring.aop.UserDaoImpl.*(String, ..))")

# 类型签名表达式

为了方便过滤类型方法如:包名、类名、接口, SpringAop 提供了 within 关键字,语法表达式如下:

within(<type name>)

示例如下:

// 匹配 top.rem.rain.spring.aop.dao 包及其子包中所有类中的所有方法
@Pointcut("within(top.rem.rain.spring.aop.dao..*)")
// 匹配 UserDaoImpl 类中所有方法
@Pointcut("within(top.rem.rain.spring.aop.UserDaoImpl)")
// 匹配 UserDaoImpl 类及其子类中所有方法
@Pointcut("within(top.rem.rain.spring.aop.UserDaoImpl+)")
// 匹配所有实现 UserDao 接口类的所有方法
@Pointcut("within(top.rem.rain.spring.aop.UserDao+)")

# 通配符

  • 当在定义匹配表达式是通配符可以说是随处可见的,如 *..+ 它们代表的含义如下:

    • * :匹配任意数量的字符。
    // 匹配 top.rem.rain.spring.aop.service 包及其子包中所有类的所有方法
    within(top.rem.rain.spring.aop.service..*)
    // 匹配以 set 开头,参数为 int 类型,任意返回值的方法
    execution(* set*(int))
    • + :匹配给定类的任意子类。
    // 匹配实现了 DaoUser 接口的所有子类的方法
    within(top.rem.rain.spring.aop.dao.DaoUser+)
    • .. :匹配方法定义中的任意数量的参数,此外还可以匹配类定义中的任意数量包。
    // 任意返回值,任意名称,任意参数的公共方法
    execution(public * *(..))
    // 匹配 top.rem.rain.spring.aop.dao 包及其子包中所有类中的所有方法
    within(top.rem.rain.spring.aop.dao..*)

# 其它标识符

beanSpringAop 扩展的,在 AspectJ 中没有对应的标识符,它用于匹配特定名称的 bean 对象的执行方法。

// 匹配名称中带有后缀 Service 的 Bean。
@Pointcut("bean(*Service)")
private void customPointcut1(){}

this :它用于匹配当前 AOP 代理对象类型的执行方法,注意:是 AOP 代理对象的类型匹配。

// 匹配任意实现 UserDao 接口的代理对象的方法进行过滤
@Pointcut("this(top.rem.rain.spring.aop.UserDao)")
private void customPointcut2(){}

target :它用于匹配当前目标对象类型的执行方法。

// 匹配任意实现 UserDao 接口的目标对象的方法进行过滤
@Pointcut("target(top.rem.rain.spring.aop.UserDao)")
private void customPointcut3(){}

@within :它用于匹配所持有指定注解类型内的方法,注意:它与 within 是有区别的, within 是用于匹配指定类型内的方法。

// 匹配使用 Transactional 注解的类
@Pointcut("@within(org.springframework.transaction.annotation.Transactional)")
private void customPointcut4(){}

@annotation :根据所应用的注解进行方法过滤。

// 匹配使用 Transactional 注解的方法
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
private void customPointcut5(){}

# 五种通知类型

通知类型主要分为五种,在 SpringAspectJ 中一样,分别是 @Before(前置通知) @AfterReturning(后置通知) @Around(环绕通知) @AfterThrowing(异常通知) @After(最终通知)

# @Before (前置通知)

前置通知通过使用 @Before 注解进行标注,可直接传入切点表达式的值,该通知在目标方法执行前执行,通过 JoinPoint 参数可以获取,目标对象的信息,如:类名称、方法名称、方法参数等,该参数可选。

UserDaoAspect.java
package top.rem.rain.spring.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
 * @Author: LightRain
 * @Description: SpringAop 切面类
 * @DateTime: 2024-01-04 21:28
 * @Version:1.0
 **/
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理
public class UserDaoAspect {
  /**
   * 前置通知
   * @param joinPoint 该参数可以获取目标对象的信息,如类名称,方法参数,方法名称等
   */
  @Before("execution(* top.rem.rain.spring.aop.UserDao.addUser(..))")
  public void before(JoinPoint joinPoint) {
    System.out.println("前置通知....");
  }
}

# @AfterReturning (后置通知)

后置通知通过使用 @AfterReturning 注解进行标注,该通知在目标方法执行完成后执行,可以获取到目标方法最终返回值 returnVal , 当目标方法没有返回值时 returnVal 将返回 null , 注意:必须通过 returning = "returnVal" 注明参数并且必须与通知方法参数名称相同,该参数可选。

UserDaoAspect.java
package top.rem.rain.spring.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
 * @Author: LightRain
 * @Description: SpringAop 切面类
 * @DateTime: 2024-01-04 21:28
 * @Version:1.0
 **/
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理
public class UserDaoAspect {
    /**
     * 后置通知
     * returnVal, 切点方法执行后的返回值
     */
    @AfterReturning(value = "execution(* top.rem.rain.spring.aop.UserDao.addUser(..))", returning = "returnVal")
    public void afterReturning(Object returnVal) {
        System.out.println("后置通知:" + returnVal);
    }
}

# @Around (环绕通知)

环绕通知通过使用 @Around 注解进行标注,它可以在目标方法前执行也可以在目标方法后执行,更重要的是环绕通知可以控制方法是否执行,即使如此我们也应该尽量以最简单的方式满足需求,在仅需要在目标方法前执行时应该采用前置通知而并非环绕通知。

环绕通知的第一个参数必须为 ProceedingJoinPoint , 通过该对象的 proceed() 方法来执行目标方法, proceed() 的返回值就是环绕通知的返回值, ProceedingJoinPoint 它同样可以获取目标对象的信息,如:类名称、方法名称、方法参数等。

UserDaoAspect.java
package top.rem.rain.spring.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
 * @Author: LightRain
 * @Description: SpringAop 切面类
 * @DateTime: 2024-01-04 21:28
 * @Version:1.0
 **/
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理
public class UserDaoAspect {
    /**
     * 环绕通知
     *
     * @param joinPoint 可用于执行切点的类
     * @return
     * @throws Throwable
     */
    @Around("execution(* top.rem.rain.spring.aop.UserDao.addUser(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知前....");
        Object obj = (Object) joinPoint.proceed();
        System.out.println("环绕通知后....");
        return obj;
    }
}

# @AfterThrowing (异常通知)

该通知只有在异常时才会被触发,并由 throwing 来声明一个接收异常信息的变量,同样异常通知也可用于 JoinPoint 参数,需要时加上即可。

UserDaoAspect.java
package top.rem.rain.spring.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
 * @Author: LightRain
 * @Description: SpringAop 切面类
 * @DateTime: 2024-01-04 21:28
 * @Version:1.0
 **/
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理
public class UserDaoAspect {
    /**
     * 异常通知
     *
     * @param e 抛出异常的信息
     */
    @AfterThrowing(value = "execution(* top.rem.rain.spring.aop.UserDao.addUser(..))", throwing = "e")
    public void afterThrowable(Throwable e) {
        System.out.println("出现异常:msg=" + e.getMessage());
    }
}

# @After (最终通知)

最终通知类似于 finally 代码块,只要应用了目标方法时不管在什么情况下都会执行。

UserDaoAspect.java
package top.rem.rain.spring.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
 * @Author: LightRain
 * @Description: SpringAop 切面类
 * @DateTime: 2024-01-04 21:28
 * @Version:1.0
 **/
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理
public class UserDaoAspect {
    /**
     * 无论在什么情况下都会执行的方法
     */
    @After(value = "execution(* top.rem.rain.spring.aop.UserDao.addUser(..))")
    public void after() {
        System.out.println("最终通知....");
    }
}

# 传递通知参数

SpringAop 中,除了 executionbean 指示符不可以传递参数给通知方法,其它指示符都可以将匹配方法相应参数传递给通知方法,获取到匹配方法参数后通过 argNames 属性指定参数名,注意 args 指示符, argNames 参数名称必须与 before() 方法中的参数名称保持一致。

UserDaoAspect.java
package top.rem.rain.spring.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
 * @Author: LightRain
 * @Description: SpringAop 切面类
 * @DateTime: 2024-01-04 21:28
 * @Version:1.0
 **/
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理
public class UserDaoAspect {
    @Before(value="args(param)", argNames="param") // 明确指定了    
    public void before(String param) {
        System.out.println("param:" + param);
    }
}

当然也可以直接使用 args 指示符不带 argNames 参数。

UserDaoAspect.java
package top.rem.rain.spring.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
 * @Author: LightRain
 * @Description: SpringAop 切面类
 * @DateTime: 2024-01-04 21:28
 * @Version:1.0
 **/
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理
public class UserDaoAspect {
    @Before("execution(public * top.rem.rain.spring.aop..*.addUser(..)) && args(userId,..)")
    public void before(int userId) {
        // 当调用 addUser 方法时如果与 addUser 的参数匹配成功则会将参数值传递进来
        System.out.println("userId:" + userId);
    }
}

# 关于 Aspect 的优先级

在不同的切面中,如果有多个通知需要在同一个切点方法指定的过滤目标方法上执行,那么那些在目标方法前执行的通知属于最高优先级,将会优先执行,在执行目标方法后执行的通知将会在最后执行,而对于在同一个切面定义的通知方法会根据在类中声明的顺序执行。

UserDaoAspect2.java
package top.rem.rain.spring.aop;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
 * @Author: LightRain
 * @Description: SpringAop 切面类
 * @DateTime: 2024-01-04 21:28
 * @Version:1.0
 **/
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理
public class UserDaoAspect2 {
    /**
     * Pointcut 定义切点函数
     */
    @Pointcut("execution(* top.rem.rain.spring.aop.UserDao.deleteUser(..))")
    private void customPointcut(){}
    @Before("customPointcut()")
    public void beforeOne(){
        System.out.println("前置通知....执行顺序1");
    }
    @Before("customPointcut()")
    public void beforeTwo(){
        System.out.println("前置通知....执行顺序2");
    }
    @AfterReturning(value = "customPointcut()")
    public void afterReturningThree(){
        System.out.println("后置通知....执行顺序3");
    }
    @AfterReturning(value = "customPointcut()")
    public void afterReturningFour(){
        System.out.println("后置通知....执行顺序4");
    }
}

xml 中配置一条 bean 信息并交由 SpringIOC 管理。

spring-aop-aspectj.xml
<bean name="userDaoAspect2" class="top.rem.rain.spring.aop.UserDaoAspect2"/>

测试方法如下:

UserDaoTest.java
package top.rem.rain.spring.aop;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
 * @Author: LightRain
 * @Description: 测试类
 * @DateTime: 2024-01-04 21:54
 * @Version:1.0
 **/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations= "classpath:spring-aop-aspectj.xml")
public class UserDaoTest {
    @Autowired
    private UserDao userDao;
    @Test
    public void testUserDao2() {
        userDao.deleteUser();
        /*
        执行结果:
            前置通知.... 执行顺序 1
            前置通知.... 执行顺序 2
            删除了一条用户记录
            后置通知.... 执行顺序 3
            后置通知.... 执行顺序 4
         */
    }
}

由此可以看出执行结果,在同一个切面中定义多个通知来处理同一个切点方法,确实会根据在类中的声明顺序执行。

执行结果
前置通知.... 执行顺序 1
前置通知.... 执行顺序 2
删除了一条用户记录
后置通知.... 执行顺序 3
后置通知.... 执行顺序 4

如果在不同切面中定义多个通知来处理同一个切点,进入时则优先级高的切面类中的通知方法优先执行,退出时则最后执行。

如下定义了两个类分别是 UserDaoAspect3UserDaoAspect4 切面类并且实现了 Ordered 接口,该接口用于控制切面类的优先级,同时重写 getOrder() 方法,返回值越小优先级越高。其中 UserDaoAspect3 的返回值是 0 ,而 UserDaoAspect4 的优先级是 1 , 显然 UserDaoAspect3 的优先级更高。

UserDaoAspect3.java
package top.rem.rain.spring.aop;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.Ordered;
/**
 * @Author: LightRain
 * @Description: SpringAop 切面类
 * @DateTime: 2024-01-04 21:28
 * @Version:1.0
 **/
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理
public class UserDaoAspect3 implements Ordered {
  /**
   * Pointcut 定义切点函数
   */
  @Pointcut("execution(* top.rem.rain.spring.aop.UserDao.deleteUser(..))")
  private void customPointcut() {
  }
  @Before("customPointcut()")
  public void beforeOne() {
    System.out.println("前置通知..UserDaoAspect3..执行顺序1");
  }
  @Before("customPointcut()")
  public void beforeTwo() {
    System.out.println("前置通知..UserDaoAspect3..执行顺序2");
  }
  @AfterReturning(value = "customPointcut()")
  public void afterReturningThree() {
    System.out.println("后置通知..UserDaoAspect3..执行顺序3");
  }
  @AfterReturning(value = "customPointcut()")
  public void afterReturningFour() {
    System.out.println("后置通知..UserDaoAspect3..执行顺序4");
  }
  /**
   * 定义优先级,值越低,优先级越高
   * @return int
   */
  @Override
  public int getOrder() {
    return 0;
  }
}
UserDaoAspect4.java
package top.rem.rain.spring.aop;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.Ordered;
/**
 * @Author: LightRain
 * @Description: SpringAop 切面类
 * @DateTime: 2024-01-04 21:28
 * @Version:1.0
 **/
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理
public class UserDaoAspect4 implements Ordered {
    /**
     * Pointcut 定义切点函数
     */
    @Pointcut("execution(* top.rem.rain.spring.aop.UserDao.deleteUser(..))")
    private void customPointcut() {
    }
    @Before("customPointcut()")
    public void beforeOne() {
        System.out.println("前置通知..UserDaoAspect4..执行顺序1");
    }
    @Before("customPointcut()")
    public void beforeTwo() {
        System.out.println("前置通知..UserDaoAspect4..执行顺序2");
    }
    @AfterReturning(value = "customPointcut()")
    public void afterReturningThree() {
        System.out.println("后置通知..UserDaoAspect4..执行顺序3");
    }
    @AfterReturning(value = "customPointcut()")
    public void afterReturningFour() {
        System.out.println("后置通知..UserDaoAspect4..执行顺序4");
    }
    /**
     * 定义优先级,值越低,优先级越高
     *
     * @return int
     */
    @Override
    public int getOrder() {
        return 1;
    }
}

xml 添加如下配置:

spring-aop-aspectj.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 定义目标对象 -->
    <bean id="userDao" class="top.rem.rain.spring.aop.UserDaoImpl" />
    <!-- 定义 aspect 类 -->
<!--    <bean name="userDaoAspect" class="top.rem.rain.spring.aop.UserDaoAspect"/>-->
<!--    <bean name="userDaoAspect2" class="top.rem.rain.spring.aop.UserDaoAspect2"/>-->
    <bean name="userDaoAspect3" class="top.rem.rain.spring.aop.UserDaoAspect3"/>
    <bean name="userDaoAspect4" class="top.rem.rain.spring.aop.UserDaoAspect4"/>
</beans>

测试方法和执行结果如下:

UserDaoTest.java
package top.rem.rain.spring.aop;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
 * @Author: LightRain
 * @Description: 测试类
 * @DateTime: 2024-01-04 21:54
 * @Version:1.0
 **/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations= "classpath:spring-aop-aspectj.xml")
public class UserDaoTest {
    @Autowired
    private UserDao userDao;
    @Test
    public void testUserDao3() {
        userDao.deleteUser();
        /*
        执行结果:
            前置通知..UserDaoAspect3.. 执行顺序 1
            前置通知..UserDaoAspect3.. 执行顺序 2
            前置通知..UserDaoAspect4.. 执行顺序 1
            前置通知..UserDaoAspect4.. 执行顺序 2
            删除了一条用户记录
            后置通知..UserDaoAspect4.. 执行顺序 3
            后置通知..UserDaoAspect4.. 执行顺序 4
            后置通知..UserDaoAspect3.. 执行顺序 3
            后置通知..UserDaoAspect3.. 执行顺序 4
         */
    }
}

# SpringAop-XML 开发

下面是基于 XML 的方式来使用 SpringAop

UserDaoAspectXML.xml
package top.rem.rain.spring.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.Ordered;
/**
 * @Author: LightRain
 * @Description: SpringAop 切面类 - XML 方式
 * @DateTime: 2024-01-05 22:18
 * @Version:1.0
 **/
public class UserDaoAspectXML {
    public void before() {
        System.out.println("UserDaoAspectXML ==== 前置通知");
    }
    public void afterReturn(Object returnVal) {
        System.out.println("后置通知 --> 返回值:" + returnVal);
    }
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("UserDaoAspectXML ===== 环绕通知前");
        Object object = joinPoint.proceed();
        System.out.println("UserDaoAspectXML ===== 环绕通知后");
        return object;
    }
    public void afterThrowing(Throwable throwable) {
        System.out.println("UserDaoAspectXML ====== 异常通知:" + throwable.getMessage());
    }
    public void after() {
        System.out.println("UserDaoAspectXML ===== 最终通知..来了");
    }
}

通过配置文件的方式声明如下:

spring-aop-aspectj.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    <!-- 定义目标对象 -->
    <bean id="userDao" class="top.rem.rain.spring.aop.UserDaoImpl" />
    <!-- 定义 aspect 类 -->
    <bean name="userDaoAspectXML" class="top.rem.rain.spring.aop.UserDaoAspectXML"/>
    <!-- 配置 AOP 切面 -->
    <aop:config>
        <!-- 定义切点函数 -->
        <aop:pointcut id="pointcut" expression="execution(* top.rem.rain.spring.aop.UserDao.addUser(..))" />
        <!-- 定义通知 order 定义优先级,值越小优先级越大 -->
        <aop:aspect ref="userDaoAspectXML" order="0">
            <!-- 定义通知
            method 指定通知方法名,必须与 userDaoAspectXML 中的相同
            pointcut 指定切点函数
            -->
            <aop:before method="before" pointcut-ref="pointcut" />
            <!-- 后置通知  returning="returnVal" 定义返回值 必须与类中声明的名称一样 -->
            <aop:after-returning method="afterReturn" pointcut-ref="pointcut"  returning="returnVal" />
            <!-- 环绕通知 -->
            <aop:around method="around" pointcut-ref="pointcut"  />
            <!-- 异常通知 throwing="throwable" 指定异常通知错误信息变量,必须与类中声明的名称一样 -->
            <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="throwable"/>
            <!--
                 method : 通知的方法 (最终通知)
                 pointcut-ref : 通知应用到的切点方法
                -->
            <aop:after method="after" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>
</beans>

测试方法和执行结果如下:

UserDaoTest.java
package top.rem.rain.spring.aop;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
 * @Author: LightRain
 * @Description: 测试类
 * @DateTime: 2024-01-04 21:54
 * @Version:1.0
 **/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations= "classpath:spring-aop-aspectj.xml")
public class UserDaoTest {
    @Autowired
    private UserDao userDao;
    @Test
    public void testUserDao4(){
        userDao.addUser();
        /*
        执行结果:
            UserDaoAspectXML ==== 前置通知
            UserDaoAspectXML ===== 环绕通知前
            添加了一条用户记录
            UserDaoAspectXML ===== 最终通知.. 来了
            UserDaoAspectXML ===== 环绕通知后
            后置通知 --> 返回值:成功添加一条用户记录
         */
    }
}

# SpringAop 应用场景

下面是两种开发中常用到的场景,注意:这些场景的代码并不一定适合所有情况,但它们确实可以运用到实际开发中。

# 性能监控

对于成熟的系统,性能监控是不可或缺的,以下代码将展示程序的消耗时间,而对于计算消耗时间的代码可能会存在于各个模块中。

// 获取当前毫秒值
long start = System.currentTimeMillis();
// 获取当前毫秒值并减去之前的毫秒值计算这段代码的运行耗时
long time = System.currentTimeMillis() - start;

而这种操作就可以使用 AOP 作为横切点来执行并减少模块中的重复代码。

为了方便演示此处只创建了一个 Controller 并未创建 Service , 其中的 for 表示要执行的业务代码的耗时。

UserController.java
package top.rem.rain.controller;
import org.springframework.stereotype.Controller;
/**
 * @Author: LightRain
 * @Description: User 控制器
 * @DateTime: 2024-01-05 22:58
 * @Version:1.0
 **/
@Controller
public class UserController {
    public Object[] addUser(String userName,int age,String sex){
        // 此处本该是调用 service 层来处理响应的业务信息,这里为了简单演示就不创建 service 层了,就直接在 Controller 层写代码了
        for (int i = 0; i < 2000000000; i++) {}
        return new Object[]{userName,age,sex};
    }
}
PerformanceMonitoringInfo.java
package top.rem.rain.monitoring.performance.bean;
import java.util.Date;
/**
 * @Author: LightRain
 * @Description: 性能监控信息
 * @DateTime: 2024-01-05 22:50
 * @Version:1.0
 **/
public class PerformanceMonitoringInfo {
    /**
     * 类名
     */
    private String className;
    /**
     * 方法名
     */
    private String methodName;
    /**
     * 日志时间
     */
    private Date logTime;
    /**
     * 耗时时间
     */
    private long timeConsuming;
    public String getClassName() {
        return className;
    }
    public void setClassName(String className) {
        this.className = className;
    }
    public String getMethodName() {
        return methodName;
    }
    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }
    public Date getLogTime() {
        return logTime;
    }
    public void setLogTime(Date logTime) {
        this.logTime = logTime;
    }
    public long getTimeConsuming() {
        return timeConsuming;
    }
    public void setTimeConsuming(long timeConsuming) {
        this.timeConsuming = timeConsuming;
    }
}
PerformanceAspect.java
package top.rem.rain.monitoring.performance.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import top.rem.rain.monitoring.performance.bean.PerformanceMonitoringInfo;
import java.util.Date;
/**
 * @Author: LightRain
 * @Description: 监控切面类
 * @DateTime: 2024-01-05 23:09
 * @Version:1.0
 **/
@Aspect // 这是一个 AspectJ 切面类
@EnableAspectJAutoProxy // 启用 AspectJ 自动代理
public class PerformanceAspect {
    /**
     * 定义切点函数,过滤 controller 包下的名称以 Controller 结尾的类所有方法
     */
    @Pointcut("execution(* top.rem.rain.controller.*Controller.*(..))")
    public void timer() {
    }
    @Around("timer()")
    public Object logTimer(ProceedingJoinPoint thisJoinPoint) throws Throwable {
        PerformanceMonitoringInfo performanceMonitoring = new PerformanceMonitoringInfo();
        // 获取目标类名称
        String clazzName = thisJoinPoint.getTarget().getClass().getName();
        // 获取目标类方法名称
        String methodName = thisJoinPoint.getSignature().getName();
        // 记录类名称
        performanceMonitoring.setClassName(clazzName);
        // 记录对应方法名称
        performanceMonitoring.setMethodName(methodName);
        // 记录时间
        performanceMonitoring.setLogTime(new Date());
        // 计时并调用目标函数
        long start = System.currentTimeMillis();
        Object result = thisJoinPoint.proceed();
        long time = System.currentTimeMillis() - start;
        // 设置消耗时间
        performanceMonitoring.setTimeConsuming(time);
        System.out.println("消耗时间 = " + time + "毫秒");
        // 如果要把 performanceMonitoring 记录的信息上传给监控系统,这里并没有实现,需要自行实现即可
        //MonitoruUtils.report(performanceMonitoring)
        return result;
    }
}

配置 XMLSpringIOC 进行管理。

performance-monitoring.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean name="userController" class="top.rem.rain.controller.UserController"/>
    <bean name="performanceAspect" class="top.rem.rain.monitoring.performance.aspect.PerformanceAspect"/>
</beans>

测试方法如下:

UserControllerTest.java
package top.rem.rain.monitoring.performance.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import top.rem.rain.controller.UserController;
import java.util.Arrays;
/**
 * @Author: LightRain
 * @Description: 测试类
 * @DateTime: 2024-01-05 23:18
 * @Version:1.0
 **/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations= "classpath:performance-monitoring.xml")
public class UserControllerTest {
    @Autowired
    private UserController controller;
    @Test
    public void testUser(){
        System.out.println(Arrays.toString(controller.addUser("ZeroTwo", 17, "女")));
        /*
        执行结果:
        消耗时间 = 4 毫秒
        [ZeroTwo, 17, 女]
         */
    }
}

在每次访问切点方法时就可以计算出接口消耗的时间了,以此来作为访问接口的性能指标。

# 异常处理

为了避免到处编写 try/catch 将统一处理异常信息,这也是 AOP 可以做的事情,可以在切面类中使用环绕通知来统一处理异常信息并上报给日志系统。

ExceptionInfo.java
package top.rem.rain.monitoring.abnormal.bean;
import java.util.Date;
/**
 * @Author: LightRain
 * @Description: 异常信息类
 * @DateTime: 2024-01-06 01:17
 * @Version:1.0
 **/
public class ExceptionInfo {
    /**
     * 类名
     */
    private String className;
    /**
     * 方法名
     */
    private String methodName;
    /**
     * 异常时间
     */
    private Date logTime;
    /**
     * 异常信息
     */
    private String message;
    public String getClassName() {
        return className;
    }
    public void setClassName(String className) {
        this.className = className;
    }
    public String getMethodName() {
        return methodName;
    }
    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }
    public Date getLogTime() {
        return logTime;
    }
    public void setLogTime(Date logTime) {
        this.logTime = logTime;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
}
ExceptionAspect.java
package top.rem.rain.monitoring.abnormal.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import top.rem.rain.monitoring.abnormal.bean.ExceptionInfo;
import java.util.Date;
/**
 * @Author: LightRain
 * @Description: 异常切面类
 * @DateTime: 2024-01-06 01:19
 * @Version:1.0
 **/
@Aspect
@EnableAspectJAutoProxy
public class ExceptionAspect {
  /**
   * 定义异常监控类
   */
  @Pointcut("execution(top.rem.rain *(..))")
  void exceptionMethod() {
  }
  @Around("exceptionMethod()")
  public Object monitorMethods(ProceedingJoinPoint thisJoinPoint) {
    try {
      return thisJoinPoint.proceed();
    } catch (Throwable e) {
      ExceptionInfo exceptionInfo=new ExceptionInfo();
      // 异常类记录
      exceptionInfo.setClassName(thisJoinPoint.getTarget().getClass().getName());
      exceptionInfo.setMethodName(thisJoinPoint.getSignature().getName());
      exceptionInfo.setLogTime(new Date());
      exceptionInfo.setMessage(e.toString());
      // 上传日志系统,自行完善
      // ExceptionReportUtils.report(info);
      return null;
    }
  }
}

这里仅简单演示代码实现,在实际开发中根据业务需求修改即可。 AOP 的应用远不止这两种,诸如缓存,权限验证、内容处理、事务控制等都可以使用 AOP 实现,其中事务控制 Spring 提供了专门的处理方式。

# SpringAop 实现原理

SpringAop 的实现原理是基于动态织入的动态代理技术,而 AspectJ 则是静态织入,而动态代理技术又分为 Java JDK 动态代理和 CGLIB 动态代理,前者是基于反射技术的实现,后者是基于继承的机制实现,关于代理可以查看 23 种设计模式中的代理模式。