Spring之AOP
前言: Spring的核心功能包括IOC,DI,AOP,IOC用于降低计算机代码之间的耦合度,DI负责依赖注入,而AOP有什么作用呢?
AOP: Aspect Oriented Programming 面向切面编程,是通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术
Spring官方文档对于AOP的介绍以及应用
https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#aop
AOP的概念
AOP,即为面向切面编程,在传统的OOP编程中,我们的代码的逻辑都是自上而下的,而这些编码的过程中,通常会出现一些横切性问题,这些横切性的问题与我们的主业务逻辑关系不大,这些横切性的问题不会影响主业务逻辑的实现,但是会散落在代码的各个部分,难以维护,而AOP就是用于处理这些横切性问题,AOP的编程思想就是将这些横切性问题与主业务逻辑分离开,达到与主业务逻辑代码解耦的目的,使代码的重用性和开发的效率更高
AOP联盟官网
http://aopalliance.sourceforge.net/
Spring AOP
AOP基本概念
https://segmentfault.com/a/1190000018120725
Spring中AOP的概念
- Join Point: 连接点,在Spring AOP中连接点就是一个符合pointcut定义规则的方法,也就是需要被拦截增强的方法
- Pointcut: 切入点: 就是对连接点的一组定义规则
- target object: 目标对象,需要被增强的对象
- aop proxy: 代理对象, 对目标对象增强后的对象
- advice: 通知,指拦截到连接点后,所需要执行的代码,Spring AOP中对通知定义为拦截器,并且为每一个连接点维护了一个以连接点为中心的拦截链
- aspect: 切面,切面由切入点和通知组成,用于将通知应用到满足pointcut(切入点)定义规则的joinpoint(连接点)上
- weaving: 织入,将切面中的通知应用到满足了pointcut(切入点)定义规则的joinpoint(连接点)的过程叫做织入,织入可以在编译期,类加载和运行时完成,在编译期织入就是静态代理,在运行时织入则是动态代理
- Introduction: 引介
Spring AOP支持AspectJ
概述: AspectJ是一个非常优秀的AOP框架,它完全兼容java,而且由于采用编译期织入,所以效率非常高效
当然需要注意的是Spring AOP只依赖了AspectJ的语法,底层AOP的实现仍然是采用的Spring AOP自己的实现
AspectJ的切入点语法
切入点格式为:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?.namepattern(param-pattern) throws-pattern?)
execution(方法修饰符? 方法返回值类型 包名?.方法名(参数类型) 声明的异常类型?
? : 不是必须的
* : 出现在修饰符处是任意修饰符,出现在返回值类型处是任意返回值类型,出现在包或类名是任意包或类
.. : 0个或多个参数
示例: * void com.git.*.*Service.test*(*,String,..):
com.git.任意子包下的类名以Service结束的类中的以test开头的任意修饰符的任意返回值方法,且方法的第一个参数类型可以任意,但是第二个参数的类型必须是String,后面可以有0个或者多个参数
- execution: 可以根据方法的访问权限修饰符,返回类型,参数个数以及所在包名,类名匹配连接点
- within: 根据包名或者类名匹配连接点
- args: 根据方法参数类型匹配连接点,与连接点的包名,类名无关
- this: 根据代理后的类型匹配连接点,与连接点的包名,类名无关
- target: 指定目标类也就是代理类的类型
- @annotation: 根据指定方法上的注解匹配连接点,与连接点的包名,类名无关
- @args: 根据指定方法参数类型的类上的注解来匹配连接点,与连接点的包名,类名无关
- @within: 根据指定类上存在的注解匹配连接点,与连接点的包名,类名无关
Spring AOP的通知类型
通知: 就是定义了在连接点前后所需要执行的代码
Spring AOP对通知作用在连接点的位置分为5种类型,分别为前置通知,后置通知,最终通知和异常通知和环绕通知
-
前置通知: 顾名思义,在连接点执行之前执行,前置通知无法阻止连接点的正常执行,除非前置通知抛出异常
-
后置通知: 在连接点执行之后执行,当连接点抛出异常,将不会执行
-
异常通知: 当前置通知,连接点或者后置通知抛出异常时才会执行,否则不会执行
-
最终通知: 无论前置通知,连接点和异常通知是否正常执行,都会执行最终通知
-
环绕通知: : 围绕连接点执行,这也是最有用的切面方式( 在我看来它只是将前置通知,连接点,后置通知,异常通知,最终通知组合在了一起,不再需要额外的定义其他的通知,当然,如果定义了其他通知,那么环绕通知中的代码逻辑会优先于其他通知执行)
-
引介通知
Spring AOP的 Introductions 概念
- Introductions: 引介,用于将一个或多个类动态的扩展一个接口,并且可以指定它的默认实现
Spring AOP中切面,切入点以及5种通知类型所对应的注解
- @Aspect: 切面
- @Pointcut: 切入点
- @Before: 前置通知
- @After: 后置通知
- @AfterThrowing: 异常通知
- @AfterReturning: 最终通知
- @Around: 环绕通知
AOP所有代码git地址
https://github.com/juziia/demo/tree/master/spring-learn3
package com.git.spring.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 切面的作用: 用于定义将通知应用到哪些连接点上,连接点就是满足切入点定义规则的一个方法
*/
@Aspect // 声明当前类是一个切面类 切面中包含 切入点以及通知
@Component
public class AopAspect {
// 定义切入点
// @Pointcut("execution (public String com.git.spring.service.TestService.*(..))")
// @Pointcut("within(com.git.spring.service.*)")
// @Pointcut("args(java.lang.String,int,..)")
// @Pointcut("@annotation(com.git.spring.aop.annotation.Juzi)")
// @Pointcut("@args(com.git.spring.aop.annotation.Juzi,..)")
// @Pointcut("this(com.git.spring.dao.TestDao)") // 在jdk动态代理时,this必须为目标类(被代理类)的接口类型,cglib则是目标类类型
// @Pointcut("target(com.git.spring.dao.TestDao)")
// @Pointcut("@within(com.git.spring.aop.annotation.Juzi)")
@Pointcut("@annotation(com.git.spring.aop.annotation.Juzi)")
public void pointcut(){}
@Before("pointcut()")
public void beforeAdvice(){
System.out.println("前置通知111");
}
@AfterThrowing("pointcut()")
public void throwingAdvice(){
System.out.println("抛出了一个异常1111");
}
@AfterReturning("pointcut()")
public void afterReturningAdvice(){
System.out.println("最终通知111");
}
@After("pointcut()")
public void afterAdvice(){
System.out.println("后置通知111");
}
}
Introductions 引介的应用
// 创建一个com.git.spring.dao.TestDao接口
public interface TestDao {
void test();
}
// 创建com.git.spring.dao.impl.TestDaoImpl类实现TestDao接口
@Repository("test")
public class TestDaoImpl implements TestDao {
public void test() {
System.out.println("dao层测试方法");
}
}
// 创建一个com.git.spring.dao.impl.UserDao类,此类将用于动态扩展接口
@Repository("userDao")
public class UserDao { }
// 在Aspec类中进行扩展
@Aspect // 声明当前类是一个切面类 切面中包含 切入点以及通知
@Component
public class AopAspect {
// 将com.git.spring.dao包下所有子包下的所有类实现(扩展)TestDao接口,并且提供默认的实现是TestDaoImpl类
@DeclareParents(value ="com.git.spring.dao.*.*",defaultImpl = TestDaoImpl.class)
private TestDao dao;
}
// 测试
public static void main(String[] args) {
AnnotationConfigApplicationContext
context = new AnnotationConfigApplicationContext(AppConfig.class);
TestDao testDao = (TestDao) context.getBean("userDao");
testDao.test();
}
// 输出
// dao层测试方法
Spring AOP中Aspect的实例化模型
Spring AOP中Aspect的实例化模型有3种,分别是singleton,perthis,pertarget,所谓切面的实例化模型就是指切面何时会被实例化
- singleton: 单例,默认
- perthis: 每个切入点表达式匹配到的连接点的所对应的对象(代理对象)都会创建一个新的切面实例
- pertarget: 每个切入点匹配到的连接点的所对应的目标对象都会创建一个新的切面实例
注意: 当切面的实例化模型不是singleton时,那么就需要将当前切面类的作用域配置为多例的
// perthis实例化模型
//@Aspect("perthis(this(com.git.spring.dao.TestDao))")
// pertarget实例化模型
@Aspect("pertarget(target(com.git.spring.dao.TestDao))")
@Component
@Scope("prototype")
public class AopAspect {
}
个人测试切面的实例化模型所得到结果: 当配置切面的实例化模型为perthis或者pertarget,当切面bean的作用域为单例时,Spring将会抛出下图的异常,我们需要修改切面bean的作用域为多例,但是运行之后,根本不起作用,这时需要修改perthis或者pertarget中的bean的作用域为多例时,运行之后就能达到想要的效果(匹配到的连接点所对应的的代理对象或者目标对象都会重新创建一个新的切面实例),但是,这根本就是bean重新被ioc初始化并动态代理了造成的,跟切面的实例化模型根本就没有关系,当把切面bean的作用域改为singleton时,刚才的效果照样能出来,我翻阅许多资料,并没有对这方面的解答,无奈只能放弃
Spring AOP切面的xml配置:
<beans>
<!-- 配置动态代理的方式 false: JDK动态代理 true: Cglib动态代理 -->
<aop:aspectj-autoproxy proxy-target-class="false"></aop:aspectj-autoproxy>
<bean id="xmlAopAspect" class="com.git.spring.aop.XmlAopAspect"></bean>
<bean id="testService" class="com.git.spring.service.TestService"></bean>
<bean id="testDaoImpl" class="com.git.spring.dao.impl.TestDaoImpl"></bean>
<!-- 当然,通过aop的配置标签也能配置Spring AOP的动态代理方式 -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut id="pointcutExecution" expression="execution(public * com.git.spring.service.*.*(..))"/>
<!-- 配置切面 -->
<aop:aspect id="aspect" ref="xmlAopAspect">
<!-- 前置通知 -->
<aop:before pointcut-ref="pointcutExecution" method="beforeAdvice"/>
<!-- 后置通知 -->
<aop:after pointcut-ref="pointcutExecution" method="afterAdvice"/>
<!-- 异常通知 -->
<aop:after-throwing pointcut-ref="pointcutExecution" method="afterThrowing"/>
<!-- 最终通知 -->
<aop:after-returning pointcut-ref="pointcutExecution" method="afterReturning"/>
<!-- 引介,对一个类动态扩展一个接口,并且可以指定接口的默认实现 -->
<!-- <aop:declare-parents types-matching="com.git.spring.dao.impl.UserDao"
implement-interface="com.git.spring.dao.TestDao"
default-impl="com.git.spring.dao.impl.TestDaoImpl"></aop:declare-parents>-->
</aop:aspect>
<!-- 配置切面 -->
<aop:aspect id="daoAspect" ref="xmlAopAspect">
<!-- 环绕通知 -->
<aop:around method="aroundAdvice" pointcut="target(com.git.spring.dao.TestDao)"/>
</aop:aspect>
</aop:config>
</beans>
Java代码
public class XmlAopAspect {
public void beforeAdvice(JoinPoint joinPoint){
System.out.println("前置通知");
}
public void afterAdvice(JoinPoint joinPoint){
System.out.println("后置通知");
}
public void afterReturning(JoinPoint joinPoint){
System.out.println("最终通知");
}
public void afterThrowing(JoinPoint joinPoint) {
System.out.println("异常通知");
}
/**
* JoinPoint: JoinPoint它能够获取连接点的相关参数,包括原生目标对象,代理对象和方法的相关参数
* ProceedingJoinPoint: 它是JoinPoint的子接口,它扩展了proceed()方法,用于执行连接点
* @param proceedingJoinPoint:
*/
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
System.out.println("=============环绕通知==========");
beforeAdvice(proceedingJoinPoint);
try {
// 执行连接点
proceedingJoinPoint.proceed();
afterAdvice(proceedingJoinPoint);
} catch (Throwable throwable) {
afterThrowing(proceedingJoinPoint);
throwable.printStackTrace();
}
afterReturning(proceedingJoinPoint);
}
}
/** 执行 输出
前置通知
执行了一个测试方法111....
后置通知
最终通知
=============================
环绕通知
前置通知
dao层测试方法
后置通知
最终通知
*/
JoinPoint与ProceedingJoinPoint
- JoinPoint: 可以获取连接点的原生目标对象,代理后的对象以及连接点的相关参数
- ProceedingJoinPoint: ProceedingJoinPoint是JoinPoint的子接口,它扩展了一个proceed()方法,用于执行连接点