修改一下上一节的测试类并运行,新增一个方法的调用:
proxyDog.sayException("二哈", 3);
完整代码如下:
@Test
public void test5() {
// 前置增强
// 1、实例化bean和增强
Animal dog = new Dog();
MyMethodBeforeAdvice advice = new MyMethodBeforeAdvice();
// 2、创建ProxyFactory并设置代理目标和增强
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(dog);
proxyFactory.addAdvice(advice);
// 3、生成代理实例
Animal proxyDog = (Animal) proxyFactory.getProxy();
proxyDog.sayHello("二哈", 3);
System.out.println("\n\n");
proxyDog.sayException("二哈", 3);
}
执行结果:
'//------sayHello打印的日志-------'
==前置增强
==方法名:sayHello
==第1参数:二哈
==第2参数:3
==目标类信息:com.lyc.cn.v2.day04.advisor.Dog@65e579dc
==名字:二哈 年龄:3
'//------sayException打印的日志-------'
==前置增强
==方法名:sayException
==第1参数:二哈
==第2参数:3
==目标类信息:com.lyc.cn.v2.day04.advisor.Dog@65e579dc
==名字:二哈 年龄:3
java.lang.ArithmeticException: / by zero
at com.lyc.cn.v2.day04.advisor.Dog.sayException(Dog.java:17)
at com.lyc.cn.v2.day04.advisor.Dog$$FastClassBySpringCGLIB$$a974b1ec.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
从测试结果上看,只要我们调用了Animal类的接口的任意方法,增强方法都会被应用到目标类的方法上,这样的增强效果肯定不能满足我们实际的应用。
那么这个时候就需要引入一个概念----切入点
(Pointcut)。通过切入点就可以有选择的将增强应用到目标类的方法上,而目标类的方法就是我们上一节说的连接点,即sayHello和sayException方法,目标类就是要被增强的类,即Dog类,所以增强描述了连接点的方位信息,例如织入到方法之前、方法之后,而切入点则进一步的描述了织入到那些类的那些方法上。到这里相信大家对连接点、切入点、增强、目标对象等概念有了更为深刻的理解。
但是这又带了一个新的问题,那就是如何将切入点定位到连接点,换言之,就是切入点如何知道自己要被应用到那些连接点上呢?
接下来就有必要看一下Pointcut接口的源码了。
public interface Pointcut {
//返回当前切点匹配的类
ClassFilter getClassFilter();
//返回当前切点匹配的方法
MethodMatcher getMethodMatcher();
/**
* Canonical Pointcut instance that always matches.
*/
Pointcut TRUE = TruePointcut.INSTANCE;
}
Pointcut接口的定义非常简单,仅仅包含了ClassFilter和MethodMatcher的定义:
这样通过Pointcut我们就可以将将增强织入到特定类的特定方法上了。再来看下ClassFilter和MethodMatcher的定义:
ClassFilter可以定位到具体的类上,matches返回true,说明该入参的类被选中了
public interface ClassFilter {
/**
* 切入点应该应用于给定的接口还是目标类
* @param 候选目标类
* @return 增强是否应用于目标类
*/
boolean matches(Class<?> clazz);
/**
* Canonical instance of a ClassFilter that matches all classes.
*/
ClassFilter TRUE = TrueClassFilter.INSTANCE;
}
MethodMatcher可以定位到具体的方法上,matches返回true,说明该入参的targetClass类中method方法被选中了
public interface MethodMatcher {
/**
* 静态方法匹配判断
*/
boolean matches(Method method, Class<?> targetClass);
/**
* 判断静态方法匹配或动态方法匹配
* true:动态方法匹配
* false:静态方法匹配
*/
boolean isRuntime();
/**
* 动态方法匹配判断
*/
boolean matches(Method method, Class<?> targetClass, Object... args);
/**
* Canonical instance that matches all methods.
*/
MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;
}
在MethodMatcher接口中又引入了一个新的概念,方法匹配模式,Spring支持两种方法匹配器:
StaticMethodMatcher 静态方法匹模式:所谓静态方法匹配器,仅对方法名签名(包括方法名和入参类型及顺序)进行匹配。
从不检查入参,后续结果放入缓存,速度快
public abstract class StaticMethodMatcher implements MethodMatcher {
@Override
public final boolean isRuntime() {
return false;
}
DynamicMethodMatcher 动态方法匹配器:动态方法匹配器会在运行期方法检查入参的值。 静态匹配仅会判断一次,而动态匹配因为每次调用方法的入参可能不一样,所以每次调用方法都必须判断。
每次都检查入参,结果无法缓存,效率低
public abstract class DynamicMethodMatcher implements MethodMatcher {
@Override
public final boolean isRuntime() {
return true;
}
动态和静态的区别在于isRuntime()方法的返回值
TruePointcut是Pointcut的一个实现类,作用是提供一个默认的Pointcut,指向所有类的所有方法:
final class TruePointcut implements Pointcut, Serializable {
public static final TruePointcut INSTANCE = new TruePointcut();
//复写getClassFilter,写死return true
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
//复写getMethodMatcher,写死return true
@Override
public MethodMatcher getMethodMatcher() {
return MethodMatcher.TRUE;
}
很简单,就是复写getClassFilter(),写死return true,表示所有的类均符合;复写getMethodMatcher(),写死return true,表示所有的方法均符合。
spring提供的切点类型是为了避免我们重复造轮子,基本覆盖常用的场景,当然你可以自定义一个Pointcut。
图一 Pointcut整体结构图
StaticMethodMatcherPointcut:静态匹配方法,实现方法级别的切入,非运行时切入表示从不检查入参,默认情况下匹配所有的类;
静态方法切点的抽象基类,从图一得知一般用户实现该类,只需实现matches():boolean
即可:
1.getClassFilter() 写死返回true,匹配所有的类
2.继承StaticMethodMatcher ,表示从不检查入参
部分实现原理如下:
public abstract class StaticMethodMatcherPointcut extends StaticMethodMatcher implements Pointcut {
@Override
public ClassFilter getClassFilter() {
//getClassFilter() 写死返回true,匹配所有的类
return ClassFilter.TRUE;
}
最常用的两个子类NameMatchMethodPointcut和 AbstractRegexpMethodPointcut , 前者提供简单字符串匹配方法签名,后者使用正则表达式匹配方法签名。
DynamicMethodMatcherPointcut:动态匹配方法,实现方法级别的切入,运行时切入,默认情况下匹配所有的类;
1.getClassFilter() 写死返回true,匹配所有的类
2.继承DynamicMethodMatcher ,表示每次都要检查入参
public abstract class DynamicMethodMatcherPointcut extends DynamicMethodMatcher implements Pointcut {
@Override
public ClassFilter getClassFilter() {
//getClassFilter() 写死返回true,匹配所有的类
return ClassFilter.TRUE;
}
AnnotationMatchingPointcut:基于注解匹配;
ExpressionPointcut:提供了对AspectJ切点表达式语法的支持
ControlFlowPointcut:控制流程切点,该切点是一个比较特殊的节点,根据程序执行堆栈的信息查看目标方法是否由某一个方法直接或间接发起调用,以此判断是否为匹配的连接点;
ComposablePointcut:复合切点,为创建多个切点而提供的方便操作类。它所有的方法都返回ComposablePointcut类,这样,我们就可以使用链接表达式对其进行操作。
由于增强Advice包括横切代码,又包含部分连接点信息(方法前、方法后主方位信息),所以可以仅通过增强类生成一个切面。
但切点仅仅代表目标类连接点的部分信息(类和方法的定位),所以仅有切点无法制作出一个切面,必须结合增强才能制作出切面。
Advice可以独立存在,因为用的时候,spring会自动带一个TruePointcut;而切点不能单独存在,要和Advice合作。
Spring使用org.springframework.aop.Advisor接口标识切面概念,一个切面同时包含横切代码和连接点信息。
切面可以分为3类:一般切面、切点切面、引介切面,后2个继承第一个:
一般切面Advisor
org.springframework.aop.Advisor是切面的顶层接口
,仅包含一个Advice ,因为这个横切面太宽泛(等价于默认的 Advisor=Advice+TruePointcut),所以一般不会直接使用。
Advisor接口定义如下,只有一个getAdvice(),表示要包含一个Advice 的私有成员变量:
public interface Advisor {
//仅定义这个抽象方法
Advice getAdvice();
}
切点切面PointcutAdvisor
org.springframework.aop.PointcutAdvisor ,代表具有切点的切面,它可以通过任意Pointcut和Advice定义一个切面,这样就可以通过类、方法名以及方位等信息灵活的定义切面的连接点,提供更具实用性的切面。
即 Advisor=Advice+Pointcut
DefaultPointcutAdvisor:最常用的切面类型,它可以通过任意Pointcut和Advice定义一个切面;
唯一不支持的就是引介的切面类型,一般可以通过扩展该类实现自定义的切面
标准用法,设置pointcut和advice属性即可:
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
advisor.setPointcut(pointcut);
advisor.setAdvice(advice);
或
<bean id="advisor" class="...">
bean>
<bean id="advisor" class="...">
bean>
ik
<bean id="advisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="advice" ref="pointcut"/>
<property name="advice" ref="advice"/>
bean>
定义了pointcut属性,并且默认值为Pointcut.TRUE,保证用户即使不设置pointcut也没关系,表示默认所有方法;定义了设置pointcut属性的方法,用户可以任意传入自定义的pointcut
定义了私有pointcut属性,并暴露setMappedName() 或 setMappedNames()方法,暴露的是方法名称字符串,实现封装一个NameMatchMethodPointcut,目的是让内部有个Pointcut
定义了私有pointcut属性,并暴露setPattern()或setPatterns()方法,暴露的是方法名称字符串,实现封装一个NameMatchMethodPointcut,目的是让内部有个Pointcut
这是一个灵活度很高的Advisor,提供了设置order、advice的方法,但是没有引入Pointcut成员变量,而是实现了Pointcut接口,自身就是一个Pointcut,必须复写matches(Method method, Class> targetClass)方法
整体来说,和DefaultPointcutAdvisor相似,StaticMethodMatcherPointcutAdvisor自身就是Pointcut,只不过是半成品,需要复写matches(),DefaultPointcutAdvisor拥有Pointcut成员变量,可以通过set方法直接设置Pointcut
引介切面IntroductionAdvisor
org.springframework.aop.IntroductionAdvisor代表引介切面, 引介切面是对应引介增强的特殊的切面,它应用于类层上面,所以引介切点使用ClassFilter进行定义。
上面已经对切入点、切面做了简介,下面通过几个例子来加深大家的印象。先来看静态普通方法名匹配切面,前面我们介绍切入点 通过ClassFilter可以定位到具体的类上,MethodMatcher可以定位到具体的方法上,那么接下来通过定义一个接口、两个类。并通过实现类中的不同方法来验证我们之前的介绍。
接口和实现类(目标对象),还是复用以前的Animal 和Dog ,新增一个Cat类:
package com.lyc.cn.v2.day05;
public interface Animal {
void sayHello();
}
package com.lyc.cn.v2.day05;
public class Cat implements Animal {
@Override
public void sayHello() {
System.out.println("我是Cat类的sayHello方法。。。");
}
public void sayHelloCat() {
System.out.println("我是一只猫。。。");
}
}
package com.lyc.cn.v2.day05;
/**
* @author: LiYanChao
* @create: 2018-11-04 22:09
*/
public class Dog implements Animal{
@Override
public void sayHello() {
System.out.println("我是Dog类的sayHello方法。。。");
}
public void sayHelloDog() {
System.out.println("我是一只狗。。。");
}
}
Advice增强(为了演示,这里只实现MethodBeforeAdvice前置增强接口),仍然是复用:
package com.lyc.cn.v2.day05;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
/**
* 前置增强
* @author: LiYanChao
* @create: 2018-11-01 21:50
*/
public class MyMethodBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("==前置增强");
System.out.println("==方法名:" + method.getName());
if (null != args && args.length > 0) {
for (int i = 0; i < args.length; i++) {
System.out.println("==第" + (i + 1) + "参数:" + args[i]);
}
}
System.out.println("==目标类信息:" + target.toString());
}
}
切面,这是我们首次出现的类,采用了StaticMethodMatcherPointcutAdvisor :
package com.lyc.cn.v2.day05;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor;
import java.lang.reflect.Method;
/**
* 静态普通方法名匹配切面
*/
public class MyStaticPointcutAdvisor extends StaticMethodMatcherPointcutAdvisor {
private static String METHOD_NAME = "sayHello";
/**
* 静态方法匹配判断,这里只有方法名为sayHello的,才能被匹配
*/
@Override
public boolean matches(Method method, Class<?> targetClass) {
return METHOD_NAME.equals(method.getName());
}
/**
* 覆盖getClassFilter,只匹配Dog类
* @return
*/
public ClassFilter getClassFilter() {
return new ClassFilter() {
@Override
public boolean matches(Class<?> clazz) {
return Dog.class.isAssignableFrom(clazz);
}
};
}
}
由前文得知,切面StaticMethodMatcherPointcutAdvisor必须复写matches方法来实现Pointcut,即过滤特定的方法:
为了大家能够更便捷的使用测试类,也为了减少大家书写配置文件的负担,我们还是采用编码的形式实现。新建MyTest类并书写测试方法。
我们的目标类是小狗,我们期望能被匹配上;小狗上面调用了2个方法,根据复写matches的方法,我们希望仅有sayHello被匹配:
@Test
public void test1() {
// 1、创建目标类、增强、切入点
Animal animal = new Dog();
MyMethodBeforeAdvice advice = new MyMethodBeforeAdvice();
MyStaticPointcutAdvisor advisor = new MyStaticPointcutAdvisor();
// 2、创建ProxyFactory并设置目标类、增强、切面
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(animal);
// 为切面类提供增强
advisor.setAdvice(advice);
proxyFactory.addAdvisor(advisor);
// 3、生成代理实例
Dog proxyDog = (Dog) proxyFactory.getProxy();
proxyDog.sayHelloDog();
System.out.println("\n\n");
proxyDog.sayHello();
}
执行结果:
'//----sayHelloDog确实没被匹配上,仅打印原始内容----'
我是一只狗。。。
'//----sayHello确实被匹配上,打印原始内容之外,额外打印了增强内容----'
==前置增强
==方法名:sayHello
==目标类信息:com.lyc.cn.v2.day05.Dog@65e579dc
我是Dog类的sayHello方法。。。
我们用cat作为目标类,根据复写的getClassFilter()方法,cat会被排除掉:
@Test
public void test2() {
// 1、创建目标类、增强、切入点
Animal animal = new Cat();
MyMethodBeforeAdvice advice = new MyMethodBeforeAdvice();
MyStaticPointcutAdvisor advisor = new MyStaticPointcutAdvisor();
// 2、创建ProxyFactory并设置目标类、增强、切面
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(animal);
// 为切面类提供增强
advisor.setAdvice(advice);
proxyFactory.addAdvisor(advisor);
// 3、生成代理实例
Cat proxyDog = (Cat) proxyFactory.getProxy();
proxyDog.sayHelloCat();
System.out.println("\n\n");
proxyDog.sayHello();
}
执行结果,确实,没有一个方法被匹配上,因为类已经被排除了,那么所有的方法也会被排除掉:
'//----sayHelloDog确实没被匹配上,仅打印原始内容----'
我是一只猫。。。
'//----sayHello确实没被匹配上,仅打印原始内容----'
我是Cat类的sayHello方法。。。
参考:
《29–Pointcut和Advisor以及静态普通方法名匹配切面》
《Spring AOP 介绍与基于接口的实现》