文章目录
1 面向切面编程简介
Aspect Oriented Programming 面向切面编程,是一种利用"横切"的技术(底层实现就是动态代理),对原有的业务逻辑进行拦截,并且可以在这个拦截的横切面上添加特定的业务逻辑,对原有的业务进行增强。
基于动态代理,实现在不改变原有业务的情况下对业务逻辑进行增强。
-
首选 JDK 做动态代理。
-
如果代理对象有接口,就用 JDK 动态代理,否则就是 Cglib 动态代理。
-
如果代理对象没有接口,那么就直接是 Cglib 动态代理。
1.1 术语解释
1、连接点(joinpoint)
程序执行过程中特定的节点,如方法的调用或异常的执行,通常是一个方法的执行。
2、切入点(pointcut)
是指需要处理的连接点,所有的方法执行都是连接点,某个特定的连接点就是切入点(被拦截的连接点)
3、目标对象(target)
是指被通知的对象,即代理的目标对象;
4、通知(advice)
也被称为增强,是由切面添加到特定的连接点的一段代码,简单来说,通知就是指拦截到的连接点之后所要做的事情,因此通知是切面的具体实现;通知分为前置通知、后置通知、异常通知、最终通知、环绕通知;
5、切面(aspect)
是指封装横向切到系统功能的类(例如事务处理),是切入点和通知的结合;
6、织入(weave)
是将切面代码插入到目标对象上,从而生成代理对象的过程;
7、代理(Proxy)
是指被应用了通知(增强)后,产生一个代理对象;
1.2 图示
2 程序环境部署
2.1 引入依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
</dependencies>
2.2 创建配置文件
<?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>
2.3 创建目标对象的类
package com.yiwu.demo2;
import com.yiwu.demo1.LandlordInterface;
public class Landlord1{
public void rentHouse(){
System.out.println("出租房子");
}
}
2.4 创建切面类
package com.yiwu.demo2;
public class MyAspect {
public void advertise(){
System.out.println("做广告");
}
public void signContract(){
System.out.println("签合同");
}
}
3 xml方式实现AOP
- 增强rentHouse方法,实现做广告,签合同的功能。
- 仅修改配置文件即可。
3.1 基本实现
第一步,修改配置文件
<?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:context="http://www.springframework.org/schema/context"
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/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="landlord1" class="com.yiwu.demo2.Landlord1"/>
<bean id="myaspect" class="com.yiwu.demo2.MyAspect"/>
<aop:config>
<!--声明切入点-->
<aop:pointcut id="landlordP" expression="execution(void com.yiwu.demo2.Landlord1.rentHouse())"/>
<!--声明切面类-->
<aop:aspect ref="myaspect">
<!--通知策略-->
<aop:before method="advertise" pointcut-ref="landlordP"/>
<aop:after method="signContract" pointcut-ref="landlordP"/>
</aop:aspect>
</aop:config>
</beans>
第二步,编写测试方法
Spring AOP 有两种代理方法,
一种是常规JDK,一种是CGLIB。
-
当代理对象实现了至少一个接口时,默认使用JDK动态创建代理对象;
-
当代理对象没有实现任何接口时,就会使用CGLIB方法。
-
如果实现了接口,强制转换必须用父类接口来定义
import com.yiwu.demo2.Landlord1;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Mytest03 {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(" applicationContext.xml");
Landlord1 landlord = (Landlord1)applicationContext.getBean("landlord1");
landlord.rentHouse();
}
}
做广告
出租房子
签合同
3.2 切入点声明要点
🔴切点是com.yiwu.demo2.Landlord1.rentHouse()方法
<aop:pointcut id="id1"
expression="execution(* com.yiwu.demo2.Landlord1.rentHouse())"/>
🔴切点是com.yiwu.demo2.Landlord1类中所有无返回值,参数为空的方法
<aop:pointcut id="id2"
expression= "execution(void com.yiwu.demo2.Landlord1.*())"/>
🔴切点是com.yiwu.demo2.Landlord1类中所有无返回值,对参数无要求的方法
<aop:pointcut id="id3"
expression="execution(void com.yiwu.demo2.Landlord1.*(..))"/>
🔴切点是com.yiwu.demo2.Landlord1类中所有对返回值无要求,参数为空方法
<aop:pointcut id="id4"
expression="execution(* com.yiwu.demo2.Landlord1.*())"/>
🔴切点是com.yiwu.demo2.Landlord1类中所有方法,对返回值,参数均无要求。
<aop:pointcut id="id5"
expression="execution(* com.yiwu.demo2.Landlord1.*(..))"/>
🔴切点是com.yiwu.demo2.包中所有类的所有方法,对返回值,参数均无要求。
<aop:pointcut id="id6" expression="execution(* com.yiwu.demo2.*.*(..))"/>
🔴切点是com.yiwu.demo2.包中所有类的rentHouse方法,对返回值,参数均无要求。
<aop:pointcut id="pc6"
expression="execution(* com.yiwu.demo2.*.rentHouse(..))"/>
🔴所有类的所有方法,对返回值,参数均无要求。
<aop:pointcut id="pc7" expression="execution(* *(..))"/>
3.3 通知策略与执行顺序
-
前置通知(before):在目标方法执行之前执行执行的通知
-
后置通知(after):在目标方法执行之后执行的通知。
-
异常通知(after-throwing):在目标方法抛出异常时执行的通知。
-
最终通知(after-returning):在目标方法成功执行执行的通知。
-
环绕通知(around):目标方法执行之前和之后都可以执行额外代码的通知
通知的执行顺序与xml文件中配置的顺序有关
第一步,新建切面类
在MyAspect中添加5个方法
package com.yiwu.demo2;
import org.aspectj.lang.ProceedingJoinPoint;
public class MyAspect {
public void method1(){
System.out.println("~~~~~~~~~~~~~前置方法method1");
}
public void method2(){
System.out.println("~~~~~~~~~~~~~后置方法method2");
}
public void method3(){
System.out.println("~~~~~~~~~~~~~异常方法method3");
}
public void method4(){
System.out.println("~~~~~~~~~~~~~最终通知method4");
}
//环绕通知的方法,必须遵守如下的定义规则
//1:必须带有一个ProceedingJoinPoint类型的参数,
//2:必须有Object类型的返回值
//3:在前后增强的业务逻辑之间执行Object v = point.proceed();
//4: 方法最后返回 return v;
public Object method5(ProceedingJoinPoint point) throws Throwable {
System.out.println("~~~~~~~~~~~~~环绕通知method5----before");
//此句代码的执行就表示切入点方法的执行
Object v = point.proceed();
System.out.println("~~~~~~~~~~~~~环绕通知method5----after");
return v;
}
}
第二步,通知规则总结
使用xml配置,通知的执行顺序与配置的顺序有关。
-
使用前置通知,后置通知,最终通知
-
无异常前置通知>切入点方法->后置通知和最终通知(执行顺序根据xml配置的顺序决定)
-
有异常前置通知>切入点方法->后置通知>异常通知
-
-
环绕通知,异常通知
- 无异常:环绕通知前置 > 切入点方法 > 环绕通知后置
- **有异常:**环绕通知前置 > 切入点方法 > 异常通知
-
使用所有通知:
-
无异常:执行顺序如下:
- 前置通知和环绕通知的前置(依据xml配置顺序)
- 切入点方法
- 后置通知,最终通知,环绕通知后置(依据xml配置顺序执行有两种情况,一是前置或环绕存在其中之一,二是前置与环绕同时存在且中间没有其他配置)
-
有异常:执行顺序如下:
- 前置通知和环绕通知的前置(依据xml配置顺序)
- 切入点方法
- 后置通知,异常通知(执行顺序同上)
总结:当前置与环绕之间没有其他配置项时,后置,异常,最终的执行顺序与配置相同
-
测试1,使用前置通知,后置通知,最终通知,无异常时
前置通知>切入点方法->后置通知和最终通知(执行顺序根据xml配置的顺序决定)
<!--声明切面类-->
<aop:aspect ref="myaspect">
<aop:before method="method1" pointcut-ref="landlordP"/>
<!--调整after与after-returning的顺序-->
<aop:after method="method2" pointcut-ref="landlordP"/>
<aop:after-returning method="method4" pointcut-ref="landlordP"/
</aop:aspect>
</aop:config>
测试1,使用前置通知,后置通知,最终通知,有异常时
前置通知>切入点方法->后置通知>异常通知
在切入点方法中制造异常 int a = 5/0,除数为0;
package com.yiwu.demo2;
public class Landlord1 {
public void rentHouse(){
System.out.println("出租房子");
int a = 5/0;
}
}
<aop:aspect ref="myaspect">
<aop:before method="method1" pointcut-ref="landlordP"/>
<aop:after-returning method="method4" pointcut-ref="landlordP"/>
<aop:after method="method2" pointcut-ref="landlordP"/>
</aop:aspect>
~~~~~~~~~~~~~前置方法
出租房子
~~~~~~~~~~~~~后置方法
测试2 环绕通知,异常通知
无异常:环绕通知 > 切入点方法 > 环绕通知
有异常:环绕通知 > 切入点方法 > 异常通知
<aop:aspect ref="myaspect">
<aop:around method="method5" pointcut-ref="landlordP"/>
<aop:after-throwing method="method3" pointcut-ref="landlordP"/>
</aop:aspect>
无异常:
~~~~~~~~~~~~~环绕通知----before
出租房子
~~~~~~~~~~~~~环绕通知----after
有异常
~~~~~~~~~~~~~环绕通知----before
出租房子
~~~~~~~~~~~~~异常方法
测试3 使用所有通知无异常
执行顺序如下:
- 前置通知和环绕通知的前置(依据xml配置顺序)
- 切入点方法
- 后置通知,最终通知,环绕通知后置(依据xml配置顺序执行有两种情况,一是前置或环绕仅存在其中之一,二是前置与环绕同时存在并且中间没有其他配置)
前置与环绕之间存在其他配置项,执行顺序与配置顺序相反。
其余情况,后置通知,最终通知,环绕通知后置(依据xml配置顺序)
测试3 使用所有通知 有异常
- 执行顺序如下:
- 前置通知和环绕通知的前置(依据xml配置顺序)
- 切入点方法
- 后置通知,异常通知(依据xml配置顺序执行有两种情况,一是前置或环绕仅存在其中之一,二是前置与环绕同时存在并且中间没有其他配置)
前置与环绕之间存在其他配置项,执行顺序与配置顺序相反。
其余情况,后置通知,异常通知(依据xml配置顺序)
测试类
public class Mytest3 {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
GeneralDao bookDao = (GeneralDao) context.getBean("bookDao");
bookDao.insert();
}
}
4 注解方式实现AOP
Spring5中执行顺序如下:
程序执行正常,无异常:
-
环绕通知(around)
-
前置通知(before)
-
切入点方法
-
最终通知(after-returning)
-
后置通知(after)
-
环绕通知(around)
程序执行异常:
-
环绕通知(around)
-
前置通知(before)
-
切入点方法
-
异常通知(after-throwing)
-
后置通知(after)
4.1 修改配置文件
conetxt:component-scan base-package=“com.qlit”
该语句作用包括注册+激活,指定包扫描范围(包含子包),可在扫描返回内使用相关注解,若存在多个包,可用逗号隔开。
要注意和 context:annotation-config区分,该语句的作用是激活那些已经在spring容器里注册过的bean,也就是使用他的前提是类已经xml中使用bean标签注册过,不能进行注册。
aop:aspectj-autoproxy
该语句开启spring对注解aop的⽀持。
<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"
xmlns:conetxt="http://www.springframework.org/schema/context"
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
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<conetxt:component-scan base-package="com.yiwu"/>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
4.2 切面类
-
使用@Aspect注解,声明切面类。
-
使用@Before,@After,@AfterThrowing,@AfterReturning, @Around声明对应的通知方法。
-
使用@Pointcut定义表达式语句,优化代码结构。
package com.yiwu.demo3;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAspect {
@Pointcut("execution(* com.yiwu.demo3.Landlord2.rentHouse())")
public void p(){}
//@Before("execution(* com.yiwu.demo3.Landlord2.rentHouse())")
@Before("p()")
public void method1(){
System.out.println("~~~~~~~~~~~~~前置方法method1");
}
//@After("execution(* com.yiwu.demo3.Landlord2.rentHouse())")
@After("p()")
public void method2(){
System.out.println("~~~~~~~~~~~~~后置方法method2");
}
//@AfterThrowing("execution(* com.yiwu.demo3.Landlord2.rentHouse())")
@AfterThrowing("p()")
public void method3(){
System.out.println("~~~~~~~~~~~~~异常方法method3");
}
//@AfterReturning("execution(* com.yiwu.demo3.Landlord2.rentHouse())")
@AfterReturning("p()")
public void method4(){
System.out.println("~~~~~~~~~~~~~最终通知method4");
}
//@Around("execution(* com.yiwu.demo3.Landlord2.rentHouse())")
@Around("p()")
public Object method5(ProceedingJoinPoint point) throws Throwable {
System.out.println("~~~~~~~~~~~~~环绕通知method5----before");
//此句代码的执行就表示切入点方法的执行
Object v = point.proceed();
System.out.println("~~~~~~~~~~~~~环绕通知method5----after");
return v;
}
}
4.3 环绕通知方法优化
3.4 四合一通知
可以优化环绕通知的方法的编写,减少通知的配置。
如下:
public Object method5(ProceedingJoinPoint point){
Object v = null;
try {
System.out.println("~~~~~~~~~~~~~环绕通知method5----前置通知");
v = point.proceed();
System.out.println("~~~~~~~~~~~~~环绕通知method5----最终通知");
} catch (Throwable throwable) {
//环绕异常通知@AfterThrowing
System.out.println("~~~~~~~~~~~~~环绕通知method5----异常通知");
throwable.printStackTrace();
}finally {
System.out.println("~~~~~~~~~~~~~环绕通知method5----后置通知");
}
return v;
}