Spring AOP的核心知识

AOP 提供一种通用的扩展机制,在目标对象的业务操作前后提供一些操作,这些操作通常是和业务无关但又必不可少的,例如:日志记录、性能统计、事务处理、异常处理等。

Spring通过动态代理技术支持AOP,但是Spring AOP的范围要小于标准的AOP规范。例如:Spring AOP 只支持Spring Bean的方法切入。所以后序的内容都是以Spring AOP的角度出发来进行说明。

1. AOP 术语

AOP(Aspect Oriented Programming)面向切面编程的概念比较抽象,主要涉及下面这些术语:

  • Aspect(切面):切面是一个类,被@Aspect注解标准或者使用XML配置文件的<aop:aspect>引用。切面是类,一般在这个类中包含了大量要“织入”目标对象的方法。这些方法会被通知、切点表达式注解,因此也被称为通知方法。

  • Target object(目标对象):目标对象就是被切面盯上,要被“织入”切面方法的对象。实现的方式是Spring AOP利用代理技术(JDK或CGLib)新建一个与目标对象类型相关的新对象,这个新对象称为代理对象(proxy object)。

  • Join point(连接点):就是目标对象被“织入”内容的位置,Spring AOP中连接点只能是目标对象程序执行的节点,也就是执行方法或抛出异常。所以在Spring AOP中,不管是连接点还是"织入"的内容都是方法,只不过连接点是目标对象的方法,织入的内容是切面的方法。

  • Advice(通知):织入内容在连接点执行的动作。例如前置,后置,环绕等。如图所示,一个Advice是前置在切入点的(方法执行),另一个Advice是后置在切入点(方法执行)的,而且那个位置恰好还是连接点抛出了异常。

  • Pointcut(切点):经常与通知写在一起,通过切入点标识符(PCD)和切入点表达式,匹配可以“织入”的目标对象的连接点。

使用 AOP 时,感觉像是目标对象中被匹配的方法在执行时被进行了“拦截”,“插入”了切面中通知方法的内容。但实际上,通过Spring AOP,我们获得的是与目标对象类型相关的代理对象,执行方法的也是代理对象。Spring AOP在生成代理对象的过程中,已经修改了目标对象原方法签名的内容,织入了切面通知方法的内容。Spring AOP生成代理对象的过程称为动态代理实现。

 2. 动态代理实现

Spring AOP底层是基于动态代理实现的,在默认情况下:

  • 对实现接口的类进行代理,默认使用 JDK 动态代理,也可以强制使用CGLIB动态代理。
  • 对没实现接口的类,使用 CGLIB 动态代理。

两种动态代理的方式最终实现的代理对象类型有些不同:

JDK动态代理通过使用拦截器和反射机制实现接口生成代理类和代理类对象(proxy)。所以目标对象的非接口自有方法,此时的代理对象是不可能拥有的。

CGLIB动态代理使用字节码技术生成生成目标对象类型的子类,再通过子类对父类的方法进行重写来实现代理对象。因此,目标类中 final 修饰的方法不能被代理。

因为不同的代理技术产生的代理对象类型有所差异,我们也可以强制要求使用CGLib去代理实现了接口的类:

<aop:aspectj-autoproxy proxy-target-class="true"/>

或者

@EnableAspectJAutoProxy(proxyTargetClass = true)

3. 通知类型

Spring AOP 提供了下面五种通知类型:

  • @Before(前置通知):连接点前面执行,不能终止后续流程,除非抛异常

  • @AfterReturning(后置通知):连接点正常返回时执行,有异常不执行

  • @AfterThrowing(异常通知):连接点方法抛出异常时执行

  • @After(最终通知):连接点退出时执行,无论是正常退出还是异常退出

  • @Around(环绕通知):围绕连接点前后执行,也能捕获处理异常

另外还有一种通知是配合Spring AOP Introduction(Spring AOP的引入)的注解,用于将任何接口的实现引入到切面中,并织入目标对象,以扩展目标对象。

  • @DeclareParents:在切面(类)的属性上声明上使用,织入到指定的目标对象(业务类),为业务模块添加新的接口和相应的实现(根据目标对象生成的代理类此时会实现@DeclareParents声明的接口)。具体的使用方式后面会进行讲解。
  • @Aspect:@Aspect不属于Advice,因为@Aspect注解用在类声明上(被@Aspect声明的类称为切面,切面中被通知和PointCut表达式修饰的方法称为通知方法,被匹配的(被拦截的)目标对象的方法或方法抛出的异常称为接入点),指明当前类是一个组织了切面逻辑的类,并且该注解中可以指定当前类是何种实例化方式,主要有三种:singleton、perthis和pertarget,具体的使用方式后面会进行讲解。

