Javassm - AOP1
AOP面向切片
AOP(Aspect Oriented Programming)思想实际上就是:在运行时,动态地将代码切入到类的指定方法、指定位置上。
也就是说,我们可以使用AOP来帮助我们在方法执行前或执行之后,做一些额外的操作,实际上,它就是代理!
通过AOP我们可以在保证原有业务不变的情况下,添加额外的动作
比如我们的某些方法执行完成之后,需要打印日志,那么这个时候,我们就可以使用AOP来帮助我们完成,它可以批量地为这些方法添加动作。可以说,它相当于将我们原有的方法,在不改变源代码的基础上进行了增强处理。

相当于我们的整个业务流程,被直接斩断,并在断掉的位置添加了一个额外的操作,再连接起来,也就是在一个切点位置插入内容。
它的原理实际上就是通过动态代理机制实现的。不过Spring底层并不是使用的JDK提供的动态代理,而是使用的第三方库实现,它能够以父类的形式代理,而不仅仅是接口。
使用配置实现AOP
准备
在开始之前,先换回之前的XML配置模式
注意这里还入了一些新的AOP相关的约束进来:
<?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">
</beans>
Spring是支持AOP编程的框架之一(实际上它整合了AspectJ框架的一部分),要使用AOP我们需要先导入一个依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.10</version>
</dependency>
那么,如何使用AOP呢?首先我们要明确,要实现AOP操作,我们需要知道这些内容:
- 需要切入的类,类的哪个方法需要被切入
- 切入之后需要执行什么动作
- 是在方法执行前切入还是在方法执行后切入
- 如何告诉Spring需要进行切入
例子
比如现在我们希望对这个学生对象的study方法进行增强,在不修改源代码的情况下,增加一些额外的操作:
public class Student {
public void study(){
System.out.println("PPPP");
//现在我们希望在这个方法执行完之后,打印一些其他的内容,在不修改原有代码的情况下,该怎么做呢?
}
}
<bean class="org.example.entity.Student"/>
那么我们按照上面的流程,依次来看
首先需要解决的问题是,找到需要切入的类,很明显,就是这个Student类,我们要切入的是这个study方法。
第二步,我们切入之后要做什么呢?这里我们直接创建一个新的类,并将要执行的操作写成一个方法:
public class StudentAOP {
//这个方法就是我们打算对其进行的增强操作
public void afterStudy() {
System.out.println("Penguin");
}
}
注意这个类也得注册为Bean才可以:
<bean id="studentAOP" class="org.example.entity.StudentAOP"/>
第三步,我们要明确这是在方法执行之前切入还是执行之后切入,很明显,按照上面的要求,我们需要执行之后进行切入。
第四步,最关键的来了,我们怎么才能告诉Spring我们要进行切入操作呢?这里我们需要在配置文件中进行AOP配置:
<aop:config>
</aop:config>
建立切点 <aop:pointcut>
这个的目的就是来找那些满足条件的匹配的函数
接着我们需要添加一个新的切点,首先填写ID,这个随便起都可以:
<aop:pointcut id="test" expression=""/>
然后就是通过后面的expression表达式来选择到我们需要切入的方法,这个表达式支持很多种方式进行选择
Spring AOP支持以下AspectJ切点指示器(PCD)用于表达式,重点是 execution:
execution:用于匹配方法执行连接点。这是使用Spring AOP时使用的主要点切割指示器。within:限制匹配到某些类型的连接点(使用Spring AOP时在匹配类型中声明的方法的执行)。this:限制与连接点匹配(使用Spring AOP时方法的执行),其中bean引用(Spring AOP代理)是给定类型的实例。target:限制匹配连接点(使用Spring AOP时方法的执行),其中目标对象(正在代理的应用程序对象)是给定类型的实例。args:限制与连接点匹配(使用Spring AOP时方法的执行),其中参数是给定类型的实例。@target:限制匹配连接点(使用Spring AOP时方法的执行),其中执行对象的类具有给定类型的注释。@args:限制匹配到连接点(使用Spring AOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注释。@within:限制与具有给定注释的类型中的连接点匹配(使用Spring AOP时在带有给定注释的类型中声明的方法的执行)。@annotation:与连接点主体(在Spring AOP中运行的方法)具有给定注释的连接点匹配的限制。
其中,execution填写格式如下:
修饰符 包名.类名.方法名称(方法参数)
- 修饰符:public、protected、private、包括返回值类型、static等等(使用*代表任意修饰符)
- 包名:如com.test(
*代表全部,比如com.*代表com包下的全部包) - 类名:使用*也可以代表包下的所有类
- 方法名称:可以使用*代表全部方法
- 方法参数:填写对应的参数即可,比如(String, String),也可以使用*来代表任意一个参数,使用..代表所有参数。
也可以使用其他属性来进行匹配,比如@annotation可以用于表示标记了哪些注解的方法被切入,这里我们就只是简单的执行,所以说只需要这样写就可以了:
<aop:pointcut id="test" expression="execution(* org.example.entity.Student.study())"/>
这样,我们就指明了需要切入的方法
选择增强方法<aop:aspect>
这则是来匹配满足条件的切点调用的是什么方法
然后就是将我们的增强方法,我们在里面继续添加aop:aspect标签,并使用ref属性将其指向我们刚刚注册的AOP类Bean:
<aop:config>
<aop:pointcut id="test" expression="execution(* org.example.entity.Student.study())"/>
<aop:aspect ref="studentAOP">
</aop:aspect>
</aop:config>
接着就是添加后续动作了,当然,官方支持的有多种多样的,比如执行前、执行后、抛出异常后、方法返回后等等:

其中around方法为环绕方法,自定义度会更高。
这里我们按照上面的要求,直接添加后续动作,注意需要指明生效的切点:
<aop:aspect ref="studentAOP">
<!-- method就是我们的增强方法,pointcut-ref指向我们刚刚创建的切点 -->
<aop:after method="afterStudy" pointcut-ref="test"/>
</aop:aspect>
这样,我们就成功配置好了
测试
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
Student bean = context.getBean(Student.class);
bean.study();
}
结果如下:

