相关文章:
一、简介
面向切面编程英文全称:Aspect-Oriented Programming (AOP)是面向对象编程(OOP)的补充。具体概念就不赘述。
1.1 AOP概念
我们首先学习一些AOP的核心概念和专业术语。这些并不是Spring特有的,不幸的是AOP的术语不是特别直观,如果Spring用它自己的术语会更让人难以理解。
- Aspect 切面。事务管理是一个很好的例子。在Spring里切面实现特殊类或者使用
@Aspect
注解的类。 - Join point 在Spring AOP中join point代表一个方法的执行。
- Advice 特定Join point产生的通知。例如:"around","before","after",就像拦截器。
- Pointcut 一种匹配Join point的描述。Advice将会作用于所有符合Pointcut描述的Join point
- Introduction 声明一些方法或者成员变量在某方面为一个类型。
Advice
的类型:
- Before advice:在一个Join point(以后都说方法)前执行,但是它没有能力阻止不执行这个方法,除非它发生异常。
- After returning advice:当一个方法正常完成后执行。
- After throwing advice:当一个方法抛出异常后执行。
- After (finally) advice:当一个方法完成后执行,不管正常还是异常。
- Around advice:在一个方法执行前和执行后调用。
二、@AspectJ支持
Spring的注解和AspectJ 5的一样,但是没有任何依赖AspectJ编译器。
2.1 启用@AspectJ支持
通过Java配置
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
通过XML配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd">
<aop:aspectj-autoproxy />
2.2 声明一个切面
首先创建一个类:
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
将它声明为一个bean:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of aspect here as normal -->
</bean>
如果配置了自动扫描发现bean的方式也可以这样:
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
@Component
public class NotVeryUsefulAspect {
}
切面可以有其他的方法和成员变量,和其他的类一样,只是多了@Aspect
注解。
2.3 声明一个切点
Spring AOP只支持在Spring beans的方法执行声明一个切点。有一个切点的声明包含两部分:一个方法签名和一个切点表达式,准确的匹配哪些方法是我们感兴趣的。在@AspectJ
注解风格的切面编程里,@Pointcut
注解表示切点(这个注解相关的方法必须使用void
返回类型)。
下面是一个例子:
@Pointcut("execution(* transfer(..))")// 切点表达式
private void anyOldTransfer() {}// 切点方法签名
这个切点将会匹配所有名字为transfer
的方法(当然是在Spring beans里)。完整的切点语法规则参见AspectJ Programming Guide。
支持的切点标识符
切点标识符就是上面例子中:execution
部分,下面是支持的列表:
- execution 用来匹配方法执行,这个将会是你在Spring AOP里主要用到的标识符
- within 只匹配指定类型(不太好理解,后面有例子,就是根据包名匹配)
- this 只匹配引用是给定类型的一个实例的方法(还是看后面的例子吧)
- target 只匹配目标对象是给定类型的一个实例的方法
- args 只匹配参数是给定类型实例的方法
@target
、@args
、@within
、@annotation
这些都是根据注解来匹配的。
注意,Spring AOP是一个基于代理的框架,被保护的方法不会被拦截,所以上面声明的切点只会对public
方法起作用。
联合切点表达式
可以使用&&
、||
、!
这些符号来联合切点表达式。还可以通过切点的名字来引用切点。请看下面的例子:
@Pointcut("execution(public * (..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.someapp.trading..)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
anyPublicOperation
切点匹配所有public
的方法。
inTrading
切点匹配所有com.xyz.someapp.trading
包下的方法。
tradingOperation
切点是上面两个切点的交集。
最佳实践是用小的切点表达式来构成复杂的切点表达式,就像上面的例子。
一些通用的切点定义
在企业环境中,你会经常引用应用程序的各个模块。我们推荐定义一个"系统架构"切面,捕获通用的切点。一个典型的切面看起来像下面这个类:
package com.xyz.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
/
* 一个web层的切点,匹配任何在com.xyz.someapp.web包下的方法
*/
@Pointcut("within(com.xyz.someapp.web..)")
public void inWebLayer() {}
/
* 一个service层的切点,匹配任何在com.xyz.someapp.service包下的方法
*/
@Pointcut("within(com.xyz.someapp.service..)")
public void inServiceLayer() {}
/
* 一个数据访问层的切点,匹配任何在com.xyz.someapp.dao包下的方法
*/
@Pointcut("within(com.xyz.someapp.dao..)")
public void inDataAccessLayer() {}
/
* 匹配在service包下的方法,例如com.xyz.someapp.user.service.addUser()这个方法
*/
@Pointcut("execution( com.xyz.someapp..service..(..))")
public void businessService() {}
/*
* 匹配在dao包下的方法
*/
@Pointcut("execution( com.xyz.someapp.dao..(..))")
public void dataAccessOperation() {}
}
2.4 一些列子
Spring AOP的用户可能和切点表达式交往最频繁。一个方法执行表达式的格式为:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?
name-pattern(param-pattern) throws-pattern?)
?表示是可选的。
- modifiers-pattern? 修饰符部分
- ret-type-pattern 返回类型部分
- declaring-type-pattern? 声明部分
- name-pattern 名称部分
- param-pattern 参数部分
- throws-pattern? 异常抛出部分
*
表示匹配某任意部分。(..)
表示任意数量的参数。(*,String)
表示匹配参数数量是2,第一个参数是任意类型,第二个参数必须是String
的方法。
下面是一些常见的切点表达式:
- 任意公开方法的执行
execution(public * *(..))
- 以set开头的任意方法
execution(* set*(..))
- 定义在service包下的任意方法
execution(* com.xyz.service..(..))
- 定义在service包以及子包下的任意方法
execution(* com.xyz.service...(..))
- service包里的任意切点 (method execution only in Spring AOP)
within(com.xyz.service.*)
- 一个参数是Serializable的任意切点 (method execution only in Spring AOP)
args(java.io.Serializable)
- 切点匹配的方法上有@Transactional的注解
@annotation(org.springframework.transaction.annotation.Transactional)
译者注:很多我也没实践过,以后实践了再加些更通俗的解释吧。
2.5 声明通知
通知(Advice)是和切点表达式关联的。在切点表达式匹配的方法之前、之后或者前后执行。
前置通知
前置通知在一个切面里使用@Before
注解:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
com.xyz.myapp.SystemArchitecture
是上面的一个例子:
@Pointcut("execution( com.xyz.someapp.dao..(..))")
public void dataAccessOperation() {}
或者直接用切点的表达式:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao..(..))")
public void doAccessCheck() {
// ...
}
}
方法返回后通知
如其字面意思,当一个匹配的方法执行并正常返回后执行。它使用@AfterReturning
注解声明:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
注意:通知和切点完全可以写在一个切面里。
有时你需要访问方法返回的值。你可以使用下面这种形式绑定返回值:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
returning
属性的名字必须与通知方法的参数名字保持一致。returning
项目还会约束方法返回值的类型(Object
在这里将会匹配任何返回值,如果是String
,将只会匹配String
类型的返回值)
方法抛出异常后通知
当切点匹配的方法抛出异常后执行的通知。使用@AfterThrowing
注解:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
如果想只获得指定类型的异常,可以使用throwing
属性,和上面的returning
类似:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
后置通知
只要是一个切点匹配的方法退出了就会执行方法后通知,不管是否成功返回。它使用@After
注解。你必须同时考虑到正常和异常的情况。一个典型的应用就是释放资源等:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
环绕通知
环绕通知经常被用在需要在一个线程安全的方法执行前后共享状态。总是使用能满足你需求的能力最弱的切面(例如,如果前置通知就可以完成的不要使用环绕通知)。
环绕通知使用@Around
注解来声明。通知方法的第一个参数必须是ProceedingJoinPoint
类型。在通知方法体内调用ProceedingJoinPoint
的proceed()
方法会执行切点所匹配的方法。proceed()
方法也可以接收一个Object[]
类型的参数给切点所匹配的方法。
译者注:proceed()
方法之前相当于前置通知处理的范围,proceed()
方法之后相当于后置通知处理的范围。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// retVal是方法的返回值
Object retVal = pjp.proceed();
return retVal;
}
}
访问当前的切点
任何通知的方法都可以声明一个org.aspectj.lang.JoinPoint
类型的参数作为它的第一个参数(环绕通知中的ProceedingJoinPoint
是JoinPoint
的子类,所以可以作为环绕通知的第一个参数)。JoinPoint
接口提供了很多实用的方法例如:
- getArgs() 返回切点匹配方法的参数
- getThis() 返回代理对象
- getTarget() 返回目标对象
- getSignature() 返回通知关联的切点方法的签名
未完,待续...