AOP能够横向地看待程序,将与逻辑无关的功能,比如说日志,事物等从代码中抽离出来。

使用AspectJ实现AOP

安装Aspect,配置好环境变量后,用记事本写一个类:

public class HelloWorld {	public void sayHello(){		System.out.println("Hello AspectJ!");	}	public static void main(String args[]){		HelloWorld h=new HelloWorld();		h.sayHello();	}}
public aspect TxAspect {	void around():call(void sayHello()){		System.out.println("Transaction Begin");		proceed();		System.out.println("Transaction End");	}}

然后使用ajc命令编译它们。ajc相当于增强的javac,能够识别里面的aspect关键字。编译的过程就是自动把around中代码添加到HelloWorld中特定位置的过程。之后运行主函数就能看到,Transaction的开始与结束方法会在目标对象的Pointcut前后执行。

通过上述例子,我们知道AOP的目的就是保证在不修改原代码的情况下为系统中业务组件的多个业务方法以非耦合的方式添加某种通用的功能。我们只需要在外部写好增强方法,架构会自动地帮我们添加到目标对象中的切入点处。AOP的实现可以分为动态跟静态。所谓静态就像这种,在编译的时候织如增强代码,以AspectJ为代表。而动态AOP实现,就像Spring的AOP一样,则是在运行阶段生成AOP代理。所谓代理就是框架会额外创建一个对象,用以代替目标对象,并且代理也会携带需要增强的功能。

AOP的基本概念

切面(Aspect):用于组织多个Advice。Advice放在切面中定义。

连接点(Joinpoint):程序执行过程中的明确的点,Spring中是方法。

增强处理(Advice):AOP框架在特定的切入点需要执行的增强处理,处理有around、after、before等类型。

切入点(Pointcut):可以插入增强处理的点,也就是那些符合特定规则的连接点。

引入:将方法或者字段添加到被处理的类中。Spring允许将新的接口引入到任何被处理的类中。

目标对象:被选定的要被增强的对象。如果AOP框架采用动态AOP,那么目标对象就是代理对象。

AOP代理:AOP框架额外创建的包含了增强处理与目标对象的新对象。Spring中的AOP可以是JDK动态代理,用于为实现接口的目标对象的代理;也可以是cglib代理,为不实现接口的目标对象进行代理。

织入:将增强处理添加到目标对象中,并创建一个被增强的对象的过程。织入可以有编译时织入,或者运行时织入。

Spring的AOP支持

Spring中的AOP代理由Spring的容器负责生成管理,依赖关系也是由容器负责,所以AOP代理可以直接使用容器中的其它bean实例作为目标。默认使用动态代理,这样就可以为任何接口实例创建代理。当需要代理类而不是接口的时候Spring也会自动切换为使用cglib代理。但因为Spring是推荐使用面向接口编程的,因此通常没机会使用cglib。

Spring AOP使用纯java类实现,无需特殊编译器。仅支持方法作为连接点。在AOP编程中,我们只需要做如下三件事:

  • 定义普通业务组件

  • 定义切入点

  • 定义增强处理

基于注解的“零配置”方式

Spring使用了和AspectJ 5一样的注解,但是底层并不是AspectJ编译器,而是Spring AOP。为了启用Spring对@AspectJ的支持,我们需要在xml文件头增添如下代码:

xmlns:context="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-3.1.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-4.0.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-4.0.xsd">

此外我们还需要将三个jar包添加到项目中。aspectjweaver.jar和aspectjrt.jar这两个包在AspectJ的lib下有。另一个aopalliance.jar需要额外下载,用以支持Spring AOP。

定义切面bean

当启动了@AspectJ支持后,只要为某一个bean增加@Aspect注解,框架就会把这个bean视为切面bean而不去实例化它。配置切面bean与普通bean一样。切面类一样可以有成员变量,方法,还可能包括切入点,增强处理定义。

