前言
之所以写这么一篇文章主要是因为下篇文章将结束Spring启动整个流程的分析,从解析配置到创建对象再到属性注入最后再将创建好的对象初始化成为一个真正意义上的Bean。因为下篇文章会设计到AOP,所以提前单独将AOP的相关API及源码做一次解读,这样可以降低阅读源码的障碍,话不多说,我们进入正文!
一个使用API创建代理的例子
在进入API分析前,我们先通过两个例子体会下如何使用API的方式来创建一个代理对象,对应示例如下:
1.定义通知
public class DmzAfterReturnAdvice implements AfterReturningAdvice { @Override public void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable { System.out.println("after invoke method [" + method.getName() + "],aop afterReturning logic invoked"); } } public class DmzAroundAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println("aroundAdvice invoked"); return invocation.proceed(); } } public class DmzBeforeAdvice implements MethodBeforeAdvice { @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println("before invoke method [" + method.getName() + "],aop before logic invoked"); } } public class DmzIntroductionAdvice extends DelegatingIntroductionInterceptor implements Runnable { @Override public void run() { System.out.println("running!!!!"); } }
2.切点
public class DmzPointcut implements Pointcut { @Override @NonNull public ClassFilter getClassFilter() { // 在类级别上不进行拦截 return ClassFilter.TRUE; } @Override @NonNull public MethodMatcher getMethodMatcher() { return new StaticMethodMatcherPointcut() { @Override public boolean matches(@NonNull Method method, Class<?> targetClass) { // 对于toString方法不进行拦截 return !method.getName().equals("toString"); } }; } }
3.目标类
public class DmzService { @Override public String toString() { System.out.println("dmzService toString invoke"); return "dmzService"; } public void testAop(){ System.out.println("testAop invoke"); } }
4.测试代码
public class Main { public static void main(String[] args) { ProxyFactory proxyFactory = new ProxyFactory(); // 一个Advisor代表的是一个已经跟指定切点绑定了的通知 // 在这个例子中意味着环绕通知不会作用到toString方法上 Advisor advisor = new DefaultPointcutAdvisor(new DmzPointcut(), new DmzAroundAdvice()); // 添加一个绑定了指定切点的环绕通知 proxyFactory.addAdvisor(advisor); // 添加一个返回后的通知 proxyFactory.addAdvice(new DmzAfterReturnAdvice()); // 添加一个方法执行前的通知 proxyFactory.addAdvice(new DmzBeforeAdvice()); // 为代理类引入一个新的需要实现的接口--Runnable proxyFactory.addAdvice(new DmzIntroductionAdvice()); // 设置目标类 proxyFactory.setTarget(new DmzService()); // 因为要测试代理对象自己定义的方法,所以这里启用cglib代理 proxyFactory.setProxyTargetClass(true); // 创建代理对象 Object proxy = proxyFactory.getProxy(); // 调用代理类的toString方法,通过控制台查看代理逻辑的执行情况 proxy.toString(); if (proxy instanceof DmzService) { ((DmzService) proxy).testAop(); } // 判断引入是否成功,并执行引入的逻辑 if (proxy instanceof Runnable) { ((Runnable) proxy).run(); } } }
这里我就不将测试结果放出来了,大家可以先自行思考这段程序将输出什么。接下来我们就来分析上面这段程序中所涉及到的API,通过这些API的学习相信大家可以彻底理解上面这段代码。
API介绍
Pointcut(切点)
对应接口定义如下:
public interface Pointcut { // ClassFilter,在类级别进行过滤 ClassFilter getClassFilter(); // MethodMatcher,在方法级别进行过滤 MethodMatcher getMethodMatcher(); // 一个单例对象,默认匹配所有 Pointcut TRUE = TruePointcut.INSTANCE; }
切点的主要作用是定义通知所要应用到的类跟方法,上面的接口定义也很明显的体现了这一点,我们可以将其拆分成为两个部分
- ClassFilter,接口定义如下:
public interface ClassFilter { boolean matches(Class<?> clazz); ClassFilter TRUE = TrueClassFilter.INSTANCE; }
ClassFilter的主要作用是在类级别上对通知的应用进行一次过滤,如果它的match方法对任意的类都返回true的话,说明在类级别上我们不需要过滤,这种情况下,通知的应用,就完全依赖MethodMatcher的匹配结果。
- MethodMatcher,接口定义如下:
public interface MethodMatcher { boolean matches(Method method, @Nullable Class<?> targetClass); boolean isRuntime(); boolean matches(Method method, @Nullable Class<?> targetClass, Object... args); MethodMatcher TRUE = TrueMethodMatcher.INSTANCE; }
MethodMatcher中一共有三个核心方法
- matches(Method method, @Nullable Class<?> targetClass),这个方法用来判断当前定义的切点跟目标类中的指定方法是否匹配,它可以在创建代理的时候就被调用,从而决定是否需要进行代理,这样就可以避免每次方法执行的时候再去做判断
- isRuntime(),如果这个方法返回true的话,意味着每次执行方法时还需要做一次匹配
- matches(Method method, @Nullable Class<?> targetClass, Object... args),当之前的isRuntime方法返回true时,会调用这个方法再次进行一次判断,返回false的话,意味这个不对这个方法应用通知
Advice(通知)
环绕通知(Interception Around Advice)
接口定义如下:
public interface MethodInterceptor extends Interceptor { Object invoke(MethodInvocation invocation) throws Throwable; }
在上面接口定义的invoke方法中,MethodInvocation就是当前执行的方法,当我们调用invocation.proceed就是在执行当前的这个方法,基于此,我们可以在方法的执行前后去插入我们自定义的逻辑,比如下面这样
// 执行前的逻辑 doSomeThingBefore(); Object var = invocation.proceed; doSomeThingAfter(); // 执行后的逻辑 retrun var;
前置通知(Before Advice)
public interface MethodBeforeAdvice extends BeforeAdvice { void before(Method m, Object[] args, Object target) throws Throwable; }
跟环绕通知不同的是,这个接口中定义的方法的返回值是void,所以前置通知是无法修改方法的返回值的。
如果在前置通知中发生了异常,那么会直接终止目标方法的执行以及打断整个拦截器链的执行
后置通知(After Returning Advice)
public interface AfterReturningAdvice extends Advice { void afterReturning(Object returnValue, Method m, Object[] args, Object target) throws Throwable; }
后置通知相比较于前置通知,主要有以下几点不同
- 后置通知可以访问目标方法的返回值,但是不能修改
- 后置通知是在方法执行完成后执行
异常通知(Throws Advice)
public interface ThrowsAdvice extends AfterAdvice { }
异常通知中没有定义任何方法,它更像一个标记接口。我们在定义异常通知时需要实现这个接口,同时方法的签名也有要求
1.方法名称必须是afterThrowing
2.方法的参数个数必须是1个或者4个,如下
public class OneParamThrowsAdvice implements ThrowsAdvice { // 如果只有一个参数,那么这个参数必须是要进行处理的异常 public void afterThrowing(RemoteException ex) throws Throwable { // Do something with remote exception } } public class FourParamThrowsAdvice implements ThrowsAdvice { // 如果定义了四个参数,那么这四个参数分别是 // 1.m:目标方法 // 2.args:执行目标方法所需要的参数 // 3.target:目标对象 // 4.ex:具体要处理的异常 // 并且参数类型必须按照这个顺序定义 public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) { // Do something with all arguments } }
我们可以在一个异常通知中定义多个方法,在后续的源码分析中我们会发现,这些方法最终会被注册成对应的异常的handler,像下面这样
public static class CombinedThrowsAdvice implements ThrowsAdvice { public void afterThrowing(RemoteException ex) throws Throwable { // Do something with remote exception } public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) { // Do something with all arguments } }
引入通知(Introduction Advice)
引入通知的主要作用是可以让生成的代理类实现额外的接口。例如在上面的例子中,我们为DmzService创建一个代理对象,同时为其定义了一个引入通知
public class DmzIntroductionAdvice extends DelegatingIntroductionInterceptor implements Runnable { @Override public void run() { System.out.println("running!!!!"); } }
在这个引入通知中,我们为其引入了一个新的需要实现的接口Runnable,同时通知本身作为这个接口的实现类。
通过这个引入通知,我们可以将生成的代理类强转成Runnable类型然后执行其run方法,同时,run方法也会被前面定义的前置通知,后置通知等拦截。
为了更好的了解引入通知,我们来需要了解下DelegatingIntroductionInterceptor这个类。见名知意,这个类就是一个委托引入拦截器,因为我们要为代理类引入新的接口,因为着我们要提供具体的实现的逻辑,而具体的实现的逻辑就可以被委托给这个DelegatingIntroductionInterceptor。
我们可以看看它的源码
public class DelegatingIntroductionInterceptor extends IntroductionInfoSupport implements IntroductionInterceptor { // 实际实现了引入逻辑的类 @Nullable private Object delegate; // 对外提供了一个带参的构造函数,通过这个构造函数我们可以传入一个 // 具体的实现类 public DelegatingIntroductionInterceptor(Object delegate) { init(delegate); } // 对子类暴露了一个空参的构造函数,默认将自身作为实现了引入逻辑的委托类 // 我们上面的例子中就是使用的这种方法 protected DelegatingIntroductionInterceptor() { init(this); } // 对这个类进行初始化,要通过实际的实现类来找到具体要实现的接口 private void init(Object delegate) { Assert.notNull(delegate, "Delegate must not be null"); this.delegate = delegate; // 找到delegate所有实现的接口 implementInterfacesOnObject(delegate); // 因为我们可能会将DelegatingIntroductionInterceptor本身作为委托者 // Spring的设计就是不对外暴露这两个接口 // 如果将其暴露,意味着我们可以将代理类强转成这种类型 suppressInterface(IntroductionInterceptor.class); suppressInterface(DynamicIntroductionAdvice.class); } // 引入通知本身也是基于拦截器实现的,当执行一个方法时需要判断这个方法 // 是不是被引入的接口中定义的方法,如果是的话,那么不能调用目标类的方法 // 而要调用委托类的方法 public Object invoke(MethodInvocation mi) throws Throwable { if (isMethodOnIntroducedInterface(mi)) { Object retVal = AopUtils.invokeJoinpointUsingReflection(this.delegate, mi.getMethod(), mi.getArguments()); // 这里是处理一种特殊情况,方法的返回值是this的时候 // 这里应该返回代理类 if (retVal == this.delegate && mi instanceof ProxyMethodInvocation) { Object proxy = ((ProxyMethodInvocation) mi).getProxy(); if (mi.getMethod().getReturnType().isInstance(proxy)) { retVal = proxy; } } // 其余情况下直接将委托类的执行结果返回 return retVal; } // 执行到这里说明不是引入的方法,这是Spring提供了一个扩展逻辑 // 正常来说这个类只会处理引入的逻辑,通过这个方法可以对目标类中的方法做拦截 // 不常用 return doProceed(mi); } protected Object doProceed(MethodInvocation mi) throws Throwable { return mi.proceed(); } }
通过查看这个类的源码我们可以发现,所谓的引入其实就是在方法执行的时候加了一层拦截,当判断这个方法是被引入的接口提供的方法的时候,那么就执行委托类中的逻辑而不是目标类中的方法