Spring3️⃣浅聊AOP、AspectJ

1、浅聊 AOP

1.1、AOP

Aspect-oriented Programming(面向切面编程)

通过预编译方式和运行时动态代理实现程序功能的统一维护。

  • 好处:对业务逻辑的各个部分进行隔离,降低耦合度,提高程序的可重用性、开发效率。
  • 作用
    • 提供声明式企业服务(如声明式事务管理)
    • 允许自定义切面,以 AOP 来补充 OOP。

1.2、动态代理

代理(Proxy)(设计模式)

给某个对象提供一个代理,以控制对该对象的访问

Spring AOP 底层使用了动态代理。

JDK 代理 CGLIB 代理
使用前提 目标对象有实现接口 目标对象非 final 修饰
技术 反射机制,生成实现类 asm 开源包,修改字节码创建子类
代理实现 实现接口 继承类并重写方法

1.3、术语

AOP 中的术语

结合例子理解:假设以下情景

  • UserServiceImpl 实现了 UserService 接口(有增删改查 4 个方法)
  • 编写一个 MyLog 类,声明一个打印日志的方法 printLog()。
  • 要在 add() 和 delete() 方法执行前调用 printLog() 方法。
  • AOP 会生成代理类(假设称为 UserServiceProxy):实现 UserService 接口,在 add() 和 delete() 方法中先调用 printLog(),再执行方法。

注意区分理解

  • 连接点和切入点:连接点是所有可以被增强的方法,而切入点其中的一个或多个方法,是实际被增强的。
  • 横切关注点和通知:横切关注点是所有可以被注意和加强程序的功能,而通知是其中的一个或多个,是实际运用到的。
含义 例子
目标对象 Target 被通知(增强)的对象,也称为通知对象 UserServiceImpl
连接点 JoinPoint 程序执行过程中的一个点。
即类中可以被增强的方法
增删改查 4 个方法
切入点 Pointcut 实际被增强的方法 add()、delete() 方法
*横切关注点 跨越多个类的方法或功能。
即与业务逻辑无关,但是需要关注的部分
安全、日志、缓存、事务等
通知 Advice 在切入点采取的行动,即增强的功能 日志,即 printLog() 方法
切面 Aspect 横切关注点的模块化
(即所在类)
MyLog 类
代理 Proxy AOP 创建的对象,用于执行通知的方法等 UserServiceProxy 类
织入 Weaving 将切面与其他应用程序或对象联系起来,以创建目标对象,在运行时执行 将日志功能添加到 add()、delete() 方法的过程

1.4、通知 Advice

1.4.1、通知类型

切入点可采用的通知有以下 5 种类型。

名称 时机 场景
前置 Before 切入点之前 日志
后置 AfterReturning 切入点正常执行后 日志
环绕 Around 前置 + 后置 事务、权限
异常 AfterThrowing 切入点执行发生异常 异常处理、事务
最终 After 切入点后(即使有异常) 日志

1.4.2、通知执行顺序

image-20220401140431773

2、AspectJ

2.1、概述

AspectJ 是一个实现了 AOP 的框架,不属于 Spring。

Spring 通常使用 AspectJ 进行 AOP 操作。

  1. Maven 依赖:使用 AspectJ 需要导入相关依赖

    <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.7</version>
    </dependency>
    
  2. XML 配置文件:使用 AOP 需要导入相关命名空间

    xmlns:aop="http://www.springframework.org/schema/aop"
    http://www.springframework.org/schema/aop
    https://www.springframework.org/schema/aop/spring-aop.xsd"
    
  3. AOP 实现方式

    • 基于 XML 配置(了解即可)
    • 基于注解(实际使用)
  4. 注意:向容器中注册或获取对象的区别

    • 注册:需要以实现类型注册而非接口类型(原因:注册的是 bean 对象)
    • 获取需要以接口类型获取而非实现类型(原因:AOP 底层生成代理类,而非实现类本身)

2.2、切入点表达式

切入点表达式:表示对哪个类的哪个方法进行增强。

语法:execute([权限修饰符] 返回值类型 全限类名.方法名(方法参数))

  • 权限修饰符:可省略
  • 返回值类型、全限类名、方法名:可使用通配符 *
  • 方法参数.. 代表任意个数、任意类型的形参列表

示例

// 对indi.jaywee.pojo.User类的add()
execution(* indi.jaywee.pojo.User.add(..))
// 对indi.jaywee.pojo.User类的所有方法
execution(* indi.jaywee.pojo.User.*(..))
// 对indi.jaywee.pojo包的所有类的所有方法
execution(* indi.jaywee.pojo.*.*(..))

