在Spring中使用AOP的详细资料可以查看这里
在Java中,从织入切面的方式上来看,存在三种织入方式:编译时织入、加载时织入和运行时织入
指在Java编译期,采用特殊的编译器, 将切面织入到Java类中,即发生在从java文件到class文件的过程.
这种方式将切面直接编译进了字节码,所以运行时不再需要动态创建代理对象, 节约了内存和CPU, 但编译过程复杂(可借助Maven AspectJ插件),编写aspect文件(.aj文件)复杂
指通过特殊的类加载器(如AspectJ compiler), 在JVM载入字节码文件时, 织入切面, 即发生在class文件加载的过程.
具体可参考这里
采用CGLib工具或JDK动态代理进行切面的织入, 如Spring AOP
asm.jar
的字节码增强技术API, 开源的, 也是个jar包:cglib.jar
cglib.jar
包, 因为它里面的类已经被spring-core.jar
包含了在怎么使用之前,最好先看看相关概念,有关Joinpoint
、Pointcut
、Advice
等概念,看这里
在Spring中使用@AspectJ
可以在配置文件中启用<aop:aspectj-autoproxy/>
, 甚至启用LTW机制<context:load-time-weaver/>
, 对应的注解分别为@EnableAspectJAutoProxy
和@EnableLoadTimeWeaving
在带有@AspectJ
注解的类上同时加上@Component
(声明为一个bean)注解并确保被自动扫描, 这样才会被Spring识别并管理
切入点决定了连接点关注的内容,使得我们可以控制通知什么时候执行, 即从哪里把要做的操作(通知)切入进去
一个切入点声明有两个部分: 切入点签名 和 切入点表达式
在@AspectJ
注解风格的AOP中, 切入点签名 通过一个普通的方法定义来提供, 该方法必需反回void
类型;
切入点表达式 使用@Pointcut
注解来表示(内容略多, 后面讲), 一个切入点声明如下:
@Pointcut(value="execution(* sayAdvisorBefore(..)) && args(param)", argNames = "param")
public void pointcutName(String param) {}
execution
、args
等,
分隔,这些参数将传递给通知方法同名的参数;@AspectJ
风格的声明通知支持5种通知类型:
@Before
: 前置通知,执行连接点方法之前执行@AfterReturning
: 后置返回通知, 一个匹配的方法返回的时候执行@AfterThrowing
: 异常通知, 在一个方法抛出异常后执行@After
: 最终通知, 不论一个方法是如何结束的,最终通知都会运行, 最终通知必须准备处理正常返回和异常返回两种情况@Around
: 环绕通知, 在一个方法执行之前和之后执行, 而且它可以决定这个方法在什么时候执行,如何执行,甚至是否执行. 通知的第一个参数必须是ProceedingJoinPoint
类型,在通知体内,调用ProceedingJoinPoint
的proceed()
方法会导致 后台的连接点方法执行如果在同一个连接点上执行多个通知,可以使用@Order
注解决定其执行顺序
下面是通知的使用方式:
@Before(value = "切入点表达式或命名切入点", argNames = "参数列表参数名")
@After(value = "切入点表达式或命名切入点", argNames = "参数列表参数名")
@Around(value = "切入点表达式或命名切入点", argNames = "参数列表参数名")
@AfterReturning(
value = "切入点表达式或命名切入点",
pointcut = "切入点表达式或命名切入点", // 如果指定了将覆盖value属性指定的,pointcut具有高优先级
argNames = "参数列表参数名",
returning = "返回值对应参数名")
@AfterThrowing(
value = "切入点表达式或命名切入点",
pointcut = "切入点表达式或命名切入点",
argNames = "参数列表参数名",
throwing = "异常对应参数名")
来个小例子:
@Aspect
public class BeforeExample {
@Before(value = "pointcutName(param)", argNames = "param")
public void beforeAdvice(String param) { // 切入点匹配到的参数将传递过来
System.out.println(param);
}
}
上例使用@Before
进行前置通知声明,其中value用于定义切入点表达式或引用命名切入点
通知方法可以获取被通知方法的参数,主要是通过JoinPoint
(环绕通知是JoinPoint
的子类ProceedingJoinPoint
)来获取, JoinPoint
必须是第一个参数, Spring会自动传入.
JoinPoint
的声明如下:
public interface ProceedingJoinPoint extends JoinPoint {
public Object proceed() throws Throwable; // 执行连接点的方法
public Object proceed(Object[] args) throws Throwable; // 执行连接点的方法,可以把原来的参数用新的args替换掉
}
// 如果我这么声明切点: @Pointcut("within(com.test.spring.bean.Hello))")
public interface JoinPoint {
String toString(); // execution(String com.test.spring.bean.Hello.hello(String))
String toShortString(); // execution(Hello.hello(..))
String toLongString(); // execution(public java.lang.String com.test.spring.bean.Hello.hello(java.lang.String))
// 上面这几个toString, 是打印切点相关信息, 注意上面是用 within声明 而打印出来的是 execution.
Object getThis(); //返回AOP代理对象
Object getTarget(); //返回目标对象(被代理的对象)
Object[] getArgs(); //返回被通知方法参数列表
Signature getSignature(); //返回当前连接点签名
SourceLocation getSourceLocation();//返回连接点方法所在类文件中的位置
String getKind(); //连接点类型
StaticPart getStaticPart(); //返回连接点静态部分
// 这是个内部接口, 提供访问连接点的静态部分,如被通知方法签名、连接点类型等
public interface StaticPart {
Signature getSignature(); //返回当前连接点签名
String getKind(); //连接点类型
int getId(); //唯一标识
String toString(); //连接点所在位置的相关信息
String toShortString(); //连接点所在位置的简短相关信息
String toLongString(); //连接点所在位置的全部相关信息
}
}
Signature
中的部分方法说明如下:
public interface Signature {
// Signature代表的是切点处的签名信息
String toString(); // String com.test.spring.bean.Hello.hello(String)
String toShortString(); // Hello.hello(..)
String toLongString(); // public java.lang.String com.test.spring.bean.Hello.hello(java.lang.String)
String getName(); // hello 可参考:java.lang.reflect.Member.getName
}
切入点表达式就是组成@Pointcut
注解的值, 用于匹配具体的连接点.
切入点表达式由切入点指示符、类型匹配语句、通配符、组合符组成
切入点指示符(PCD)
在切入点表达式中可以使用如下的AspectJ切入点指示符(PCD):
看不明白还是看最后面的例子吧
类型匹配语句
类型匹配语句格式像下面这样(带?
的属于可选,可以不写):
注解? 类的全限定名字
注解? 修饰符? 返回值类型 类型声明? 方法名(参数列表) 异常列表?
类型匹配的通配符
*
: 匹配任何数量字符;..
:(两个点)匹配任何数量字符的重复;如在类型模式中匹配任何数量子包,而在方法参数模式中匹配任何数量参数+
: 匹配指定类型的子类型,仅能作为后缀放在类型模式后边组合切入点表达式
AspectJ使用 与(&&)、或(||)、非(!)来组合切入点表达式, 在xml文件中可使用and、or、not
execution
使用execution(方法表达式)
匹配方法执行表达式 | 描述 |
---|---|
public * *(..) | 所有public方法 |
* cn.test..IService.*() | cn.test及子包下IService中任何无参方法 |
* cn.test..IService+.*() | cn.test及子包下IService及子类中任何无参方法 |
* cn.test..IService.*(*) | cn.test及子包下IService中只有一个参数的方法 |
* cn.test..IService.*(..) | cn.test及子包下IService中所有方法 |
* cn.test..IService.*(java.util.Date) | cn.test及子包下IService中只有一个Date类型参数的方法 |
* cn.test..IService.test(..) | cn.test及子包下IService前缀类型中test前缀开头的任何方法 |
* cn.test...(..) | cn.test及子包下任何类的任何方法 |
@java.lang.Deprecated * *(..) | 任何持有@Deprecated注解的方法 |
@(java.lang.Deprecated && cn.javass..Secure) * *(..) | 任何持有@java.lang.Deprecated和@ cn.javass..Secure注解的方法 |
within
使用within(类型表达式)
匹配指定类型内的方法执行表达式 | 描述 |
---|---|
within(cn.test..*) | cn.test及子包下的任何方法 |
within(cn.test..IService+) | cn.test及子包下IService及子类的任何方法 |
within(@cn.test.Secure *) | cn.test及子包下带有@cn.test.Secure注解的任何类(接口不行)的任何方法 |
this
使用this(类型全限定名)
匹配当前AOP代理对象类型的执行方法,包括引入接口,不支持通配符表达式 | 描述 |
---|---|
this(cn.test.IService) | 当前AOP对象实现了IService接口的任何方法 |
target
使用target(类型全限定名)
匹配当前目标对象类型的执行方法,不包括引入接口,不支持通配符表达式 | 描述 |
---|---|
target(cn.test.IService) | 当前目标对象(非AOP对象)实现了IService接口的任何方法 |
args
使用args(参数类型列表)
匹配传入参数(不是声明时的参数)为指定类型的执行方法,参数类型必须是全限定名, 不支持通配符args属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用
表达式 | 描述 |
---|---|
args(java.lang.String, ..) | 第一个参数为String,后面有任意个参数的方法 |
@within
使用@within(注解类型全限定名)
匹配所有持有指定注解的类里面的方法, 即要把注解加在类上表达式 | 描述 |
---|---|
@within(cn.test.Secure) | 任何目标对象对应的类型持有Secure注解的类方法 |
@target
使用@target(注解类型全限定名)
匹配当前目标对象类型的执行方法, 必须是在目标对象上声明注解,在接口上声明不起作用表达式 | 描述 |
---|---|
@target(cn.test.Secure) | 任何目标对象对应的类型持有Secure注解的类方法 |
@args
使用@args(注解类型全限定名)
匹配当前执行的方法传入的参数持有指定注解的执行表达式 | 描述 |
---|---|
@args(cn.test.Secure) | 任何只接受一个参数的方法,且方法运行时传入的参数持有Secure注解 |
@annotation
使用@annotation(注解类型全限定名)
匹配持有指定注解的方法, 即要把注解加在方法上才管用表达式 | 描述 |
---|---|
@annotation(cn.test.Secure) | 当前执行方法上持有Secure注解的方法 |