AOP 概述
AOP 是什么
- AOP - Aspect Oriented Programming,即面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
说白了,就是把我们重复使用的代码抽取出来,在运行时,动态地将代码切入到类的指定方法、指定位置上。
- 好处
AOP 把与业务逻辑无关的,却被各个业务模块大量调用的逻辑给封装起来,进而减少了系统的重复代码量,提高了代码的可重用性,降低了模块间的耦合度,以及有利于未来的扩展和维护,降低了维护成本。我认为最重要的是实现了各个方法本身只关注了核心业务逻辑。
AOP 相关术语
- 通知(Advice)
就是使用(织入)到目标类连接点上的一段程序代码,比如上张图中的日志。 - 连接点(JoinPoint)
就是 Spring 允许你使用通知的地方,基本每个方法的前,后或抛出异常时都可以是连接点,spring只支持方法连接点。 - 切入点(Pointcut)
所有连接点都可以使用通知,但开发时并不是所有连接点都使用通知,所以切入点就是用来筛选或指定在哪些连接点上使用通知。 - 切面(Aspect)
切面就是通知和切入点的结合,通知说明了要干什么,而切入点说明了要在哪个位置干。 - 目标对象(Target )
被通知的对象,或者说被代理的对象,也就是真正的业务逻辑,它可以在毫不知情的情况下,被织入切面。 - 代理对象(Proxy)
一个类被 AOP 织入通知后,就产生一个结果类,它是一个融合了原类和通知逻辑的代理类。 - 织入(Weaving)
把切面应用到目标对象来创建新的代理对象的过程。
基于 XML 配置 AOP
简单实现
依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.7.4</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
业务层接口及实现类
public interface StudentService {
void insertStudent();
void deleteStudent(Integer id);
}
public class StudentServiceImpl implements StudentService {
@Override
//模拟添加学生信息
public void insertStudent() {
System.out.println("执行添加学生方法");
//int i = 1/0;
}
@Override
//模拟删除学生信息
public void deleteStudent(Integer id) {
System.out.println("执行删除学生方法");
}
}
通知类
public class Logger {
public void beforeLog() {
System.out.println("执行前置通知");
}
public void afterReturningLog() {
System.out.println("执行后置通知");
}
public void afterThrowingLog() {
System.out.println("执行异常通知");
}
public void afterLog() {
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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="studentService" class="com.hn.service.impl.StudentServiceImpl"></bean>
<!-- 通知类 -->
<bean id="logger" class="com.hn.utils.Logger"></bean>
<!-- 配置 AOP -->
<aop:config>
<!-- 配置切面 -->
<aop:aspect id="loggerAdvice" ref="logger">
<!-- 配置切入点表达式 -->
<aop:pointcut id="ptStudentService" expression="execution(* com.hn.service.*.*(..))"/>
<!-- 配置前置通知,在切入点方法执行之前执行 -->
<aop:before method="beforeLog" pointcut-ref="ptStudentService"></aop:before>
<!-- 配置后置通知,在切入点方法正常执行之后执行 -->
<aop:after-returning method="afterReturningLog" pointcut-ref="ptStudentService"></aop:after-returning>
<!-- 配置异常通知,在切入点方法执行时产生异常后执行 -->
<aop:after-throwing method="afterThrowingLog" pointcut-ref="ptStudentService"></aop:after-throwing>
<!-- 配置最终通知,无论切入点方法是否正常执行它都会在其后面执行 -->
<aop:after method="afterLog" pointcut-ref="ptStudentService"></aop:after>
</aop:aspect>
</aop:config>
</beans>
AOP 的配置步骤
- 在 Spring 核心配置文件中引入约束并将通知类注入到 Spring 容器中;
- 使用
<aop:config>
标签声明AOP配置,所有关于 AOP 的配置都写在该标签内; - 使用
<aop:aspect>
标签配置切面
<aop:config>
<!-- 配置切面 -->
<aop:aspect id="loggerAdvice" ref="logger">
...
</aop:aspect>
</aop:config>
id
属性为该切面的唯一标识,ref
属性用于引用通知类
此标签写在 aop:aspect 标签内部时只能供当前切面使用;写在 aop:aspect 标签外部时则供全部切面使用
受到约束,该标签在 aop:aspect 标签外部时必须出现在 aop:aspect 标签之前
- 使用
<aop:pointcut>
标签配置切入点表达式,指定对哪些方法进行增强(通知),
<aop:config>
<aop:aspect id="loggerAdvice" ref="logger">
<!-- 配置切入点 -->
<aop:pointcut id="ptStudentService" expression="execution(* com.hn.service.*.*(..))"></aop:pointcut>
...
</aop:aspect>
</aop:config>
id
属性为该切入点的唯一标识,expression
属性用于指定切入点表达式
切入点表达式的规范:execution([修饰符] 返回值类型 包路径.类名.方法名(参数))
标准的切入点表达式:public void com.hn.service.StudentServiceImpl.deleteStudent( int )
;全通配方式::* *..*.*(..)
一般我们都是对业务层实现类的方法进行增强,因此切入点表达式写法通常为:expression="execution(* com.hn.service.*.* (..)
- 在切面内配置通知
<aop:before>
标签:前置通知,指定在切入点方法执行之前执行
<aop:after-returning>
标签:后置通知,指定在切入点方法正常执行之后执行
<aop:afterthrowing>
标签:异常通知,指定在切入点方法执行时产生异常后执行
<aop:after>
标签:最终通知,指定无论切入点方法是否正常执行它都会在其后面执行
<aop:arround>
标签:环绕通知,可以在代码中手动控制通知的执行时机
单元测试
public class StudentServiceImplTest {
@Test
public void testDeleteStudent(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
StudentService studentService = (StudentService) applicationContext.getBean("studentService");
studentService.deleteStudent(1);
}
}
执行结果
基于注解配置 AOP
需要用到的新注解
注解 | 作用 |
---|---|
@EnableAspectJAutoProxy | 开启 AOP 对注解的支持 |
@Aspect | 声明当前类是切面类 |
@Pointcut | 声明切入点表达式 |
@Before | 声明该方法为前置通知 |
@AfterReturning | 声明该方法为后置通知 |
@AfterThrowing | 声明该方法为异常通知 |
@After | 声明该方法为最终通知 |
@Around | 声明该方法为环绕通知 |
简单实现
修改通知类,添加相应注解
@Configuration //声明该类为配置类
@ComponentScan("com.hn") //声明创建容器时要扫描的包
@Component("logger") //将当前类对象存入容器中
@EnableAspectJAutoProxy //开启 AOP 对注解的支持
@Aspect //声明当前类是切面类
public class Logger {
@Pointcut("execution(* com.hn.service.*.*(..))") //声明切入点表达式
public void pointcut(){}
@Before("pointcut()") //声明该方法为前置通知
public void beforeLog() {
System.out.println("执行前置通知");
}
@AfterReturning("pointcut()") //声明该方法为后置通知
public void afterReturningLog() {
System.out.println("执行后置通知");
}
@AfterThrowing("pointcut()") //声明该方法为异常通知
public void afterThrowingLog() {
System.out.println("执行异常通知");
}
@After("pointcut()") //声明该方法为最终通知
public void afterLog() {
System.out.println("执行最终通知");
}
}
在业务层实现类上添加注解
@Service("studentService")
public class StudentServiceImpl implements StudentService {
...
}
单元测试
public class StudentServiceImplTest {
@Test
public void testInsertStudent(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(com.hn.utils.Logger.class);
StudentService studentService = (StudentService) applicationContext.getBean("studentService");
studentService.insertStudent();
}
}
运行结果
使用注解配置 AOP 的 bug
- 在使用注解配置 AOP 时,会出现一个bug:四个通知的正常调用顺序原本应该为:前置通知 --> 后置通知/异常通知 --> 最终通知,而此时的调用顺序变成了 前置通知 --> 最终通知 --> 后置通知/异常通知,这会导致一些资源在执行最终通知时提前被释放掉了,而执行后置通知时就会出错。
- 如果想解决这个问题,那么需要使用环绕通知,因为在环绕通知中每种通知的调用顺序是由我们自己决定的,不会受 Spring 的影响。