Spring AOP中的连接点只能是指目标类的方法,五种通知类型执行的节点如下:

4. PointCut表达式配置

1. 内置配置
定义切面通知时,在 @Before 或 @AfterReturning 等通知注解中指定表达式。

@Aspect
@Component
public class DemoAspect {

    @Before("execution(* cn.codeartist.spring.aop.advice.*.*(..))")
    public void doBefore() {
        // 自定义逻辑
    }
}

使用@Aspect修饰的类就是切面。

要将切面纳入Spring体系中,也需要使用构建型注解进行说明,比如@Component

切面中的方法被通知和切面表达式所修饰,用来匹配目标对象的切入点

@Before是通知(advice),而切面表达式可以作为通知的属性。

既然是表达式,所以被双引号包裹,表达式的具体含义先不解释,但是我们可以看到,表达式由标识符和括号中的表达式内容组成。execution就是标识符的一种,不同标识符构成的表达式有不同的匹配特性,不同标识符对应的表达式内容结果也不尽相同。但可以明确的是,表达式内容中可以使用通配符*和..:

  • *通配符,该通配符主要用于匹配单个单词,或者是以某个词为前缀或后缀的单词
  • ..通配符,该通配符表示0个或多个项,主要用于declaring-type-pattern和param-pattern中,如果用于declaring-type-pattern中,则表示匹配当前包及其子包,如果用于param-pattern中,则表示匹配0个或多个参数。
  • +通配符,该通配符表示某个类型的子类。比如*Service+表示名称以Service结尾的类的所有子类。

2. 注解配置
在切面类中,先定义一个方法并使用 @Pointcut 注解来指定表达式。然后在定义切面通知时,在通知注解中指定定义表达式的方法签名。

@Aspect
@Component
public class DemoAspect {

    @Pointcut("execution(* cn.codeartist.spring.aop.aspectj.*.*(..))")
    private void pointcut() {
        // 切点表达式定义方法,方法修饰符可以是private或public
    }

    @Before("pointcut()")
    public void doBefore(JoinPoint joinPoint) {
        // 自定义逻辑
    }
}

在切面中使用@PointCut显式的将一个方法声明为切点。但是如何使用起点时,依然要于某个通知结合使用。

3. 公共配置
在任意类中,定义一个公共方法并使用 @Pointcut 注解来指定表达式。

@Aspect
public class CommonPointcut {

    @Pointcut("execution(* cn.codeartist.aop.*..*(..))")
    public void pointcut() {
        // 注意定义切点的方法的访问权限为public
    }
}

随后在切面中定义通知时,在通知注解中指定刚才定义表达式的方法签名全路径。

@Aspect
@Component
public class DemoAspect {

    @Before("com.piglite.app.CommonPointcut.pointcut()")
    public void commonPointcut() {
        // 自定义逻辑
    }
}

5. PointCut表达式详解

PointCut表达式由两部分组成:标识符(内容)

标准的AspectJ Aop的PointCut标识符类型是很丰富的,但是Spring Aop只支持其中的10种,外加Spring Aop自己扩充的一种一共是11种标识符,每个标识符还会配合相应的表达式内容,通过标识符与表达式匹配接入点,拦截目标对象方法的执行。

首先是11个标识符分别如下:

  1. execution:一般用于指定拦截的方法执行,用的最多。
  2. within:指定某些类,也可用来指定一个包,用来拦截这些类的全部方法的执行
  3. this:Spring Aop是基于动态代理的,生成的bean也是一个代理对象。而this就是这个代理对象,当这个对象可以转换为表达式描述的指定类型时,会拦截该类型的全部方法执行。
  4. target:当被代理的对象(也就是目标对象)可以转换为表达式描述的指定类型时,会拦截该类型的全部方法执行。
  5. args:当目标对象执行的方法的参数是指定类型时产生拦截。
  6. @target:当被代理的目标对象上拥有指定的注解时,会拦截该类型的全部方法执行。
  7. @args:当目标对象执行的方法参数类型上拥有指定的注解时产生拦截。
  8. @within:与@target类似。
  9. @annotation:当执行的方法上拥有指定的注解时产生拦截。
  10. reference Pointcut:(经常使用)表示使用以@PoingCut命名的切入点。
  11. bean:表达式内容匹配Spring中bean的标识内容(id、name),bean的所有方法执行会被拦截。(Spring AOP自己扩展支持的)

