上文我们介绍了Spring AOP的原理-Java代理,本文就来介绍如何使用Spring AOP。
一、介绍
AOP(Aspect Oriented Programming)即面向切面编程,是一种面向对象编程(Object-Oriented Programming、OOP)的补充,提供了另一种方法构建软件。面向对象编程(OOP)的核心单元是对象(Object),而面向切面编程(AOP)的核心单元是切面(aspect)。切面能够用来模块化那些贯穿多个类型和对象的关注点,比如事务管理、日志管理等。(在AOP术语中这些关注点被称为横切关注点(crosscutting)。)如下图所示:
二、AOP概念
需求:统计一个方法的运行时间。步骤为:
- 在主方法执行之前记录当前系统时间,存储在变量
startTime
中。 - 执行主方法
- 主方法返回后,记录当前系统时间,存在变量
endTime
。 - 使用
endTime
-startTime
则为主方法执行时间elapsedTime
。
涉及到的方面:主方法,统计主方法执行的代码。那么,我们怎么使用AOP在不修改方法主体的基础上统计方法的执行时间呢。首先,我们介绍Spring AOP的基本概念。
AOP的核心概念有:通知(Advice)、切入点(pointcuts)、连接点(join points)。当然还有其他的概念,如下:
- 通知(Advice):定义切面的内容,以及何时应用切面。需求中执行方法执行的时间就是切面的内容,而何时则表示应该何时在主代码流程中插入这些代码。这些时间包括环绕(around),之前(before),之后(after)。我们将会在后面详细讨论通知的类型。
-
连接点(Join Points):程序执行过程中的一个点。比如方法的调用,异常的抛出都可以作为一个连接点。在我们的需求中,连接点就是方法的执行。在Spring AOP中连接点只能是方法的执行。(想支持更多的连接点请参考
AspectJ
) - 切入点(Pointcuts):用来匹配一个或多个的连接点。比如,我们需要统计很多主方法执行(连接点)的时间。那么,如何选取这些连接点(比如一个包下面的方法)的概念就是切入点。如果通知定义了何时和内容,那么切入点则定义了何地。
- 切面(Aspect):通知和切入点的合并。也就是说它表示整个对关注点进行模块化的过程,也就是何时,何地,干什么。
- 引入(Introduction):为已存在的类(Class)添加额外的方法和属性。Spring AOP允许你添加额外的接口(interface)与实现到类中。
- 目标(Target):被Spring AOP代理的对象,也就是主方法所在的对象。
- AOP代理(AOP proxy):Spring为被代理对象创建的代理对象。在Spring AOP中,一个AOP代理会是一个Java动态代理或则CGLIB代理。
- 编织(Weaving):把切面应用到目标对象生成一个新的代理对象的过程。这个过程可以在三个时期完成:编译时期(Compile time)、加载时期(load time)、运行时期(runtime)。Spring AOP支持在运行时期进行编制。
通知(Advice)的类型:
类型 | 执行点 |
---|---|
Before | 在主方法调用之前执行 |
After | 通知在主方法完成之后执行,不管主方法的调用结果如何 |
After-returnning | 通知在主方法正常返回后执行。比如在不抛出异常时正常返回 |
After-throwing | 通知在主方法抛出异常后执行 |
Around | 通知包装了主方法,提供在方法调用一直或之后提供一些功能 |
各种类型的通知可由下图表示:
三、代码示例
首先,我们定义被代理的接口和实现:
清单1. 被代理接口和实现
// MyBean.java
package org.archerie.aop.bean;
public interface MyBean {
void sayHello(String msg);
}
// MyBeanImpl.java
package org.archerie.aop.bean;
import org.springframework.stereotype.Component;
// 使用spring @Component注解,加载到spring上下文中
@Component
public class MyBeanImpl implements MyBean {
public void sayHello(String msg) {
System.out.println(msg);
}
}
接下来,我们就开始定义我们的切面。如下:
清单2. 切面
package org.archerie.aop.aspect;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class AdviceExampleAspect {
// 定义切点
@Pointcut("execution(** org.archerie..*Bean.*(..))")
public void beanPointCut() {}
@Before("beanPointCut()")
public void silenceCellPhone() {
System.out.println("手机静音!");
}
@Before("execution(** org.archerie..MyBean.sayHello(String)) && args(msg)")
public void printMsg(String msg) {
System.out.println("MyBean将要说的是:" + msg);
}
@After("beanPointCut()")
public void applause() {
System.out.println("鼓掌!鼓掌!");
}
}
现在,我们就需要定义我们的spring配置文件了。我们选择Java配置的方式来配置Spring。
清单3. Spring配置
package org.archerie.aop.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = {"org.archerie.aop"})
public class AopJavaConfig {
}
以上,切面就准备好了,现在就差使用了。下面,使用JUnit来进行测试:
清单4. JUnit测试切面
package org.archerie.aop;
import org.archerie.aop.bean.MyBean;
import org.archerie.aop.bean.MyOtherBean;
import org.archerie.aop.config.AopJavaConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AopJavaConfig.class)
public class AspectTest {
@Autowired
MyBean myBean;
@Autowired
MyOtherBean otherBean;
@Test
public void testAspect() {
myBean.sayHello("你好!");
}
}
以上,就是完成的代码示例。现在运行查看结果:
MyBean将要说的是:你好!
手机静音!
你好!
鼓掌!鼓掌!
那么,到底这些代码都是什么意思?它们又是如何工作的呢?
四、Spring AOP详解
前面,我们已经讨论过,Spring AOP主要由连接点、切入点和通知组成切面。而切入点是用来选择某一范围的连接点的,所以我们首先讨论如何定义切入点。这里要说明一点的就是:Spring AOP只支持一种Join point,就是方法的执行。所以,以下切入点(Pointcut)只能选取方法执行的连接点(Join Point)。
4.1 切入点(Pointcuts)
Spring AOP使用AspectJ的切入点表达式来声明切入点。一下是Spring AOP支持的AspectJ切入点:
AspectJ表达式 | 描述 |
---|---|
execution | 匹配方法执行的连接点,这是使用Spring AOP时主要使用的切入点 |
within | 匹配特定类型中的连接点(在Spring AOP中则限制为匹配类型中的方法执行) |
this | 匹配Spring AOP代理对象中的连接点(在Spring AOP中为方法的执行),注意匹配的是spring aop代理对象为指定的类型。this表达式必须使用完整的限定类名,不能使用通配符。 |
target | 匹配目标对象中的连接点(在Spring AOP中为方法的执行),但是目标对象得为特定的类型。target表达式必须使用完整的限定类名,不能使用通配符。 |
args | 匹配参数为特定类型实例的连接点(Spring AOP中为方法的执行) |
@target | 匹配特定的连接点(Spring AOP中为方法执行),执行方法的类拥有指定类型的注解 |
@args | 匹配特定的连接点(Spring AOP中为方法的执行),运行时传入的参数必须拥有特定类型的注解 |
@within | 用于匹配拥有特定注解的类型中的连接点 |
@annotation | 用于匹配拥有特定注解的连接点(Spring AOP中为方法的执行) |
bean | Spring AOP扩展的切入点,可以匹配特定的类名中的连接点(方法的执行) |
4.1.1 通配符(Wildcards)
在使用切入点表达式的时候,有些时候我们可以使用通配符:*、..、+。
符号 | 含义 |
---|---|
.. | 在类型匹配时,匹配任何以.,以.结尾的包名。在方法定义时匹配任意数量的参数。 |
+ | 匹配给定类型的任意子类型。 |
* | 匹配数量的任意字符,除了*字符。 |
4.1.2 类型(Type)指示符
通过类型来过滤方法,比如接口、类名或者是包名。Spring提供within
切入点,使用方式如下。type name
可以被替换为package name
或者class name
。
within(<type name>)
以下是一些例子:
-
within(com.xyz.web..*)
:匹配com.xyz.web
包下面的所有类中方法的执行,而且因为使用了..通配符,所以可以匹配com.xyz.web
的所有子包。*通配符匹配所有的类名,所以可以匹配所有类中方法的执行。 -
with(com.xyz.web.*)
:匹配com.xyz.web
包下面所有类中方法的执行。因为没有使用..通配符,所有只是匹配到web
包,不包括子包。*一样匹配所有的类名。 -
with(com.xyz.service.AccountService)
:匹配AccountService
类下面所有方法的执行。 -
with(com.xyz.interface.MyServiceInterface+):匹配所有实现了
MyServiceInterface`接口的类中的所有方法的执行。 -
with(com.xyz.service.MyBaseService+):匹配
MyBaseService`类和它的子类。
4.1.3 方法(Method)指示符
匹配特定方法的执行,可以使用execution
关键字。execution表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern
declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
其中,所有的部分除了返回值类型(ret-type-pattern)、方法名(name-pattern)和参数(param-pattern)都是可选的。修饰符(modifiers-pattern)可以是public
、protected
或private
,也可以使用*
匹配所有的修饰符。返回值类型(ret-type-pattern)匹配特定的返回类型。大多数情况都是使用*
通配符匹配所有的返回类型。方法名(name-pattern)匹配执行方法的名称,可以使用*
通配符匹配任意数量够的字符。如果要匹配特定类中方法的执行,就必须指定类名(declaring-type-pattern)部分,这部分使用的格式参考4.1.2 类型指示符。参数列表(param-pattern)部分,指定方法的参数必须满足的格式。()
匹配没有参数的方法,(..)
匹配任意数量的参数。当然你也可以使用*
匹配任意一个参数的类型,比如(\*, String)
匹配第二个参数为String
类型,第一个参数为任意类型的情况。异常列表(throws-pattern)匹配全限定类名异常类型,如果有多个异常,使用,
分割,比如throws java.lang.IllegalArgumentException, java.lang.ArrayIndexOutOfBoundsException
。
示例如下:
-
execution(public * *(..))
:匹配所有的public
方法的执行。 -
execution(* set*(..))
:匹配所有方法名以set开头的方法的执行。 -
execution(* com.xyz.service.AccountService.*(..))
:匹配AccountService
接口下所有方法的执行。 -
execution(* com.xyz.service.*.*(..))
:匹配包com.xyz.service
下所有类(或接口)下的所有方法的执行。 -
execution(* com.xyz.service..*.*(..))
:匹配包com.xyz.service
和其子包中的类(或接口)下的所有方法的执行。 -
execution(* *(.., String)
:匹配所有最后一个参数为String
的方法的执行。 -
execution(* ..Sample+.sampleGenericCollectionMethod(*))
:匹配任意以.Sample结尾的包,以及其子包中的sampleGenericCollectionMethod
方法的执行,且具有唯一的任意类型的参数。 -
execution(* *(*, String, ..)
:匹配第一个参数为任意类型,第二个参数为String
,后面可拥有任意个参数的方法的执行。
4.1.4 其他的切入点指示符
-
bean(*Service)
:所有bean名称以Service结尾的bean。 -
@annotation(org.springframework.transaction.annotation.Transactional)
:匹配所有连接点(Spring AOP中方法的执行)拥有@Transaction
注解在方法上。 -
this(com.xyz.service.AccountService)
:匹配实现了AccountService
接口的代理类中的所有连接点(Spring AOP中方法的执行) -
target(com.xyz.service.AccountService)
:匹配实现了AccountService
接口的目标类的所有连接点(Spring AOP中方法的执行)
4.1.5 组合多个切入点
很多时候可能一个切入点并不能满足我们的需求,这时候就需要组合使用切入点来限制匹配的切入点。在Spring AOP中可以使用and
(&&
)、or
(||
)和not
(!
)。既可以使用文字的形式也可以使用符号的形式。比如 execution(* concert.Performance.perform(..)) && within(concert.*))
。
4.2 定义切面(@Aspect)
现在,我们可以使用我们学到的来定义切面了。在Spring中可以使用XML和注解的方式来定义切面,本文将只讨论使用注解定义切面的方式。Spring支持使用AspectJ的注解@Aspect
来定义切面,就像清单2所使用的那样。但是,如果我们要让Spring知道我们定义了一个切面的话,还必须把这个切面声明为一个Bean。所以我们使用@Component
注解,这样Spring就会识别它,并把它当做切面来看待了。
@Component
@Aspect
public class AdviceExampleAspect {}
4.3 定义切点(@Pointcut)
切点(Pointcut)定义使用@Pointcut
注解。注解中我们就可以使用4.1 切点中的表达式来选择连接点。定义切点时,可以组合多个切点。
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}
// 组合切点
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
4.4 定义通知
在清单2中我们已经定义了@Before
和@After
通知,也可以使用其他类型的通知。这里主要介绍如何使用@Around
注解,定义环绕(Around)通知类型。
package org.archerie.aop.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class AdviceAroundAspect {
@Pointcut("execution(** org.archerie..*Bean.*(..))")
public void beanPointCut() {}
@Around("beanPointCut()")
public void watchBean(ProceedingJoinPoint jp) {
try {
System.out.println("手机静音!");
jp.proceed();
System.out.println("鼓掌!鼓掌!");
} catch (Throwable e) {
System.out.println("投诉!投诉!");
}
}
}
在Around通知类型中,我们可以定义具体方法运行前或者后的逻辑。
4.5 使用参数
目前为止,我们都没有在通知中使用参数,如果我们想在通知中获取代理方法的参数,就要使用args
切点标识符了。就像清单2中第二个@Before
通知一样。详细使用请参考Spring AOP文档。
五、总结
Spring AOP能够让我们在处理跨多个类型和对象的切点时很方便。使用Spring AOP能够实现代码的解耦,增强和维护性。在Spring中事务管理等都用到了AOP。
六、AspectJ问题列表
这里记录自己一些AspectJ的问题的回答,做个备忘吧。
6.1 call
vs execution
call
和execution
都是匹配方法的执行。区别在于call
表示方法被调用,execution
表示方法真正执行。具体体现在两点:
第一,当和within
或withincode
一起使用时会有不同。call(void m()) && withincode(void m())
会捕获任何递归调用。而execution(void m()) && withincode(void m())
不会,因为方法中递归调用自己,也还是在方法的执行中。所以只捕获一次,就像execution(void m())
一样。
第二,call
连接点不会捕获super calls to non-static methods,关于这点,我也不是很明白。
6.2 this()
vs target()
this(Atype)
匹配所有this instanceof Atype
的情况。
target(Atype)
匹配所有anObject instanceof Atype
的情况。如果你在一个对象中调用一个方法,而且这个对象符合instanceof Atype
,那么这就是一个合法的连接点。
-
execution( void Dog.bark() ) && this( thisObject ) && target( targetObject )
:thisObject
会是一个Dog
实例,targetObject
也会是一个Dog
实例。因为,execution
表示方法真正执行的时候,所以这两个对象都是一样的。 -
call( void Dog.bark() ) && this( thisObject ) && target( targetObject )
:targetObject
表示目标对象,所以还是Dog
实例。但是,thisObject
将会是任何一个调用dog.bark()
方法的对象,所以可能是任何类型。因为,call
表示方法在调用的时候。
七、参考
《Spring in Action Fourth Edition Edition》
10. Aspect Oriented Programming with Spring
Join Points and Pointcuts
Spring Aop详尽教程
AspectJ: this() vs. target()
Spring AOP target() vs this()