Spring AOP 和 AspectJ

来源:互联网 发布:大数据产业规模 编辑:程序博客网 时间:2024/04/30 12:05

在本节将学习Spring AOP的用法和一些高级的AOP主题,如通知优先权(Advice precedence)和引入(Introduction)

使用Spring AOP的核心实现技术在所有版本中都一样:动态代理;因为AspectJ已经成长为一个完整流行的AOP框架,Spring在其AOP框架中支持使用AspectJ注解编写POJO aspect。但是在Spring AOP中使用AspectJ aspect有一些限制,因为Spring只允许aspect应用到IOC容器中申明的Bean。如果你希望这个范围之外的应用aspect,就必须使用AspectJ款那个价。

启用Spring的AspectJ注解支持

在Bean配置文件中定义<aop:aspectj-autoproxy/> 他讲自动为匹配AspectJ aspect的Bean创建代理。

用Aspect注解编写的aspect只是一个带有@Aspect注解的Java类。通知(Advice)是带有一个通知注解的简单简单Java方法。AspectJ支持5种通知注解:@Before、@After、@AfterReturning、@AfterThrowing和@Around。

前置通知

切入点匹配的执行点称为连接点(JoinPoint),切入点是匹配一组连接点的表达式,而通知是特定连接点采取的行动。

最终通知

后置通知(after advice)在连接点结束之后执行,不管返回结果还是抛出异常。一个aspect可以包含一个或多个通知。

后置通知

最终通知不管连接点正常返回还是抛出异常都执行,如果希望仅当连接点返回时记录,应该用后置通知(after returning advice)替换最终通知。在后置通知(@AfterReturning)注解中添加一个returning属性,访问连接点返回值。然后,你必须用这个名称通知方法的签名中添加一个参数,在运行时,Spring AOP通过这个参数传入返回值,注意切入点表达式要在pointcut属性中表现。

    @AfterReturning(pointcut = "execution(* Arithmeticalculator.add(..))",            returning = "result")    public void logAfterReturning(JoinPoint joinPoint,Object result){        log.info(joinPoint.getSignature().getName()+"() ends with "+result);    }

异常通知

@AfterThrowing,当且仅当连接点抛出异常时执行。可通过throwing属性来访问。Throwable类型是Java语言只能怪所有错误和异常的超类。

环绕通知

