[TOC]
概述
在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern),一般而言这些横切关注点与具体的业务逻辑是相互分离的,如记录日志、控制事务等,往往被嵌入到业务代码中。AOP就是为了将这些横切关注点和业务逻辑分离。
在IOC容器中,DI实现了对象之间的依赖解耦,aop可以实现横切关注点和与他们所影响的对象之间的解耦。日志是应用切面的常见范例,除此之外切面还适用于多个场景,如声明式事务、安全和缓存。
AOP术语
切面能帮助我们模块化横切关注点,并使之能够复用。在面向对象的设计里,复用功能的实现方式有继承和委托。但是如果在整个应用中使用相同的基类,继承往往会导致一个脆弱的对象体系,而委托可能需要对委托对象进行复杂的调用。
面向切面编程(AOP)的切面,就是分散在应用中的横切关注点,AOP的目的是将横切逻辑从业务逻辑中抽离出来,再通过声明的方式定义这个功能要在何处以何种方式使用,而无需修改受影响的类。
这样做有两个好处,一方面是横切逻辑代码能够集中于一处,便于维护,另外业务代码可以专注业务逻辑。
描述切面的常用术语:
- 通知(advice)
- 切点(pointcut)
- 连接点(joinpoint)
实际上在AOP中,本质上只是要定义一个通用的行为以及此行为要切入的位置和时机。通知描述了一个切面的动作和触发时机,切点和连接点则描述切入的位置。(Spring AOP只支持方法级别的切入,所以位置只有方法级别的)
通知(advice)
通知,advice,就是动作。通知定义了切面是什么,以及何时使用,除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。Spring切面可以使用五种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用advice
- 后置通知(After): 在目标方法调用之后调用advice,不会关心方法的输出是什么
- 返回通知(After-returning):在目标方法成功执行之后调用advice
- 异常通知(After-throwing):在方法抛出异常之后调用通知
- 环绕通知(Around):advice包裹了被通知的方法,在被通知的方法调用之前和之后执行自定义的行为,这是权限最高的advice
连接点(joinpoint)
连接点是应用执行过程中能够插入一个切面的一个点,这个点可以是调用方法时、抛出异常时、甚至是修改一个字段时,切面代码可以利用这些点插入到应用的正常流程之中,来添加新的行为。
连接点似乎和切面切入的时机相关,和位置似乎无关,其实不是的,程序中代码的执行时机和位置是相互关联的,只有执行中的应用才有切入的意义,所以连接点定义一个时机,实际上也就是代码的位置,只是在运行时位置和时机就等价了。
切点(pointcut)
切点的定义会匹配所要织入的一个或多个连接点。我们通常使用明确的类或者方法名称,或是利用正则表达式定义所要匹配的类和方法名称来指定这些切点。
切面(aspect)
通知和切点的结合就是一个切面。切点和通知定义了位置、执行时机和行为。
引入(introduction)
引入允许我们向现有的类添加新方法或属性。
织入(weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多少个点可以进行织入:
- 编译期
切点在目标类编译时被织入,此方式需要特殊的编译器,AspectJ的织入编译器就是以这种方式织入切面的。
- 类加载期
切面在目标诶加载到JVM时被织入,这种方式需要特殊的类加载器,它可以在目标了被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入就支持这种方式织入切面。
- 运行期
切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,aop容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。
AOP代理
由AOP框架创建的用来实现切面逻辑(切面切入目标执行)的对象,在Spring AOP中,这个对象是jdk动态代理或CGLIB生成的代理对象。
Spring对AOP的支持
Spring提供了4种类型的AOP支持:
- 基于代理的经典SpringAOP
- 纯pojo切面
- @AspectJ注解驱动的切面
- 注入式AspectJ切面(使用AspectJ扩展来实现AOP)
Spring AOP的局限:
- Spring AOP构建在动态代理的基础之上,因此Spring对AOP的支持局限于方法拦截。
- Spring通知是java编写的
- Spring在运行时通知对象,直到应用需要被代理的bean时,Spring才创建代理对象。
Spring AOP in action
定义切面
切面由advice和pointcut组成。
声明Aspect
使用@Aspect声明一个POJO为aspect类,但是使用时还需加上@Component来交给Spring管理,或者使用xml方式,总之要注册成bean,否则不能被Spring识别。
- 基于注解风格的Aspect
@Component
@Aspect
public class UserServiceAspect {
// ...
}
声明advice
对应五种advice,有五种注解:
- @Before
- @AfterReturning
- @AfterThrowing
- @After
- @Around
Advice的注解是标注在方法上的,这个方法的逻辑是切入到切点的横切逻辑。注解的含义是发生的时机,如before、after等,注解内需要注明此advice对于的PointCut,即切点的定义,主要一个完整的横切逻辑的定义才完整(动作、时机、位置)。
// 引用类中定义的pointcut方法
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
@Around("execution(* com.springinaction.aop.DemoService.print(String))")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
Around的优先级高于其他的advice,如果在Around的advice中没有调用proceed方法,目标方法不会被调用。故Around的advice方法第一个参数需要设置为ProceedingJoinPoint。
定义切点
Spring借助AspectJ的切点表达式语言来定义Spring切点:
AspectJ指示器 | 描述 |
---|---|
args() | 限制匹配的方法的参数,更常用的是绑定形式,将方法参数传递给advice |
@args() | 限制匹配方法的参数由指定注解标注 |
execution() | 匹配方法 |
this() | 匹配目标对象的类型 |
target() | 匹配指定类型 |
@target() | 匹配目标对象且类上有指定类型的注解 |
within() | 匹配对象的类 |
@within() | 匹配注解标注的类型 |
@annotation() | 匹配指定注解 |
除以上之外的AspectJ指示器都不可用,会抛出IllegalArgumentException。
另外Spring引入了一个bean()
指示器,运行切点通过beanid来匹配bean。
例:
-
execution(* com.test.Test.method(..) )
:匹配任意参数的任意返回值的 com.test.Test类的method()方法 -
execution(* com.test.Test.method(..) && within(com.*))
:匹配com.test.Test.method方法和com包下任意方法。
使用多个指示器时,可以使用and
,or
,not
或者对应的&&
,||
,!
进行连接(not是条件取反)。
advice方法和Joinpoint交互
通过给advice方法声明JoinPoint 参数来获取当前的joinpoint,另外Around类型的advice使用ProceedingJoinPoint
(它是JoinPoint类的子类,是@Around方法的必选参数)。
传递方法参数到advice
关于前述的多个指示器,官方reference有这些说明:
- 'this' is more commonly used in a binding form
- 'target' is more commonly used in a binding form
- 'args' is more commonly used in a binding form
- '@target' can also be used in a binding form
- '@within' can also be used in a binding form
- '@annotation' can also be used in a binding form
- '@args' can also be used in a binding form
也就是说除了within()和execution(),其余的都可以用来绑定参数。
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
or
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
or @annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
or generic type
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}
启用AOP
- JavaConfig方式
@Configuration
@ComponentScan // 配置自动扫描,默认是本类所在的包及子包
@EnableAspectJAutoProxy // 启用Aspectj自动代理 ,不开启AOP无效
public class RootConfig {
}
Sample
// configuration
@Configuration
@ComponentScan // 配置自动扫描,默认是本类所在的包及子包
@EnableAspectJAutoProxy // 启用Aspectj自动代理 ,不开启AOP无效
@PropertySource("classpath:/conf.properties")
public class SpringAopConfig {
@Autowired // 可以将Property注入到env中来
private Environment env;
@Bean
public PrintStream stream() {
return System.out;
}
/**
* Java中配置占位符的方式,需要此Bean和上面的@PropertySource("classpath:/conf.properties")注解支持,
* 而后可以在Component中使用{@value使用占位符注入属性值}
*
* @return
*/
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
public Environment env() {
return env;
}
}
// Service
@Service
public class DemoService {
@Autowired
private PrintStream stream;
public void print(String msg) {
stream.println(msg);
}
}
// Aspect
@Component
@Aspect
public class DemoAspect {
@Before("execution(* com.springinaction.aop.DemoService.print(String))")
public void beforePrint() {
System.out.println("--beforePrint--");
}
@Before("execution(* com.springinaction.aop.DemoService.print(String)) && args(msg)")
public void beforePrintwithArgs(String msg) {
System.out.println("--beforePrintwithArgs--");
System.out.println("in advice get msg:" + msg);
}
@Pointcut("execution(* com.springinaction.aop.DemoService.print(String)) && args(msg)")
public void print(String msg) {
}
@Around("print(msg)")
public void holdPrint(/* ProceedingJoinPoint jp, */String msg) throws Throwable {
System.out.println("holdPrint");
// jp.proceed(); // 注掉就不会进入目标方法
// jp.proceed(new Object[] { "what" });
System.out.println("holdPrint end");
}
}
// Test
@Test
public void testPrint() {
ApplicationContext context = new AnnotationConfigApplicationContext(SpringAopConfig.class);
DemoService service = context.getBean(DemoService.class);
System.out.println(service);
service.print("hello aop");
}
Around方法的执行内会环绕在before、after等方法的执行之外。
给目标对象引入新功能(introduction)
使用@DeclareParents注解为bean引入接口方法
// 增强接口
public interface Enhance {
public void enhance();
}
// 增强接口的默认实现
public class DefaultEnhance implements Enhance {
@Override
public void enhance() {
System.out.println("this is an enhance");
}
}
// 增强Aspect
@Component
@Aspect
public class Introducer {
@DeclareParents(value = "com.springinaction.aop.DemoService", defaultImpl = DefaultEnhance.class)
public Enhance defaultEnhance;
}
// Test
@Test
public void testIntroducer() {
ApplicationContext context = new AnnotationConfigApplicationContext(SpringAopConfig.class);
Enhance service = (Enhance) context.getBean(DemoService.class);
service.enhance();
((DemoService) service).print("tets");
}
小结
Spring AOP的原理是动态代理;
Spring AOP只能拦截方法;
Spring AOP的使用方法包括启动aop支持、定义横切逻辑两大部分。定义横切逻辑是定义切面和advice,用于拦截目标方法。在定义横切逻辑方面,需求大致在于方法调用前、执行正常返回、执行抛异常、执行返回(finally)来添加与方法内实质逻辑无关的逻辑,另外还有一个强大的Around advice能够包裹目标方法,可以干涉方法的执行过程。
Advice在拦截目标方法时,可以获取到方法的参数,Around方法甚至可以对参数进行操作来修改参数。