@Aspectpublic class LogAspect{//其它内容}

定义Before增强处理

在一个切面类中用@Before修饰的方法会在Pointcut之前运行。通常Before需要一个value值,这个值表明了一个切入点表达式,用以指定增强处理被织入哪些切入点。

@Aspectpublic class LogAspect{    //表示com.cm包下面的所有类的所有方法都是切入点,..表示任意个数、类型不限的形参    @Before("execution(* com.cm.*.*(..)");    public void authority()    {        System.out.println("模拟执行权限检查");    }        }

只需如此配置,在com.cm下面的bean执行方法之前,这段增强代码就会自动执行。如果没有特殊处理,目标方法随后就会执行。如果不想目标方法执行,就抛出一个异常。

定义AfterReturning增强处理

这个注解可以定义AfterReturning增强处理,增强代码将在目标方法执行之后执行。这个注解中需要有两个属性。pointcut/value这两个属性的作用一样,都用于指定切入点的表达式。既可以是已经有的切入点,也可以直接定义切入点表达式。当制定了pointcut属性后,value属性值将会被覆盖。returning属性制定一个形参名,用于表示Advice方法中可定义与此同名的形参,该形参能够访问目标方法的反沪指。除此之外,在Advice中定义该形参时指定的类型,会限制目标方法必须返回指定类型的值或者没有返回值。

@Aspectpublic class LogAspect{@AfterReturning(returning="rvt",pointcut="execution(* com.cm.*.*(..))")public void log(Object rvt)    {    System.out.println("获取目标方法返回值"+rvt);    System.out.println("模拟记录日志功能");    }}

上述程序注解中的rvt,可以理解为它就是目标方法的返回值。returning属性所指定的形参名必须对应增强处理中的一个形参名,当目标方法执行后,返回值作为相应的参数传入增强处理方法。我们可以利用这个形参来过滤切入点,只选取那些返回值类型与增强函数中形参类型一致的目标方法。此处的形参类型是Object,那么所有符合value表达式的方法都可以。如果这里指定的是String,那么除了要满足value表达式,只有那些返回值为String的方法才能够作为目标方法。这里只能访问返回值,不能改变它。

定义AfterThrowing增强处理

使用@AfterThrowing注释可以修饰AfterThrowing增强处理,用于处理程序中未处理的异常。pointcut/value属性用于指定一个value表达式,与上述相同。throwing属性用于制定一个形参名,用于表示Advice方法中可定义与此同名的从餐,该形参可用于访问目标方法抛出的异常。跟上述类似,也会起到限制抛出异常类型的作用。

@Aspectpublic class LogAspect{@AfterThrowing(throwing="ex",pointcut="execution(* com.cm.*.*(..))")public void log(Object ex)    {    System.out.println("获取目标方法抛出的异常"+ex);    System.out.println("模拟对异常的修复");    }}

注意只有当目标方法抛出一个未处理的异常时,才会启动织入。注意这种机制与catch完全不同。catch能够完全处理异常,只要catch中没有新的异常,该方法可以正常结束。而AfterThrowing虽然能处理异常,不过不能完全处理。

After增强处理

AfterReturning只有在目标方法成功完成后才会被织入,而After增强处理则无论目标方法如何结束,是否成功完成还是遇到了异常终止,增强代码都会被织入,所以它必须应对正常结束和异常终止两种情况。

@Aspectpublic class LogAspect{@After("execution(* com.cm.*.*(..))")public void log()    {    System.out.println("模拟方法结束后的释放资源");    }}

Around增强处理

近似于Before和AfterReturning的总和,可以在执行目标方法之前织入,也可之后织入。Around可以决定目标方法在什么时候执行,如何执行,甚至可以完全阻止目标方法的执行。Around增强处理可以改变执行目标方法的参数值,可一个改变执行目标方法之后的返回值。不过虽然功能强大,但必须在线程安全下使用。Around需要指定一个value。方法的第一个参数必须为ProceedingJoinPoint类型。也就是说至少要包含一个形参。调用这个形参的proceed方法才会执行目标方法。如果没有显式调用,目标方法就不会被执行。当使用proceed方法时,还可以传入一个Object[]对象作为参数,该数组中的值将被传入目标方法作为执行方法的实参。

@Aspectpublic class LogAspect{@Around("execution(* com.cm.*.*(..))")public Object log(ProceedingJoinPoint jp)    throws java.lang.Throwable    {    System.out.println("执行目标方法前,模拟事物开始");    //得到了目标方法的原始参数    Object[] args=jp.getArgs();    if(args!=null&&args.length>1)    {    args[0]="增加的前缀“+args[0];    }    //修改了目标方法的参数    Object rvt=jp.proceed(args);    System.out.println("执行目标方法后,模拟事物结束");    if(rvt!=null&&rvt instanceof Integer)    {    //修改了目标方法的返回值    rvt=(Integer)rvt*(Integer)rvt;    return rvt;}

访问目标方法的参数

最简单的方法就是定义增强处理方法是降低一个参数定义为JoinPoint类型的。当增强方法被调用是,这个参数就代表了织入增强处的连接点。需要注意在around标注中使用的是ProceedingJoingPoint,因为需要这里面的proceed。除了这点,基本用法没有区别。JoingPoint里面有如下四个常用方法:

  • Object[] getArgs():返回执行目标方法时的参数,入上例所示。

  • Signature getSignature():返回被增强的方法的相关信息,也就是Pointcut方法的信息。

  • Object getTarget():返回被植入增强处理的目标对象,也就是包含了被增强方法的对象。

  • Object getThis():返回AOP框架为目标对象生成的代理对象,也就是包含了增强方法和原方法的超级大方法。

注意当一个切面需要两个以上不同的增强处理时,默认顺序是随机的,但也可以认为确定顺序,只需让切面类实现org.springframework.core.Ordered接口,该接口的实现类只需要实现一个int getOrder()方法。该方法的返回值越小,顺序越靠前。或者也可以在切面类中使用@Order来注释,它可以用有一个int类型的value属性,用于指定顺序。数值越小,优先级越高。

除此之外还有一种更简单的方法来访问目标方法参数。在切面类定义切入点表达式时后面&&args(arg0,arg1),这样的话在增强方法中也可以增加两个任意类型的形参,参数名就是arg0,arg1。一旦确定之后,切入点方法也必须含有这两个形参类型的参数才可以被选中,而匹配之后这两个形参就是目标方法的参数了。实际上这相当于给切入点表达式增加了限制。还记得之前表达式中有(..)这种写法吗,这就是说参数不限。而本例中限制了参数。

定义切入点

所谓定义切入点,实际上就是为一个切入点表达式起一个名字,这样在不同的切面类中只要使用表达式的名字就可以了。以后如果需要修改表达式,只需要在一个地方修改即可。切入点定义包含两个部分,一个是切入点表达式,另一个是包含名字和任意参数的方法签名。前面决定论了哪些方法可以作为切入点,而后面的则代表了表达式。我们需要使用@Point注解来指明切入点。

@Aspectpublic class SystemArchitecture{@Pointcut("exection(* transfer(..))")public void myPointcut(){}}

只需如此这般,就定义好了一个名为myPointcut的切入点表达式。使用的时候,我们只需要在注解中使用pointcut属性,另它的值为myPointcut即可。

切入点指示符

之前我们在定义切入点表达式时大量使用execution,实际上这就是一个切入点指示符。

组合切入点表达式

我们可以通过&&,||,!来对切入点表达式进行逻辑处理。