Spring AOP
1 基本概念
AOP的全称是Aspect-Oriented Programming,即面向切面编程(也称面向方面编程)。它是面向对象编程(OOP)的一种补充,目前已成为一种比较成熟的编程方式
在传统的业务处理代码中,通常都会进行事务处理、日志记录等操作。虽然使用OOP可以通过组合或者继承的方式来达到代码的重用,但如果要实现某个功能(如日志记录),同样的代码仍然会分散到各个方法中。这样,如果想要关闭某个功能,或者对其进行修改,就必须要修改所有的相关方法。这不但增加了开发人员的工作量,而且提高了代码的出错率。
AOP采取横向抽取机制,将分散在各个方法中的重复代码提取出来,然后在程序编译或运行时,再将这些提取出来的代码应用到需要执行的地方
可以看出,通过Aspect(切面)分别在Class1和Class2的方法中加入了事务、日志、权限和异常等功能
目前最流行的AOP框架有两个,分别为Spring AOP和AspectJ。
SpringAOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类织入增强的代码。
AspectJ是一个基于Java语言的AOP框架,从Spring 2.0开始,Spring AOP引入了对AspectJ的支持,AspectJ扩展了Java语言,提供了一个专门的编译器,在编译时提供横向代码的织入。
1.1 AOP相关术语
术语 | 解释 |
---|---|
Aspect(切面) | 在实际应用中,切面通常是指封装的用于横向插入系统功能(如事务、日志等)的类,如上图中的Aspect。该类要被Spring容器识别为切面,需要在配置文件中通过< bean>元素指定 |
Joinpoint(连接点) | 在程序执行过程中的某个阶段点,它实际上是对象的一个操作,例如方法的调用或异常的抛出。在Spring AOP中,连接点就是指方法的调用 |
Pointcut(切入点) | 是指切面与程序流程的交叉点,即那些需要处理的连接点,如图3-2所示。通常在程序中,切入点指的是类或者方法名,如某个通知要应用到所有以add开头的方法中,那么所有满足这一规则的方法都是切入点 |
Advice(通知/增强处理) | AOP框架在特定的切入点执行的增强处理,即在定义好的切入点处所要执行的程序代码。可以将其理解为切面类中的方法,它是切面的具体实现 |
Target Object(目标对象) | 是指所有被通知的对象,也称为被增强对象。如果AOP框架采用的是动态的AOP实现,那么该对象就是一个被代理对象 |
Proxy(代理) | 将通知应用到目标对象之后,被动态创建的对象 |
Weaving(织入) | 将切面代码插入到目标对象上,从而生成代理对象的过程 |
个人理解:在一个业务逻辑中,程序A首先通过切入点,产生了织入过程,此时A是目标对象,织入过程产生了代理对象,在产生代理对象的时候,进入了切面B,完成了通知。
1.假设全局方法A
2.假设局部方法B,有变量b
简单理解,就是在B执行的过程中,通过切面执行了A,A在对b处理了之后,给了B新的b,B用新的b往下执行
2 动态代理
AOP中的代理就是由AOP框架动态生成的一个对象,该对象可以作为目标对象使用
Spring中的AOP代理有两种,可以是JDK动态代理,也可以是CGLIB代理
JDK动态代理有一定的局限性——使用动态代理的对象必须实现一个或多个接口
要对没有实现接口的类进行代理,使用CGLIB代理
2.1 JDK动态代理
JDK动态代理是通过java.lang.reflect.Proxy类来实现的,我们可以调用Proxy类的newProxyInstance()方法来创建代理对象。对于使用业务接口的类,Spring默认会使用JDK动态代理来实现AOP
1.代理类
//代理类必须实现InvocationHandler
// 创建代理方法
public Object createProxy(UserDao userDao) {
this.userDao = userDao;//是为了让代理方法获得userDao,方便invoke()
// 1. 类加载器
ClassLoader classLoader = JdkProxy.class.getClassLoader();
//反射机制
// 2. 被代理对象实现的所有接口
Class[] classInterface = userDao.getClass().getInterfaces();
// 3. 使用代理类,进行增强处理,返回的是代理后的对象
return Proxy.newProxyInstance(classLoader, classInterface, this);
/*
* newProxyInstance()方法中包含3个参数,
* 第1个参数是当前类的类加载器
* 第2个参数表示的是被代理对象实现的所有接口
* 第3个参数this代表的就是代理类JdkProxy本身
*/
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// TODO Auto-generated method stub
// 声明切面
MyAspect myAspect = new MyAspect();
//前增强,希望在方法前执行
myAspect.check_Permissions();
//在目标类上调用方法,传递参数
/*
* method具体内容取决于在原程序中所执行的方法
* 比如,调用addUser时method是addUser
* 参数userDao表示目标类
*/
Object obj = method.invoke(userDao, args);
//后增强
myAspect.log();
return obj;
}
2.代理类使用
// 此时自动生成了代理,调用userDao1时会将调用给jdkProxy的invoke()
// jdkProxy成了userDao1的代理(就好比明星与经纪人)
UserDao userDao1 = (UserDao)jdkProxy.createProxy(userDao);
执行结果
2.2 CGLIB代理
CGLIB(Code Generation Library)是一个高性能开源的代码生成包,它采用非常底层的字节码技术,对指定的目标类生成一个子类,并对子类进行增强。在Spring的核心包中已经集成了CGLIB所需要的包,所以开发中不需要另外导入JAR包
1. 代理类
//实现接口MethodInterceptor
//代理方法
public Object createProxy(Object target) {
// 创建一个动态类对象
Enhancer enhancer = new Enhancer();
// 确定需要增强的类,确定其父类
enhancer.setSuperclass(target.getClass());
// 添加回调函数
enhancer.setCallback(this);
// 返回创建的代理类
return enhancer.create();
/**
* 相比于jdk动态代理,没有方法newProxyInstance
* 相当于创建了一个空壳对象 enhancer,并进行了增强
*/
}
@Override
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
// 创建切面类对象
MyAspect myAspect = new MyAspect();
// 前增强
myAspect.check_Permissions();
// 目标方法执行
Object object = methodProxy.invokeSuper(proxy, args);
// 后增强
myAspect.log();
return object;
}
执行结果
2.3 两种方法的异同
在写法和逻辑上都是相同的,主要的区别在于JDK代理的对象必须要有接口实现,Cglib不需要
3 基于代理类的AOP实现
Spring中的AOP代理默认就是使用JDK动态代理的方式来实现的。在Spring中,使用Proxy FactoryBean是创建AOP代理的最基本方式
3.1 Spring的通知类型
类型 | 功能解释 |
---|---|
org.aopalliance.intercept.MethodInterceptor(环绕通知) | 在目标方法执行前后实施增强,可以应用于日志、事务管理等功能 |
org.springframework.aop.MethodBeforeAdvice(前置通知) | 在目标方法执行前实施增强,可以应用于权限管理等功能 |
org.springframework.aop.AfterReturningAdvice(后置通知) | 在目标方法执行后实施增强,可以应用于关闭流、上传文件、删除临时文件等功能 |
org.springframework.aop.ThrowsAdvice(异常通知) | 在方法抛出异常后实施增强,可以应用于处理异常记录日志等功能 |
org.springframework.aop.IntroductionInterceptor(引介通知) | 在目标类中添加一些新的方法和属性,可以应用于修改老版本程序(增强类) |
3.2 ProxyFactoryBean
ProxyFactoryBean是FactoryBean接口的实现类,FactoryBean负责实例化一个Bean,而ProxyFactoryBean负责为其他Bean创建代理实例
ProxyFactoryBean类中的常用可配置属性
属性名称 | 描述 |
---|---|
target | 代理的目标对象 |
proxyInterfaces | 代理要实现的接口,如果是多个接口,可以使用以下格式赋值 |
proxyTargetClass | 是否对类代理而不是接口,设置为true时,使用CGLIB代理 |
interceptorNames | 需要织入的目标Advice |
singleTon | 返回的代理是否为单实例,默认为true |
optimize | 设置为true时,强制使用CGLIB |
项目中新添包:
spring-aop-4.3.6.RELEASE.jar:是Spring为AOP提供的实现包,Spring的包中已经提供
aopalliance-1.0.jar:是AOP联盟提供的规范包,该JAR包可以通过地址“http://mvnrepository.com/artifact/aopalliance/aopalliance/1.0”下载
经过测试,在没有aopalliance-1.0.jar的情况下也能成功运行,猜测可能已经被整合到spring-aop-4.3.6.RELEASE.jar中
1. xml文件配置
<!-- 1. 目标类 -->
<bean id="userDao" class="com.clarence.jdk.UserDaoImpl"></bean>
<!-- 2. 切面类 -->
<bean id="myAspect" class="com.clarence.factorybean.MyAspect"/>
<!-- 3. 使用Spring代理工程定义一个名称为userDaoProxy的代理对象 -->
<bean id="userDaoProxy"
class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 3.1 指定代理实现的接口 -->
<property name="proxyInterfaces" value="com.clarence.jdk.UserDao"/>
<!-- 3.2 指定目标对象 -->
<property name="target" ref="userDao"></property>
<!-- 3.3 指定切面,织入环绕通知 -->
<property name="interceptorNames" value="myAspect"></property>
<!-- 3.4 指定代理方式 true:cgilib;false(默认):jdk动态代理 -->
<property name="proxyTargetClass" value="true"></property>
</bean>
2. 切面方法,环绕通知
public Object invoke(MethodInvocation arg0) throws Throwable {
check_Permission();
//执行目标方法
Object object = arg0.proceed();
log();
return object;
}
private void log() {
System.out.println("模拟记录日志......");
}
private void check_Permission() {
System.out.println("模拟权限检查......");
}
3. 测试方法
public static void main(String[] args) {
String xmlPath = "com/clarence/factorybean/applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
// 从Spring容器中获得内容
UserDao userDao = (UserDao)applicationContext.getBean("userDaoProxy");
// 执行方法
userDao.addUser();
userDao.deleteUser();
}
运行结果
总结比较
相比于之前的JDK动态代理和CGLIB代理,ProxyFactoryBean更加的依赖于框架,让程序更简便,简而言之,是让spring框架完成了上面的代理设置。
4 AspectJ开发
AspectJ是一个基于Java语言的AOP框架,它提供了强大的AOP功能
Spring 2.0以后,SpringAOP引入了对AspectJ的支持,并允许直接使用AspectJ进行编程,而Spring自身的AOP API也尽量与AspectJ保持一致。新版本的Spring框架,也建议使用AspectJ来开发AOP
使用AspectJ实现AOP有两种方式:
一种是基于XML的声明式AspectJ,另一种是基于注解的声明式AspectJ
4.1 基于XML的声明式AspectJ
基于XML的声明式AspectJ是指通过XML文件来定义切面、切入点及通知,所有的切面、切入点和通知都必须定义在< aop:config>元素内
Spring配置文件中的< beans>元素下可以包含多个< aop:config>元素,一个< aop:config>元素中又可以包含属性和子元素,其子元素包括< aop:pointcut>、< aop:advisor>和< aop:aspect>。在配置时,这3个子元素必须按照此顺序来定义。在< aop:aspect>元素下,同样包含了属性和多个子元素,通过使用< aop:aspect>元素及其子元素就可以在XML文件中配置切面、切入点和通知。
添加依赖包
1. xml文件格式
<!-- 基于xml的声明式AspcetJ的xml文件配置格式 -->
<beans ...>
<!-- 新添约束
xmlns:aop="http://www.srpingframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd" -->
<!-- 定义切面 Bean -->
<bean id="myAspect" class="com.clarence.aspcetj.xml.MyAspect" />
<aop:config>
<!-- 1. 配置切面 -->
<aop:aspect id="myAspect" ref="myAspect">
<!-- 2. 配置切入点 -->
<aop:pointcut
expression="execution(* com.clarence.jdk.*.*(..))" id="myPointCut"></aop:pointcut>
<!-- 前置通知 -->
<aop:before method="myBefore" pointcut-ref="myPointCut" />
<!-- 后置置通知 -->
<aop:after-returning method="myAfterRetuening"
pointcut-ref="myPointCut" returning="returnVal" />
<!-- 环绕通知 -->
<aop:around method="myAround" pointcut-ref="myPointCut" />
<!-- 异常通知 -->
<aop:after-throwing method="myAfterThrowing"
pointcut-ref="myPointCut" throwing="e" />
<!-- 最终通知 -->
<aop:after method="myAfter" pointcut-ref="myPointCut"/>
</aop:aspect>
</aop:config>
</beans>
元素 | 功能 | 属性 | 描述 | 备注 |
---|---|---|---|---|
< aop:aspect> | 配置切面 | id | 用于定义该切面唯一标识名称 | |
ref | 用于引用普通Spring Bean | |||
< aop:pointcut> | 配置切入点 | id | 指定切入点的唯一标识名称 | 注1 |
expression | 指定切入点关联的切入点表达式 | 注2 | ||
< aop:aspect> | 配置通知 | pointcut | 该属性用于指定一个切入点表达式, Spring将在匹配该表达式的连接点时织入该通知 | |
pointcut-ref | 该属性指定一个已经存在的切入点名称,如配置代码中的 mypointcut。通常 pointcut和pointcut-ref两个属性只需要使用其中之一 | |||
method | 该属性指定一个方法名,指定将切面Bean中的该方法转换为增强处理 | |||
throwing | 该属性只对< after- throwing>元素有效,它用于指定一个形参名,异常通知方法可以通过该形参访问目标方法所抛出的异常 | |||
returning | 该属性只对< after- returning>元素有效,它用于指定一个形参名,后置通知方法可以通过该形参访问目标方法的返回值 |
注1:当< aop:pointcut>元素作为< aop:config>元素的子元素定义时,表示该切入点是全局切入点,它可被多个切面所共享;当< aop:pointcut>元素作为< aop:aspect>元素的子元素时,表示该切入点只对当前切面有效
注2:execution(* com.clarence.jdk.* .* (…))就是定义的切入点表达式,该切入点表达式的意思是匹配com.clarence.jdk包中任意类的任意方法的执行。其中execution()是表达式的主体,第1个* 表示的是返回类型,使用* 代表所有类型;com.clarence.jdk表示的是需要拦截的包名,后面第2个* 表示的是类名,使用* 代表所有的类;第3个* 表示的是方法名,使用* 表示所有方法;后面(…)表示方法的参数,其中的“. .”表示任意参数。需要注意的是,第1个*与包名之间有一个空格
注3:在AOP的配置信息中,使用< aop:after-returning>配置的后置通知和使用< aop:after>配置的最终通知虽然都是在目标方法执行之后执行,但它们也是有所区别的。后置通知只有在目标方法成功执行后才会被织入,而最终通知不论目标方法如何结束(包括成功执行和异常中止两种情况),它都会被织入
1.切面类注意点
1.1 注意是org.aspectj.lang.JoinPoint
1.2 环绕通知条件
ProceedingJoinPoint是 JoinPoint子接口,表示可执行目标方法
1. 必须是Object类型的返回值
2. 必须接受一个参数,类型为ProceedingJoinPoint
3. 必须throws Throwable
/**
* 环绕通知
*/
public Object myAround(ProceedingJoinPoint proceedingJoinPoint)
throws Throwable{
System.out.println("环绕开始:执行目标方法之前,模拟开启事务....");
// 执行目标方法
Object object = proceedingJoinPoint.proceed();
System.out.println("环绕结束:执行目标方法之后,模拟关闭事务....");
return object;
}
运行结果
与基于代理类的AOP实现相比,基于XML的声明式ApectJ要便捷得多,但是它也存在着一些缺点,那就是要在Spring文件中配置大量的代码信息
4.2 基于注解的声明式AspectJ
AspectJ框架为AOP的实现提供了一套注解,用以取代Spring配置文件中为实现AOP功能所配置的臃肿代码
注解及其描述
注解名称 | 描述 |
---|---|
@Aspect | 用于定义一个切面 |
@Pointcut | 用于定义切入点表达式。在使用时还需定义一个包含名字和任意参数的方法签名来表示切入点名称。实际上,这个方法签名就是一个返回值为void,且方法体为空的普通的方法 |
@Before | 用于定义前置通知,相当于 Beforeadvice。在使用时,通常需要指定一个 value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式) |
@AfterReturning | 用于定义后置通知,相当于 Afterreturning Advice。在使用时可以指定 pointcut/value和returning属性,其中 pointcut/value这两个属性的作用一样,都用于指定切入点表达式。returning属性值用于表示 Advice方法中可定义与此同名的形参,该形参可用于访问目标方法的返回值 |
@Around | 用于定义环绕通知,相当于 Methodinterceptor。在使用时需要指定一个vaue属性,该属性用于指定该通知被植入的切入点 |
@AfterThrowing | 用于定义异常通知来处理程序中未处理的异常,相当于 Throwadvice。在使用时可指定pointcut/value和 throwing属性。其中 pointcut/value用于指定切入点表达式,而 throwing属性值用于指定一个形参名来表示 Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常 |
@After | 于定义最终fina通知,不管是否异常,该通知都会执行。使用时需要指定一个 value属性,该属性用于指定该通知被植入的切入点 |
@DeclareParents | 用于定义引介通知,相当于 Introductioninterceptor(不要求掌握) |
1.xml文件配置
<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:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
">
<!-- 指定扫描范围,使注解生效 -->
<context:component-scan base-package="com.clarence"/>
<!-- 启动AOP注解支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
2. MyAspect实现
@Aspect
@Component
//由于该类在Spring中是作为组件使用的,所以还需要添加@Component注解才能生效
public class MyAspect {
@Pointcut("execution(* com.clarence.jdk.*.*(..))")
// 使用一个返回值为void,方法体为空的方法来命名切入点
private void myPointCut() {
}
...
在配置是要注意切入点的名称应该是方法名,即myPointCut()
注意不要少括号
}
实现代码与XML方式类似,主要区别在于xml文件的配置
运行结果
从运行结果可以看出,最终通知要在后置通知的前面,后置通知是最晚的
4.3 AOP增强处理存在的问题
如果在同一个连接点有多个通知需要执行,那么在同一切面中,目标方法之前的前置通知和环绕通知的执行顺序是未知的,目标方法之后的后置通知和环绕通知的执行顺序也是未知的
在上述两种方式中可以看出,虽然方法一样,但是前后输出的位置有一些区别。
为了避免出现错误,个人觉得不要使用环绕通知,以前置后置通知来代替