around advice,是所有通知类型中最强大的。它获得连接点的完全控制,这样可以在一个通知总组合的使用前面的所有行为,甚至可以控制合适以及是否继续原来的连接点执行。注意,对于环绕通知,连接点参数类型必须是ProceedingJoinPoint。这是JoinPoint的一个子接口,允许你控制何时继续原始连接点。

    @Around("execution(* *.*(..))")    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {        //do something        Object result = joinPoint.proceed();        log.info("The method "+joinPoint.getSignature().getName()+"() ends with "+result);        return result;    }

环绕通知类型非常强大和灵活,甚至可以修改原始参数值和最后返回值。你必须非常小心的使用这一通知,因为很容易忘记继续原始的连接点

访问连接点信息

/** * Created by leon on 2017/5/19. */@Aspect@Componentpublic class CalculatorLoggingAspect {    private Log log = LogFactory.getLog(this.getClass());    @Before("execution(* Arithmeticalculator.add(..))")    public void logBefore(JoinPoint joinPoint){        log.info("The method add() begins");        log.info("Join point kind:"+ joinPoint.getKind());        log.info("Signature declaring type:"+ joinPoint.getSignature().getDeclaringTypeName());        log.info("Signature name:"+joinPoint.getSignature().getName());        log.info("Arguments:"+ Arrays.toString(joinPoint.getArgs()));        log.info("Target class:"+joinPoint.getTarget().getClass().getName());        log.info("This class:"+joinPoint.getThis().getClass().getName());    }}

代理封装的原始Bean称作目标(Target)对象,而代理对象称作this对象。这两个对象可以有连接点 getTarget( ) 和 getThis( ) 访问,从输出结果可以看出,二者的不同:

Join point kind:method-executionSignture declaring type: com.zy.AOP.calulator.ArithmeticCalculatorSignature name: addArguments : [1.0, 2.0]Target class: com.zy.AOP.calculator.ArithmeticCalculatorImplThis class: $Proxy6

指定aspect优先级

当多于一个aspect适用于相同的连接点时,方面的优先级不是明确的,除非显式的指定。可以通过实现Ordered接口,或者是哟好难过Order注解实现

public class CalculatorLoggingAspect implements Ordered{    private Log log = LogFactory.getLog(this.getClass());    public int getOrder() {        return 0;    }

编写AspectJ切入点表达式

AspectJ切入点是一种强大的表达式语言,能够匹配各种类型的连接点。但是,Spring AOP用AspectJ切入点语言定义其切入点。实际上,Spring AOP在运行时说那个AspectJ提供的库解释切入点表达式。注意只支持IOC容器内Bean的方法执行连接点。

最典型的切入点表达式按照签名匹配许多方法。

匹配 Arithmeticalculator的任意方法,‘..’表示任意参数。同一个包下可省略包名

execution(* com.zy.aop.Arithmeticalculator.*(..))

匹配所有公共方法

execution(public * com.zy.aop.Arithmeticalculator.*(..))

指定返回类型

execution(public double com.zy.aop.Arithmeticalculator.*(..))

对参数列表进行限制

execution(public double com.zy.aop.Arithmeticalculator.*(double, ..))

尽管AspectJ切入点语言在匹配各种连接点方面很强大,但是有些时候可能无法找到想要匹配的方法的任何共同的特征(例如修饰符、返回类型、方法名称模式或参数),在这种情况下可以考虑为他们提供一个自定义注解。

@Target({ElementType.METHOD,ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface RequestLimit {    String whiteList() default "";}

接下来,我们可以用这个注解为所需要记录的方法加上注解。注意这些注解必须加载实现类上而不是接口,因为接口不能被继承。

现在,可以使用如下切点表达式匹配具有@RequestLimit注解的方法。

@annotation(com.zy.AOP.annotation.RequestLimit)

类型签名模式

另一种切点表达式匹配某种类型中的所有连接点。应用到Spring AOP时,这些切入点的作用域将被缩小为匹配类型中的所有方法执行。例如:
匹配com.zy.AOP.calculator包中的方法执行连接点

within(com.zy.AOP.calculator.*)

为匹配一个包及其子包中的连接点,必须在通配符前多加一个点

within(com.zy.AOP.calculator..*)

匹配特定类中的方法执行连接点

within(com.zy.AOP.calculator.ArithmeticCalculatorImpl)

可以添加一个加号,匹配所有实现ArithmeticCalculator接口的类中的方法执行连接点

with(ArithmeticCalculator**+**)

Bean名称模式
有一种切入点类型用于匹配Bean名称。例如,下面的切入点表达式匹配一Calculator结尾的bean(注意,仅支持XML配置,在AspectJ注解中不支持):

bean(*Calculator)

合并切入点表达式
可以使用&& || ! 操作符合并,例如

@Aspectpublic class CalculatorPointCuts{  @Pointcut("arithemticOperation() || unitOperation()")  public void loggingOperaion(){}}

切入点参数申明

申明一个暴露参数的切入点时,必须将他们包含在切入点方法的参数列表中。

@Aspectpublic class CalculatorPointCuts{   @Pointcut("execution(* *.*(..)) && target(target) && args(a,b)")    public void logParameter(Object target,double a,double b){        log.info("Target class:"+target.getClass().getName());        log.info("Arguments : "+a+","+b);    }}

引用这个参数化切入点的任何通知,可以通过同名的方法参数访问切入点参数。

    @Before("com.zy.AOP.calculator.CalculatorPointcuts.logParameter(target,a,b)")    public void paramterPointcut(Object target,double a,double b){        log.info("Target class:"+target.getClass().getName());        log.info("Arguments:{}, {}",a,b);}

在你的Bean中引入行为

问题

有时候,可能有一组共享公共行为的类。在OOP中,它们必须扩展相同的基类或者实现相同的接口。这个问题确实是可以用AOP模块化的一个横切关注点。此外,java的单继承机制。所以不能同时从多个实现类中继承行为。

解决方案

引入(Introduction)是AOP中的一种特殊通知,它允许为一个接口提供实现类,使对象动态的实现接口。这看上去像使对象在运行时扩展了实现类。而且,你可以用多个实现类将多个接口同时引入对象。这可以实现与多重继承相同的效果。

现定义两个接口及实现

public interface MinCalculator {    double min(double a,double b);}public class MinCalculatorImpl implements MinCalculator {    public double min(double a, double b) {        double result = (a >= b) ? a : b;        System.out.println("max("+a+","+b+")= "+result);        return result;    }}public interface MaxCalculator {    double max(double a,double b);}public class MaxCalculatorImpl implements MaxCalculator {    public double max(double a, double b) {        return 0;    }}

现在假定ArithmeticCalculatorImpl也执行max() 和 min() 计算,由于Java单继承机制唯一可能的办法就是复制实现或者处理委派的实现类。这两种情况,都必须重复方法申明。

而使用引入,可以实现MaxCalculatorImpl 和 MinCalculatorImpl动态地实现MaxCalculator和MinCalculator接口,这和从MaxCalculatorImpl 和 MinCalculatorImpl多重继承有相同的效果。引入这一想法的非凡之处在于,你不需要修改ArithmeticCalculatorImpl类引入新方法。这意味着,你可以在没有源代码可用的情况下,将方法引入现有的类中。

然而,引入怎么能在Spring AOP中做到这一切?答案是:动态代理。引入就是通过向动态代理添加一个接口(如MaxCalculator)来工作,当这个接口中声明的方法在代理对象调用时,代理将把调用委派给后端实现类(如MaxCalculatorImpl)。

引入和通知类似,必须在一个aspect中声明。在这个aspect中,你可以用@DeclareParents注解任意一个字段来声明引入。

@Aspectpublic class CalculatorIntroduction {    @DeclareParents(            value = "com.zy.AOP.calculator.ArithmeticaCalculatorImpl",            defaultImpl = MaxCalculatorImpl.class)    public MaxCalculator maxCalculator;    @DeclareParents(            value = "com.zy.AOP.calculator.ArithmeticaCalculatorImpl",            defaultImpl = MinCalculatorImpl.class)    public MinCalculator minCalculator;}

@DeclareParents注解类型的value属性表示引入的目标类。引入的接口由注解字段的类型确定。最后,用语实现这个新街口的实现类在defaultImpl属性中指定。通过这两个引入,可以动态的为ArithmeticCalculatorImpl类引入两个接口,实际上DeclareParents注解的value属性可以使用aspectJ类型匹配表达式,将一个接口引入多个类。

为你的Bean引入状态

类似引入行为

Spring中AspectJ加载时织入aspect

问题

Spring AOP框架只支持有限的AspectJ切入点类型,并允许aspect应用到IOC容器中申明的Bean。如果希望使用更多类型的切入点,或者将aspect应用到spring IOC容器之外创建的对象,就必须在Spring应用程序总使用AspectJ框架。

解决方案

织入(Weaving)是aspect应用到你的目标对象的过程。使用Spring AOP,织入在运行时动态代理发生。

未完待续······