30-AOP入门案例
需求分析
案例设定:测算接口执行效率,但是这个案例稍微复杂了点,我们对其进行简化。
简化设定:在方法执行前输出当前系统时间。
那现在我们使用SpringAOP的注解方式完成在方法执行的前打印出当前系统时间。
思路分析
- 导入坐标 (pom.xml)
- 制作连接点(原始操作,Dao接口及实现类)
- 制作共性功能(通知类和通知)
- 定义切入点
- 绑定切入点和通知的关系(切面)
环境准备
- 创建一个Maven项目
- pom.xml添加Spring依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
- 添加BookDao和BookDaoImpl类
public interface BookDao {
public void save();
public void update();
}
@Repository
public class BookDaoImpl implements BookDao {
@Override
public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("book dao save...");
}
@Override
public void update() {
System.out.println("book dao update...");
}
}
- 创建Spring的配置类
@Configuration
@ComponentScan("com.yolo")
public class SpringConfig {
}
- 编写App运行类
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.save();
bookDao.update();
}
}
运行结果
说明:
目前打印save方法的时候,因为方法中有打印系统时间,所以运行的时候是可以看到系统时间
对于update方法来说,就没有该功能
我们要使用SpringAOP的方式在不改变update方法的前提下让其具有打印系统时间的功能。
AOP实现步骤
- 步骤一:添加依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
因为spring-context中已经导入了spring-aop,所以不需要再单独导入spring-aop
导入AspectJ的jar包,AspectJ是AOP思想的一个具体实现,Spring有自己的AOP实现,但是相比于AspectJ来说比较麻烦,所以我们直接采用Spring整合ApsectJ的方式进行AOP开发。
-
步骤二:定义接口和实现类
准备环境的时候我们已经完成了 -
步骤三:定义通知类和通知
通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印。
类名和方法名没有要求,可以任意。 -
步骤四:定义切入点
BookDaoImpl中有两个方法,分别是update()和save(),我们要增强的是update方法,那么该如何定义呢?
public class MyAdvice {
@Pointcut("execution(void com.yolo.dao.BookDao.update())")
private void pt() {}
public void method() {
System.out.println(System.currentTimeMillis());
}
}
说明:
切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑。
execution及后面编写的内容,之后我们会专门去学习。
- 步骤五:制作切面
切面是用来描述通知和切入点之间的关系,如何进行关系的绑定?
public class MyAdvice {
@Pointcut("execution(void com.yolo.dao.BookDao.update())")
private void pt() {}
@Before("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置
说明:@Before翻译过来是之前,也就是说通知会在切入点方法执行之前执行,除此之前还有其他四种类型,后面会讲。
那这里就会在执行update()之前,来执行我们的method(),输出当前毫秒值
- 步骤六:将通知类配给容器并标识其为切面类
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.yolo.dao.BookDao.update())")
private void pt() {}
@Before("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
- 步骤七:开启注解格式AOP功能
使用@EnableAspectJAutoProxy
注解
@Configuration
@ComponentScan("com.yolo")
@EnableAspectJAutoProxy
public class SpringConfig {
}
- 步骤八:运行程序
这次我们再来调用update()
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}
控制台成功输出了当前毫秒值
知识点1:@EnableAspectJAutoProxy
知识点2:@Aspect
知识点3:@Pointcut
知识点4:@Before
31-AOP工作流程
由于AOP是基于Spring容器管理的bean做的增强,所以整个工作过程需要从Spring加载bean说起
流程一:Spring容器启动
- 容器启动就需要去加载bean,哪些类需要被加载呢?
- 需要被增强的类,如:BookServiceImpl
- 通知类,如:MyAdvice
- 注意此时bean对象还没有创建成功
流程二:读取所有切面配置中的切入点
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.yolo.dao.BookDao.save())")
private void ptx() {}
@Pointcut("execution(void com.yolo.dao.BookDao.update())")
private void pt() {}
@Before("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
上面这个例子中有两个切入点的配置,但是第一个ptx()并没有被使用,所以不会被读取。
流程三:初始化bean,判定bean对应的类中的方法是否匹配到任意切入点
- 注意第一步在容器启动的时候,bean对象还没有被创建成功。
- 要被实例化bean对象的类中的方法和切入点进行匹配
- 匹配失败,创建原始对象,如UserDao
- 匹配失败说明不需要增强,直接调用原始对象的方法即可。
- 匹配成功,创建原始对象(目标对象)的代理对象,如:BookDao
- 匹配成功说明需要对其进行增强
- 对哪个类做增强,这个类对应的对象就叫做目标对象
- 因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
- 最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强
- 匹配失败,创建原始对象,如UserDao
上述匹配失败的情况即与AOP无关的情况,相当于我们学习AOP之前书写的那些代码
流程四:获取bean执行方法
- 获取的bean是原始对象时,调用方法并执行,完成操作
- 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
验证容器中是否为代理对象
下面我们来验证一下容器中是否为代理对象
理论依据:
- 如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象
- 如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身
接下来我们对之前的代码进行操作
- 步骤一:修改App运行类,获取类的类型并输出
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
System.out.println(bookDao);
System.out.println(bookDao.getClass());
}
}
- 步骤二:修改MyAdvice类,改为不增强
将定义的切入点改为update1,而BookDaoImpl类中不存在该方法,所以BookDao中的update方法在执行的时候,就不会被增强
所以此时容器中的对象应该是目标对象本身。
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.yolo.dao.BookDao.update1())")
private void pt() {}
@Before("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
- 步骤三:运行程序
输出结果如下,确实是目标对象本身,符合我们的预期
- 步骤四:修改MyAdvice类,改为增强
将定义的切入点改为update,那么BookDao中的update方法在执行的时候,就会被增强
所以容器中的对象应该是目标对象的代理对象
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.yolo.dao.BookDao.update())")
private void pt() {}
@Before("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
- 步骤五:运行程序
结果如下
至此对于刚才的结论,我们就得到了验证,这块我们需要注意的是:
不能直接打印对象,从上面两次结果中可以看出,直接打印对象走的是对象的toString方法,不管是不是代理对象,打印的结果都是一样的,原因是内部对toString方法进行了重写。
AOP核心概念
在上面介绍AOP的工作流程中,我们提到了两个核心概念,分别是:
- 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
- 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
上面这两个概念比较抽象,简单来说:
-
目标对象就是要增强的类如:BookServiceImpl类对应的对象,也叫原始对象,不能说它不能运行,只能说它在运行的过程中对于要增强的内容是缺失的。
-
SpringAOP是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知如:MyAdvice中的method方法内容加进去,就实现了增强,这就是我们所说的代理(Proxy)。