初识 AOP

什么是AoP

AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程

AOP 是 OOP (面向对象编程) 的延,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合降低,提高程序的可重用性,同时提高了开发的效率。

这样做的好处是:

  1. 在程序运行期间,在不修改源码的情况下对方法进行功能增强
  2. 逻辑清晰,开发核心业务的时候,不必关注增强业务的代码
  3. 减少重复代码,提高开发效率,便于后期维护

AOP底层实现

实际上,AOP的底层是通过 Spring 提供的的动态代理技术实现的。在运行期间, Spring 通过动态代理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入,在去调用目标对象的方法,从而完成功能的增强。

AOP 相关概念

Spring 的 AOP 实现底层就是对上面的动态代理的代码进行了封装,封装后我们只需要对需要关注的部分进行代码编写,并通过配置的方式完成指定目标的方法增强。

AOP 常用的术语如下:

  • Target(目标对象):代理的目标对象。
  • Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类。
  • Joinpoint(连接点):所谓连接点是指那些可以被拦截到的点。在 Spring中,这些点指的是方法,因为 Spring 只支持方法类型的连接点。
  • PoIntcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。
  • Advice(通知/增强):所谓通知是指拦馘到 Joinpoint之后所要做的事情就是通知
    分类:前置通知、后置通知、异常通知、最终通知、环绕通知。
  • Aspect(切面):是切入点和通知(引介)的结合。
  • Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。 Spring 采用动态代理织入,而 Aspect 采用编译期织入和类装载期织入。
基于 XML 的 AOP 开发

Maven 相关依赖

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.10</version>
</dependency>
<!-- aspect织入(切点表达式需要用到该jar包) -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>


创建主体方法和接口

public interface AccountService {
public void transfer();
}

public class AccountServiceImpl implements AccountService {
@Override
public void transfer() {
System.out.println("开始转账");
}
}


创建通知类及方法

package com.orginly.advice;

public class TransferAdvice {
public void before() {
System.out.println("前置通知执行了");
}

public void afterReturning() {
System.out.println("后置通知执行了");
}
}


将目标类和通知类的创建权交给 Spring

<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- 将目标类交给 IOC 容器 -->
<bean id="accountService" class="com.orginly.service.impl.AccountServiceImpl"/>
<!-- 将通知类交给 IOC 容器 -->
<bean id="transferAdvice" class="com.orginly.advice.TransferAdvice"/>
</beans>


配置 AOP 前置增强

<!-- 将目标类交给 IOC 容器 -->
<bean id="accountService" class="com.orginly.service.impl.AccountServiceImpl"/>
<!-- 将通知类交给 IOC 容器 -->
<bean id="transferAdvice" class="com.orginly.advice.TransferAdvice"/>

<!-- AOP 配置 -->
<aop:config>
<!-- 配置切面:切入点 + 通知 -->
<aop:aspect ref="transferAdvice">
<!-- 前置通知 pointcut:切点表达式 -->
<aop:before method="before" pointcut="execution(public void com.orginly.service.impl.AccountServiceImpl.transfer())"/>
<!-- 后置通知 -->
<aop:after-returning method="afterReturning" pointcut="execution(public void com.orginly.service.impl.AccountServiceImpl.transfer())"/>
</aop:aspect>
</aop:config>


XML 配置 AOP 详解

切点表达式

表达式语法:

execution([修饰符]返回值类型 包名.类名.方法名(参数))


  • 访问修饰符可以省略
  • 返回值类型、包名、类名、方法名可以使用星号 * 代替,代表任意
  • 包名与类名之间一个点,代表当前包下的类,两个点 ​​..​​ 表示当的包及其子包下的类
  • 参数列表可以使用两个点​​..​​表示任意个数,任意类型的参数列表

例如:

execution(public void com.orginly.service.impl.AccountServiceImpl.transfer());

execution(public void com.orginly.service.impl.AccountServiceImpl.transfer(java.lang.String));

execution(void com.orginly.service.impl.AccountServiceImpl.*(..));

execution(* com.orginly.service.impl.*.*(..));

execution(* com.orginly.service..*.*(..));


切点表达式抽取

当多个增强的切点表达式相同时,可以将切点表达式进行抽取,在增强中使用 pointcut-ref 属性代替 pointcut 属性来引用抽取后的切点表达式。