另外Pointcut定义时,还可以使用&&(and)、||(or)、!(negation) 这三个逻辑运算,将多个PointCut表达式组合起来进行逻辑运算。

标识符的应用举例:

execution是使用的最多的一种Pointcut表达式,表示某个方法的执行,其标准语法如下。

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
  • modifier:匹配修饰符,public, private 等,?表示可以省略不写,省略时匹配任意修饰符
  • ret-type:匹配返回类型,必须写明,可以使用通配符 * 匹配任意返回值类型
  • declaring-type:匹配目标类,?表示可以省略不写,省略时匹配任意类型。如果声明declaring-type,可以在声明中使用通配符.. 匹配包及其子包下的所有类
  • name-pattern:匹配方法名称,必须写明,可以使用通配符 * 表示匹配任意方法,set* 匹配名称以 set 开头的方法
  • param-pattern:匹配参数类型和数量
    • ( ) 匹配没有参数的方法
    • (..) 匹配有任意数量参数的方法(0个或多个)
    • (*) 匹配有一个任意类型参数的方法
    • (*,String) 匹配有两个参数的方法,第一个为任意类型,第二个为 String 类型
  • throws-pattern:匹配抛出异常类型,可以省略,省略时表示匹配任意类型异常

下面看几个例子execution表达式的例子:

// 匹配public方法
execution(public * *(..))

// 匹配名称以set开头的方法
execution(* set*(..))

// 匹配AccountService类或接口的所有方法
execution(* com.xyz.service.AccountService.*(..))

// 匹配service包及其子包的类或接口的所有方法
execution(* com.xyz.service..*(..))

//匹配com.spring.service.BusinessObject类中,参数个数为零的所有方法
execution(* com.spring.service.BusinessObject.*())

//匹配com.spring.service包中,以Business为前缀的类中,参数个数为零方法
execution(* com.spring.service.Business*.*())

//匹配com.spring.service包及其子包下的所有类中,方法名称为businessService且没有参数
execution(* com.spring.service..*.businessService())

within是用来指定类型的,被指定类型中的所有方法将被拦截。注意,类型不能是接口

标准语法:within(declaring-type)

// 匹配service包下的所有类,这些类的任意方法执行都会被拦截
within(com.xyz.service.*)

// 匹配service包及其子包下的所有类,这些类的任意方法执行都会被拦截
within(com.xyz.service..*)

// 匹配AccountServiceImpl类,该类的任意方法执行都会被拦截
within(com.xyz.service.AccountServiceImpl)

this标识符表示代理对象的实例,而this的表达式内容也是类型(可以是接口),只要表达式描述的类型可以作为代理对象实例的类型,就算匹配成功。匹配成功就意味着表达式内容描述类型的任意方法在被执行时都会被拦截。根据前面Spring AOP的分析,this不能匹配的是默认状态下实现了接口的类。因为默认情况下,Spring AOP会使用JDK动态代理去实现实现了接口的目标类,换句话说,this的类型是接口类型,它不能向下转换为接口的实现类型。

语法格式:this(declaring-type)

使用示例:

// 匹配代理对象类型为service包下的类
this(com.xyz.service.*)

// 匹配代理对象类型为service包及其子包下的类
this(com.xyz.service..*)

// 匹配代理对象类型为AccountServiceImpl的类
this(com.xyz.service.AccountServiceImpl)

拿最后一个例子来说,如果AccountServiceImpl是.AccountService接口的实现类,那么此时this是AccountService的实例,它不能自动下转型为AccountServiceImpl,所以匹配一定会失败。除非,此时修改了Spring AOP默认的动态代理模式,强制使用CGLib的方式去生成代理对象,那么是可以匹配成功的,匹配成功后,AccountServiceImpl任何方法被调用时都会被拦截。

target标识符表示目标对象实例的类型,而target的表达式内容也是类型(可以是接口),只要表达式描述的类型可以作为目标对象实例的类型,就算匹配成功。

