Spring 的 AOP
模块,是Spring框架体系结构中十分重要的内容,本次学习和理解什么是AOP
一、Spring AOP 简介
1.什么是AOP
AOP 的全称是 Aspect-Oriented Programming,即面向切面编程(也称面向方面编程)。它是面向对象编程(OOP)的一种补充,目前已成为一种比较成熟的编程方式。
在传统业务处理代码中,通常都会进行事务处理、日志记录等操作。虽然使用OOP可以通过组合或者继承的方式来达到代码的重用,但如果要实现某个功能(如日志记录),同样的代码仍然会分散到各个方法中。这样,如果想要关闭某个功能,或者对其进行修改,就必须要修改所有的相关方法。这不但增加了开发人员的工作量,而且提高了代码的出错率。
为了解决这一问题,AOP思想随之产生。AOP采取横向抽取机制,将分散在各个方法中的重复代码提取出来,然后再程序编译或运行时,再将这些提取出来的代码应用到需要执行的地方。这种采用横向抽取机制的方式,采用传统的OOP思想显然是无法办到的,因为OOP只能实现父子关系的纵向的重用。虽然AOP是一种新的编程思想,但却不是OOP的替代品,它只是OOP的延伸和补充。
在AOP思想中,类与切面的关系如图:
AOP的使用,使开发人员在编写业务逻辑时可以专心于核心业务,而不是过多地关注与其他业务逻辑的实现,这不但提高了开发效率,而且增强了代码的可维护性。
目前最流行的AOP框架有两个,分别为Spring AOP 和 AspectJ。Spring AOP 使用纯Java 实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类织入增强的代码。AspectJ 是一个基于java语言的AOP框架,从Spring2.0开始,Spring AOP 引入了对AspectJ的支持,AspectJ 扩展了java语言,提供了一个专门的编译器,在编译时提供横向代码的织入。
2. AOP 术语
AOP 术语有包括:Aspect、Joinpoint、Pointcut、Target Object、Proxy 和 WeAVing。
- Aspect(切面):在实际应用中,切面通常是指封装的用于横向插入系统功能(如事务、日志等)的类,该类要被Spring容器识别为切面,需要在配置文件中通过<bean>元素指定。
- Jointpoint(连接点):在程序执行过程中的某个阶段点,它实际上是对象的一个操作,例如方法的调用或异常的抛出。在Spring AOP 中,连接点就是指方法的调用
- Pointcut(切入点):是指切面与程序流程的交叉点,即那些需要处理的连接点,如下图所示:通常在程序中,切入点指的是类或者方法名,如某个通知要应用到所有以add开头的方法中,那么所有满足这一规则的方法都是切入点。
- Advice(通知/增强处理):AOP 框架在特定的切入点执行的增强处理,即在定义好的切入点处所要执行的程序代码。可以将其理解为切面类中的方法,它是切面的具体实现。
- Target Object(目标对象):是指所有被通知的对象,也称为被增强对象。如果AOP 框架采用的是动态的AOP实现,那么该对象就是一个被代理对象。
- Proxy(代理):将通知应用到目标对象之后,被动态创建的对象。
- WeAVing(织入):将切面代码插入到目标对象上,从而生成代理对象的过程。
二、动态代理
1. JDK 动态代理
JDK 动态代理是通过 java.lang.reflect.Proxy 类来实现的,我们可以调用Proxy 类的newProxyInstance()
方法来创建代理对象。对于使用业务接口的类,Spring默认会使用JDK 动态代理来实现AOP。
下面即一个简单的例子演示Spring中JDK 动态代理的实现过程,步骤如下
1.新建一个项目,导入Spring所需的框架包到lib目录下。
2.在src目录下,创建一个com.cn.jdk包,并在该包下创建接口UserDao,并在接口写入添加和删除方法,代码如下:
package com.cn.jdk;
public interface UserDao {
public void addUser();
public void deleteUser();
}
3.在com.cn.jdk包中,创建UserDao接口的实现类UserDaoImlp,分别实现接口中的方法,并在每个方法中添加一条输出语句,代码如下:
package com.cn.jdk;
//目标类
public class UserDaoImpl implements UserDao{
@Override
public void addUser() {
System.out.println("添加用户");
}
@Override
public void deleteUser() {
System.out.println("删除用户");
}
}
需要注意的是:此案例会将实现类UserDaoImpl作为目标类,对其中的方法进行增强处理。
4.在src目录下,创建一个com.cn.aspect包,并在该包下创建切面类MyAspect,在该类中定义一个模拟权限检查的方法和一个模拟记录日志的方法,这两个方法就表示切面中的通知,代码如下:
package com.cn.aspect;
//切面类,可以存在多个通知Advice(即增强方法)
public class MyAspect {
public void check_Permissions() {
System.out.println("模拟检查权限...");
}
public void log() {
System.out.println("模拟记录日志...");
}
}
5.在com.cn.jdk包下,创建代理类JdkProxy,该类需要实现InvocationHandler接口,并编写代理方法。在代理方法中,需要通过Proxy类实现动态代理,代码如下:
package com.cn.jdk;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import com.cn.aspect.MyAspect;
public class JdkProxy implements InvocationHandler{
//声明目标类接口
private UserDao userDao;
//创建代理方法
public Object createProxy(UserDao userDao){
this.userDao = userDao;
//1.类加载器
ClassLoader classLoader = JdkProxy.class.getClassLoader();
//2.被代理对象实现的所有接口
Class[] clazz = userDao.getClass().getInterfaces();
//3.使用代理类,进行增强,返回的是代理后的对象
return Proxy.newProxyInstance(classLoader, clazz, this);
}
/*
* 所有动态代理类的方法调用,都会交由invoke()方法去处理
* proxy被代理后的对象
* method将要被执行的方法信息(反射)
* args执行方法时需要的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//声明切面
MyAspect myAspect = new MyAspect();
//前增强
myAspect.check_Permissions();
//在目标类上调用方法,并传入参数
Object obj = method.invoke(userDao, args);
//后增强
myAspect.log();
return obj;
}
}
上述代码中,JdkProxy 类实现了InvocationHandler
接口,并实现了接口中的invoke()
方法,所有动态代理类所调用的方法都会交由该方法处理。在创建的代理方法createProxy()
中,使用了Proxy类的newProxyInstance()
方法来创建代理对象。newProxyInstance()
方法中包含3个参数,其中第一个参数是当前类的类加载器,第2个参数表示的是被代理对象实现的所有接口,第3个参数this代表的就是代理类 JdkProxy 本身。在invoke()方法中,目标类方法执行的前后,会分别执行切面类中的check_Permissions()方法和log()方法。
6.在com.cn.jdk包中,创建测试类JdkTest。在该类中的测试方法中创建代理对象和目标对象,然后从代理对象中获的对象目标userDao增强后的对象,最后调用该对象中的添加删除方法,代码如下:
package com.cn.jdk;
import org.junit.jupiter.api.Test;
public class JdkTest {
@Test
public void Test() {
//创建代理对象
JdkProxy jdkProxy = new JdkProxy();
//创建目标对象
UserDao userDao = new UserDaoImpl();
//从代理对象中获取增强后的目标对象
UserDao userDao1 = (UserDao) jdkProxy.createProxy(userDao);
//执行方法
userDao.addUser();
userDao.deleteUser();
}
}
执行结果如下图所示:
从图中可以看出,userDao 实例中的添加用户和删除用户的方法已被成功调用,并且在调用前后分别增加了检查权限和记录日志的功能。这种实现了接口的代理方式,就是Spring 中的JDK动态代理。
2.CGLB 代理
CGLIB(Code Generation Library)是一个高性能开源的代码生成包,它采用非常底层的字节码技术,对指定的目标类生成一个子类,并对子类进行管理。在Spring的核心包中已经集成了CGLIB所需的包,所以开发中不需要另外导入JAR包。
示例如下:
1.创建一个com.cn.cglib包,在包中创建一个目标类UserDao,UserDao不需要实现任何借口,只需定义一个添加用户的方法和一个删除用户的方法,代码如下:
package com.cn.cglib;
//目标类
public class UserDao {
public void addUser() {
System.out.println("添加用户");
}
public void deleteUser() {
System.out.println("删除用户");
}
}
2.在com.cn.cglib包中,创建代理类CglibProxy,该代理类需要实现MethodInterceptor接口,并实现接口中的intercept()方法,代码如下:
package com.cn.cglib;
import java.lang.reflect.Method;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import com.cn.aspect.MyAspect;
//代理类
public class CglibProxy implements MethodInterceptor{
//代理方法
public Object createProxy(Object target) {
//创建一个动态类对象
Enhancer enhancer = new Enhancer();
//确定需要增强的类,设置其父类
enhancer.setSuperclass(target.getClass());
//添加回调函数
enhancer.setCallback(this);
//返回创建的代理类
return enhancer.create();
}
/*
* proxy CGlib 根据指定父类生成的代理对象
* method 拦截的方法
* args 拦截方法的参数数组
* methodProxy方法的代理对象,用于执行父类的方法
*
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//创建切面类对象
MyAspect myAspect = new MyAspect();
//前增强
myAspect.check_Permissions();
//目标方法执行
Object obj = methodProxy.invokeSuper(proxy, args);
//后增强
myAspect.log();
return obj;
}
}
如上所示,首先创建了一个动态类对象Enhancer,它是CGLIB的核心类;然后调用了Enhancer类的setSuperclass()方法来确定目标对象;接下来调用了setCallback()方法添加回调函数,其中的this代表的就是代表类CglibProxy本身;最后通过return语句将创建的代理类对象返回。intercept()方法会在程序执行目标方法时被调用,方法运行时将会执行切面类中的增强方法
3.在com.cn.cglib包中,创建测试类CglibTest。在该类的main()方法中首先创建代理对象和目标对象、然后从代理对象中获得增强后的目标对象,最后调用对象的添加和删除方法,代码如下:
package com.cn.cglib;
//测试类
public class CglibTest {
public static void main(String[] args) {
//创建代理对象
CglibProxy cglibProxy = new CglibProxy();
//创建目标对象
UserDao userDao = new UserDao();
//获取增强后的目标对象
UserDao userDao1 = (UserDao)cglibProxy.createProxy(userDao);
//执行方法
userDao1.addUser();
userDao1.deleteUser();
}
}
执行结果如下:
三、基于代理类AOP实现
1. Spring的通知类型
spring中的通知按照在目标类方法的链接位置,可以分为一下5中类型
- org.aopalliance.intercept.MethodInterceptor(环绕通知)
在目标方法执行前后实施增强,可以应用于日志、事务管理等功能。 - org.springframework.aop.MethodBeforeAdvice(前置通知)
在目标方法执行前实施增强,可以应用于权限管理等功能。 - org.springframework.aop.AfterReturningAdvice(后置通知)
在目标方法执行后实施增强,可以用于关闭流、上传文件、删除临时文件等功能。 - org.springframework.aop.ThrowsAdvice(异常通知)
在方法抛出异常后实施增强,可以应用于处理异常记录日志等功能 - org.springframework.aop.IntroductionInterceptor(引介通知)
在目标类中添加一些新的方法和属性,可以应用于修改老版本程序(增强类)。
2. ProxyFactoryBean
ProxyFactoryBean是FactoryBean接口的实现类,FactoryBean 负责实例化一个Bean,而ProxyFactoryBean 负责为其他Bean创建代理实例。在spring中,使用ProxyFactoryBean时创建AOP代理的基本方式
ProxyFactoryBean类中的常用可配置属性如下所示:
属性名称 | 描述 |
---|---|
target | 代理的目标对象 |
proxyInterfaces | 代理要实现的接口,如果是多个接口,可以使用一下格式赋予<list><value></value>...</list> |
proxyTargetClass | 是否对类代理而不是接口,设置为true时,使用CGLIB代理 |
interceptorNames | 需要织入目标的Advice |
singleton | 返回的代理是否为单实例,默认为true(返回即单实例) |
optimize | 当设置为true是,强制使用CGLIB |
简单使用ProxyFactoryBean创建AOP代理的过程如下:
1.在核心JAR包的基础上,再向项目的lib目录中导入AOP的jar,包`spring-aop-4.3.6.RELEASE.jar 和 aopalliance-1.0.jar
- spring-aop.4.3.6.RELEASE.jar:是Spring为AOP提供的实现包,Spring的包中已经提供。
- aopalliancec-1.0.jar:是AOP联盟提供的规范包,该jar包可以通过地址点我下载获得
2.在src目录下,创建一个com.cn.factorybean包,在该包中创建切面类 MyAspect。由于实现环绕通知需要实现org.aopalliance.intercept.MethodInterceptor接口,所以MyAspect类需要实现该接口,并实现接口中的invoke()方法,来执行目标方法,代码如下所示:
package com.cn.factorybean;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
//切面类
public class MyAspect implements MethodInterceptor{
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
check_Permissions();
//执行目标方法
Object obj = mi.proceed();
log();
return obj;
}
public void check_Permissions() {
System.out.println("模拟检查权限...");
}
public void log() {
System.out.println("模拟记录日志...");
}
}
3.在com.cn.factorybean包中,创建配置文件applicationContext.xml,并制定代理对象,代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 1 目标类 -->
<bean id="userDao" class="com.cn.jdk.UserDaoImpl"/>
<!-- 2 切面类 -->
<bean id="myAspect" class="com.cn.factorybean.MyAspect"/>
<!-- 3 使用Spring代理工厂定义一个名称为userDaoProxy的代理对象 -->
<bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 3.1 指定代理实现的接口 -->
<property name="proxyInterfaces" value="com.cn.jdk.UserDao"/>
<!-- 3.2 指定目标对象 -->
<property name="target" ref="userDao"/>
<!-- 3.3 指定切面,植入环绕通知 -->
<property name="interceptorNames" value="myAspect"/>
<!-- 3.4 指定代理方式,true:使用cglib,false(默认):使用jdk动态代理 -->
<property name="proxyTargetClass" value="true"/>
</bean>
</beans>
以上首先通过<bean>元素定义了目标类和切面,然后使用ProxyFactoryBean类定义了代理对象。在定义的代理对象中,分别通过<property>子元素指定了代理实现的接口、代理的目标对象、需要织入目标类的通知以及代理方式。
4.在com.cn.factorybean包中,创建测试类ProxyFactoryBeanTest,在类中通过Spring容器获取代理的实例,并执行目标方法,代码如下:
package com.cn.factorybean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.cn.jdk.UserDao;
public class ProxyFactoryBeanTest {
@org.junit.Test
public void Test() {
ApplicationContext appc = new ClassPathXmlApplicationContext("com/cn/factorybean/applicationContext.xml");
UserDao userDao = (UserDao)appc.getBean("userDaoProxy");
//执行方法
userDao.addUser();
userDao.deleteUser();
}
}
运行结果如下:
四、AspectJ开发
AspectJ是一个基于java语言的AOP框架,它提供了强大的AOP功能。Spring2.0以后,Spring AOP 引入了对AspectJ的支持,并允许直接使用AspectJ进行编程,而Spring自身的AOP API 也尽量与AspectJ保持一致。新版本的Spring框架,也建议使用AspectJ来开发AOP。使用AspectJ实现AOP有两种方式:一种是基于XML的声明式AspectJ,另一种是基于注解的声明式AspectJ。
1.基于XML的声明式AspectJ
基于XML的声明式AspectJ是通过XML文件来定义切面、切入点及通知、所有的切面、切入点和通知都必须定义在<aop:config>元素内。<aop:config>元素及其子元素如下图:
如图,Spring配置文件中<beans>元素下可以包含多个<aop:config>元素,一个<aop:config>元素中又可以包含属性和子元素,其子元素包括<aop:pointcut>、<aop:advisor>和<aop:aspect>。在配置时,这三个子元素必须按照此顺序来定义。在<aop:aspect>元素下,同样包含了属性和多个子元素,通过使用<aop:aspect>元素及其子元素就可以在XML文件中配置切面、切入点和通知。图中灰色部分标注的元素即为常用的配置元素,这些常用元素的配置代码如下:
<!-- 定义切面Bean -->
<bean id="myAspect" class="com.cn.aspectj.xml.MyAspect" />
<aop:config>
<!-- 1.配置切面 -->
<aop:aspect id="aspect" ref="myAspect">
<!-- 2配置切入点 -->
<aop:pointcut expression="execution(* com.cn.jdk.*.*(..))" id="myPointCut" />
<!-- 3.配置通知 -->
<!-- 前置通知 -->
<aop:before method="myBefore" pointcut-ref="myPointCut" />
<!-- 后置通知 -->
<aop:after-returning method="myAfterReturning" 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>
1.切面配置
在Spring的配置文件中,配置切面使用的是<aop:aspect>元素,该元素会将一个已定义好的Spring Bean转换成切面Bean,所以要在配置文件中先定义一个普通的Spring Bean。定义完成后,通过<aop:aspect>元素的ref属性即可引用该Bean。
配置<aop:aspect>元素时,通常会指定id和ref两个属性,表如下所示:
属性名称 | 描述 |
---|---|
id | 用于定义该切面的唯一标识名称 |
ref | 用于引用普通的Spring Bean |
2.配置切入点
在Spring的配置文件中,切入点是通过<aop:pointcut>元素来定义的。当<aop:pointcut>元素作为<aop:config>元素的子元素定义时,表示该切入点是全局切入点,它可以被多个切面所共享;当<aop:pointcut>元素作为<aop:aspect>元素的子元素时,表示该切入点只对当前切面有效。
在定义<aop:pointcut>元素时,通常会指定id和expression两个属性。表示如下:
属性名称 | 描述 |
---|---|
id | 用于指定切入点的唯一标识名称 |
expression | 用于指定切入点关联的切入点表达式 |
上述配置代码片段中,execution(* com.cn.jdk.*.*(..))就是定义的切入点表达式,该切入点表达式的意思是匹配com.cn.jdk包中任意类的任一方法的执行。其中execution()是表达式的主体,第一个*表示的是返回类型,使用*代表所有类型;com.cn.jdk表示的是需要拦截的包名,后面第2个*表示类名,使用*代表所有的类;第3个*表示的是方法名,使用*表示所有方法;后面(..)表示方法的参数,其中“..”表示任意参数。需要注意的是,第1个*与包名之间有一个空格。
上面示例中定义的切入点表达式只是开发中常用的配置方式,而Spring AOP 中切入点表达式的基本格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
各部分说明如下:
- modifiers-pattern:表示定义的目标方法的访问修饰符,如public、private等
- ret-type-pattern:表示定义的目标方法的返回值类型,如void、String等
- declaring-type-pattern:表示定义的目标方法的类路径,如com.cn.jdk.UserDaomImpl
- name-pattern:表示具体需要被代理的目标方法,如add()方法
- param-pattern:表示需要被代理的目标方法包含的参数,本节示例中目标方法参数都为空
- throws-pattern:表示需要被代理的目标方法抛出的异常类型
其中带有问号(?)的部分,如modifiers-pattern、declaring-type-pattern和throws-pattern表示可配置项;而其他部分属于必须配置项。
3.配置通知
在配置代码中,分别使用<aop:aspect>的子元素配置了5种常用通知,这5个子元素不支持使用子元素,但在使用时可以指定一些属性,如下表所示:
属性名称 | 描述 |
---|---|
pointcut | 该属性用于指定一个切入点表达式,Spring将在匹配该表达式的连接点时织入该通知 |
pointcut-ref | 该属性指定一个已经存在的切入点名称,如配置代码中的myPointCut。通常pointcut和pointcut-ref两个属性只需要使用其中之一 |
method | 该属性指定一个方法名,指定将切面Bean中的该方法转换为增强处理 |
throwing | 该属性只对<after-throwing>元素有效,它用于指定一个形参名,异常通知方法可以通过该形参访问目标方法抛出的异常 |
returning | 该属性只对<after-returning>元素有效,它用于指定一个形参名,后置通知方法可以通过该形参访问目标方法的返回值 |
简单的示例如下:
(1)导入AspectJ框架相关的JAR包,具体如下:
- spring-aspects-4.3.6.RELEASE.jar:Spring 为 AspectJ 提供的实现,Spring的包中已经提供
- aspectjweaver-1.8.10.jar:是AspectJ 框架所提供的规范,可以通过点我下载下载
(2)创建一个com.cn.aspectj.xml包在该包中创建切面类MyAspect,并在类中分别定义不通类型的通知,代码如下:
package com.cn.aspectj.xml;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
public class MyAspect {
//前置通知
public void myBefore(JoinPoint joinPoint) {
System.out.print("前置通知:模拟执行权限检查...,");
System.out.print("目标类是:"+joinPoint.getTarget());
System.out.println(",被植入增强处理的目标方法为:"+joinPoint.getSignature().getName());
}
//后置通知
public void myAfterReturning(JoinPoint joinPoint) {
System.out.print("后置通知:模拟记录日志...,");
System.out.println("被植入增强处理的目标方法为:"+joinPoint.getSignature().getName());
}
/**
* 环绕通知
* ProceedingJoinPoint是JoinPoint子接口,表示可以执行目标方法
* 1.必须是Object类型的返回值
* 2.必须接收一个参数,类型为ProceedingJoinPoint
* 3.必须throws Throwable
*/
public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
//开始
System.out.println("环绕开始:执行目标方法之前,模拟开启事务...");
//执行当前目标方法
Object obj = proceedingJoinPoint.proceed();
//结束
System.out.println("环绕结束:执行目标方法后,模拟关闭事务...");
return obj;
}
//异常通知
public void myAfterThrowing(JoinPoint joinPoint,Throwable e) {
System.out.println("异常通知:"+"出错了"+e.getMessage());
}
//最终通知
public void myAfter() {
System.out.println("最终通知:模拟方法结束后的释放资源...");
}
}
上述代码中,分别定义了5种不同类型的通知,再通知中使用了JoinPoint接口及其子类接口 ProceedingJoinPoint作为参数来获得目标对象的类名、目标方法名和目标方法参数等。
需要注意的是,环绕通知必须接收一个类型为ProceedingJoinPoint的参数,返回值也必须是Object类型,且必须抛出异常。异常通知中可以传入Throwable类型的参数来输出异常信息
(3)在com.cn.aspectj.xml包中,创建配置文件applicationContext.xml,配置如下:
<?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-4.3.xsd">
<!-- 1.目标类 -->
<bean id="userDao" class="com.cn.jdk.UserDaoImpl" />
<!-- 2.切面 -->
<bean id="myAspect" class="com.cn.aspectj.xml.MyAspect"/>
<!-- 3. AOP编程 -->
<aop:config>
<!-- 配置切面 -->
<aop:aspect ref="myAspect">
<!-- 3.1配置切入点,通知最后增强哪些方法 -->
<aop:pointcut expression="execution(* com.cn.jdk.*.*(..))" id="myPointCut"/>
<!-- 3.2关联通知Advice和切入点PointCut -->
<!-- 3.2.1前置通知 -->
<aop:before method="myBefore" pointcut-ref="myPointCut"/>
<!-- 3.2.2后置通知,在方法返回之后执行,就可以获得返回值
returning 属性:用于设置后置通知的第二个参数的名称,类型是Object -->
<aop:after-returning method="myAfterReturning" pointcut-ref="myPointCut" returning="returnVal"/>
<!-- 3.2.3环绕通知 -->
<aop:around method="myAround" pointcut-ref="myPointCut"/>
<!-- 3.2.4抛出通知:用于处理程序发生异常 -->
<!-- *注意:如果程序没有异常,将不会执行增强 -->
<!-- *throwing 属性:用于设置通知第二个参数的名称,类型Throwable -->
<aop:after-throwing method="myAfterThrowing" pointcut-ref="myPointCut" throwing="e"/>
<!-- 3.2.5最终通知:无论程序发生任何事情,都将执行 -->
<aop:after method="myAfter" pointcut-ref="myPointCut"/>
</aop:aspect>
</aop:config>
</beans>
tips:在AOP 的配置信息中,使用<aop:after-returning>配置的后置通知和使用<aop:after>配置的最终通知虽然都是在目标方法执行之后执行,但他们也是有所区别的。后置通知只有在目标方法成功执行后才会被织入,而最终通知不论目标方法如何结束(包括成功执行和异常终止两种情况),它都会被织入。
(4)在com.cn.aspectj.xml包下,创建测试类TextXmlAspectj,在类中为了更加清晰地演示几种通知的执行情况,现在只对addUser()方法进行增强测试,代码如下:
package com.cn.aspectj.xml;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.cn.jdk.UserDao;
public class TestXmlAspectj {
public static void main(String args[]) {
ApplicationContext appc = new ClassPathXmlApplicationContext("com/cn/aspectj/xml/applicationContext.xml");
UserDao userDao = (UserDao)appc.getBean("userDao");
userDao.addUser();
}
}
正常执行和异常执行(在UserDaoImpl类的addUser()方法下添加如"int i=10/0")的结果如下图所示:
2.基于注解的声明式AspectJ
关于AspectJ注解的介绍,如下:
注解名称 | 描述 |
---|---|
@Aspect | 用于定义一个切面 |
@PointCut | 用于定义切入点表达式。在使用时还需定义一个包含名字和任意参数的方法签名来表示切入点名称。实际上,这个方法签名就是一个返回值void,且方法体为空的普通的方法 |
@Before | 用于定义前置通知,相当于BeforeAdvice。在使用时,通常需要指定一个value属性值,该属性值用于制定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式) |
@AfterReturning | 用于定义后置通知,相当于AfterReturningAdvice。在使用时可以指定pointcut/value 和returning属性,其中pointcut/value这两个属性的作用一样,都用于指定切入点表达式。returning属性值用于表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法的返回值 |
@Around | 用于定义环绕通知,相当于MethodInterceptor。在使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点 |
@AfterThrowing | 用于定义异常通知来处理程序中未处理的异常,相当于ThrowAdvice。在使用时可指定pointcut/value和throwing属性。其中pointcut/value用于指定切入点表达式,而throwing属性值用于指定一个形参名来表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常 |
@After | 用于定义最终final通知,不管是否异常,该通知都会执行。使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点 |
@DeclareParents | 用于定义引介通知,相当于IntroductionInterceptor |
实例步骤如下:
(1)在src下创建com.cn.aspectj.annotation包,将上一个MyAspect复制到该包下,进行修改,代码如下:
package com.cn.aspectj.annotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MyAspect {
//定义切入点表达式
@Pointcut("execution(* com.cn.jdk.*.*(..))")
//使用一个返回值为void、方法体为空的方法来命名切入点
private void myPointCut() {}
//前置通知
@Before("myPointCut()")
public void myBefore(JoinPoint joinPoint) {
System.out.print("前置通知:模拟执行权限检查...,");
System.out.print("目标类是:"+joinPoint.getTarget());
System.out.println(",被植入增强处理的目标方法为:"+joinPoint.getSignature().getName());
}
//后置通知
@AfterReturning("myPointCut()")
public void myAfterReturning(JoinPoint joinPoint) {
System.out.print("后置通知:模拟记录日志...,");
System.out.println("被植入增强处理的目标方法为:"+joinPoint.getSignature().getName());
}
//环绕通知
@Around("myPointCut()")
public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
//开始
System.out.println("环绕开始:执行目标方法之前,模拟开启事务...");
//执行当前目标方法
Object obj = proceedingJoinPoint.proceed();
//结束
System.out.println("环绕结束:执行目标方法后,模拟关闭事务...");
return obj;
}
//异常通知
@AfterThrowing(value="myPointCut()",throwing="e")
public void myAfterThrowing(JoinPoint joinPoint,Throwable e) {
System.out.println("异常通知:"+"出错了"+e.getMessage());
}
//最终通知
@After("myPointCut()")
public void myAfter() {
System.out.println("最终通知:模拟方法结束后的释放资源...");
}
}
上述代码中,首先使用@Aspect注解定义了切面类,由于该类在Spring中是作为组件使用的,所以还需要添加@Component注解才能生效。然后使用了@Pointcut注解来配置切入点表达式,并通过定义方法来表示切入点名称。接下来在每个通知相应的方法上添加了相应的注解,并将切入点名称"myPointCut"作为参数传递给需要执行增强的通知方法。如果需要其他参数(如异常通知的异常参数),可以根据代码提示传递相应的属性值。
(2)在目标类com.cn.jdk.UserDaoImpl中,添加注解@Repository("userDao")。代码如下:
package com.cn.jdk;
import org.springframework.stereotype.Repository;
//目标类
@Repository("userDao")
public class UserDaoImpl implements UserDao{
@Override
public void addUser() {
System.out.println("添加用户");
}
@Override
public void deleteUser() {
System.out.println("删除用户");
}
}
(3)在com.cn.aspectj.annotation包下,创建配置文件applicationContext.xml,并对该文件进行编辑,代码如下:
<?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"
xmlns:context="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-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<!-- 指定需要扫描的包,使注解生效 -->
<context:component-scan base-package="com.cn"/>
<!-- 启动基于注解的声明式AspectJ支持 -->
<aop:aspectj-autoproxy/>
</beans>
(4)在com.cn.aspectj.annotation包中,创建测试类TestAnnotation,代码如下:
package com.cn.aspectj.annotation;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.cn.jdk.UserDao;
public class TestXmlAspectj {
public static void main(String args[]) {
ApplicationContext appc = new ClassPathXmlApplicationContext("com/cn/aspectj/annotation/applicationContext.xml");
UserDao userDao = (UserDao)appc.getBean("userDao");
userDao.addUser();
}
}
正常与异常输入运行结果如下:
注意:如果在同一个连接点有多个通知需要执行,name在同一切面中,目标方法之前的前置通知和环绕通知的执行顺序是未知的,目标方法之后的后置通知和环绕通知的执行顺序也是未知的。