AOP(Aspect Orient Programming):面向切面编程。
AOP和OOP(Object Orient Programming)互为补充,面向对象编程将程序分解成各个层次的对象,而面向切面编程将程序运行过程分解成各个切面。可以这样理解,面向对象编程是从静态角度考虑程序结构,面向切面编程是从动态角度考虑程序运行过程。
AOP从程序运行角度考虑程序的流程,提取业务处理过程的切面。AOP面向的是程序运行中各个步骤,希望以更好的方式来组合业务处理的各个步骤。
AOP并不与特定的代码耦合,AOP能处理程序执行中特定切入点(Pointcut),而不与具体类耦合。
AOP具有如下两个特性:
> 各步骤之间的良好隔离性
> 源代码无关性
下面是关于面向切面编程的一些术语:
> 切面(Aspect):业务流程运行的某个特定步骤,也就是应用运行过程的关注点,关注点可能横切多个对象,所以常常也称为横切关注点。
> 连接点(Joinpoint):程序执行过程中明确的点,如方法的调用,或者异常的抛出。Spring AOP 中,连接点总是方法的调用。
> 增强处理(Adive):AOP在特定的切入点执行的增强处理。处理有“around”、“before”和“after”等类型。
> 切入点(Pointcut):可以插入增强处理的连接点。简而言之,当某个连接点满足指定要求时,该连接点将被添加增强处理,该连接点也就变成了切入点。
> 引入:将方法或字段添加到被处理的类中。Spring允许引入新的接口到任何被处理的对象。例如,使用一个引入,使任何对象实现IsModified接口,依次来简化缓存。
> 目标对象:被AOP进行增强处理的对象,也被成为被增强的对象。如果AOP是通过运行时代理来实现的,那么这个对象将是一个被代理的对象。
> AOP代理:AOP创建的对象,简单地说,代理就是对目标对象的加强。Spring中的AOP代理可以是JDK动态代理,也可以是CGLIB代理。前者为实现接口的目标对象的代理,后者为不实现接口的目标对象的代理。
> 织入(Weaving):将增强处理添加到目标对象中,并创建一个被增强的对象(AOP代理)的过程就是织入。织入有两种实现方式:编译时增强(例如AspectJ)和运行时增强(例如CGLIB)。Spring和其他纯JavaAOP框架一样,在运行时完成织入。
现在通常使用AspectJ方式来定义切入点和增强处理,在这种方式下,Spring依然有如下两种选择来定义切入点和增强处理:
> 基于Annotation的“零配置”方式:使用@Aspect、@Point和Annotation来标注切入点和增强处理。
> 基于XML配置文件的管理方式:使用Spring配置文件来定义切入点和增强处理。
需要导入的包:aspectjrt.jar、aspectjweaver.jar and aopalliance-1.0
一、基于Annotation的“零配置”方式
Aspect允许使用Annotation用来定义切面、切入点和增强处理,而Spring框架则可识别并根据这些Annotation来生成AOP代理。Spring只是使用了和AspectJ 一样的注释,但并没有使用AspectJ的编译器或者织入器,底层依然使用的是Spring AOP ,依然是在运行时动态生成AOP代理,并不依赖于AspectJ的编译器或者织入器。
1、定义Before增强处理
package Advice;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
//定义一个切面
@Aspect
public class BeforeAdviceTest {
//配置Aop包下所有类的、
//所有方法的执行作为切入点
@Before("execution(* AOP.*.*(..))")
public void authority(){
System.out.println("模拟执行权限检查before.");
}
}
package AOP;
public interface Person {
public String sayHello(String name);
}
package AOP;
import java.util.Date;
import org.springframework.stereotype.Component;
@Component
public class Chinese implements Person {
@Override
public String sayHello(String name) {
// TODO Auto-generated method stub
System.out.println("sayHello方法开始被执行!");
return name+" Hello!Spring AOP.";
}
//定义一个eat()方法
public void eat(String food){
System.out.println("我正在吃"+food);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
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-4.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd"
>
<!-- 指定自动搜索Bean组件、自动搜索切面类 -->
<context:component-scan base-package="AOP,Advice">
<context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/>
</context:component-scan>
<!-- 启动@AspectJ支持 -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
package AOP;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("beanAOP.xml");
Chinese p = ctx.getBean("chinese",Chinese.class);
System.out.println(p.sayHello("张三"));
p.eat("西瓜");
}
}
2、定义AfterReturning增强处理
AfterReturning增强处理将在目标方法正常完成后被织入。
package Advice;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class AfterReturningAdviceTest {
//配置AOP包下所有类的、
//所有方法的执行作为切入点
@AfterReturning(returning="rvt",pointcut="execution(* AOP.*.*(..))")
public void log(Object rvt){ //形参rvt的类型可用于限定切入点只匹配具有对应返回值类型的方法
System.out.println("AfterReturning获取目标方法返回值:"+rvt);
System.out.println("AfterReturning模拟记录日志功能..");
}
}
PS:使用returning属性还有一个额外的作用:它可用于限定切入点只匹配具有对应返回值类型的方法——假如在上面的log() 方法中定义rvt 形参的类型是String,则该切入点职匹配该包下返回值类型为String的所有方法。
3、定义AfterThrowing增强处理
AfterThrowing增强处理主要用于处理程序中未处理的异常。
package Advice;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class AfterThrowingAdviceTest {
//配置AOP包下所有类的、
//所有方法的执行作为切入点
@AfterThrowing(throwing="ex",pointcut="execution(* AOP.*.*(..))")
public void doRecoveryActions(Throwable ex){
System.out.println("AfterThrowing目标方法中抛出的异常:"+ex);
System.out.println("AfterThrowing模拟抛出异常后的增强处理...");
}
}
Chinese类增加方法
public void divide(){
try{
System.out.println("divide执行开始!");
new java.io.FileInputStream("a.txt");
}catch(Exception ex){
System.out.println("目标类的异常处理:"+ex.getMessage());
}
int a = 5/0;
System.out.println("divide执行完成!");
}
测试代码增加
p.divide();
结果:
PS:使用throwing属性还有一个额外的作用:它可用于限定切入点只匹配指定类型的异常——假如在上面的doRecoveryActions() 方法中定义了ex形参的类型是NullPointerException,则该切入点只匹配抛出NullPointerException异常的情况。上面的doRecoveryActions() 方法的ex形参类型是Throwable,这表明该切入点可匹配抛出任何异常的情况。
4、After增强处理
After增强处理与AfterReturning增强处理有点相似,但也有区别:
> AfterReturning 增强处理只有在目标方法成功完成后才会被织入
> After 增强处理不管目标方法如何结束(包括成功完成或遇到异常中止)都会被织入
package Advice;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class AfterAdviceTest {
//配置AOP包下所有类的、
//所有方法的执行作为切入点
@After("execution(* AOP.*.*(..))")
public void release(){
System.out.println("After模拟方法结束后的释放资源....");
}
}
测试代码不变,结果如下:
5、Around增强处理
Around增强处理是功能比较强大的增强处理,它近似等于Before增强处理和AfterReturning增强处理的总和,Around增强处理既可以在执行目标方法之前织入增强动作,也可以在执行目标方法之后织入增强动作。
与Before增强处理和AfterReturning增强处理不同的是,Around增强处理可以决定目标方法在什么时候执行,如何执行,甚至可以完全阻止目标方法的执行。
Around增强处理可以改变执行目标方法的参数值,也可改变执行目标方法之后的返回值。
Around增强处理的功能虽然强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before增强处理、AfterReturning增强处理就能解决的问题,就没有必要使用Around增强处理了。如果需要目标方法执行之前和之后共享某种状态数据,则考虑使用Around增强处理;尤其是需要使用增强处理阻止目标方法的执行,或需要改变目标方法返回值时,则只能使用Around增强处理了。
package Advice;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class AroundAdviceTest {
//配置AOP包下所有类的、
//所有方法的执行作为切入点
@Around("execution(* AOP.*.*(..))")
public Object processTx(ProceedingJoinPoint jp) throws java.lang.Throwable
{
System.out.println("Around执行目标方法之前,模拟开始事务.....");
//执行目标方法,并保存目标方法执行后的返回值
Object rvt = jp.proceed(new String[]{"被改变的参数"});
System.out.println("Around执行目标方法之后,模拟结束事务.....");
return rvt+" 新增的内容";
}
}
package AOP;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("beanAOP.xml");
Chinese p = ctx.getBean("chinese",Chinese.class);
System.out.println(p.sayHello("张三"));
p.eat("西瓜");
}
}
6、访问目标方法的参数
访问目标方法最简单的做法是定义增强处理方法时将第一个参数定义为JoinPoint类型,当该增强处理方法被调用时,该JoinPoint参数就代表了织入增强处理的连接点。
JoinPoint里包含了如下几个常用的方法:
> Object[] getArgs(): 返回执行目标方法时的参数。
> Signature getSignature():返回被增强的方法的相关信息。
> Object getTarget():返回被织入增强处理的目标对象。
> Object getThis():返回AOP框架为目标对象生成的代理对象。
PS:当使用Around处理时,我们需要将第一个参数定义为ProceedingJoinPoint类型,该类型是JoinPoint类型的子类。
package Advice;
import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect
public class FourAdviceTest {
@Around("execution(* AOP.*.*(..))")
public Object processTx(ProceedingJoinPoint jp) throws java.lang.Throwable{
System.out.println("Around增强:执行目标方法之前,模拟开始事务.");
//访问执行目标方法的参数
Object[] args=jp.getArgs();
//当执行目标方法的参数存在,且第一个参数时字符串参数
if(args!=null&&args.length>0&&args[0].getClass()==String.class){
//改变第一个目标方法的第一个参数
args[0]="被改变的参数";
}
//执行目标方法,并保存目标方法执行后的返回值
Object rvt = jp.proceed(args);
System.out.println("Around增强:执行目标方法之后,模拟事务结束.");
return rvt+" 新增的内容";
}
@Before("execution(* AOP.*.*(..))")
public void authority(JoinPoint jp){
System.out.println("Before增强:模拟执行权限检查..");
//返回被织入增强处理的目标方法
System.out.println("Before增强:被织入增强处理的目标方法为:"+jp.getSignature().getName());
//返回执行目标方法的参数
System.out.println("Before增强:目标方法的参数为:"+Arrays.toString(jp.getArgs()));
//返回被增强处理的目标对象
System.out.println("Before增强:被织入增强处理的目标对象为:"+jp.getTarget());
}
@AfterReturning(pointcut="execution(* AOP.*.*(..))",returning="rvt")
public void log(JoinPoint jp,Object rvt){
System.out.println("AfterReturning增强:获取目标方法返回值:"+rvt);
System.out.println("AfterReturning增强:模拟记录日志功能...");
//返回被织入增强处理的目标方法
System.out.println("AfterReturning增强:被织入增强处理的目标方法为:"+jp.getSignature().getName());
//返回执行目标方法的参数
System.out.println("AfterReturning增强:目标方法的参数为:"+Arrays.toString(jp.getArgs()));
//返回被增强处理的目标对象
System.out.println("AfterReturning增强:被织入增强处理的目标对象为:"+jp.getTarget());
}
@After("execution(* AOP.*.*(..))")
public void release(JoinPoint jp){
System.out.println("After增强:模拟方法结束后的释放资源...");
//返回被织入增强处理的目标方法
System.out.println("After增强:被织入增强处理的目标方法为:"+jp.getSignature().getName());
//返回执行目标方法的参数
System.out.println("After增强:目标方法的参数为:"+Arrays.toString(jp.getArgs()));
//返回被增强处理的目标对象
System.out.println("After增强:被织入增强处理的目标对象为:"+jp.getTarget());
}
}
Spring AOP采用和AspectJ 一样的优先顺序来织入增强处理:在“进入”连接点时,最高优先级的增强处理将先被织入(所以给定的两个Before处理中,优先级高的那个会先执行)。在“退出”连接点时,最高优先级的增强处理会最后被织入(所以给定的两个After处理中,优先级高的那个会后执行)。
当不同的切面里的两个增强处理需要再同一个连接点被织入时,Spring AOP 将以随机的顺序来织入这两个增强处理。如果应用需要指定不同切面类里增强处理的优先级,Spring提供了如下两种解决方案:
> 让切面类实现 org.springframework.core.Ordered 接口,实现该接口只需实现一个 int getOrder() 方法,该方法返回值越小,则优先级越高。
> 直接使用@Order Annotation 来修饰一个切面类,使用@Order Annotation 时可指定一个int 型的value 属性,该属性值越小,则优先级越高。
如果只要访问目标方法的参数,Spring还提供了一种更简单的方法:我们可以在程序中使用args 来绑定目标方法的参数。如果在一个args 表达式中指定了一个或多个参数,则该切入点将只匹配具有对应形参的方法,且目标方法的参数值将被传入增强处理方法。
package Advice;
import java.util.Date;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class AccessArgAspect {
//下列args(food,time)保证该切入点只匹配
//具有第一个参数时字符串,第二个参数时Date的方法
@AfterReturning(pointcut="execution(* AOP"+".*.*(..))&&args(food,time)",returning="retVal")
public void access(Date time,String food,Object retVal){
System.out.println("AccessArg目标方法中String参数为:"+food);
System.out.println("AccessArg目标方法中Date参数为:"+time);
System.out.println("AccessArg模拟记录日志.");
}
}
Chinese类增加方法:
public void eat2(String food,Date time){
System.out.println("我正在吃"+food+",现在时间是:"+time);
}
测试代码增加
p.eat2("哈密瓜", new Date());
结果:
从执行结果可以看出:AOP只对eat2() 方法织入AfterReturning增强处理
归纳:args表达式有如下两个作用:
> 提供了一种简单的方式来访问目标方法的参数。
> 可用于对切入表达式增加额外的限制。
除此之外,使用args 表达式时还可使用如下形式:args(food,time,..) ,这表明增强处理方法中可通过food、time来访问目标方法的参数。括号中的2点,表示可匹配更多参数,只要目标方法第一个参数是String类型、第二个参数是Date类型,则该方法就可匹配该切入点。
上面的AccessArgAspect 切面类中
@AfterReturning(pointcut="execution(* AOP"+".*.*(..))&&args(food,time)",returning="retVal")
改为
@AfterReturning(pointcut="execution(* AOP"+".*.*(..))&&args(food,time,..)",returning="retVal")
效果也一样。
7、定义切入点
正如在前面的FourAdviceTest.java 程序中看到的,这个切面类中定义了四个增强处理,定义4个增强处理时分别指定了相同的切入点表达式,这种做法显然不太符合软件设计的原则,将切入点表达式重复了4次,如果需要修改该切入点表达式时,就得修改4个地方。
为了解决这个问题,AspectJ和Spring都允许定义切入点。所谓定义切入点,其实质就是为了一个切入点表达式起一个名称,从而允许在多个增强处理中重用该名称。
@Aspect
public class SystemArchitecture{
@Pointcut("execution(* a.b.c"+".one.T*.th*(..))")
public void myPointcut()
{
}
}
上面程序的切面类仅定义了一个切入点,下面的切面类中将直接使用定义的myPointcut 切入点。
@Aspect
public class LogAspect{
//直接使用SystemArchitecture 切面类的myPointcut 切入点
//args(msg) 保证该切入点只匹配只有一个字符串参数的方法
@AfterReturning(pointcut="SystemArchitecture.myPointcut()"+"&&args(msg)",returning="retVal")
public void writeLog(String msg,Object retVal){
...
}
}
二、基于XML配置文件的管理方式。
Spirng提供了一个“aop”命名空间来定义切面、切入点和增强处理。
实际上,使用XML配置方式与前面介绍的@Aspect 方式的实质是一样的,同样需要指定相关数据:配置切面、切入点、增强处理所需要的信息完全一样,只是提供这些信息的位置不同而已。前者是通过XML文件来提取这些信息,后者是通过Annotation。
相比之下,使用XML配置方式有如下几个优点:
> 如果应用没有使用1.5以上版本的JDK,那么应用只能使用XML配置方式来管理切面、切入点和增强处理等。
> 采用XML配置方式时对早期的Spring用户来说更加习惯,而且这种方式允许使用纯粹的POJO(Plain Ordinary Java Object)来支持AOP。当使用AOP作为工具来配置企业服务时,XML会是一个很好的选择。
当使用XML风格时,我们可以在配置文件中清晰地看出系统中存在哪些切面。
使用XML配置方式也存在如下几个缺点:
> 使用XML配置方式不能将切面、切入点、增强处理等封装到一个地方。当我们需要查看切面、切入点、增强处理时,必须同时结合Java文件和XML配置文件来查看;但使用@Aspect 时,则只需一个单独的类文件即可看到切面、切入点、增强处理的全部信息。
> XML配置方式比@Aspect 方式有更多的限制:仅支持“singleton”切面Bean,不能再XML中组合多个命名连接点的声明。
除此之外,@Aspect 切面还有一个优点就是能被Spring AOP和AspectJ 同时支持。
package Advice;
import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
public class FourAdviceTest {
public Object processTx(ProceedingJoinPoint jp) throws java.lang.Throwable{
System.out.println("Around增强:执行目标方法之前,模拟开始事务.");
//访问执行目标方法的参数
Object[] args=jp.getArgs();
//当执行目标方法的参数存在,且第一个参数时字符串参数
if(args!=null&&args.length>0&&args[0].getClass()==String.class){
//改变第一个目标方法的第一个参数
args[0]="被改变的参数";
}
//执行目标方法,并保存目标方法执行后的返回值
Object rvt = jp.proceed(args);
System.out.println("Around增强:执行目标方法之后,模拟事务结束.");
return rvt+" 新增的内容";
}
public void authority(JoinPoint jp){
System.out.println("Before增强:模拟执行权限检查..");
//返回被织入增强处理的目标方法
System.out.println("Before增强:被织入增强处理的目标方法为:"+jp.getSignature().getName());
//返回执行目标方法的参数
System.out.println("Before增强:目标方法的参数为:"+Arrays.toString(jp.getArgs()));
//返回被增强处理的目标对象
System.out.println("Before增强:被织入增强处理的目标对象为:"+jp.getTarget());
}
public void log(JoinPoint jp,Object rvt){
System.out.println("AfterReturning增强:获取目标方法返回值:"+rvt);
System.out.println("AfterReturning增强:模拟记录日志功能...");
//返回被织入增强处理的目标方法
System.out.println("AfterReturning增强:被织入增强处理的目标方法为:"+jp.getSignature().getName());
//返回执行目标方法的参数
System.out.println("AfterReturning增强:目标方法的参数为:"+Arrays.toString(jp.getArgs()));
//返回被增强处理的目标对象
System.out.println("AfterReturning增强:被织入增强处理的目标对象为:"+jp.getTarget());
}
public void release(JoinPoint jp){
System.out.println("After增强:模拟方法结束后的释放资源...");
//返回被织入增强处理的目标方法
System.out.println("After增强:被织入增强处理的目标方法为:"+jp.getSignature().getName());
//返回执行目标方法的参数
System.out.println("After增强:目标方法的参数为:"+Arrays.toString(jp.getArgs()));
//返回被增强处理的目标对象
System.out.println("After增强:被织入增强处理的目标对象为:"+jp.getTarget());
}
}
package Advice;
public class SecondAdviceTest {
//定义Before增强处理
public void authority(String aa){
System.out.println("目标方法的参数为:"+aa);
System.out.println("② Before增强:模拟执行权限检查");
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd"
>
<aop:config proxy-target-class="true">
<!-- 将fourAdviceBean转换成切面Bean 切面Bean的新名称:fourAdviceAspect 指定该切面耳的优先级为2 -->
<aop:aspect id="fourAdviceAspect" ref="fourAdviceBean" order="2" >
<!-- 定义 After/Before/AfterReturning/Around 增强处理,
分别直接指定切入点表达式
以切面Bean中的 release()/authority()/log()/processTx() 方法作为增强处理方法 -->
<aop:after pointcut="execution(* AOP.*.*(..))" method="release"/>
<aop:before pointcut="execution(* AOP.*.*(..))" method="authority"/>
<aop:after-returning pointcut="execution(* AOP.*.*(..))"
method="log" returning="rvt"/>
<aop:around pointcut="execution(* AOP.*.*(..))" method="processTx"/>
</aop:aspect>
<!-- 将secondAdviceBean转换成切面Bean 切面Bean的新名称:secondAdviceAspect 指定该切面耳的优先级为1 -->
<aop:aspect id="secondAdviceAspect" ref="secondAdviceBean" order="1">
<!-- 定义一个Before 增强处理,
直接指定切入点表达式
以切面Bean中的 authority()方法作为增强处理方法
且该参数必须为String类型(由authority方法声明中msg参数的类型决定) -->
<aop:before pointcut="execution(* AOP.*.*(..)) and args(aa)" method="authority"/>
</aop:aspect>
</aop:config>
<bean id="chinese" class="AOP.Chinese"/>
<!-- 定义普通的Bean实例,下面的Bean实例将被作为Aspect Bean -->
<bean id="fourAdviceBean" class="Advice.FourAdviceTest"/>
<bean id="secondAdviceBean" class="Advice.SecondAdviceTest"/>
</beans>
配置切入点
类似于@Aspect 方式,XML配置方式也可以通过定义切入点来重用切入点表达式
<aop:config>
<!-- 定义一个切入点:myPointcut 直接指定它对应的切入点表达式 -->
<aop:pointcut id="myPointcut" expression="excution(* a.b.c.*.*(..))"/>
<aop:aspect id="aAdviceAspect" ref="aAdviceBean" order="1">
<aop:after-throwing pointcut-ref="myPointcut" method="doAction" throwing="ex"/>
</aop:aspect>
</aop:config>