语法格式:target(declaring-type)

使用示例:

// 匹配目标对象类型为service包下的类
target(com.xyz.service.*)

// 匹配目标对象类型为service包及其子包下的类
target(com.xyz.service..*)

// 匹配目标对象类型为AccountServiceImpl的类
target(com.xyz.service.AccountServiceImpl)

within、this和target的表达式内容都是与类型相关的,从结论的角度看:

表达式匹配范围withinthistarget
接口
实现接口的类
不实现接口的类

args标识符匹配方法参数类型和数量,参数类型可以为表达式内容中指定类型或其子类。需要注意的是,args指定的参数必须是全路径的。

如下是args表达式的语法格式:args(param-pattern)

使用时:

//若方法只有一个String类型的参数,该方法执行时就会被拦截
args(java.lang.String)

//若方法参数只有一个且为Serializable类型或实现Serializable接口的类
args(java.io.Serializable)

bean标识符通过 bean 的 id 或名称匹配,支持 * 通配符。

语法格式:bean(bean-name)

使用示例:

// 匹配名称以Service结尾的bean
bean(*Service)

// 匹配名称为demoServiceImpl的bean
bean(demoServiceImpl)

@within标识符用来匹配匹配带有指定注解的类。在接口上使用指定注解的不匹配。

其使用语法如下所示:

@within(annotation-type)

如下所示示例表示匹配使用com.spring.annotation.BusinessAspect注解标注的类:

@within(com.spring.annotation.BusinessAspect)

@target与@within类似,不再赘述。

@annotation匹配方法是否含有指定的注解。当方法上使用了注解,该方法会被匹配,但在接口方法上使用指定的注解不匹配。

@annotation(annotation-type)

如下示例表示匹配使用com.spring.annotation.BusinessAspect注解标注的方法:

@annotation(com.spring.annotation.BusinessAspect)

@args标识符表示使用指定注解标注的类作为某个方法的参数时该方法将会被匹配。

如下是@args注解的语法:

@args(annotation-type)

如下示例表示:

@args(com.spring.annotation.FruitAspect)

如果一个类使用了com.spring.annotation.FruitAspect作为注解,那么当这个类作为了某个方法的参数时,在方法在执行时会被拦截。

上述所有被通知和切点表达式修饰的通知方法都可以额外增加一个JointPoint参数(@Around注解的通知方法需要使用ProcedingJoinPoint类型的参数,ProcedingJoinPoint是JoinPoint的子类),通过这个参数可以更全面的了解被“织入”的目标对象的连接点:

    @Before("execution(* add(..))")
    public void logBefore(JoinPoint joinPoint){
        System.out.println("-----------------------------");
        System.out.println("数学计算器的加法运算...");
        System.out.println("正要执行的方法是:"+joinPoint.getSignature().getName()+" ,方法的参数是:"+ Arrays.toString(joinPoint.getArgs()));

    }

    @After("execution(* *(..))")
    public void logAfter(JoinPoint jp){
        System.out.println(jp.getSignature().getName()+"方法执行完毕");
        System.out.println("-----------------------------");
    }

JointPoint提供的常用分析连接点的方法包括:

  • getArgs(): 连接点使用的参数.

  • getThis(): 连接点所属的代理类型是什么.

  • getTarget():连接点所属的目标类型是什么

  • getSignature(): 作为连接点的方法签名

除了JoinPoint/ProcedingJoinPoint之外,还有三个切点表达式this、target和args其实也能实现部分JoinPoint的功能,从而了解接入点的特性。从表达式的名字就可以看出来,this表达式近似JoinPoint的getThis函数,target表达式近似JoinPoint的getTarget函数,而args表达式近似JoinPoint的getArgs函数的作用。通过切点表达式获取的连接点信息将被以通知方法参数的方式由Spring灌入。

举一个比较完整的例子:

现在有一个转换器接口UnitCalculator

public interface UnitCalculator {
    double kiloToPound(double kilo);
    double kmToMile(double km);

}

该接口的实现类是MyUnitCalculator

@Service
public class MyUnitCalculator implements UnitCalculator {
    @Override
    public double kiloToPound(double kilo) {
        double rst = kilo*2.2;
        System.out.println(kilo + "kg = "+rst+" pounds.");
        return rst;
    }