<!-- AOP 配置 -->
<aop:config>
<!-- 配置切面:切入点 + 通知 -->
<aop:aspect ref="transferAdvice">
<!-- 切点表达式 -->
<aop:pointcut id="acService" expression="execution(public void com.orginly.service.impl.AccountServiceImpl.*())"/>
<!-- 前置通知 pointcut:切点表达式 -->
<aop:before method="before" pointcut-ref="acService"/>
<!-- 后置通知 -->
<aop:after-returning method="afterReturning" pointcut-ref="acService" />
</aop:aspect>
</aop:config>


通知类型详解

通知的配置语法:

<aop:通知类型 method="通知类中方法名" pointcut=“切点表达式"></aop:通知类型>


名称

标签

说明

前置通知

< aop:before >

用于配置前置通知。指定增强的方法在切入点方法之前执行

后置通知

< aop:afterReturning >

用于配置后置通知。指定增强的方法在切入点方法之后执行

异常通知

< aop:afterThrowing >

用于配置异常通知。指定增强的方法出现异常后执行

最终通知

< aop:after >

用于配置最终通知。无论切入点方法执行时是否有异常,都会执行

环绕通知

< aop:around >

用于配置环绕通知。开发者可以手动控制增强代码在什么时候执行

注意:通常情况下,环绕通知都是独立使用的

环绕通知

<aop:config>
<!-- 配置切面:切入点 + 通知 -->
<aop:aspect ref="transferAdvice">
<!-- 切点表达式 -->
<aop:pointcut id="acService" expression="execution(public void com.orginly.service.impl.AccountServiceImpl.*())"/>
<aop:around method="around" pointcut-ref="acService"/>
</aop:aspect>
</aop:config>


注意:如果只配置 XML 是不生效的,需要在环绕通知方法中进行调用

// ProceedingJoinPoint 正在执行的连接点:切点
public Object around(ProceedingJoinPoint pjp) {
Object proceed = null;
try {
System.out.println("前置通知...");
// 切点方法执行
proceed = pjp.proceed();
System.out.println("后置通知...");
} catch (Throwable e) {
e.printStackTrace();
System.out.println("异常通知...");
} finally {
System.out.println("最终通知...");
}
return proceed;
}


基于注解 AOP 开发

创建主体方法和接口

public interface AccountService {
public void transfer();
}

@Service
public class AccountServiceImpl implements AccountService {
@Override
public void transfer() {
System.out.println("开始转账");
}
}


通知类升级为切面类

**在通知类中添加 ​​@Aspect​​ 升级为切面类

@Component
@Aspect // 升级为切面类
public class TransferAdvice {

// 配置切入点和通知的关系
@Before("execution(* com.orginly.service.impl.AccountServiceImpl.*(..))")
public void before() {
System.out.println("前置通知执行了");
}


开启 AOP 自动代理

<!-- 开启 IOC 注解扫描 -->
<context:component-scan base-package="com.orginly"/>

<!-- 开启 AOP 自动代理 -->
<aop:aspectj-autoproxy />


如果是纯注解模式可以在核心配置类增加

@EnableAspectJAutoProxy  // 开启 AOP 自动代理


抽取切点表达式

// 抽取切点表达式
@Pointcut("execution(* com.orginly.service.impl.AccountServiceImpl.*(..))")
private void myPointcut() {}

// 配置切入点和通知的关系
@Before("myPointcut()")
public void before() {
System.out.println("前置通知执行了");
}


通知类型

通知的配置语法:​​@诵知注解("切点表达式")​

名称

标签

说明

前置通知

@Before

用于配置前置通知。指定增强的方法在切入点方法之前执行

后置通知

@AfterReturning

用于配置后置通知。指定增强的方法在切入点方法之后执行

异常通知

@AfterThrowing

用于配置异常通知。指定增强的方法出现异常后执行

最终通知

@After

用于配置最终通知。无论切入点方法执行时是否有异常,都会执行

环绕通知

@Around

用于配置环绕通知。开发者可以手动控制增强代码在什么时候执行

注意:

当前四个通知组合在一起时,执行顺序如下:

@Before → @Ater → @After → Returning(如果有异常:@AfterThrowing)