2.3、实现

本例中会使用到 AOP 术语,可以回顾 1.3 节

以 UserService 为例。

public interface UserService {
    void add();
    void delete();
}

public class UserServiceImpl implements UserService {
    @Override
    public void add() { System.out.println("add..."); }
    @Override
    public void delete() { System.out.println("delete..."); }
}

2.3.1、基于注解实现

在实际开发中,使用注解实现 AOP 技术。

配置

  • 导入 AspectJ 的 Maven 依赖

  • 导入相关命名空间、开启注解扫描

  • 开启 AspectJ 自动代理

    <?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">
        
        <context:component-scan base-package="包名">
        </context:component-scan>
        
        <aop:aspectj-autoproxy/>
    </beans>
    

目标对象

注册 bean:UserServiceImpl 类添加 @Service 注解

  • 注意:是在实现类添加注解,而不是在接口上添加。
  • 原因:是将具体的一个实现类注入到 IoC 容器中,作为一个 bean 对象。

创建切面

  • 切面:创建一个类,添加 @Aspect 注解。
  • 通知:创建一个方法,添加相应类型的注解、切入点表达式。
  • 注册 bean:@Component

  • 注册切面:@Aspect

  • 使用通知

    // 省略部分代码
    @Component
    @Aspect
    public class MyLog {
        @Before("execution(* indi.jaywee.annotation.UserService.*(..))")
        public void beforeLog() {...}
    
        @AfterReturning("execution(* indi.jaywee.annotation.UserService.*(..))")
        public void afterReturningLog() {...}
    
        @Around("execution(* indi.jaywee.annotation.UserService.*(..))")
        public void aroundLog(ProceedingJoinPoint joinPoint) throws Throwable {
            // 1、环绕前
            // 2、执行连接点
            joinPoint.proceed();
            // 3、环绕后
        }
    
        @AfterThrowing("execution(* indi.jaywee.annotation.UserService.*(..))")
        public void afterThrowingLog() {...}
    
        @After("execution(* indi.jaywee.annotation.UserService.*(..))")
        public void afterLog() {...}
    }
    

测试一下

  • 需要以接口类型获取 bean:因为获取的是代理类,而不是原本的实现类。
  • 顺序:正常执行时、发生异常时(手动在 add() 中添加一个异常)
  • 正常

    image-20220310163652020
  • 异常

    image-20220310164830345

抽取切入点

从以上例子看出:不同通知的使用注解的不同,但切入点表达式相同

  • 使用 @PointCut 注解,定义切入点表达式。

  • 通知只需引用该表达式。

    // @Pointcut只能作用于方法,声明一个空方法,方法名即为表达式名
    @Pointcut("execution(* indi.jaywee.annotation.UserService.*(..))")
    public void expression() {
    }
    
    // 通过方法名引用切入点表达式
    @Before("expression()")
    public void beforeLog() {
        System.out.println("== 前置");
    }
    

2.3.2、基于 XML 实现

XML 方式实现,了解即可。

有 2 种实现方式

  1. 自定义切面,通过配置文件注册
  2. 使用 Spring 提供的 Advice 相关的 API

以环绕通知为例,演示 XML 的自定义切面方式。

创建切面

public class MyXmlLog {
    public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("===== 环绕 =====");
        long start = System.nanoTime();

        joinPoint.proceed();
        long end = System.nanoTime();

        System.out.println("== 耗时" + (end - start) / 1000 + "微秒 ==");
    }
}

配置

  1. 注册 bean

  2. AOP 配置<aop:config>标签

    • 切入点<aop:pointcut>

    • 切面、通知<aop:aspect>注册切面,<aop:around>注册通知并关联切入点。

      <bean id="userService" class="indi.jaywee.annotation.UserServiceImpl">
      </bean>
      <bean id="xmlLog" class="indi.jaywee.annotation.MyXmlLog">
      </bean>
      
      <aop:config>
          <!-- 切入点 -->
          <aop:pointcut id="userPoint" expression="execution(* indi.jaywee.annotation.UserService.*(..))"/>
          <!-- 切面 -->
          <aop:aspect ref="xmlLog">
              <!-- 通知 -->
              <aop:around method="aroundAdvice" pointcut-ref="userPoint"/>
          </aop:aspect>
      </aop:config>
      
posted @ 2021-07-31 22:45  Jaywee  阅读(111)  评论(0编辑  收藏  举报

👇