    @Override
    public double kmToMile(double km) {
        double rst = km*0.62;
        System.out.println(km + "kms = "+rst+" miles.");
        return rst;
    }
}

现在构建一个切面:MyAspect

@Aspect
@Component
public class MyAspect {

    @Before("execution(* k*(double)) && this(p) && target(t) && args(a)")
    public void logBefore(JoinPoint joinPoint,Object p,Object t,double a){
        System.out.println("-----------------------------");
        System.out.println("正要执行的方法是:"+joinPoint.getSignature().getName());
        System.out.println("代理对象类型:"+joinPoint.getThis().getClass().getName()+" , "+  p.getClass().getName());
        System.out.println("目标对象类型:"+joinPoint.getTarget().getClass().getName()+" , "+  t.getClass().getName());
        System.out.println("方法的参数是:"+Arrays.toString(joinPoint.getArgs())+" , "+a);

    }
    @After("execution(* k*(*))")
    public void logAfter(JoinPoint jp){
        System.out.println(jp.getSignature().getName()+"方法执行完毕");
        System.out.println("-----------------------------");
    }

}

可以看到logBefore的切点表达式中通过逻辑运算符拼接了this、target和args这三个可以用来窥探连接点方法特性的表达式,窥探到的内容将以通知方法的参数由Spring灌入值。

编写配置类MyConfig并直接作为测试类进行代码测试:

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.piglite.ch5aop.demo3")
public class MyConfig {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.register(MyConfig.class);
        ctx.refresh();
        UnitCalculator unitCalculator = ctx.getBean("myUnitCalculator", UnitCalculator.class);
        unitCalculator.kiloToPound(100);
        unitCalculator.kmToMile(50);
        ctx.close();

    }
}

MyConfig是一个Spring的配置类,并直接在main方法中测试,运行后的结果是:

-----------------------------
正要执行的方法是:kiloToPound
代理对象类型:jdk.proxy2.$Proxy20 , jdk.proxy2.$Proxy20
目标对象类型:com.piglite.ch5aop.demo3.MyUnitCalculator , com.piglite.ch5aop.demo3.MyUnitCalculator
方法的参数是:[100.0] , 100.0
100.0kg = 220.00000000000003 pounds.
kiloToPound方法执行完毕
-----------------------------
-----------------------------
正要执行的方法是:kmToMile
代理对象类型:jdk.proxy2.$Proxy20 , jdk.proxy2.$Proxy20
目标对象类型:com.piglite.ch5aop.demo3.MyUnitCalculator , com.piglite.ch5aop.demo3.MyUnitCalculator
方法的参数是:[50.0] , 50.0
50.0kms = 31.0 miles.
kmToMile方法执行完毕
-----------------------------

另外,对于@AfterReturning可以增加一个returning属性,以通知方法参数的形式接收连接点方法正常执行结束后的返回值。例如:

@Aspect
public class LoggingAspect{

    @AfterReturning(
        pointcut="execution(* *.*(..))",
        returning="result"
    )
    public void logAfterReturning(JoinPoint jp, Object result){

        System.out.println("method: "jp.getSignature().getName()+" get result "+result);
    }

}

需要注意的地方包括:

1.  一旦在@AfterReturning中使用了属性,就要分别用pointcut表示切点表达式,用returning表示切入的接入点方法的返回值。

2. 接入点方法返回值会以参数的形式由Spring灌入通知方法中。如果可以明确返回值的数据类型可以写具体类型,当不能明确描述切入的接入点方法的返回值,那么就使用Object类型(根据切点表达式,logAfterReturning可能被织入很多个切入点,不能保证这么多的接入点都返回同一个类型,所以就用Object表示接入点的返回值类型)。

与@AfterReturning类似,可以为@AfterThrowing增加一个throwing属性以通知方法参数的形式接收连接点执行过程中抛出的异常。例如:

@Aspect
public class LoggingAspect{

    @AfterThrowing(
        pointcut="execution(* *.*(..))",
        throwing="exp"
    )
    public void logAfterReturning(JoinPoint jp, Throwable exp){
        System.out.println("an exception "+e+" has been thrown in "+
                            jp.getSignature().getName()+" method");
    }
}

需要注意的地方包括:

1.  一旦在@AfterThrowing中使用了属性,就要分别用pointcut表示切点表达式,用throwing表示切入的接入点方法抛出的异常。

