Spring AOP 的具体应用
1. AOP的本质
在不改变原有业务逻辑的情况下增强横切逻辑,横切逻辑代码往往是权限校验代码、⽇志代码、事务控制代码、性能监控代码。在原有业务的上下层添加增强额外的功能。下图描述的就是未采⽤AOP思想设计的程序,当我们红⾊框中圈定的⽅法时,会带来⼤量的重复劳动。程序中充斥着⼤量的重复代码,使我们程序的独⽴性很差。
⽽下图中是采⽤了AOP思想设计的程序,它把红框部分的代码抽取出来的同时,运⽤动态代理技术,统一在运⾏期对需要使⽤的业务逻辑⽅法进⾏增
强
2. AOP的常用术语
名词 | 解释 |
---|---|
Joinpoint(连接点) | 它指的是那些可以⽤于把增强代码加⼊到业务主线中的点,那么由上图中我们可 以看出,这些点指的就是⽅法。在⽅法执⾏的前后通过动态代理技术加⼊增强的 代码。在Spring框架AOP思想的技术实现中,也只⽀持⽅法类型的连接点。 |
Pointcut(切⼊点) | 它指的是那些已经把增强代码加⼊到业务主线进来之后的连接点。由上图中,我 们看出表现层transfer ⽅法就只是连接点,因为判断访问权限的功能并没有对其增强。 |
Advice(通知/增强) | 它指的是切⾯类中⽤于提供增强功能的⽅法。并且不同的⽅法增强的时机是不⼀ 样的。⽐如,开启事务肯定要在业务⽅法执⾏之前执⾏;提交事务要在业务⽅法 正常执⾏之后执⾏,⽽回滚事务要在业务⽅法执⾏产⽣异常之后执⾏等等。那么 这些就是通知的类型。其分类有:前置通知 后置通知 异常通知 最终通知 环绕通知。 |
Target(⽬标对象) | 它指的是代理的⽬标对象。即被代理对象。 |
Proxy(代理) | 它指的是⼀个类被AOP织⼊增强后,产⽣的代理类。即代理对象。 |
Weaving(织入) | 它指的是把增强应⽤到⽬标对象来创建新的代理对象的过程。spring采⽤动态代 理织⼊,⽽AspectJ采⽤编译期织⼊和类装载期织⼊。 |
Aspect(切⾯) | 它指定是增强的代码所关注的⽅⾯,把这些相关的增强代码定义到⼀个类中,这 个类就是切⾯类。例如,事务切⾯,它⾥⾯定义的⽅法就是和事务相关的,像开 启事务,提交事务,回滚事务等等,不会定义其他与事务⽆关的⽅法。我们前⾯ 的案例中TrasnactionManager 就是⼀个切⾯。 |
连接点:⽅法开始时、结束时、正常运⾏完毕时、⽅法异常时等这些特殊的时机点,我们称之为连接点,项⽬中每个⽅法都有连接点,连接点是⼀种候选点
切⼊点:指定AOP思想想要影响的具体⽅法是哪些,描述感兴趣的⽅法
Advice增强:
第⼀个层次:指的是横切逻辑
第⼆个层次:⽅位点(在某⼀些连接点上加⼊横切逻辑,那么这些连接点就叫做⽅位点,描述的是具体的特殊时机)
Aspect切⾯:切⾯概念是对上述概念的⼀个综合 Aspect切⾯ = 切⼊点+增强 = 切⼊点(锁定⽅法) + ⽅位点(锁定⽅法中的特殊时机)+ 横切逻辑众多的概念,⽬的就是为了锁定要在哪个地⽅插⼊什么横切逻辑代码
3. Spring中AOP的代理选择
Spring 实现AOP思想使⽤的是动态代理技术
默认情况下,Spring会根据被代理对象是否实现接⼝来选择使⽤JDK还是CGLIB。当被代理对象没有实现任何接⼝时,Spring会选择CGLIB。当被代理对象实现了接⼝,Spring会选择JDK官⽅的代理技术,不过我们可以通过配置的⽅式,让Spring强制使⽤CGLIB。
4. AOP的具体使用xml模式下
<!--spring aop的jar包支持-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.1.12.RELEASE</version>
</dependency>
<!--第三方的aop框架aspectj的jar-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
</dependencies>
/**
* aop横切面
* @author lane
* @date 2021年03月28日 下午1:23
*/
public class AopLogUtils {
/*
* aop之业务方法之前执行
* @author lane
* @date 2021/3/28 下午1:24
*/
public void beforeMethod(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
System.out.println("aop之业务方法之前执行......获取参数0为"+args[0]);
}
/*
* aop之业务方法之后执行(无论异常与否)
* @author lane
* @date 2021/3/28 下午1:24
*/
public void afterMethod(){
System.out.println("aop之业务方法之后执行(无论异常与否)......");
}
/*
* aop之业务方法异常时执行
* @author lane
* @date 2021/3/28 下午1:24
*/
public void exceptionMethod(){
System.out.println("aop之业务方法之异常时执行......");
}
/*
* aop之业务方法成功时执行
* @author lane
* @date 2021/3/28 下午1:24
*/
public void successMethod(){
System.out.println("aop之业务方法之成功时执行......");
}
/*
* aop之业务方法之环绕通知1v5
* @author lane
* @date 2021/3/28 下午1:24
*/
public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint )throws Throwable {
Object result = null;
System.out.println("环绕通知方法之前执行类似beforeMethod......");
try{
//此方法类似method.invoke(),不执行的话原方法就不会执行,所以可以加前置后置通知,也可以控制原有方法是否执行
result = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
}catch (Exception e){
System.out.println("环绕通知方法之后执行类似exceptionMethod......");
} finally {
System.out.println("环绕通知方法之后执行类似afterMethod......");
}
System.out.println("环绕通知方法成功之后执行类似successMethod......");
return result;
}
}
<!--
在Spring的配置⽂件中加⼊aop的约束 xmlns:aop
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://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
">
<!-- Spring基于XML的AOP配置步骤
第⼀步:把通知Bean交给Spring管理
第⼆步:使⽤aop:config开始aop的配置
第三步:使⽤aop:aspect配置切⾯
第四步:使⽤对应的标签配置通知的类型-->
<!--进行aop相关的xml配置,配置aop的过程其实就是把aop相关术语落地-->
<!--配置横切逻辑bean-->
<bean id="aopLogUtils" class="com.lagou.edu.utils.AopLogUtils"></bean>
<!--使用aop config 标签表明aop开始配置,在内部配置切面aspect-->
<!--aspect = 切入点(锁定方法) + 方位点(锁定方法中的特殊时机) +横切逻辑-->
<aop:config>
<aop:aspect id="logAspect" ref="aopLogUtils">
<!--切入点锁定我们感兴趣的方法,使用aspectj语法表达式 -->
<aop:pointcut id="pt1" expression="execution(public void com.lagou.edu.service.impl.TransferServiceImpl.transfer(java.lang.String,java.lang.String,int))"/>
<!--方位点 pointcut-ref引用切入点,pointcut则是直接指定具的方法中表达式内容-->
<aop:before method="beforeMethod" pointcut-ref="pt1"/>
<aop:after method="afterMethod" pointcut-ref="pt1" />
<aop:after-returning method="successMethod" pointcut-ref="pt1"/>
<aop:after-throwing method="exceptionMethod" pointcut-ref="pt1"/>
<!--环绕通知1v5尽量不要和其它方法一起执行-->
<aop:around method="aroundMethod" pointcut-ref="pt1"/>
</aop:aspect>
</aop:config>
/**
* 测试bean的aop xml配置
*/
@Test
public void testAopXml() throws Exception {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
TransferService transferService = (TransferService) applicationContext.getBean("transferService");
transferService.transfer("6029621011000","6029621011001",100);
}
aop之业务方法之前执行…获取参数0为6029621011000
环绕通知方法之前执行类似beforeMethod…执行转账业务逻辑
环绕通知方法之后执行类似afterMethod…
环绕通知方法成功之后执行类似successMethod…aop之业务方法之成功时执行…
aop之业务方法之后执行(无论异常与否)…
全限定⽅法名 访问修饰符 返回值 包名.包名.包名.类名.⽅法名(参数列表)
全匹配⽅式:
public void com.lagou.service.impl.TransferServiceImpl.updateAccountByCardNo(com.lagou.pojo.Account)
访问修饰符可以省略
void com.lagou.service.impl.TransferServiceImpl.updateAccountByCardNo(com.lagou.pojo.Account)
返回值可以使⽤*,表示任意返回值
* com.lagou.service.impl.TransferServiceImpl.updateAccountByCardNo(com.lagou.pojo.Account)
包名可以使⽤.表示任意包,但是有⼏级包,必须写⼏个
* ....TransferServiceImpl.updateAccountByCardNo(com.lagou.pojo.Account)
包名可以使⽤..表示当前包及其⼦包
* ..TransferServiceImpl.updateAccountByCardNo(com.lagou.pojo.Account)
类名和⽅法名,都可以使⽤.表示任意类,任意⽅法
* ...(com.lagou.pojo.Account)
参数列表,可以使⽤具体类型
基本类型直接写类型名称 : int
引⽤类型必须写全限定类名:java.lang.String
参数列表可以使⽤*,表示任意参数类型,但是必须有参数
* *..*.*(*)
参数列表可以使⽤..,表示有⽆参数均可。有参数可以是任意类型
* *..*.*(..)
全通配⽅式:
* *..*.*(..)
举个栗子从单个方法到全部拦截
拦截表达式
<aop:pointcut id="pt1" expression="execution(public void com.lagou.edu.service.impl.TransferServiceImpl.transfer(java.lang.String,java.lang.String,int))"/>
一级变换每一个包、类、方法、返回值都变成*替换
<aop:pointcut id="pt1" expression="execution(* *.*.*.*.*.*.*(java.lang.String,java.lang.String,int))"/>
二级变换所有的包变成*.. 类变成*.
<aop:pointcut id="pt1" expression="execution(* *..*.*(java.lang.String,java.lang.String,int))"/>
三级变换参数变成*,必须有参数的
<aop:pointcut id="pt1" expression="execution(* *..*.*(*))"/>
四级变换,参数可有可无的,不建议这样配哟
<aop:pointcut id="pt1" expression="execution(* *..*.*(..))"/>
在前⾯我们已经说了,Spring在选择创建代理对象时,会根据被代理对象的实际情况来选择的。被代理对象实现了接⼝,则采⽤基于接⼝的动态代理。当被代理对象没有实现任何接⼝的时候,Spring会⾃动切换到基于⼦类的动态代理⽅式。但是我们都知道,⽆论被代理对象是否实现接⼝,只要不是final修饰的类都可以采⽤cglib提供的⽅式创建代理对象。所以Spring也考虑到了这个情况,提供了配置的⽅式实现强制使⽤基于⼦类的动态代理(即cglib的⽅式),配置的⽅式有两种
- 使⽤aop:config标签配置
<aop:config proxy-target-class="true">
- 使⽤aop:aspectj-autoproxy标签配置
<!--此标签是基于XML和注解组合配置AOP时的必备标签,表示Spring开启注解配置AOP的⽀持-->
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectjautoproxy>
5. AOP的具体使用注解模式下
1.依赖同上
2.切面类配置信息
/**
* aop横切面注解模式
* @author lane
* @date 2021年03月28日 下午1:23
*/
@Component
@Aspect
//@EnableAspectJAutoProxy //纯注解模式下在配置类上加上这个,只是在这做示范
public class AopLogUtilsAnno {
//指定切入点
@Pointcut("execution(* com.lagou.edu.service.impl.TransferServiceImpl.*(..))")
public void pt1(){
}
/*
* aop之业务方法之前执行
* @author lane
* @date 2021/3/28 下午1:24
*/
@Before("pt1()")
public void beforeMethod(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
System.out.println("aop之业务方法之前执行......获取参数0为"+args[0]);
}
/*
* aop之业务方法之后执行(无论异常与否)
* @author lane
* @date 2021/3/28 下午1:24
*/
@After("pt1()")
public void afterMethod(){
System.out.println("aop之业务方法之后执行(无论异常与否)......");
}
/*
* aop之业务方法异常时执行
* @author lane
* @date 2021/3/28 下午1:24
*/
@AfterThrowing("pt1()")
public void exceptionMethod(){
System.out.println("aop之业务方法之异常时执行......");
}
/*
* aop之业务方法成功时执行
* @author lane
* @date 2021/3/28 下午1:24
*/
@AfterReturning("pt1()")
public void successMethod(){
System.out.println("aop之业务方法之成功时执行......");
}
/*
* aop之业务方法之环绕通知1v5
* @author lane
* @date 2021/3/28 下午1:24
*/
@Around("pt1()")
public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint )throws Throwable {
Object result = null;
System.out.println("环绕通知方法之前执行类似beforeMethod......");
try{
//此方法类似method.invoke(),不执行的话原方法就不会执行,所以可以加前置后置通知,也可以控制原有方法是否执行
result = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
}catch (Exception e){
System.out.println("环绕通知方法之后执行类似exceptionMethod......");
} finally {
System.out.println("环绕通知方法之后执行类似afterMethod......");
}
System.out.println("环绕通知方法成功之后执行类似successMethod......");
return result;
}
}
3. xml配置
- 有xml模式下的配置
<!--开启aop注解驱动,proxy-target-class = true则强制使用cglib动态代理-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<!--开启aop注解驱动spring自己选择动态代理-->
<aop:aspectj-autoproxy/>
-
纯注解模式下开起aop注解驱动需要在配置类上加如下注解
@EnableAspectJAutoProxy
4. 测试类
/**
* 测试bean的aop anno配置
*/
@Test
public void testAopAnno() throws Exception {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
TransferService transferService = (TransferService) applicationContext.getBean("transferService");
transferService.transfer("6029621011000","6029621011001",100);
}
5. 测试结果
环绕通知方法之前执行类似beforeMethod…
aop之业务方法之前执行…获取参数0为6029621011000执行转账业务逻辑
环绕通知方法之后执行类似afterMethod…
环绕通知方法成功之后执行类似successMethod…
aop之业务方法之后执行(无论异常与否)…
aop之业务方法之成功时执行…