1.术语解释
连接点(Joinpoint)
程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后。一个类或一段程序代码拥有一些具有边界性质的特定点,这些点中的特定点就称为“连接点”。Spring仅支持方法的连接点,即仅能在方法调用前、方法调用后、方法抛出异常时以及方法调用前后这些程序执行点织入通知。连接点由两个信息确定:第一是用方法表示的程序执行点;第二是用相对点表示的方位。连接点是在应用执行过程中能够插入切面的一个点。
以查电表为例子:电力公司为多个住户提供服务,连接点就是每一家的电表所在的位置(类中的方法的调用前、调用后…)。
切点(Pointcut)
AOP通过“切点”定位特定的连接点。切点和连接点不是一对一的关系,一个切点可以匹配多个连接点。在Spring中,切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件,Spring AOP的规则解析引擎负责切点所设定的查询条件,找到对应的连接点。其实确切地说,不能称之为查询连接点,因为连接点是方法执行前、执行后等包括方位信息的具体程序执行点,而切点只定位到某个方法上,所以如果希望定位到具体连接点上,还需要提供方位信息。
电力公司为每一个抄表员都分别指定某一块区域的住户。切点就是划分的区域。
通知(Advice)
切面的工作被称为通知。是织入到目标类连接点上的一段程序代码。
Spring切面可以应用5种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能;
-后置通知(After):在目标方法完成之后调用通知,此时不会关心方 法的输出是什么;
-返回通知(After-returning):在目标方法成功执行之后调用通知;
-异常通知(After-throwing):在目标方法抛出异常后调用通知;
-环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调 用之前和调用之后执行自定义的行为。
抄表员的本职工作是登记用电量,但他们还需要向电力公司汇报的信息。
登记用电量是目标对象,汇报的信息就是通知。
引介(Introduction)
引入允许我们向现有的类添加新方法或属性,是一种特殊的通知。这样,即使一个业务类原本没有实现某个接口,通过AOP的引介功能,我们可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。
切面(Aspect)
切面由切点和通知(引介)组成,它既包括了横切逻辑的定义,也包括了连接点的定义。
抄表员的开始一天的工作时,他要知道从哪些区域(切点)收集信息,从而进行汇报(通知)。
织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。
AOP有三种织入的方式:
a、编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
b、类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。
c、运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。
2.例子:
//描述切面类
@Aspect
@Configuration
public class TestAop {
/*
* 定义一个切入点
*/
// @Pointcut("execution (* findById*(..))")
@Pointcut("execution(* com.test.service.CacheDemoService.find*(..))")
public void excudeService(){}
/*
* 通过连接点切入
*/
@Before("execution(* findById*(..)) &&" + "args(id,..)")
public void twiceAsOld1(Long id){
System.err.println ("切面before执行了。。。。id==" + id);
}
@Around("excudeService()")
public Object twiceAsOld(ProceedingJoinPoint thisJoinPoint){
System.err.println ("切面执行了。。。。");
try {
Thing thing = (Thing) thisJoinPoint.proceed ();
thing.setName (thing.getName () + "=========");
return thing;
} catch (Throwable e) {
e.printStackTrace ();
}
return null;
}
}
/*
* 定义一个切入点
*/
@Pointcut("@annotation(com.aiatss.coast.th.test.async.NeedTest)")
Public void exeServ(){}
//切入点是自定义注释@NeedTest
@AfterReturning(value="@annotation(com.aiatss.coast.th.test.async.NeedTest)", returning = "returnVal")
public void needTest(JoinPoint joinPoint, Object returnVal) {
Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
boolean value = method.getAnnotation(NeedTest.class).value();
if(value){
Logger logger = ((LogInterface)joinPoint.getThis()).getLogger();
logger.info("needTest() executed,some logic is here,return is {}", returnVal.toString());
}
}
3.使用的注解:
@Aspect:描述一个切面类,定义切面类的时候需要打上这个注解
@Configuration:spring-boot配置类
@Pointcut:声明一个切入点,切入点决定了连接点关注的内容,使得我们可以控制通知什么时候执行。Spring AOP只支持Spring bean的方法执行连接点。所以你可以把切入点看做是Spring bean上方法执行的匹配。一个切入点声明有两个部分:一个包含名字和任意参数的签名,还有一个切入点表达式,该表达式决定了我们关注那个方法的执行。
注:作为切入点签名的方法必须返回void 类型
Spring AOP支持在切入点表达式中使用如下的切入点指示符:
○ execution - 匹配方法执行的连接点,这是你将会用到的Spring的最主要的切入点指示符。
○ within - 限定匹配特定类型的连接点(在使用Spring AOP的时候,在匹配的类型中定义的方法的执行)。
○ this - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中bean reference(Spring AOP 代理)是指定类型的实例。
○ target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中目标对象(被代理的应用对象)是指定类型的实例。
○ args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中参数是指定类型的实例。
○ @target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中正执行对象的类持有指定类型的注解。
○ @args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中实际传入参数的运行时类型持有指定类型的注解。
○ @within - 限定匹配特定的连接点,其中连接点所在类型已指定注解(在使用Spring AOP的时候,所执行的方法所在类型已指定注解)。
○ @annotation - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中连接点的主题持有指定的注解。
其中execution使用最频繁,即某方法执行时进行切入。定义切入点中有一个重要的知识,即切入点表达式,我们一会在解释怎么写切入点表达式。
切入点意思就是在什么时候切入什么方法,定义一个切入点就相当于定义了一个“变量”,具体什么时间使用这个变量就需要一个通知。
即将切面与目标对象连接起来。
如例子中所示,通知均可以通过注解进行定义,注解中的参数为切入点。
spring aop支持的通知:
@Before:前置通知:在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。
@AfterReturning :后置通知:在某连接点正常完成后执行的通知,通常在一个匹配的方法返回的时候执行。
可以在后置通知中绑定返回值,如:
@AfterReturning(
pointcut=com.test.service.CacheDemoService.findById(..))",
returning="retVal")
public void doFindByIdCheck(Object retVal) {
// ...
}
@AfterThrowing:异常通知:在方法抛出异常退出时执行的通知。
@After 最终通知。当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
@Around:环绕通知:包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。
环绕通知最麻烦,也最强大,其是一个对方法的环绕,具体方法会通过代理传递到切面中去,切面中可选择执行方法与否,执行方法几次等。
环绕通知使用一个代理ProceedingJoinPoint类型的对象来管理目标对象,所以此通知的第一个参数必须是ProceedingJoinPoint类型,在通知体内,调用ProceedingJoinPoint的proceed()方法会导致后台的连接点方法执行。proceed 方法也可能会被调用并且传入一个Object[]对象-该数组中的值将被作为方法执行时的参数。
4.通知参数
任何通知方法可以将第一个参数定义为org.aspectj.lang.JoinPoint类型(环绕通知需要定义第一个参数为ProceedingJoinPoint类型,它是 JoinPoint 的一个子类)。JoinPoint接口提供了一系列有用的方法,比如 getArgs()(返回方法参数)、getThis()(返回代理对象)、getTarget()(返回目标)、getSignature()(返回正在被通知的方法相关信息)和 toString()(打印出正在被通知的方法的有用信息)
有时候我们定义切面的时候,切面中需要使用到目标对象的某个参数,如何使切面能得到目标对象的参数。
从例子中可以看出一个方法:
使用args来绑定。如果在一个args表达式中应该使用类型名字的地方 使用一个参数名字,那么当通知执行的时候对应的参数值将会被传递进来。
@Before("execution(* findById*(..)) &&" + "args(id,..)")
public void twiceAsOld1(Long id){
System.err.println ("切面before执行了。。。。id==" + id);
}
@Around("execution(List<Account> find*(..)) &&" +
"com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp, String accountHolderNamePattern)
throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
5.切入点表达式
现在我们介绍一下最重要的切入点表达式:
如上文所说,定义切入点时需要一个包含名字和任意参数的签名,还有一个切入点表达式,就是* findById*(…)这一部分。
切入点表达式的格式:execution([可见性] 返回类型 [声明类型].方法名(参数) [异常])
其中【】中的为可选,其他的还支持通配符的使用:
*:匹配所有字符
…:一般用于匹配多个包,多个参数
+:表示类及其子类
运算符有:&&、||、!
切入点表达式关键词:
1)execution:用于匹配子表达式。
//匹配com.cjm.model包及其子包中所有类中的所有方法,返回类型任意,方法参数任意
@Pointcut("execution(* com.cjm.model..*.*(..))")
public void before(){}
2)within:用于匹配连接点所在的Java类或者包。
//匹配Person类中的所有方法
@Pointcut("within(com.cjm.model.Person)")
public void before(){}
//匹配com.cjm包及其子包中所有类中的所有方法
@Pointcut("within(com.cjm..*)")
public void before(){}
3) this:用于向通知方法中传入代理对象的引用。
@Before("before() && this(proxy)")
public void beforeAdvide(JoinPoint point, Object proxy){
//处理逻辑
}
4)target:用于向通知方法中传入目标对象的引用。
@Before("before() && target(target)")
public void beforeAdvide(JoinPoint point, Object proxy){
//处理逻辑
}
5)args:用于将参数传入到通知方法中。
@Before("before() && args(age,username)")
public void beforeAdvide(JoinPoint point, int age, String username){
//处理逻辑
}
6)@within :用于匹配在类一级使用了参数确定的注解的类,其所有方法都将被匹配。
@Pointcut("@within(com.cjm.annotation.AdviceAnnotation)") - 所有被@AdviceAnnotation标注的类都将匹配
public void before(){}
7)@target :和@within的功能类似,但必须要指定注解接口的保留策略为RUNTIME。
@Pointcut("@target(com.cjm.annotation.AdviceAnnotation)")
public void before(){}
8)@args :传入连接点的对象对应的Java类必须被@args指定的Annotation注解标注。
@Before("@args(com.cjm.annotation.AdviceAnnotation)")
public void beforeAdvide(JoinPoint point){
//处理逻辑
}
9)@annotation :匹配连接点被它参数指定的Annotation注解的方法。也就是说,所有被指定注解标注的方法都将匹配。
@Pointcut("@annotation(com.cjm.annotation.AdviceAnnotation)")
public void before(){}
10)bean:通过受管Bean的名字来限定连接点所在的Bean。该关键词是Spring2.5新增的。
@Pointcut("bean(person)")
public void before(){}