2. 接入点方法抛出的异常会以参数的形式由Spring灌入通知方法中。参数类型一般使用异常的总类型Throwable,如果可以明确知道被切入的接入点方法抛出的异常类型,也可以使用具体的类型。不过那样的话,只有抛出的异常确实符合该类型的(该类或其子类),那么通知才会被织入。

6. @DeclareParents

@DeclareParents也称为Introduction(引入),目的是为指定的目标类引入新的属性和方法,只不过引入的方式比较独特。

关于@DeclareParents的原理其实比较好理解,因为无论是Jdk代理还是Cglib代理,想要引入新的方法,只需要通过一定的方式将新声明的方法织入到代理类中即可,因为代理类都是新生成的类,因而织入过程也比较方便。

如下是@DeclareParents的使用语法:

@DeclareParents(value = "TargetType", defaultImpl = WeaverType.class)
private WeaverInterface attribute;

注意,@DeclareParents注解的attribute是一个切面的属性(@Aspect修饰的类),而属性的类型WeaverInterface是一个接口,这个接口中声明了要为目标类添加的方法或属性。而@DeclareParents注解的TargetType属性表示了要织入的目标类(带全路径),WeaverType中声明的是WeaverInterface接口的实现类。

所以@DeclareParents其实就是在已有代理类型的基础上再来一个代理类型。

如下示例表示在Apple类中织入IDescriber接口声明的方法:

首先是目标类Apple,Apple类没有实现任何接口:

// 织入方法的目标类
public class Apple {
  public void eat() {
    System.out.println("Apple.eat method invoked.");
  }
}
// 要织入的接口
public interface IDescriber {
  void desc();
}
// 要织入接口的默认实现
public class DescriberImpl implements IDescriber {
  @Override
  public void desc() {
    System.out.println("this is an introduction describer.");
  }
}

接下来就是切面类:

// 切面实例
@Aspect
public class MyAspect {
  @DeclareParents(value = "com.spring.service.Apple", defaultImpl = DescriberImpl.class)
  private IDescriber describer;
}

最后是测试类,测试IDescriber是否织入了Apple类:

// 驱动类
public class AspectApp {
  public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    IDescriber bean = (IDescriber) context.getBean("apple");
    bean.desc();
  }
}

配置文件省略,直接看测试类运行结果。

在切面MyAspect中通过@DeclareParents声明了要将IDescriber的方法织入到Apple实例中。在测试类中我们可以看到,我们获取的是apple实例,但是得到的bean却可以强转为IDescriber类型,因而说明我们的织入操作成功了。bean以IDescriber类型执行着接口中定义的方法。

实现的过程无非就是CGLib创建了Apple子类代理类织入切面的切入点逻辑,然后子类代理类实现IDescriber,由JDK继续创建了一个IDescriber的代理类并织入了IDescriber方法。所以bean就是一个代理代理类,继承了Apple实现了IDescriber的一个代理对象。

7. perthis和pertarget

在Spring AOP中,切面类的实例只有一个,比如前面我们使用的MyAspect类。假设我们使用的切面类需要具有某种状态,以适用某些特殊情况的使用,比如多线程环境,此时单例的切面类就不符合我们的要求了。在Spring AOP中,切面类默认都是单例的,但其还支持另外两种多例的切面实例的切面,即perthis和pertarget。需要注意的是perthis和pertarget都是使用在切面类的@Aspect注解中的。

这里perthis和pertarget都是指定一个切面表达式,其语义与前面讲解的this和target非常的相似:

  • perthis表示如果某个目标类的代理类符合其指定的切面表达式,那么就会为每个符合条件的目标类都声明一个切面实例;
  • pertarget表示如果某个目标类符合其指定的切面表达式,那么就会为每个符合条件的类声明一个切面实例。

从上面的语义可以看出,perthis和pertarget的含义是非常相似的。如下是perthis和pertarget的使用语法:

perthis(pointcut-expression)
pertarget(pointcut-expression)

由于perthis和pertarget的使用效果大部分情况下都是一致的,我们这里主要讲解perthis和pertarget的区别。关于perthis和pertarget的使用,需要注意的一个点是,由于perthis和pertarget都是为每个符合条件的类声明一个切面实例,因而切面类在Spring配置文件中的bean声明上一定要加上prototype,否则Spring启动是会报错的。如下是我们使用的示例:

