Spring AOP 面向切面编程
初识 AOP
什么是AoP
AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程
AOP 是 OOP (面向对象编程) 的延,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合降低,提高程序的可重用性,同时提高了开发的效率。
这样做的好处是:
- 在程序运行期间,在不修改源码的情况下对方法进行功能增强
- 逻辑清晰,开发核心业务的时候,不必关注增强业务的代码
- 减少重复代码,提高开发效率,便于后期维护
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)