可以看到在我们原本的方法执行完成之后,它还继续执行了我们的增强方法,这实际上就是动态代理做到的,实现在不修改原有代码的基础上,对方法的调用进行各种增强,在之后的SpringMVC学习中,我们甚至可以使用它来快速配置访问日志打印。
分析
前面我们说了,AOP是基于动态代理实现的,所以说我们如果直接获取Bean的类型,会发现不是原本的类型了:
Student bean = context.getBean(Student.class);
System.out.println(bean.getClass());

这里其实是Spring通过CGLib为我们生成的动态代理类,也就不难理解为什么调用方法会直接得到增强之后的结果了。
包括我们前面讲解Spring的异步任务调度时,为什么能够直接实现异步,其实就是利用了AOP机制实现的方法增强。
JoinPoint 获取切点信息
虽然这些功能已经非常强大了,但是仅仅只能简单的切入还是不能满足一些需求,在某些情况下,我们可以需求方法执行的一些参数,比如方法执行之后返回了什么,或是方法开始之前传入了什么参数等等
现在我们修改一下Student中study方法的参数:
public class Student {
public void study(String str){
//现在方法有一个String类型的参数
System.out.println("都别学Java了,根本找不到工作,快去卷"+str);
}
}
我们希望在增强的方法中也能拿到这个参数,然后进行处理
这个时候,我们可以为我们切入的方法添加一个JoinPoint参数,通过此参数就可以快速获取切点位置的一些信息:
public void afterStudy(JoinPoint point) {
//JoinPoint实例会被自动传入
//这里我们直接通过getArgs()返回的参数数组获取第1个参数
System.out.println("学什么"+point.getArgs()[0]+",Rust天下第一!");
}
接着我们修改一下刚刚的AOP配置(因为方法参数有变动)看看结果吧:
<aop:pointcut id="test" expression="execution(* org.example.entity.Student.study(String))"/>
around环绕方法
我们接着来看自定义度更高的环绕方法,现在我们希望在方法执行前和执行后都加入各种各样的动作,此时我们就可以使用环绕方法。
此时,我们就相当于需要自己来调用代理方法,然后就可以在其前后写我们的动作来执行
环绕方法相当于完全代理了此方法,它完全将此方法包含在中间,需要我们手动调用才可以执行此方法,并且我们可以直接获取更多的参数
注意,如果代理方法存在返回值,那么环绕方法也需要有一个返回值,通过proceed方法来执行代理的方法
也可以修改参数之后调用proceed(Object[]),使用我们给定的参数再去执行:
public Object test1(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("方法开始之前");
String arg = joinPoint.getArgs()[0] + "伞兵一号";
Object value = joinPoint.proceed(new Object[]{arg});
System.out.println("方法执行完成,结果为:"+value);
return value;
}
这里我们还是以study方法为例,现在我们希望在调用前修改这个方法传入的参数值,改成我们自己的,然后在调用之后对返回值结果也进行处理:
public String study(String str){
if(str.equals("Java"))
System.out.println("我的梦想是学Java");
else {
System.out.println("我就要学Java,不要修改我的梦想!");
str = "Java";
}
return str;
}
现在我们编写一个环绕方法,对其进行全方面处理:
public Object test1(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("我是她的家长,他不能学Java,必须学Rust,这是为他好");
Object value = joinPoint.proceed(new Object[]{"Rust"});
if(value.equals("Java")) {
System.out.println("听话,学Rust以后进大厂!");
value = "Rust";
}
return value;
}
同样的,因为方法变动了,现在我们去修改一下我们的AOP配置:
<aop:pointcut id="test" expression="execution(* org.example.entity.Student.study(String))"/>
<aop:aspect ref="studentAOP">
<aop:around method="test1" pointcut-ref="test"/>
</aop:aspect>
环绕方法的图标是全包的,跟我们之前的图标不太一样。
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
Student bean = context.getBean(Student.class);
System.out.println("已报名:"+bean.study("Java"));
}

