前面讲过多种类型的joinpoint,如构造方法调用、字段的设置即获取、方法调用、方法执行等,但是在spring aop中之实现了方法级别的joinpoint,确切来说是只支持方法执行类型的joinpoint。虽然spring aop仅提供方法拦截,但是实际开发过程中,这已经可以满足80%的开发需求了。
如果要使用超出spring aop之外的功能,可以借助其他aop实现产品,如aspectj
spring中以接口org.springframework.aop.Pointcut作为aop框架中所有Pointcut的最顶层抽象,该接口定义了两个方法用来帮助系统捕捉相应的joinpoint,并提供一个TruePointcut类型实例,如果Pointcut类型为TruePointcut,默认会对系统中的所有对象,以及对象上所有被支持的Joinpoint匹配。
Pointcut接口定义如下:
public interface Pointcut {
/**
* 匹配将被执行织入操作的类对象
*/
ClassFilter getClassFilter();
/**
* 匹配将被执行织入操作的对象以及相应的方法
*/
MethodMatcher getMethodMatcher();
Pointcut TRUE = TruePointcut.INSTANCE;
}
ClassFilter和MethodMather之所以将类型匹配和方法匹配分开定义,是因为可以重用不同级别的匹配定义,并且可以在不同的级别或者相同的级别上进行组合操作,或者强制某个子类只覆写相应的方法定义。
ClassFilter接口的作用是对Joinpoint所处的对象进行Class级别的类型匹配:
@FunctionalInterface
public interface ClassFilter {
/**
* 当织入的目标对象的class类型与pointcut所规定的类型相符时,matches方法将会返回true,否则返回false(意味着不会对该类型的目标对象进行织入操作)
*/
boolean matches(Class<?> clazz);
/**
* 当我们对于类型没有要求时,那么可以使用TRUE,对系统中所有的目标类以及它们的实例进行织入
*/
ClassFilter TRUE = TrueClassFilter.INSTANCE;
}
相比于ClassFilter来说,MethodMatcher更加复杂,Spring主要支持的就是方法级别的拦截,其定义如下:
public interface MethodMatcher {
/**
* 根据方法以及目标对象类型判断是否为要拦截的方法
* 当isRuntime方法返回false时,表示不会考虑连接点方法的具体参数
* 此时也成为StaticMethodMatcher,对于同样类型方法的匹配结果可以在内部缓存提高性能
*/
boolean matches(Method method, Class<?> targetClass);
/**
* 返回false表示执行不考虑参数的方法匹配,StaticMethodMathcer
* 返回true表示执行考虑参数的方法匹配,DynamicMethodMatcher
*/
boolean isRuntime();
/**
* 当isRuntime返回true时会调用该方法
* 对方法调用的参数进行匹配检查,称为DynamicMethodMatcher,每次都要检查参数所以不能缓存
* 匹配效率相对于StaticMethodMatcher来说要差
* 大部分情况下StaticMethodMathcer已经能满足需求,尽量避免使用DynamicMethodMatcher类型
*/
boolean matches(Method method, Class<?> targetClass, Object... args);
/**
* Canonical instance that matches all methods.
*/
MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;
}
我们看看Pointcut都有哪些实现(通过idea查看):
接下来介绍几种较为常用的Pointcut实现。
这是最简单的Pointcut实现,属于StaticMethodMatcherPointcut的子类,可以根据自身指定的一组方法名称与Joinpoint处的方法名称进行匹配,例如:
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("mathches");
// 或者传入多个方法名称
pointcut.setMappedNames(new String[] {"select", "update"});
NameMatchMethodPointcut无法对重载的方法名进行匹配,因为它仅对方法名进行匹配,不会考虑参数相关的信息,而且也没有提供可以指定参数匹配信息的途径。
NameMatchMethodPointcut除了可以指定方法名之外,还可以对指定的Joinpoint进行匹配,还可以使用"*"通配符,实现简单的模糊匹配,如下所示:
pointcut.setMappedNames(new string[] {"match", "sele*", "up*"});
它的底层使用ArrayList存储传入的要匹配的方法名称,然后调用matches方法对传入的Method方法进行类型匹配,判断Method的名称是否与我们设置的要匹配的方法名称一样。看看它的部分源码:
public class NameMatchMethodPointcut extends StaticMethodMatcherPointcut implements Serializable {
// 存储要匹配的方法名称
private List<String> mappedNames = new ArrayList<>();
// 设置要匹配的方法名称
public void setMappedName(String mappedName) {
setMappedNames(mappedName);
}
// 设置多个要匹配的方法的名称
public void setMappedNames(String... mappedNames) {
this.mappedNames = new ArrayList<>(Arrays.asList(mappedNames));
}
// 添加要匹配的方法名称
public NameMatchMethodPointcut addMethodName(String name) {
this.mappedNames.add(name);
return this;
}
// 匹配传入的方法和我们注册的方法名是否相同,相同则返回true,证明匹配成功,否则返回false
@Override
public boolean matches(Method method, Class<?> targetClass) {
for (String mappedName : this.mappedNames) {
if (mappedName.equals(method.getName()) || isMatch(method.getName(), mappedName)) {
return true;
}
}
return false;
}
protected boolean isMatch(String methodName, String mappedName) {
return PatternMatchUtils.simpleMatch(mappedName, methodName);
}
}
StaticMethodMatcherPointcut的子类有一个专门提供基于正则表达式的实现分支,其顶层实现是一个抽象类AbstractRegexpMethodPointcut,它声明了patterns和excludedPatterns属性,可以指定多个正则表达式的匹配模式(patterns),或者不参与代理的匹配模式。JdkRegexpMethodPointcut为其具体的实现,它基于JDK1.4之后引入的JDK标准正则表达式。
它的简单使用方法如下:
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
pointcut.setPattern(".*match.*");
// 或者
pointcut.setPatterns(new String[] {".*matches", "*select*"});
当我们使用正则表达式来匹配连接点所处的方法时,必须写出方法的全限定类命加方法名,否则无法匹配到对应的方法。假如我们要匹配aop.AopTest.select()方法,那么使用".*select"则会匹配到select方法,但是如果使用"select. *"作为匹配的正则表达式,那就无法捕捉到select方法。
AnnotationMatchingPointcut根据目标对象中是否存在指定类型的注解来匹配Joinpoint,要使用该类型的Pointcut,首先需要声明相应的注解。
假设我们定义了两个注解,如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ClassLevelAnnotation {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodLevelAnnotation {
}
假设我们在某个类上加了注解:
@ClassLevelAnnotation
public class AopTest {
@MethodLevelAnnotation
public void test() {
System.out.println("test!");
}
}
针对AOPTest类,不同的AnnotationMatchingPointcut定义会产生不同的匹配行为。
之前说过,Pointcut通常提供逻辑运算功能,而ComposablePointcut就是Spring aop提供的可以进行Pointcut逻辑运算的Pointcut实现,它可以进行Pointcut之间的并和交运算。例如:
ComposablePointcut pointcut1 = new ComposablePointcut(classFilter1,methodMatcher1);
ComposablePointcut pointcut2 = new ComposablePointcut(classFilter2,methodMatcher2);
ComposablePointcut union = pointcut1.union(pointcut2);
ComposablePointcut intersection = pointcut1.intersection(union);
Pointcut定义根据ClassFilter和MethodMatcher划分为两部分,一部分是为了重用这些定义,另一部分是为了可以互相结合。而通过ComposablePointcut,我们可以看出这两点目的:
ComposablePointcut pointcut3 = pointcut2.union(classFilter1).intersection(methodMatcher1);
我们在pointcut1和pointcut3中复用了classFilter1和methodMatcher1以及pointcut2的定义,同时,还进行了pointcut同classfilter和methodmatcher之间的逻辑组合运算。
ControlFlowPointcut在理解和使用上面可能是比较难的,它可能不是很常用,但是某些场合可能会用到。ControlFlowPointcut主要用于匹配程序的调用流程,不是对某个方法执行所在的Joinpoint处的单一特征进行匹配。
假设我们所拦截的目标对象及调用类如下:
public class TargetObject {
public void method() {}
}
public class TargetCaller {
private TargetObject target;
public void callMethod() {
target.method();
}
public void setTarget(TargetObject target) {
this.target = target;
}
}
如果使用之前的任何Pointcut实现,我们只能指定在TargetObject的method方法每次执行的时候,都织入相应横切逻辑。也就是说,一旦通过pointcut指定method处为joinpoint,那么对该方法的执行进行拦截是必定的,不管method是被谁调用。
而通过ControlFlowPointcut,我们可以指定,只有当TargetObject的method方法在TargetCaller类所声明的方法中被调用的时候,才对方法method进行拦截,其他地方调用method不会拦截。
那么具体如何实现呢?
ControlFlowPointcut pointcut = new ControlFlowPointcut(TargetCaller.class);
Advice advice = ...;
TargetObject target = new TargetObject();
TargetObject use = weaver.weave(advice).to(target).accordingto(pointcut);
// advice的逻辑在这里将不会被触发执行
use.method();
// advice的逻辑在这里会被触发执行
// 这里ControlFlowPointcut构造函数指定的类调用连接处的方法时才会执行横切逻辑
TargetCaller caller = new TargetCaller();
caller.setTarget(use);
caller.callMethod();
如果ControFlowPointcut的构造方法中单独指定class类型的参数,那么ControlFlowPointcut将尝试匹配指定的class中声明的所有方法,根目标对象的joinpoint处的方法流程组合。所以如果只想完成TargetCaller.callMethod调用TargetObject.method这样的匹配流程,而忽略TargetCaller中其他方法时,可以在构造函数中传入第二个参数,即调用方法的名称,例如:
ControlFlowPointcut pointcut = new ControlFlowPointcut(TargetCaller.class,
"callMethod");
如果有特殊的需求,spring aop提供的pointcut无法满足,那么我们可以自定义pointcut。
自定义pointcut,我们只需要继承spring aop已经提供的相应的抽象父类,然后实现或者覆写相应的方法逻辑即可。spring aop的pointcut类型可以划分为StaticMethodMatcherPointcut和DynamicMethodMatcherPointcut两类,我们要实现自定义的pointcut,在这两个抽象类的基础上实现相应子类即可。
自定义StaticMethodMatcherPointcut
StaticMethodMatcherPointcut为子类提供了几个方面的默认实现。
最终我们只需要实现两个参数的matches方法。
如果我们想对数据访问层的数据访问对象中的查询方法所在的Joinpoint进行捕捉,那么可以这样做:
public class QueryMethodPointcut extends StaticMethodMatcherPointcut {
public boolean matches(Method method, Class clazz) {
return method.getName().startWith("get")
&& clazz.getPackage().getName().startWith("..dao");
}
}
spring aop加入了开源组织aop alliance,主要用于标准化aop的使用,促进各个aop的实现产品之间的交互性,鉴于此,spring中的各种advice类型实现与aop alliance中标准接口之间的关系如图:
Advice实现了将被织入到Pointcut规定的joinpoint处的横切逻辑,在spring中,advice按照其自身的实例能否在目标对象类所在的所有实例中共享这一标准,可以划分为两大类,也就是:per-class类型的Advice和per-instance类型的Advice
per-class类型的Advice是指,该类型的Advice的实例可以在目标对象类的所有实例之间共享,这种类型的Advice通常只是提供方法拦截的功能,不会为目标对象类保存任何状态或者添加新的特性。除了Introduction类型的Advice不属于per-class类型的Advice之外,上图中的所有Advice均属此列。
1、Before Advice
Before Advice所实现的横切逻辑将在相应的Joinpoint之前执行,在Before Advice执行完成之后,程序执行流程将从Joinpoint处继续执行,所以Before Advice不会打断程序的执行流程。
在spring中需要实现MethodBeforeAdvice接口来实现Before Advice,该接口定义如下:
public interface MethodBeforeAdvice extends BeforeAdvice {
/**
*
*/
void before(Method method, Object[] args, @Nullable Object target) throws Throwable;
}
MethodBeforeAdvice继承自BeforeAdvice,BeforeAdvice与Advice一样,都是标志接口,没有定义任何方法。
我们可以使用Before Advice进行整个系统的某些资源初始化或者其他一些准备性的工作。当然还有其他很多场景。
2、ThrowsAdvice
spring中ThrowsAdvice接口对应aop概念中的AfterThrowingAdvice,虽然该接口没有定义任何方法,但是在实现相应的ThrowsAdvice时,我们的方法定义需要遵循如下规则:
void afterThrowing([Method, args, target], ThrowableSubclass);
其中[]中的三个参数可以省略,可以根据将要拦截的Throwable的不同类型,在同一个ThrowsAdvice中实现多个afterThrowing方法。
框架将会使用Java反射机制来调用这些方法。
例如,我们可以实现多个afterThrowing方法
public class ExceptionBarrierThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(Throwable t) {
// 普通异常处理
}
public void afterThrowing(RuntimeException e) {
// 运行时异常处理
}
public void afterThrowing(Method m, Object[] args, Object target, ApplicationException e) {
// 处理应用程序生成的异常
}
}
ThrowsAdvice通常用于对系统中特定的异常情况进行监控,以统一的方式对所发生的异常进行处理。当然也可以根据具体的应用场景来使用ThrowsAdvice。
我们可以对系统中的运行时异常进行监控,一旦捕捉到异常,需要马上以某种方式通知系统的监控人员或者运营人员。例如通过email的方式发送通知
例如:
public class ExceptionBarrierThrowsAdvice implements ThrowsAdvice {
private JavaMailSender mailSender;
private String[] receiptions;
public void afterThrowing(Method m, Object[] args, Object target, RuntimeException e) {
// 处理应用程序生成的异常
final String exceptionMessage = ExceptionUtils.getFullStacktrace(e);
getMailSender().send(new MimeMessagePreparator() {
public void prepare(MimeMessage message) throws Exception {
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setSubject("...");
helper.steTo(getReceiptions());
helper.setText(exceptionMessage);
}
});
}
// 省略getter和setter方法
}
3、AfterReturingAdvice
spring的AfterReturingAdvice定义如下:
public interface AfterReturningAdvice extends AfterAdvice {
void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable;
}
通过AfterReturningAdvice,我们可以访问当前连接点的方法返回值、方法、方法参数以及所在的目标对象。
只有方法正常返回的情况下,AfterReturningAdvice才会执行,所以用来处理资源清理制类的工作并不合适。不过如果有需要方法成功执行后进行的横切逻辑,使用AfterReturningAdvice倒比较合适。虽然AfterReturningAdvice可以访问方法的返回值,但是不可以更改返回值。我们可以通过Around Advice实现。
4、Around Advice
spring aop没有提供After Advice,使得我们没有一个合适的Advice类型来承载类似于系统资源清除之类的横切逻辑。
spring中没有直接定义对应around advice的实现接口,而是直接采用aop alliance的标准接口:org.aopalliance.intercept.MethodInterceptor,其定义如下:
@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
@Nullable
Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}
MethodInterceptor作为Around Advice非常强大,前面提到的几种Advice它都可以完成,系统安全验证、性能检测、简单日志记录等场景都可以使用它。
以简单的检测系统某些方法执行性能为例,实现一个PerformanceInterceptor:
public class PerformanceInterceptor implements MethodInterceptor {
private final Log Logger = LogFactory.getLog(this.getClass());
public Object invoke(MethodInvocation invocation) throws Throwable {
StopWatch watch = new StopWatch();
try {
watch.start();
return invocation.proceed();
} finally {
watch.stop();
if (logger.isInfoEnables()) {
logger.info(watch.toString());
}
}
}
}
通过MethodInvocation的proceed方法可以让程序继续沿着调用链传播,如果没有调用proceed方法的话,那么程序在这里将会被中断,其他的MethodInterceptor逻辑和joinpoint处的方法逻辑将不会被执行。所以,不要忘记调用proceed()方法
per-instance类型的Advice不会在目标类所有对象实例之间共享,而是会为不同的实例对象保存它们各自的状态以及相关逻辑。
Introduction是spring aop中唯一的一种per-instance型Advice。
Introduction可以在不改动目标类定义的情况下,为目标类添加新的属性以及行为。
在spring中,为目标对象添加新的属性和行为必须声明相应的接口以及相应的实现。然后通过特定的拦截器将新的接口定义以及实现类中的逻辑附加到目标对象上,之后目标对象也可以说是目标对象的代理对象就拥有了新的状态和行为。
这个特定的拦截器就是org.springframework.aop.IntroductionInterceptor,其定义如下:
public interface IntroductionInterceptor extends MethodInterceptor, DynamicIntroductionAdvice {}
public interface DynamicIntroductionAdvice extends Advice {
boolean implementsInterface(Class<?> intf);
}
IntroductionInterceptor继承了DynamicIntroductionAdvice接口,界定当前的IntroductionInterceptor为那些接口类提供相应的拦截功能,通过MethodInterceptor可以处理新添加的接口上的方法调用了。对于IntroductionInterceptor来说,如果是新增加的接口上的方法调用,不必调用MethodInterceptor的proceed()方法,当前被拦截的方法实际上就是整个调用链中要最终执行的唯一方法。
如果把每个目标对象实例看作盒装牛奶生产线上的那一盒盒牛奶,那么生产合格证就是新的Introduction逻辑,而IntroductionInterceptor就是把这些生产合格证贴到一盒盒牛奶上的那个人。
我们来看看Introduction相关的类图结构:
使用DynamicIntroductionAdvice可以到运行时再判定当前Introduction可应用到的目标接口类型,而不用预先就设定。而IntroductionInfo类型则完全相反,其定义如下:
public interface IntroductionInfo {
/**
* 返回预定的目标接口类型
*/
Class<?>[] getInterfaces();
}
当对IntroductionInfo型的Introduction进行织入时,实际上就不需要指定目标接口类型了,因为它自身就带有这些必要的信息。
在大多数时候,直接使用spring提供的两个现成的实现类就可以对目标对象进行拦截并添加Introduction逻辑。
DelegatingIntroductionInterceptor不会自己实现将要添加到目标对象上的新的逻辑行为,而是委派给其他实现类。
假如我们要给程序员添加软件测试的职责时,可以这样编码:
public interface IDevelpoer {
void developSoftware();
}
public class Developer implements IDevelpoer{
@Override
public void developSoftware() {
System.out.println("我要开发一款风靡全国的软件");
}
}
public interface ITest {
boolean isBusyAsTester();
void testSoftware();
}
public class Tester extends extends DelegatingIntroductionInterceptor implements ITest{
private boolean busyAsTester;
public void setBusyAsTester(boolean busyAsTester) {
this.busyAsTester = busyAsTester;
}
@Override
public boolean isBusyAsTester() {
return false;
}
@Override
public void testSoftware() {
System.out.println("我要测试一款软件,保证它的质量!");
}
}
通过DelegatingIntroductionInterceptor进行Introduction的拦截。
public static void main(String[] args) {
ProxyFactory weaver = new ProxyFactory();
Developer developer = new Developer();
weaver.setTarget(developer);
weaver.setInterfaces(new Class[] {IDevelpoer.class, ITest.class});
ITest delegate = new Tester();
DelegatingIntroductionInterceptor advice = new DelegatingIntroductionInterceptor(delegate);
DefaultIntroductionAdvisor advisor = new DefaultIntroductionAdvisor(advice, advice);
weaver.addAdvisor(advisor);
Object proxy = weaver.getProxy();
((IDevelpoer) proxy).developSoftware();
((ITest) proxy).testSoftware();
}
DelegatingIntroductionInterceptor是Introduction型Advice的一个实现,它并没有兑现Introduction作为per-instance型Advice的承若。实际上DelegatingIntroductionInterceptor会使用它所持有的同一个delegate接口实例,供同一目标类的所有实例共享使用。如果真要达到Introduction型Advice所有宣称的那样的效果。所以我们要使用:DelegatePerTargetObjectIntroductionInterceptor。
DelegatePerTargetObjectIntroductionInterceptor会在内部持有一个目标兑现与相应Introduction逻辑实现类之间的映射关系,当每个目标对象上的新定义的接口方法被调用时,DelegatePerTargetObjectIntroductionInterceptor会拦截这些调用,然后以目标对象实例为键,到它持有的那个映射关系中取得对应当前目标对象实例的Introduction实现类实例。
DelegatePerTargetObjectIntroductionInterceptor不是自己构造delegate接口实例,而只需告知DelegatePerTargetObjectIntroductionInterceptor相应的delegate接口类型和对应实现类的类型。剩下的工作留给DelegatePerTargetObjectIntroductionInterceptor就可以了,代码如下所示:
DelegatePerTargetObjectIntroductionInterceptor interceptor = new DelegatePerTargetObjectIntroductionInterceptor(DelegateImpl.class, IDelegate.class);
如果上述两个类不能满足需求,也可以扩展这两个类,覆写相应的方法。
当所有的pointcut和advice准备好之后,就到了该把它们分门别类地装进箱子的时候了。这个箱子就是Aspect了。
Advisor代表Spring中的Aspect,Advisor通常只持有一个pointcut和一个advice,理论上,aspect定义中可以有多个point和多个advice,所以,我们可以认为advisor是一种特殊的aspect。
Advisor简单分为两个分支,一个分支以PointcutAdvisor为首,一个以IntroductionAdvisor为首。
org.springframework.aop.PointcutAdvisor才是真正定义一个Pointcut和一个Advice的Advisor,大部分的Advisor实现全都是PointcutAdvisor的实现类。
1、DefaultPointcutAdvisor
DefaultPointcutAdvisor是PointcutAdvisor实现,除了不能为其指定Introduction类型的Advice之外,剩下的任何类型的Pointcut、任何类型的Advice都可以通过DefaultPointcutAdvisor使用。我们可以在构造DefaultPointcutAdvisor的时候,明确指定当前DefaultPointcutAdvisor实例的Pointcut和Advice,也可以在DefaultPointcutAdvisor实例构造完成后,再通过setPointcut以及setAdvice方法设置相应的Pointcut和Advice。
Pointcut point = ...; // 任何类型的Pointcut类型
Advice advice = ...; // 除了Introduction类型外的任何Advice类型
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
这里不是实际的环境中使用的代码,只是演示事实的真相。
spring中任何的bean都可以通过IOC容器来管理,spring aop中的任何概念对此也同样使用,大多时候,我们会通过ioc容器来注册和使用spring aop的各种概念实体。
2、NameMatchMethodPointcutAdvisor
NameMatchMethodPointcutAdvisor是细化后的DefaultPointcutAdvisor,它限定了自身可以使用的pointcut类型为NameMatchMethodPointcut,并且外部不可更改。
NameMatchMethodPointcutAdvisor内部持有一个NameMatchMethodPointcut类型的pointcut实例,可以通过setMappedName和setMappedNames方法设置将被拦截的方法名称。
其简单用法如下:
ProxyFactory weaver = new ProxyFactory(new Executable());
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor();
advisor.setMappedName("execute");
advisor.setAdvice(new PerformanceMethodInterceptor());
weaver.addAdvisor(advisor);
Executable proxy = (Executable) weaver.getProxy();
proxy.execute();
System.out.println(proxy.getClass());
3、RegexpMethodPointcutAdvisor
RegexpMethodPointcutAdvisor也限定了自身可以使用的Pointcut类型,只能通过正则表达式为其设置相应的Pointcut。
RegexpMethodPointcutAdvisor内部持有一个AbstractRegexpMethodPointcut实例,它有一个实现类JdkRegexpMethodPointcut。我们可以在构造时指定Pointcut的正则表达式匹配模式以及相应的Advice,也可以构造完成之后再指定,在使用上与其他的Advisor实现并无太多差别。
4、DefaultBeanFactoryPointcutAdvisor
DefaultBeanFactoryPointcutAdvisor是使用比较少的一个Advisor实现,因为自身绑定到了BeanFacotry,所以,要使用DefaultBeanFactoryPointcutAdvisor,我们的应用铁定要绑定到Spring的IoC容器。通常情况下,DefaultPointcutAdvisor已经完全满足需求了。
DefaultBeanFactoryPointcutAdvisor的作用,我们可以通过容器中的Advice注册的beanName来关联对应的Advice。只有当对应的Pointcut匹配成功之后,才去实例化对应的Advice,减少了容器启动出其Advisor和Advice之间的耦合性。
IntroductionAdvisor与PointcutAdvisor最本质的区别就是,IntroductionAdvisor只能用于类级别的拦截,只能使用Introduction型的Advice,而不能像PointcutAdvisor那样,可以使用任何类型的Pointcut,以及差不多任何类型的Advice,也就是说,IntroductionAdvisor纯粹是为了Introduction而生的。
IntroductionAdvisor只有一个默认的实现DefaultIntroductionAdvisor,继承层次如上图。
系统中只存在单一的横切关注点的情况太少,大多数时候,都会有多个横切关注点需要处理,那么,系统实现中就会有多个Advisor存在。
当多个Advisor的pointcut匹配同一个joinpoint时,就会在同意额joinpoint处执行多个Advice的横切逻辑。如果这些Advice逻辑存在优先顺序依赖的话,就需要我们手动实现,系统不会自动帮我们实现。
Spring处理同一Joinpoint多个Advisor时,会按照指定的顺序和优先级来执行它们,顺序号决定优先级,顺序号越小,优先级越高,优先级排在前面的,将被优先执行,我们可以从0或者1开始执行,小于0的顺序号原则上由spring aop框架内部使用。如果我们不明确指定各个Advisor的执行顺序,那么Spring会按照它们的声明顺序来应用它们,最先声明的顺序号最小但优先级最大,其次次之。
Joinpoint、Pointcut、Advice、Aspect都准备好了,现在就差把这些模块拼装到一起,也就是进行织入,在spring aop中,org.springframework.aop.framework.ProxyFactory是织入器。
spring aop是通过代理模式实现的,所以在我们完成织入后,会返回织入了横切逻辑的目标对象的代理对象。使用ProxyFactory需要指定两个最基本的东西:
在不同的应用场景中,我们可以指定更多ProxyFactory的控制属性,让ProxyFactory帮我们生成必要的代理对象。
1、基于接口的代理
我们来看一个简单的应用场景,为接口的实现方法添加横切逻辑。首先定义接口及其实现:
interface ITask {
void execute();
}
class MockTask implements ITask {
@Override
public void execute() {
System.out.println("task executed!");
}
}
有了要拦截的目标类,我们还有有织入到Joinpoint处的横切逻辑,也就是某个Advisor实现:
class PerformanceMethodInterceptor implements MethodInterceptor {
private final Log logger = LogFactory.getLog(UseProxyFactory.class);
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
StopWatch watch = new StopWatch();
try {
watch.start();
return invocation.proceed();
} finally {
watch.stop();
if (logger.isInfoEnabled()) {
logger.info(watch.toString());
}
}
}
}
有了这些之后,就要使用ProxyFactory对ITask接口的实现类进行代理,上述是基于接口的代理:
MockTask mockTask = new MockTask();
ProxyFactory weaver = new ProxyFactory(mockTask);
// 设置要代理的目标接口,不设置也可以,ProxyFactory会自动根据类实现的接口转换
weaver.setInterfaces(new Class[] {ITask.class});
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor();
advisor.setMappedName("execute");
advisor.setAdvice(new PerformanceMethodInterceptor());
weaver.addAdvisor(advisor);
ITask task = (ITask) weaver.getProxy();
task.execute();
在这里我们通过NameMatchMethodPointcutAdvisor指定Pointcut和相应的Advice,默认情况下,ProxyFacory只要检测到目标类实现了相应的接口,也会对目标类进行基于接口的代理。
通过运行结果,我们可以看出weaver.getProxy()获得的就是代理类,是通过JDK动态代理实现的。
2、基于类的代理
有了前面的基础,那么基于类的代理就是目标类没有实现任何接口,ProxyFactory会通过CGLIB执行基于类的代理。
class Executable {
public void execute() {
System.out.println("task executed!");
}
}
使用Executable作为目标对象类,ProxyFacotry会对其进行基于类的代理:
ProxyFactory weaver = new ProxyFactory(new Executable());
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor();
advisor.setMappedName("execute");
advisor.setAdvice(new PerformanceMethodInterceptor());
weaver.addAdvisor(advisor);
Executable proxy = (Executable) weaver.getProxy();
proxy.execute();
System.out.println(proxy.getClass());
运行结果:
当然,即使是实现了接口的MockTask类也可以采用基于类的代理,我们可以通过ProxyFacotry#setProxyTargetClass(true)方法强制其基于CGLIB实现代理。
3、Introduction的织入
Introduction可以为已经存在的对象类型添加新的行为,只能应用于对象级别的拦截,而不是通常Advice的方法级别拦截,Spring的Introduction只能通过接口定义为当前对象添加新的行为,所以我们需要在织入是指定新织入的接口类型。
就拿上文的为程序员添加测试职责例子来说,看看如何进行Introduction的织入:
ProxyFactory weaver = new ProxyFactory();
Developer developer = new Developer();
weaver.setTarget(developer);
weaver.setInterfaces(new Class[] {IDevelpoer.class, ITest.class});
ITest delegate = new Tester();
DelegatingIntroductionInterceptor advice = new DelegatingIntroductionInterceptor(delegate);
DefaultIntroductionAdvisor advisor = new DefaultIntroductionAdvisor(advice, advice);
weaver.addAdvisor(advisor);
Object proxy = weaver.getProxy();
((IDevelpoer) proxy).developSoftware();
((ITest) proxy).testSoftware();
ProxyFacotry会调用AopProxy实现类的getProxy()方法来完成代理工作,首先看看AopProxy:
public interface AopProxy {
/**
* Create a new proxy object.
* 使用AopProxy的默认类加载器,也就是线程上下文类加载器
*/
Object getProxy();
/**
* Create a new proxy object.
* 通过指定的类加载器加载对象
*/
Object getProxy(@Nullable ClassLoader classLoader);
}
spring aop框架内部使用AopProxy对使用的不同代理实现机制进行了适度的抽象,针对不同的实现机制提供相应的AopProxy子类实现。
AopProxy的实现类有两个:CglibAopProxy和JdkDynamicAopProxy,JdkDynamicAopProxy同时也实现了InvocationHandler接口。
不同AopProxy实现类的实例化过程通过抽象工厂进行封装,也就是通过org.springframework.aop.framework.AopProxyFactory进行的。
AopProxy的定义如下:
public interface AopProxyFactory {
// 根据传入的AdvisedSupport实例提供的信息来决定生成什么类型的AopProxy
AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException;
}
它的唯一实现类如下所示:
如果传入的AdvisedSupport实例config的isOptimize或者isProxyTargetClass方法返回true,或者目标没有实现任何接口,则采用CGLIB生成代理对象,否则使用动态代理。
AdvisedSupport其实就是一个生成代理对象所需要的信息载体:
AdvisedSupport所承载的信息可以划分为两类:ProxyConfig记载生成代理对象的控制信息;Advised承载生成代理对象所需要的必要信息,如相关目标类、Advice和Advisor等。
ProxyConfig包含5个属性:
只有ProxyConfig这些信息是不够的,我们还可以通过Advised接口访问相应代理对象所持有的Advisor,进行添加Advisor、移除Advisor等相关操作,即使代理对象已经生成完毕,也可对其进行这些操作。
AdvisedSupport继承自ProxyConfig可以设置代理对象生成的一些控制属性,同时通过实现Advised接口,可以设置代理对象相关的目标类、Advice等必要信息。
ProxyFactory集AopProxy和AdvisedSupport于一身,所以,可以通过ProxyFactory设置生成代理对象所需要的相关信息,也可以通过ProxyFactory取得最终生成的代理对象。
为了重用相关的逻辑,Spring AOP框架在实现的时候,将一些公用的逻辑抽取到了org.springframework.aop.framework.ProxyCreatorSupport中。它继承自AdvisedSupport,所以它从自身可以获得生成代理对象的必要信息。其内部持有AopProxyFactory实例,默认采用DefaultAopProxyFactory。它的继承体系如下:
ProxyFactory是Spring aop最基本的织入器,ProxyFactoryBean通过其名字就可以知道它是与Spring IoC容器结合的织入器。
通过ProxyFactory可以让我们独立于spring ioc容器使用spring aop。通过ProxyFactoryBean可以将spring aop与spring ioc容器相结合。
在容器中,ProxyFactoryBean的使用方法和ProxyFactory无太大差别。
ProxyFactoryBean的本质是Proxy+FactoryBean,它是用于生产proxy的FactoryBean。当我们获取容器中的FactoryBean时,会获得其getObject()方法返回的对象,而不是FactoryBean本身。所以,如果容器中的某个对象依赖于ProxyFactoryBean,它将会获得ProxyFactoryBean#getObject()方法所返回的代理对象。
ProxyFactoryBean#getObject()方法返回相应目标对象类的代理类很简单,通过ProxyCreatorSupport#createAopProxy()取得相应的AopProxy,然后再调用AopProxy的getProxy()方法即可。
ProxyFactoryBean独有的属性配置:
1、实现原理
spring aop给出了自动代理机制,帮助我们解决使用ProxyFactoryBean配置工作量较大的问题。
spring aop的自动代理实现建立再IoC容器的BeanPostProcessor概念之上,通过BeanPostProcessor我们可以再遍历容器中所有bean的基础上,对遍历的bean进行操作。
只需要提供一个BeanPostProcessor,然后在这个BeanPostProcessor内部实现这样的逻辑:当对象实例化的时候,为其生成实例对象并返回,而不是返回目标对象本身。
2、可用的AutoProxyCreator
org.springframework.aop.framework.autoproxy包中提供了两个常用的AutoProxyCreator,即BeanNameAutoProxyCreator和DefaultAdvisorAutoProxyCreator。
3、扩展AutoProxyCreator
如果BeanNameAutoProxyCreator和DefaultAdvisorAutoProxyCreator不能满足我们的要求,或者我们想要实现自己的自动代理,那么可以继承AbstractAutoProxyCreator,在其基础上实现我们自己的逻辑
通常在使用ProxyFactory时,我们是通过setTarget()方法指定具体的目标对象,使用ProxyFactoryBean也是如此,ProxyFactoryBean还可以通过setTargetName()指定目标对象在IOC容器中的bean定义名称。
此外,我们还可以通过setTargetSource()方法来指定目标对象。
TargetSource的作用就好像是为目标对象在外面加了一个壳,或者说它就像目标对象的容器,当每个针对目标对象的方法调用经历层层拦截而到达调用链终点的时候,就该调用目标对象上定义的方法了。但是spring aop此时不直接调用这个目标对象上的方法,而是通过"插足于"调用链与实际目标对象之间的某个TargetSource来取得具体目标对象,然后再调用从TargetSource中取得的目标对象上的相应方法。
在通常情况下,无论是通过setTarget,还是通过setTargetName等方法设置的目标对象,框架内部都会通过一个TargetSource实现类对这个设置的目标对象进行封装。
TargetSource最主要的特性就是:每次方法调用都回触发TargetSource的getTarget方法,getTarget方法将从相应的TargetSource实现类中取得具体的目标对象。这样我们就可以控制每次方法调用作用到的具体对象实例:
当然,还可以让TargetSource只持有一个目标对象实例,这样每次方法调用都回针对这一个目标对象实例。
1、SingletonTargetSource
org.springframework.aop.target.SingletonTargetSource是使用最多的TargetSource实现类,因为通过ProxyFactoryBean的setTarget()方法设置完目标对象之后,ProxyFactoryBean内部会自行使用一个SingletonTargetSource对设置的目标对象进行封装。
SingletonTargetSource的实现很简单,就是内部只持有一个目标对象,当每次方法调用到达时,SingletonTargetSource都会返回同一个目标对象
2、PrototypeTargetSource
与SingletonTargetSource相反,如果为ProxyFactoryBean设置一个PrototypeTargetSource类型的TargetSource时,每次方法带哦用达到调用链终点并即将调用目标对象上的方法时,PrototypeTargetSource都会返回一个新的目标对象实例供调用。此时目标对象的作用域必须是prototype
3、HotSwappableTargetSource
使用HotSwappableTargetSource封装目标对象,可以让我们在应用程序运行的时候,根据某种特定条件,动态地替换目标对象类的具体实现。例如,IService有多个实现类,如果程序启动后,默认的IService实现类出现了问题,我们可以马上切换到另一个IService的实现上,而这些对于调用者来说是透明的。
使用HotSwappableTargetSource可以用新的目标对象实例将旧的目标对象实例替换掉。
4、ThreadLocalTargetSource
如果想为不同的线程调用提供不同的目标对象,那么可以使用ThreadLocalTargetSource,它可以保证各自线程上对目标对象的调用,可以被分配到当前线程对应的那个目标对象实例上。ThreadLocalTargetSource只是对JDK的ThreadLocal进行了简单的封装。
如果在特殊情况下,原有的TargetSource无法满足要求,那么我们可以自定义TargetSource,直接扩展TargetSource接口:
public interface TargetSource extends TargetClassAware {
/**
* 返回目标对象类型
*/
@Override
@Nullable
Class<?> getTargetClass();
/**
* 表明是否要返回同一个目标对象实例,SingletonTargetSource肯定返回true,其他方法通常返回false
*/
boolean isStatic();
/**
* 核心方法,要返回哪个目标对象实例由它决定
*/
@Nullable
Object getTarget() throws Exception;
/**
* 具体调用过程的结束,如果isStatic返回false,
* 则会调用releaseTarget()以释放当前调用的目标对象
* 大部分情况下可以不实现该方法
*/
void releaseTarget(Object target) throws Exception;
}
本章介绍了Spring AOP的各种概念和原理,这些概念和实现原理是Spring AOP发布之初就确定的,是整个框架的基础,纵使版本如何升级,这些基础的概念都不会改变。所以掌握了这些知识以后,我们才能对Spring AOP有更加深刻的认识,不仅学习如何使用Spring AOP,还要掌握这些功能是如何编码实现的。
Java中就是一个个类和一个个不同功能的接口,再搭配Java的一些特性,来实现各种各样的复杂功能,我们以后做项目开发也是这样,通过不同的类和函数的组合来实现不同的功能,完成不同的任务。