四、Spring的AOP
AOP实现可分为两类(按AOP框架修改源代码的时机):
- 静态AOP实现:AOP框架在编译阶段对程序进行修改,即实现对目标类的增强,生成静态的AOP代理类(生成的*.class文件已经被改掉了,需要使用特定的编辑器)。以AspectJ为代表。
- 动态AOP实现:AOP框架在运行阶段动态生成AOP代理(在内存中以JDK动态代理或cglib动态代理生成AOP代理类)。以实现对目标类的增强。以Spring AOP为代表。
AOP的基本概念:
AOP框架具有如下两个特征:
- 个步骤之间的良好隔离性。
- 源代码无关性。
关于面向切面编程的一些术语:
- 切面(Aspect):切面用于组织多个Advice,Advice放在切面中定义。
- 连接点(Joinpoint):程序执行过程中明确的点,如方法的调用,或者异常的抛出。在Spring AOP中,连接点总是方法的调用。
- 增强处理(Advice):AOP框架在特定的点执行的增强处理。处理有“before”,“around”,“after”等。
- 切入点(Pointcut):可以插入增强处理的连接点。简而言之,当某个连接点满足指定要求时,该连接点将被添加增强处理,该连接点也就变成了切入点。
pointcut xxxPointcut():execution(void H*.say*())
如何使用表达式定义切入点是AOP的核心,Spring默认使用AspectJ切入点语法:
- 引入:将方法或字段添加到被处理的类中。Spring允许将新的接口引入到任何被处理的对象中。例如,你可以使用一个引入,使任何对象实现IsModified接口,以此来简化缓存。
- 目标对象:被AOP框架进行增强处理的对象,也被称为增强的对象。如果AOP框架采用的是动态AOP实现,那么该对象就是一个被代理的对象。
- AOP代理:AOP框架创建的对象,代理就是对目标对象的增强。Spring中的AOP代理可以是JDK动态代理,也可以是cglib代理。前者为实现接口的目标对象的代理,后者不实现接口的目标对象的代理。
- 织入(Weaving):将增强处理添加到目标对象中,并创建一个被增强的对象(AOP代理)的过程就是织入。植入有两种实现方式---编译时增强(如AspectJ)和运行时增强(如Spring AOP)。Spring和其他纯Java AOP框架一样,在运行时完成织入。
Spring的AOP支持:
Spring中的AOP代理由Spring的IOC容器负责生成、管理,其依赖关系也由IOC容器负责管理。AOP代理可以直接使用容器中的其他Bean实例作为目标,这种关系可以由IOC容器的依赖注入提供。Spring默认使用Java动态代理来创建AOP代理。Spring目前仅支持将方法调用作为连接点(Joinpoint),如果需要把对成员变量的访问和更新也作为增强处理的连接点,则可以考虑使用AspectJ。Spring侧重于AOP实现和IOC容器之间的整合,用于帮助解决企业级开发中常见问题。Spring AOP采用基于代理的AOP实现方案,而AspectJ则采用编译时增强的解决方案。
AOP编程中需要程序员参与的只有三个部分:
- 第一普通业务组件。
- 定义切入点,一个切入点可以横切多个业务组件。
- 定义增强处理,增强处理就是在AOP框架为普通业务组织织入的处理动作。
AOP代理方法=增强处理+目标对象的方法
Spring有如下两种选择来定义切入点和增强处理:
- 基于注解的“零配置”方式:使用@Aspect、@Pointcut等注解来标注切入点和增强处理。
- 基于XML配置文件的管理方式:使用Spring配置文件来定义切入点和增强处理。
基于注解的“零配置”方式:
Spring依然采用运行时生成动态代理的方式来增强目标对象,所以它不需要增加额外的编译,也不需要AspectJ的织入器支持;而AspectJ采用编译时增强,所以AspectJ需要自己的编译器来编译Java文件,还需要织入器。
Spring中启用对@AspectJ切面配置的支持:
<?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:p="http://www.springframework.org/schema/p"
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/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<!-- 启动@AspectJ支持 -->
<aop:aspectj-autoproxy/>
</beans>
以及
<!-- 启动@AspectJ支持 -->
<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator"/>
Spring应用中启动@AspectJ支持还需要在应用的类加载路径下添加AspectJ库:
- aspectjweaver.jar
- aspectjrt.jar
以上两个jar文件直接在AspectJ安装目录的lib中获取,Spring AOP还需要依赖aopalliance.jar。
1、定义切面:
当启动了@AspectJ支持后,只要在Spring容器中配置一个带@AspectJ注解的Bean,Spring将会自动识别该Bean,并将该Bean作为切面处理。
//使用@AspectJ定义一个切面类
@Aspect
public class LogAspect{
//定义该类的其他类容
.......
}
当使用@AspectJ来修饰一个Java类之后,Spring不会把该Bean当成组件Bea处理,因此负责增强后处理的Bean将会略过该Bean,不会对该Bean进行任何增强处理。
2、定义Before增强处理:
在一个切面类里使用@Before来修饰一个方法时,该方法将作为Before增强处理。使用@Before修饰时,通常需要指定一个value属性,该属性指定一个切入点表达式(既可以是一个已有的切入点,也可以直接定义切入点表达式),用于指定该增强处理将被织入哪些切入点。
AuthAspect.java
package entity;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class AuthAspect {
//匹配entity包下所有的类的所有方法作为切入点
@Before("execution(* entity.*.*(..))")
public void authority(){
System.out.println("模拟执行权限检查!");
}
}
HelloImpl.java
package entity;
import org.springframework.stereotype.Component;
import inter.Hello;
@Component("hello")
public class HelloImpl implements Hello{
//定义一个简单方法,模拟应用中的业务逻辑方法
public void foo() {
System.out.println("执行Hello组件的foo()方法");
}
//定义一个addUser()方法,模拟应用中添加用户的方法
public void addUser(String name,String pass){
System.out.println("执行Hello组件的addUser添加用户:"+name);
}
}
beans.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:p="http://www.springframework.org/schema/p"
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/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<!-- 启动@AspectJ支持 -->
<aop:aspectj-autoproxy/>
<!-- 指定自动搜索Bean文件,自动搜索切面文件 -->
<context:component-scan base-package="entity">
<context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/>
</context:component-scan>
</beans>
AspectjTest.java
package test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import entity.HelloImpl;
public class AspecjTest {
public static void main(String[] args) {
ApplicationContext ctx=new ClassPathXmlApplicationContext("beans.xml");
HelloImpl hello=(HelloImpl) ctx.getBean("hello");
hello.foo();
hello.addUser("张三", "123456");
}
}
输出:
模拟执行权限检查!
执行Hello组件的foo()方法
模拟执行权限检查!
执行Hello组件的addUser添加用户:张三
使用Before增强处理只能在目标方法执行之前织入增强,如果Before增强处理没有特殊处理,目标方法总会自动执行,如果Before处理需要阻止目标方法的执行,可通过抛出一个异常来实现。Before增强处理时,目标方法还未获得执行的机会,所以Before增强处理无法访问目标方法的返回值。
3、定义AfterReturning增强处理:
AfterReturning增强处理将在目标方法正常完成后被织入。
@AfterReturning注解可指定如下两个常用属性:
- pointcut/value:它们都用于指定该切入点对应的切入点表达式。一样即可是一个已有的切入点,也可以直接定义切入点表达式。当指定了pointcut属性值后,value属性将被覆盖。
- returning:该属性指定一个形参名,用于表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法的返回值。除此之外,在Advice方法中定义该形参(代表目标方法的返回值)时指定的类型,会限制目标方法必须返回指定类型的值或没有返回值。
//定义一个切面
@Aspect
public class LogAspect{
//匹配entity包下所有类的所有方法的执行作为切入点
@AfterReturning(returning="rvt",pointcut="execution(* entity.*.*())")
//声明rvt时指定的类型会限制目标方法必须返回指定类型的值或没有返回值
//此处将rvt的类型声明为Object,意味着对目标方法的返回值不加限制
public void log(Object rvt){
System.out.println("获得目标方法返回值"+rvt);
System.out.println("模拟记录日志功能....");
}
}
@AfterReturning注解的returning属性所指定的形参名对应于增强处理中的一个形参名,当目标方法执行返回后,返回值作为相应的参数值传入增强处理方法。使用returning属性还有 一个额外的作用:它可用于限定切入点只匹配具有对应返回值类型的方法。
4、定义AfterThrowing增强处理:
AfterThrowing增强处理主要用于处理程序中未处理的异常。
使用@AfterThrowing注解时可指定如下两个常用属性:
- pointcut/value:都用于指定该切入点对应的切入表达式。
- throwing:该属性值指定一个形参名,用于表示Advice方法中可定义与此同名的形参,该形参用于访问目标方法抛出的异常。在Advice方法中定义该形参(代表目标方法抛出的异常)时指定的类型,会限制目标方法必须抛出指定类型的异常。
//定义一个切面
@Aspect
public class RepairAspect{
//匹配entity包下所有类的所有方法的执行作为切入点
@AfterThrowing(throwing="ex",pointcut="execution(* entity.*.*())")
//声明ex时指定的类型会限制目标方法必须抛出指定的类型的异常
//此处将ex的类型声明为Throwable,意味着对目标方法抛出的异常不加限制
public void doRecoveryActions(Throwable ex){
System.out.println("目标方法中抛出的异常"+ex);
System.out.println("模拟Advice对异常的修复....");
}
}
使用throwing属性还用一个额外的作用:它可用于限定切入点只匹配指定类型的异常。
5、After增强处理:
- AfterReturning增强处理只有在目标方法成功完成后才会被织入。
- After增强处理不管目标方法如何结束(包括成功完成和遇到异常终止两种情况),他都会被织入。
After增强处理必须准备处理正常返回或异常返回两种情况,这种处理通常用于释放资源。使用@After注解修饰一个方法,即可将该方法转成After增强处理。使用@After注解时需要指定一个value属性,该属性值用于指定该增强处理被织入的切入点。
//定义一个切面
@Aspect
public class ReleaseAspect{
//匹配entity包下所有的类的所有方法的执行作为切入点
@After("execution(* entity.*.*())")
public void release(){
System.out.println("模拟方法结束后的释放资源...");
}
}
After增强处理的作用非常类似于异常处理中的finally块的作用。
6、Around增强处理:
@Around注解用于修饰Around增强处理,Around增强处理是功能较强大得增强处理,它近似等于Before和AfterReturning增强处理的总和,Around增强处理即可在执行目标方法之前织入增强动作,也可以在执行目标方法之后织入增强处理。
Around增强处理可以决定目标方法在什么时候执行,如何执行,甚至可以完全阻止目标方法的执行。Around增强处理的功能虽然强大,但通常需要在线程安全的环境下使用。如果需要目标方法执行之前和执行之后共享某种状态数据,则该考虑使用Around增强处理;尤其是需要改变目标方法的返回值时,则只能使用Around增强处理。
当定义一个Around增强处理方法时,该方法的第一个形参必须是ProceedingJoinPoint类型(至少包含一个形参),在增强处理方法体内,调用ProceedingJoinPoint参数的proceed()方法才会执行目标方法---这就是Around增强处理可以完全控制目标方法的执行时机、如何执行的关键;如果程序没有调用proceedingJoinPoint参数的proceed()方法,则目标方法不会被执行。
使用Around增强处理可以取得目标方法最大的控制权,既可以完全控制目标方法的执行,一刻改变执行目标方法的参数,还可改变目标方法的返回值。
7、访问目标方法的参数:
访问目标方法最简单的做法是定义增强处理方法时将第一个参数定义为JoinPoint类型,当该增强处理方法被调用时,该JoinPoint参数就代表了织入增强的连接点。JoinPoint里包含如下几个常用的方法:
- Object[] getArgs():返回执行目标方法时的参数。
- Signature getSignature():返回被增强的方法的相关信息。
- Object getTarget():返回被增强处理的目标对象。
- Object getThis():返回AOP框架为目标对象生成的代理对象。
当使用Around增强处理是,需要将第一个参数定义为ProceedingJoionPoint类型,该类型是JoinPoint类型的子类。
Spring AOP 采用和AspectJ一样的优先顺序来织入增强处理:在“进入连接点时,具有最高优先级的增强处理将先被织入。在“退出”连接点时,具有最高优先级的增强处理会最后被织入。
当不同切面的两个增强处理需要在同一个连接点被织入时,Spring AOP将以随机的顺序来织入这两个增强处理。如果应用需要指定不同切面类里增强处理的优先级,Spring提供了如下两种解决方案:
- 让切面实现org.springframework.core.Ordered接口,实现该接口只需要实现一个int getOrder()方法,该方法的返回值越小,优先级越高。
- 直接使用@Order注解来修饰一个切面类,使用@Order注解时可指定一个int型的value属性,该值越小,则优先级越高。
8、定义切入点:
定义切入点,其实质就是为一个切入点表达式起一个名称,从而允许在多个增强处理中重用该名称。Spring AOP只支持将Spring Bean的方法执行作为连接点,所以可以吧切入点看成所有能和切入点表达式匹配的Bean方法,切入点定义包含两个部分:
- 一个切入点表达式。
- 一个包含名字和任意参数的方法名。
其中切入点表达式用于指定该切入点和哪些方法进行匹配,包含名字和任意参数的方法签名将作为该切入点的名称。
//使用@Pointcut注解定义切入点
@Pointcut("execution(* transfer(..))")
//使用一个返回值为void、方法体为空的方法来命名切入点
private void anyOldTransfer(){...}
如果需要使用本切面中的切入点,则可在使用@Before、@After、@Around等注解来定义Advice时,使用pointcut或value属性值引入已有的切入点。
@AfterReturning(pointcut="myPointcut()",returning="retVal")
public void writeLog(String msg,Object retVal){...}
使用其他切面类中的切入点时,应该使用切面类作为前缀来限制切入点。
9、切入点指示符:
- execution:用于匹配执行方法的连接点,这是Spring AOP中最主要的切入点指示符。
- within:用于限定匹配特定类型的连接点,当使用Spring AOP的时候,只能匹配方法执行的连接点。
//在entity包中的任意连接点(在Spring AOP中只是方法执行的连接点)
within(entity.*)
//在entity包或子包中的任意连接点(在Spring AOP中只是方法执行的连接点)
within(entity..*)
- this:用于限定AOP代理必须必须是指定类型的实例,匹配该对象的所有连接点。当使用Spring AOP的时候,只能匹配方法执行的连接点。
- target:用于限定目标对象必须是指定类型的实例,匹配该对象的所有连接点。当使用Spring AOP的时候,只能匹配方法执行的连接点。
- args:用于对连接点的参数类型进行限制,要求参数类型是指定类型的实例。当使用Spring AOP的时候,只能匹配方法执行的连接点。
- bean:用于限定只匹配指定Bean实例内的连接点,实际上只能使用方法执行作为连接点。定义表达式时需要传入Bean的id或name属性,表示值匹配该Bean实例内的连接点。支持使用“*”通配符。
//匹配tradeService Bean实例内方法执行的连接点
bean(tradeService)
//匹配名字以Service结尾的Bean实例内方法执行的连接点
bean(*Service)
10、组合切入点表达式:
- &&:要求连接点同时匹配两个切入点表达式。
- ||:只要求连接点匹配任意一个切入点表达式。
- !:要求切入点不匹配指定的切入点表达式。