Spring AOP 应用
AOP本质:在不改变原有业务代码逻辑的情况下增强横切逻辑,横切逻辑代码往往是权限校验代码、日志代码、事务控制代码、性能监控代码。
AOP 相关术语
业务主线
在讲解AOP术语之前,我们先来看一下下面这两张图
上图描述的就是未采用AOP思想设计的程序,当我们红色框中圈定的方法时,会带来大量的重复劳动,程序中充斥着大量的重复代码,使我们的程序独立性很差。
而下图中是采用了AOP思想设计的程序,它把红框部分的代码抽取出来的同时,运用动态代理技术,在运行期对需要使用的业务逻辑方法进行增强。
AOP 术语
名称 | 解释 |
---|---|
Joinpoint(连接点) | 它指的是那些可以用于把增强代码加入到业务主线中的点,那么由上图我们可以看出,这些点指的就是方法。在方法执行的前后通过动态代理技术加入增强的代码。在Spring框架AOP思想的技术实现中,也只支持方法类型的连接点。 |
Pointcut(切入点) | 它指的是那些已经把增强代码加入到业务主线进来之后的连接点。由上图中,我们看出表现层transfer 方法就只是连接点,因为判断访问权限的功能并没有对其增强。 |
Advice(通知/增强) | 它指的是切面类中用于提供增强功能的方法,并且不同的方法增强的时机是不一样的。比如,开启事务肯定要在业务方法执行之前执行;提交事务要在业务方法正常执行之后执行;而回滚事务要在要在业务方法执行产生异常后执行等等。那么这就是通知的类型,其分类有:前置通知、后置通知。异常通知、最终通知、环绕通知 |
Target(目标对象) | 它指的是代理的目标对象,即被代理对象。 |
Proxy(代理) | 它指的是一个类被AOP织入增强后产生的代理类,即代理对象。 |
Weaving(织入) | 它指的是把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。 |
Aspect(切面) | 它指的是增强的代码所关注的方面,把这些相关的增强代码定义到一个类中,这个类就是个切面类。例如事务切面,它里面定义的方法就是和事务相关的,像开启事务、提交事务、回滚事务等,不会定义其他与事务无关的方法。 |
重点:
-
连接点:方法开始时、结束时、正常运行完毕时、方法异常时等这些特殊的时机点,我们称之为连接点,项目中每个方法都有连接点,连接点是一种候选点
-
切入点:指定AOP思想想要影响的具体方法是哪些,描述那些需要增强的方法
-
Advice 增强:
- 第一个层次:指的是横切逻辑
- 第二个层次:方位点(在某一些连接点上加入横切逻辑,那么这些连接点就叫方位点,描述的是具体的特殊时机)
-
Aspect切面:
-
切面概念是对上述概念的一个综合
-
Aspect切面 = 切入点 + 增强
= 切入点(锁定方法)+ 方位点(锁定方法中的特殊时机)+ 横切逻辑
众多的概念,目的就是为了锁定要在哪个地方插入什么横切逻辑代码
-
Spring中AOP的代理选择
Spring 实现AOP思想使用的是动态代理技术
默认情况下,Spring会根据被代理对象是否实现接口来选择JDK还是CGLIB。当代理对象没有实现任何接口时,Spring会选择CGLIB。当代理对象实现了接口,Spring会选择JDK动态代理。不过我们也可以通过配置的方式,让Spring强制使用CGLIB
xml方式:
<aop:aspectj-autoproxy proxy-target-class="true"/>
注解方式:
@EnableAspectJAutoProxy(proxyTargetClass = true)
一般情况下,使用默认让spring自动选择即可。
Spring中AOP的配置方式
在Spring的AOP配置中,也和IoC配置一样,支持三种配置方式
- 使用XMl配置
- 使用XML+注解配置
- 使用纯注解配置
Spring中AOP的实现
打印日志需求:把打印日志的代码逻辑织入service层中
XML模式
-
引入AOP依赖
<!-- 引入AOP --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.5</version> </dependency>
-
切面类LogUtils:
package com.test.utils; import org.aspectj.lang.ProceedingJoinPoint; public class LogUtil { public void beforeLog() { System.out.println("前置通知..."); } public void afterReturningLog(){ System.out.println("正常执行通知..."); } public void afterLog() { System.out.println("后置通知..."); } public void afterThrowingLog() { System.out.println("抛出异常通知..."); } public Object aroundLog(ProceedingJoinPoint pjp) throws Throwable { System.out.println("环绕通知1..."); // 获取参数 Object[] args = pjp.getArgs(); System.out.println("环绕通知2..."); // 调用方法 Object result = pjp.proceed(args); System.out.println("环绕通知3..."); return result; } }
-
applicationContext.xml配置
在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 https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> ...... <!--把通知bean交给spring来管理--> <bean id="logUtil" class="com.test.utils.LogUtil"></bean> <!-- 开始AOP配置 --> <aop:config> <!-- 配置切面 --> <aop:aspect id="logAdvice" ref="logUtil"> <!-- 切入点配置 --> <aop:pointcut id="pt1" expression="execution(public void com.test.service.impl.TransferServiceImpl.transfer(java.lang.String, String, int))"/> <!-- 配置前置通知 --> <aop:before method="beforeLog" pointcut-ref="pt1"/> <!-- 正常执⾏时通知 --> <aop:after-returning method="afterReturningLog" pointcut-ref="pt1"/> <!-- 配置异常通知 也可以直接指定切入点 --> <aop:after-throwing method="afterThrowingLog" pointcut="execution(public void com.test.service.impl.TransferServiceImpl.transfer(java.lang.String, String, int))"/> <!-- 配置最终通知,无论是否异常,都会触发 --> <aop:after method="afterLog" pointcut-ref="pt1"/> <!-- 环绕通知 --> <aop:around method="aroundLog" pointcut-ref="pt1"/> </aop:aspect> </aop:config> <aop:aspectj-autoproxy/> </beans>
执行输出:
前置通知... 环绕通知1... 环绕通知2... 业务核心代码 环绕通知3... 最终通知... 正常执行通知...
上述配置实现了对
TransferServiceImpl#transfer
方法进行增强。-
关于切入点表达式:
-
概念及作用
切入点表达式,也称之为AspectJ切入点表达式,指的是遵循特定语法结构的字符串,其作用是对于符合语法格式的连接点进行增强,它是AspectJ表达式的一部分。
-
关于AspectJ
AspectJ是一个机遇Java语言的AOP框架,Spring框架从2.0版本之后就集成了AspectJ框架中切入点表达式的部分,开始支持AspectJ的切入点表达式。
-
表达式使用示例
public void com.test.service.impl.TransferServiceImpl.transfer(java.lang.String, java.lang.String, int) 访问修饰符 返回值 全限定方法名(参数类型列表)
- 访问修饰符可以省略
- 返回值可以用
*
,表示任意返回值 `* - 包名可以使用
.
,但是有几级包,必须写几个void ...impl.TransferServiceImpl.transfer(java.lang.String, java.lang.String, int)
- 包名可以使用
..
表示当前包及其子包* ..TransferServiceImpl.transfer(java.lang.String, java.lang.String, int)
- 参数:基本类型直接写类型名称,引用类型需要写全限定类名
*
可以表示参数列表,但至少要有一个参数;..
可以表示任意参数个数
-
-
五种通知类型:
-
before前置通知
- 执行时机是在切入点方法(业务核心方法)执行之前执行
- 前置通知可以获取切入点方法的参数,并对其进行增强
-
after-returning正常执行通知
- 执行时机是在切入点方法(业务核心方法)正常执行后执行
-
after-throwing异常通知
- 执行时机是在切入点方法(业务核心方法)产生异常后执行,如果没有异常不会执行
-
after最终通知
- 执行时机是在切入点方法(业务核心方法)执行完成之后,返回之前执行,无论是否产生异常,都会执行。
-
around环绕通知
环绕通知有别于以上四种通知类型。前面四种通知它们都是指定何时增强的通知类型;而环绕通知,它是Spring框架为我们提供一种可以通过编码的方式,控制增强代码何时执行的通知类型。
它里面借助的ProceedingJoinPoint接口及其实现类,实现手动自定义除服切入点方法的调用。
-
Object result = pjp.proceed(args);
需要调用该方法,原代码才会执行,并且需要返回返回值
-
-
-
XML+注解模式
-
在xml文件中配置
<!-- 开启spring对注解aop的支持 --> <aop:aspectj-autoproxy/>
-
修改切面类LogUtils
@Component @Aspect public class LogUtil { /** * 提供切入点表达式 */ @Pointcut("execution(public void com.test.service.impl.TransferServiceImpl.transfer(java.lang.String, java.lang.String, int))") 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("最终通知..."); } @Around("pointcut()") public Object aroundLog(ProceedingJoinPoint pjp) throws Throwable { System.out.println("环绕通知1..."); // 获取参数 Object[] args = pjp.getArgs(); System.out.println("环绕通知2..."); // 调用方法 Object result = pjp.proceed(args); System.out.println("环绕通知3..."); return result; } }
首先要添加一个方法,使用**@Pointcut** 注解给后续的通知方法提供切入点表达式,然后只需把对应方法上添加对应的注解即可
纯注解模式
在上面xml+注解的基础上,只需用注解替换掉配置文件中的这行配置即可:
<!-- 开启spring对注解aop的支持 -->
<aop:aspectj-autoproxy/>
只需在配置类用注解替换即可
@Configuration
@ComponentScan("com.test")
@EnableAspectJAutoProxy //开启spring对注解AOP的⽀持
public class SpringConfiguration {
}