使用Spring进行切面(AOP)编程
一、 AOP理论
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程(也叫面向方面),可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
u 横切性关注点
对哪些方法进行拦截,拦截该方法后怎么处理,这些关注就称之为横切面关注点。
u Aspect(切面)
指横切性的关注点的抽象即为切面,它与类相似,只是两者的关注点不一样,类是对物体特征的抽象,而切面是对横切性关注点的抽象。
u Joinpoint(连接点)
所谓连接点就是那些被拦截的点(也就是需要被拦截的类的方法)。在Spring中,这些点指的只能是方法,因为Spring只支持方法类型的拦截,实际上Joinpoint还可以拦截field或类构造器。
u Pointcut(切入点)
所谓切入点是指那些我们对Jointpoint(连接点)进行拦截的定义,也就是在切面中定义的方法,并声明该方法拦截了Jointpoint(连接点)。
u Advice(通知)
所谓通知就是指切入点拦截到Jointpoint(连接点)之后要做的事情。通知分为前置通知(@Before)、后置通知(@AfterReturning)、异常通知(@AfterThrowing)、最终通知(@After)和环绕通知(@Around)。
u Target(目标对象)
代理的目标对象。
u Weave(织入)
指将aspects应用到target对象并导致proxy对象创建的过程称为织入。
u Introduction(引入)
在不修改类代码的前提下,introduction可以在运行期为类动态的添加一些方法或Field(字段)。
二、 AOP编程的方式
要使用AOP进行编程,我们需要在Spring的配置文件(比如名为applicationContext.xml)中先引入AOP的命名空间,配置文件的引入内容如下面红色字体:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd "> |
Spring的提供了两种AOP编程方式,我们可以选择其中的任何一种来进行AOP编程。
1. 基于注解方式的AOP编程
2. 基于XML配置方式的AOP编程
三、 实例演示AOP编程
这里通过演示AspectBean切面来拦截StudentMgr的update方法(连接点)来描述切面编程具体是如何工作的。先以注解的方式来说明,后面会给出相应的xml配置信息。
1. 搭建AOP编程环境
AOP环境的搭建参考文章 利用Maven搭建Spring开发环境 。
2. 开启注解方式
在Spring的配置文件中增加下面的一个bean以开启Spring的注解编程
<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" /> |
3. 定义IstudentMgr接口及bean类StudentMgr,代码如下
IstudentMgr接口Java代码:
- package com.trs.components.mgr;
- public interface IStudentMgr {
- public void save(String _sStudentName) throws Exception;
- public void update(String _sStudentName) throws Exception;
- }
package com.trs.components.mgr;
public interface IStudentMgr {
public void save(String _sStudentName) throws Exception;
public void update(String _sStudentName) throws Exception;
}
StudentMgr的bean类java代码:
- package com.trs.components.mgr;
- public class StudentMgr implements IStudentMgr {
- public void save(String _sStudentName) throws Exception {
- System.out.println("保存了学生对象..");
- }
- public void update(String _sStudentName)throws Exception{
- System.out.println("更新了学生对象..");
- }
- }
package com.trs.components.mgr;
public class StudentMgr implements IStudentMgr {
public void save(String _sStudentName) throws Exception {
System.out.println("保存了学生对象..");
}
public void update(String _sStudentName)throws Exception{
System.out.println("更新了学生对象..");
}
}
4. 编写切面类AspectBean,代码如下
- package com.trs.components.aop;
- import org.aspectj.lang.JoinPoint;
- import org.aspectj.lang.ProceedingJoinPoint;
- import org.aspectj.lang.annotation.After;
- import org.aspectj.lang.annotation.AfterThrowing;
- import org.aspectj.lang.annotation.Around;
- import org.aspectj.lang.annotation.Aspect;
- import org.aspectj.lang.annotation.Before;
- /**
- * 拦截StudentMgr的切面
- *
- *
- */
- @Aspect
- // 声明一个切面
- public class AspectBean {
- @Before("execution (* com.trs.components.mgr.StudentMgr.update*(..))")
- // 声明前置通知,拦截StudentMgr的所有以update开头的方法
- public void validateUser(JoinPoint pjp) throws Throwable {
- System.out.println("执行用户验证!");
- }
- @Before("execution (* com.trs.components.mgr.StudentMgr.update*(..))")
- // 声明前置通知,拦截StudentMgr的所有以update开头的方法
- public void beginTransaction(JoinPoint _jp) throws Throwable {
- // 如果需要对连接点传入的参数进行处理,可以通过代码获取:_jp.getArgs();
- System.out.println("开始事务");
- }
- @After("execution (* com.trs.components.mgr.StudentMgr.update*(..))")
- // 声明最终通知,拦截StudentMgr的所有以update开头的方法
- public void endTransaction() throws Throwable {
- System.out.println("结束事务");
- }
- @After("execution (* com.trs.components.mgr.StudentMgr.update*(..))")
- // 声明最终通知,拦截StudentMgr的所有以update开头的方法
- public void writeLogInfo() {
- System.out.println("记录日志信息");
- }
- @AfterThrowing(pointcut = "execution (* com.trs.components.mgr.StudentMgr.update*(..))", throwing = "ex")
- // 声明异常,StudentMgr类的update方法出现异常时执行
- public void printException(Exception ex) {
- System.out.println("执行update方法时发生错误" + ex.getMessage());
- }
- @Around("execution (* com.trs.components.mgr.StudentMgr.update*(..))")
- // 声明环绕通知,拦截StudentMgr的所有以update开头的方法
- public Object doSurround(ProceedingJoinPoint _pjp)throws Throwable {
- // 如果需要对连接点传入的参数进行处理,可以通过代码获取:pjp.getArgs();
- Object result;
- if (true) {
- // 这里可以根据条件决定是否进入这个if
- System.out.println("进入环绕通知...");
- result = _pjp.proceed();// 使用环绕通知时,这个方法必须要执行,否则被拦截的方法就不会再执行了
- System.out.println("退出环绕通知...");
- }
- // 返回执行后的返回值
- return result;
- }
- }
package com.trs.components.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
/**
* 拦截StudentMgr的切面
*
*
*/
@Aspect
// 声明一个切面
public class AspectBean {
@Before("execution (* com.trs.components.mgr.StudentMgr.update*(..))")
// 声明前置通知,拦截StudentMgr的所有以update开头的方法
public void validateUser(JoinPoint pjp) throws Throwable {
System.out.println("执行用户验证!");
}
@Before("execution (* com.trs.components.mgr.StudentMgr.update*(..))")
// 声明前置通知,拦截StudentMgr的所有以update开头的方法
public void beginTransaction(JoinPoint _jp) throws Throwable {
// 如果需要对连接点传入的参数进行处理,可以通过代码获取:_jp.getArgs();
System.out.println("开始事务");
}
@After("execution (* com.trs.components.mgr.StudentMgr.update*(..))")
// 声明最终通知,拦截StudentMgr的所有以update开头的方法
public void endTransaction() throws Throwable {
System.out.println("结束事务");
}
@After("execution (* com.trs.components.mgr.StudentMgr.update*(..))")
// 声明最终通知,拦截StudentMgr的所有以update开头的方法
public void writeLogInfo() {
System.out.println("记录日志信息");
}
@AfterThrowing(pointcut = "execution (* com.trs.components.mgr.StudentMgr.update*(..))", throwing = "ex")
// 声明异常,StudentMgr类的update方法出现异常时执行
public void printException(Exception ex) {
System.out.println("执行update方法时发生错误" + ex.getMessage());
}
@Around("execution (* com.trs.components.mgr.StudentMgr.update*(..))")
// 声明环绕通知,拦截StudentMgr的所有以update开头的方法
public Object doSurround(ProceedingJoinPoint _pjp)throws Throwable {
// 如果需要对连接点传入的参数进行处理,可以通过代码获取:pjp.getArgs();
Object result;
if (true) {
// 这里可以根据条件决定是否进入这个if
System.out.println("进入环绕通知...");
result = _pjp.proceed();// 使用环绕通知时,这个方法必须要执行,否则被拦截的方法就不会再执行了
System.out.println("退出环绕通知...");
}
// 返回执行后的返回值
return result;
}
}
说明:
[1] @Before("execution (* com.trs.components.mgr.StudentMgr.update*(..))") 第一个*号表示匹配方法的返回值可以使任何类型;第二个*号表示所有以update开头的方法都会被拦截;(..)表示这些方法输入的参数可以0或者多个。
[2] @AfterThrowing中的 throwing = "ex",表示方法出错的时候会返回一个名称为ex的异常,在切入点中通过参数ex即可获取到出错的异常对象。
[3] 环绕方法doSurround的写法需要注意
l 环绕方法中的第一个参数类型必须是ProceedingJoinPoint类型(环绕的方法名可以随便起名)。
l 环绕方法抛的异常需要时Throwable类型
l 环绕方法中需要执行_pjp.proceed()方法,否则被拦截的方法不会执行
l 在_pjp.proceed()方法前后各加一个时间点,可以用来测试被拦截的方法的执行时间
5. 切面类的一些bean配置
在Spring的配置文件中加入如下代码,配置相关的bean类。
<?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:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd ">
<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" /> <bean id="aspectBean" class="com.trs.components.aop.AspectBean"></bean> <bean id="IStudentMgr" class="com.trs.components.mgr.StudentMgr"></bean> </beans> |
6. 编写测试用例
利用Junit编写测试StudentMgr.update方法的测试用例,测试用例相关方法代码如下:
@Test public void testUpdate() throws Exception { // 使用ApplicationContext来初始化系统 ApplicationContext context = new ClassPathXmlApplicationContext( "applicationContext.xml"); IStudentMgr studentMgr = (IStudentMgr) context.getBean("IStudentMgr"); // studentMgr.saveOne("wuguowei"); System.out.println("-----------"); studentMgr.update("wuguowei"); } |
7. 执行测试用例,查看打印结果
开始事务 执行用户验证! 进入环绕通知... 更新了学生对象.. 结束事务 记录日志信息 退出环绕通知... |
8. 实例对应的XML配置的AOP编程
上面我们给出的是注解风格的AOP编程,上面实例的编程同样可以采用XML配置方式的AOP编程,我们只需要在Spring的配置文件中添加如下代码即可(AOP的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:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd "> <aop:config> <aop:aspect id="asp_bean_id" ref="aspectBean"> <aop:pointcut id="mypointcut" expression="execution (* com.trs.components.mgr.StudentMgr.update*(..))"/> <aop:before pointcut-ref="mypointcut" method="validateUser"/> <aop:before pointcut-ref="mypointcut" method="beginTransaction"/> <aop:after pointcut-ref="mypointcut" method="endTransaction"/> <aop:after pointcut-ref="mypointcut" method="writeLogInfo"/> <aop:after-throwing pointcut-ref="mypointcut" method="printException"/> <aop:around pointcut-ref="mypointcut" method="doSurround"/> </aop:aspect> </aop:config>
<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" /> <bean id="AnotherAspect" class="com.trs.components.aop.AnotherAspect"></bean> <bean id="aspectBean" class="com.trs.components.aop.AspectBean"></bean> <bean id="IStudentMgr" class="com.trs.components.mgr.StudentMgr"></bean> </beans> |
四、 常见问题
1. 同一个切面中的通知的执行顺序如何确定?
当定义在同一个切面中多个通知想在同一个相同的连接点中运行的话,这些通知的执行顺序是未知的(查询后原因是因为没有方法能通过反射javac生成的类来获取方法的声明顺序)。如果我们出现这种情况,需要把这些方法压缩到一个通知中,或者把这些方法抽象到不同的切面中,通过切面的优先级来指定执行顺序。
2. 不同切面中的通知的执行顺序如何确定?
不同切面中的通知的执行顺序顺序是根据切面的优先级来确定的,我们在切面中需要实现“org.springframework.core.Ordered”接口,通过实现接口的getOrder()方法来确定优先级,返回的数字越小,优先级越高,优先级越高,切面中的通知则优先执行。对于前置通知,优先级越高,则优先执行;对于后置通知,优先级越高,则越后执行。
3. 通过new产生的实例也会触发通知吗?
通过new方法产生的实例是不会触发切面通知的,比如上面的实例中,我们通过下面的代码来调用update方法是不会触发通知的。
StudentMgr aStudentMgr = new StudentMgr(); aStudentMgr.update(“wuguowei”);//new实例对象是不会触发切面通知的。 |
4. ApplicationContext获得的bean可以不是接口吗?
当我们在通过Spring的ApplicationContext来获取bean的时候,返回的对象可以是接口或者接口的实现,如果一个实现类继承了接口,但是把获取到的类转化为了实现类,则会报类转化的错误。比如我们执行下面的代码
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); StudentMgr studentMgr = (StudentMgr) context.getBean("IStudentMgr");//这里会报错,StudentMgr实现了接口,这里转化为接口的话则不会报错 |
则会报下面的转化错误,信息如下:
java.lang.ClassCastException: $Proxy9 cannot be cast to com.trs.components.mgr.StudentMgr at com.trs.components.mgr.StudentMgrTest.testUpdate(StudentMgrTest.java:29) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) |
5. BeanFactory和ApplicationContext的区别
l BeanFactory主要负责配置和管理bean对象及其加载
l ApplicationContext是BeanFactory的扩展类,包含了BeanFactory的所有功能,同时增加了更多支持企业核心内容的功能,比如AOP
l BeanFactory为延迟加载,而ApplicationContext为初始化的时候就把所有的bean加载进来。这样ApplicationContext能够更早的发现程序中的Bug,而BeanFactory只有在使用的时候才能发现程序中的Bug。
6. 切入点表达式的说明
Spring AOP中我们经常使用execution pointcut designator来定义切入点,表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
除了返回类型模式(上面代码片断中的ret-type-pattern),名字模式和参数模式以外,所有的部分都是可选的。 返回类型模式决定了方法的返回类型必须依次匹配一个连接点。 你会使用的最频繁的返回类型模式是 *,它代表了匹配任意的返回类型。 一个全称限定的类型名将只会匹配返回给定类型的方法。名字模式匹配的是方法名。 你可以使用 * 通配符作为所有或者部分命名模式。 参数模式稍微有点复杂:() 匹配了一个不接受任何参数的方法, 而 (..) 匹配了一个接受任意数量参数的方法(零或者更多)。 模式 (*) 匹配了一个接受一个任何类型的参数的方法。 模式 (*,String) 匹配了一个接受两个参数的方法,第一个可以是任意类型,第二个则必须是String类型。 请参见AspectJ编程指南的Language Semantics 部分。
u 任意公共方法的执行:
execution(public * *(..))
u 任何一个以“set”开始的方法的执行:
execution(* set*(..))
u AccountService 接口的任意方法的执行:
execution(* com.xyz.service.AccountService.*(..))
u 定义在service包里的任意方法的执行:
execution(* com.xyz.service.*.*(..))