<!-- xml配置文件 -->
<bean id="apple" class="ch7.demo6.Apple"/>
<bean id="aspect" class="ch7.demo6.MyAspect" scope="prototype"/>
<aop:aspectj-autoproxy/>
// 目标类实现的接口
public interface Fruit {
  void eat();
}
// 业务类
public class Apple implements Fruit {
  public void eat() {
    System.out.println("Apple.eat method invoked.");
  }
}
// 切面类
@Aspect("perthis(this(com.spring.service.Apple))")
public class MyAspect {

  public MyAspect() {
    System.out.println("create MyAspect instance, address: " + toString());
  }

  @Around("this(com.spring.service.Apple)")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("this is before around advice");
    Object result = pjp.proceed();
    System.out.println("this is after around advice");
    return result;
  }
}
// 测试类
public class AspectApp {
  public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    Fruit fruit = context.getBean(Fruit.class);
    fruit.eat();
  }
}

这里我们使用的切面表达式语法为perthis(this(com.spring.service.Apple)),这里this表示匹配代理类是Apple类型的类,perthis则表示会为这些类的每个实例都创建一个切面类。本例中由于Apple实现了Fruit接口,因而Spring使用Jdk动态代理为其生成代理类,也就是说代理类与Apple都实现了Fruit接口,但是代理类不是Apple类型(代理类是Fruit类型),因而这里声明的切面不会匹配到Apple类。执行上述驱动类,结果如下:

Apple.eat method invoked.

结果表明Apple类确实没有被环绕。现在将切面类中的perthis和this修改为pertarget和target,效果如何呢:

@Aspect("pertarget(target(com.spring.service.Apple))")
public class MyAspect {

  public MyAspect() {
    System.out.println("create MyAspect instance, address: " + toString());
  }

  @Around("target(com.spring.service.Apple)")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("this is before around advice");
    Object result = pjp.proceed();
    System.out.println("this is after around advice");
    return result;
  }
}

       执行结果如下:

create MyAspect instance, address: chapter7.eg6.MyAspect@48fa0f47
this is before around advice
Apple.eat method invoked.
this is after around advice

可以看到,Apple类被切面环绕了。这里target表示目标类是Apple类型,虽然Spring使用了Jdk动态代理实现切面的环绕,代理类虽不是Apple类型,但是目标类却是Apple类型,符合target的语义,而pertarget会为每个符合条件的表达式的类实例创建一个代理类实例,因而这里Apple会被环绕。

       由于代理类与目标类的差别非常小,因而与this和target一样,perthis和pertarget的区别也非常小,大部分情况下其使用效果是一致的。

关于切面多实例的创建,其演示比较简单,我们可以将xml文件中的Apple实例修改为prototype类型,并且在驱动类中多次获取Apple类的实例:

<!-- xml配置文件 -->
<bean id="apple" class="ch7.demo6.Apple" scope="prototype"/>
<bean id="aspect" class="ch7.demo6.MyAspect" scope="prototype"/>
<aop:aspectj-autoproxy/>
public class AspectApp {
  public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    Fruit fruit = context.getBean(Fruit.class);
    fruit.eat();
    fruit = context.getBean(Fruit.class);
    fruit.eat();
  }
}

       执行结果如下:

create MyAspect instance, address: ch7.demo6.MyAspect@48fa0f47
this is before around advice
Apple.eat method invoked.
this is after around advice
create MyAspect instance, address: ch7.demo6.MyAspect@56528192
this is before around advice
Apple.eat method invoked.
this is after around advice

执行结果中两次打印的create MyAspect instance表示当前切面实例创建了两次,这也符合我们进行的两次获取Apple实例。

8. @Order

 Spring AOP的一个应用程序中允许有多个切面。不同的切面在切入的时候很可能会切入到同一个连接点上。此时那个切面的切入点(通知方法)先获得执行呢?可以切面添加@Order注解,并为@Order注解明确写入从0开始的数字,数字越小切入同一连接点时,切面的切入点获得执行的时机就越靠前。

如果不使用@Order注解,也可以让切面实现Ordered接口,通过接口的getOrder函数返回值来决定执行属性,依然是从0开始,字越小切入同一连接点时,切面的切入点获得执行的时机就越靠前。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值