这样,我们就实现了环绕方法,通过合理利用AOP带来的便捷,可以使得我们的代码更加清爽和优美。
实现异步效果
实现异步效果,可以用AOP来实现
首先在Student类中创立一个task
public class Student {
public void task(Runnable runnable){
runnable.run();
}
}
这样在main里
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
Student student = (Student) context.getBean(Student.class);
student.task(()->{
try {
Thread.sleep(3000);
System.out.println("Penguin");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
System.out.println("PPPP!");
}
此时显然是同步的,要等task结束才会输出PPPP!
这时候借助AOP, 我们在StudentAOP类用一个async
public void async(ProceedingJoinPoint joinPoint) throws Throwable {
new Thread(()->{
try {
joinPoint.proceed(joinPoint.getArgs());
} catch (Throwable e) {
throw new RuntimeException(e);
}
}).start();
}
然后去配置文件修改
<aop:config>
<aop:pointcut id="task" expression="execution(* com.ekko.entity.Student.task(Runnable))"/>
<aop:aspect ref="penguin">
<aop:around method="async" pointcut-ref="task"/>
</aop:aspect>
</aop:config>
此时就可以实现异步,相当于调用task方法会独立开一个线程

术语
这里介绍一下 AOP 领域中的特性术语,防止自己下来看不懂文章:
- 通知(Advice): AOP 框架中的增强处理,通知描述了切面何时执行以及如何执行增强处理,也就是我们上面编写的方法实现。
- 连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出,实际上就是我们在方法执行前或是执行后需要做的内容。
- 切点(PointCut): 可以插入增强处理的连接点,可以是方法执行之前也可以方法执行之后,还可以是抛出异常之类的。
- 切面(Aspect): 切面是通知和切点的结合,我们之前在xml中定义的就是切面,包括很多信息。
- 引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。
- 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,我们之前都是在将我们的增强处理添加到目标对象,也就是织入(这名字挺有文艺范的)
